Passed
Pull Request — master (#249)
by Vincent
13:26
created

generate(AI,ActionsFactory)   B

Complexity

Conditions 6

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 14
dl 0
loc 24
ccs 12
cts 12
cp 1
crap 6
rs 8.6666
c 0
b 0
f 0
1
/*
2
 * This file is part of Araknemu.
3
 *
4
 * Araknemu is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU Lesser General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * Araknemu is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU Lesser General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU Lesser General Public License
15
 * along with Araknemu.  If not, see <https://www.gnu.org/licenses/>.
16
 *
17
 * Copyright (c) 2017-2020 Vincent Quatrevieux
18
 */
19
20
package fr.quatrevieux.araknemu.game.fight.ai.action;
21
22
import fr.arakne.utils.maps.CoordinateCell;
23
import fr.quatrevieux.araknemu.game.fight.ai.AI;
24
import fr.quatrevieux.araknemu.game.fight.ai.action.util.Movement;
25
import fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation;
26
import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator;
27
import fr.quatrevieux.araknemu.game.fight.ai.util.AIHelper;
28
import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter;
29
import fr.quatrevieux.araknemu.game.fight.map.FightCell;
30
import fr.quatrevieux.araknemu.game.fight.turn.action.Action;
31
import fr.quatrevieux.araknemu.game.fight.turn.action.factory.ActionsFactory;
32
33
import java.util.Collection;
34
import java.util.HashMap;
35
import java.util.Map;
36
import java.util.Optional;
37
import java.util.stream.Collectors;
38
39
/**
40
 * Try to move for perform an attack
41
 *
42
 * The nearest cell for perform an attack is selected.
43
 * If the current cell permit attacking, the fighter will not perform any move.
44
 *
45
 * For select the cell, the generator will iterate over all reachable cells
46
 * with the current amount of MPs, sort them by distance,
47
 * and check all spells on all available cells.
48
 * The first matching cell is selected.
49
 */
50
public final class MoveToAttack<F extends ActiveFighter> implements ActionGenerator<F> {
51
    private final Simulator simulator;
52
    private final Attack<F> attack;
53
    private final TargetSelectionStrategy<F> strategy;
54
55 1
    private MoveToAttack(Simulator simulator, TargetSelectionStrategy<F> strategy) {
56 1
        this.simulator = simulator;
57 1
        this.attack = new Attack<>(simulator);
58 1
        this.strategy = strategy;
59 1
    }
60
61
    @Override
62
    public void initialize(AI<F> ai) {
63 1
        attack.initialize(ai);
64 1
    }
65
66
    @Override
67
    public Optional<Action> generate(AI<F> ai, ActionsFactory<F> actions) {
68 1
        final AIHelper helper = ai.helper();
69 1
        final F fighter = ai.fighter();
70
71
        // Cannot move or attack
72 1
        if (fighter == null || !helper.canCast() || !helper.canMove()) {
73 1
            return Optional.empty();
74
        }
75
76 1
        final GenerationScope scope = new GenerationScope(ai.fighter(), actions, ai.helper());
77
78
        // Can attack, but there is at least 1 enemy : do not perform move because of potential tackle
79 1
        if (helper.enemies().adjacent().findFirst().isPresent() && scope.canAttackFromCell(fighter.cell())) {
80 1
            return Optional.empty();
81
        }
82
83 1
        final Movement<F> movement = new Movement<>(
84 1
            coordinates -> strategy.score(scope, coordinates),
85 1
            scoredCell -> scope.canAttackFromCell(scoredCell.coordinates().cell())
86
        );
87 1
        movement.initialize(ai);
88
89 1
        return movement.generate(ai, actions);
90
    }
91
92
    /**
93
     * Select the nearest cell where a cast is possible
94
     *
95
     * Note: This selected cell is not the best cell for perform an attack, but the nearest cell.
96
     *       So, it do not perform the best move for maximize damage.
97
     */
98
    public static <F extends ActiveFighter> MoveToAttack<F> nearest(Simulator simulator) {
99 1
        return new MoveToAttack<>(simulator, new NearestStrategy<>());
100
    }
101
102
    /**
103
     * Select the best target cell for cast a spell, and maximizing damage
104
     */
105
    public static <F extends ActiveFighter> MoveToAttack<F> bestTarget(Simulator simulator) {
106 1
        return new MoveToAttack<>(simulator, new BestTargetStrategy<>());
107
    }
108
109
    /**
110
     * Store parameters and possible actions of current action generator
111
     */
112
    public final class GenerationScope {
113
        private final F fighter;
114
        private final ActionsFactory<F> actions;
115
        private final AIHelper helper;
116 1
        private final Map<FightCell, Collection<CastSimulation>> possibleActionsCache = new HashMap<>();
117
118 1
        public GenerationScope(F fighter, ActionsFactory<F> actions, AIHelper helper) {
119 1
            this.fighter = fighter;
120 1
            this.actions = actions;
121 1
            this.helper = helper;
122 1
        }
123
124
        /**
125
         * Fighter handle by the AI, which will perform the action
126
         */
127
        public F fighter() {
128 1
            return fighter;
129
        }
130
131
        /**
132
         * Compute the score of the cast action
133
         * Higher is the score, more effective is the action
134
         */
135
        public double attackScore(CastSimulation simulation) {
136 1
            return attack.score(simulation);
137
        }
138
139
        /**
140
         * Check if there is at least one attack possible from the given cell
141
         */
142
        private boolean canAttackFromCell(FightCell cell) {
143 1
            return !computePossibleCasts(cell).isEmpty();
144
        }
145
146
        /**
147
         * Simulate possible attacks from the given cell
148
         *
149
         * - List available spells
150
         * - Combine with all accessible cells
151
         * - Check if the action is valid
152
         * - Simulate the action
153
         * - Keep only simulation results with an effective attack
154
         *
155
         * Note: Because the fighter should be move to the tested cell, values cannot be computed lazily, like with a stream
156
         *
157
         * @param cell The cell from which spells will be casted
158
         *
159
         * @see Attack#valid(CastSimulation) To check if the attack is effective
160
         */
161
        public Collection<CastSimulation> computePossibleCasts(FightCell cell) {
162 1
            Collection<CastSimulation> possibleCasts = possibleActionsCache.get(cell);
163
164 1
            if (possibleCasts != null) {
165 1
                return possibleCasts;
166
            }
167
168 1
            possibleCasts = helper.withPosition(cell).spells().caster(actions.cast().validator())
169 1
                .simulate(simulator)
170 1
                .filter(attack::valid) // Keep only effective attacks
171 1
                .collect(Collectors.toList())
172
            ;
173
174 1
            possibleActionsCache.put(cell, possibleCasts);
175
176 1
            return possibleCasts;
177
        }
178
    }
179
180
    public interface TargetSelectionStrategy<F extends ActiveFighter> {
181
        /**
182
         * Compute the score of a given target cell
183
         *
184
         * @param scope Scope which contains parameters for perform action selection
185
         * @param target The cell to check
186
         *
187
         * @return The score as double. The highest value will be selected
188
         */
189
        public double score(MoveToAttack<F>.GenerationScope scope, CoordinateCell<FightCell> target);
190
    }
191
192 1
    public static final class BestTargetStrategy<F extends ActiveFighter> implements TargetSelectionStrategy<F> {
193
        @Override
194
        public double score(MoveToAttack<F>.GenerationScope scope, CoordinateCell<FightCell> target) {
195 1
            return maxScore(scope, target.cell()) - target.distance(scope.fighter().cell());
196
        }
197
198
        /**
199
         * Compute the max spell score from the given cell
200
         */
201
        private static <F extends ActiveFighter> double maxScore(MoveToAttack<F>.GenerationScope scope, FightCell cell) {
202 1
            return scope.computePossibleCasts(cell).stream()
203 1
                .mapToDouble(scope::attackScore)
204 1
                .max().orElse(0)
205
            ;
206
        }
207
    }
208
209
    private static final class NearestStrategy<F extends ActiveFighter> implements TargetSelectionStrategy<F> {
210
        @Override
211
        public double score(MoveToAttack<F>.GenerationScope scope, CoordinateCell<FightCell> target) {
212 1
            return -target.distance(scope.fighter().cell()) + sigmoid(BestTargetStrategy.maxScore(scope, target.cell()));
213
        }
214
215
        /**
216
         * Transform score value in interval [-inf; +inf] to bounded value [0; 1]
217
         *
218
         * @param value Score to transform
219
         */
220
        private double sigmoid(double value) {
221 1
            return 0.5 + value / (2 * (1 + Math.abs(value)));
222
        }
223
    }
224
}
225