Passed
Pull Request — master (#281)
by Vincent
15:23
created

generate(AI,AiActionFactory)   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
eloc 14
dl 0
loc 24
ccs 12
cts 12
cp 1
rs 8.6666
c 0
b 0
f 0
cc 6
crap 6
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.CastSpell;
25
import fr.quatrevieux.araknemu.game.fight.ai.action.util.Movement;
26
import fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation;
27
import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator;
28
import fr.quatrevieux.araknemu.game.fight.ai.util.AIHelper;
29
import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter;
30
import fr.quatrevieux.araknemu.game.fight.map.BattlefieldCell;
31
import fr.quatrevieux.araknemu.game.fight.turn.action.Action;
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 a cast
41
 *
42
 * Two strategies are available :
43
 * - Select the nearest cell which allows to cast
44
 * - Select the best cell for maximize cast effect
45
 *
46
 * If the current cell permit cast and the fighter is surrounded by enemies, the fighter will not perform any move.
47
 *
48
 * For select the cell, the generator will iterate over all reachable cells
49
 * with the current amount of MPs,
50
 * and check all spells on all available cells.
51
 */
52
public final class MoveToCast<F extends ActiveFighter> implements ActionGenerator<F> {
53
    private final Simulator simulator;
54
    private final CastSpell.SimulationSelector selector;
55
    private final TargetSelectionStrategy<F> strategy;
56
57 1
    public MoveToCast(Simulator simulator, CastSpell.SimulationSelector selector, TargetSelectionStrategy<F> strategy) {
58 1
        this.simulator = simulator;
59 1
        this.selector = selector;
60 1
        this.strategy = strategy;
61 1
    }
62
63
    @Override
64
    public void initialize(AI<F> ai) {
65
        // No-op
66 1
    }
67
68
    @Override
69
    public Optional<Action> generate(AI<F> ai, AiActionFactory actions) {
70 1
        final AIHelper helper = ai.helper();
71 1
        final F fighter = ai.fighter();
72
73
        // Cannot move or cast
74 1
        if (fighter == null || !helper.canCast() || !helper.canMove()) {
75 1
            return Optional.empty();
76
        }
77
78 1
        final GenerationScope scope = new GenerationScope(ai.fighter(), actions, ai.helper());
79
80
        // Can cast, but there is at least 1 enemy : do not perform move because of potential tackle
81 1
        if (helper.enemies().adjacent().findFirst().isPresent() && scope.canCastFromCell(fighter.cell())) {
82 1
            return Optional.empty();
83
        }
84
85 1
        final Movement<F> movement = new Movement<>(
86 1
            coordinates -> strategy.score(scope, coordinates),
87 1
            scoredCell -> scope.canCastFromCell(scoredCell.coordinates().cell())
88
        );
89 1
        movement.initialize(ai);
90
91 1
        return movement.generate(ai, actions);
92
    }
93
94
    /**
95
     * Store parameters and possible actions of current action generator
96
     */
97
    public final class GenerationScope {
98
        private final F fighter;
99
        private final AiActionFactory actions;
100
        private final AIHelper helper;
101 1
        private final Map<BattlefieldCell, Collection<CastSimulation>> possibleActionsCache = new HashMap<>();
102
103 1
        public GenerationScope(F fighter, AiActionFactory actions, AIHelper helper) {
104 1
            this.fighter = fighter;
105 1
            this.actions = actions;
106 1
            this.helper = helper;
107 1
        }
108
109
        /**
110
         * Fighter handle by the AI, which will perform the action
111
         */
112
        public F fighter() {
113 1
            return fighter;
114
        }
115
116
        /**
117
         * Compute the score of the cast action
118
         * Higher is the score, more effective is the action
119
         */
120
        public double castScore(CastSimulation simulation) {
121 1
            return selector.score(simulation);
122
        }
123
124
        /**
125
         * Check if there is at least one cast possible from the given cell
126
         */
127
        private boolean canCastFromCell(BattlefieldCell cell) {
128 1
            return !computePossibleCasts(cell).isEmpty();
129
        }
130
131
        /**
132
         * Simulate possible casts from the given cell
133
         *
134
         * - List available spells
135
         * - Combine with all accessible cells
136
         * - Check if the action is valid
137
         * - Simulate the action
138
         * - Keep only simulation results with an effective effect
139
         *
140
         * Note: Because the fighter should be moved to the tested cell, values cannot be computed lazily, like with a stream
141
         *
142
         * @param cell The cell from which spells will be cast
143
         *
144
         * @see CastSpell.SimulationSelector#valid(CastSimulation) To check if the cast is effective
145
         */
146
        public Collection<CastSimulation> computePossibleCasts(BattlefieldCell cell) {
147 1
            Collection<CastSimulation> possibleCasts = possibleActionsCache.get(cell);
148
149 1
            if (possibleCasts != null) {
150 1
                return possibleCasts;
151
            }
152
153 1
            possibleCasts = helper.withPosition(cell).spells().caster(actions.castSpellValidator())
154 1
                .simulate(simulator)
155 1
                .filter(selector::valid) // Keep only effective effects
156 1
                .collect(Collectors.toList())
157
            ;
158
159 1
            possibleActionsCache.put(cell, possibleCasts);
160
161 1
            return possibleCasts;
162
        }
163
    }
164
165
    public interface TargetSelectionStrategy<F extends ActiveFighter> {
166
        /**
167
         * Compute the score of a given target cell
168
         *
169
         * @param scope Scope which contains parameters for perform action selection
170
         * @param target The cell to check
171
         *
172
         * @return The score as double. The highest value will be selected
173
         */
174
        public double score(MoveToCast<F>.GenerationScope scope, CoordinateCell<BattlefieldCell> target);
175
    }
176
177
    /**
178
     * Select the best target cell for cast a spell, and maximizing effects
179
     */
180 1
    public static final class BestTargetStrategy<F extends ActiveFighter> implements TargetSelectionStrategy<F> {
181
        @Override
182
        public double score(MoveToCast<F>.GenerationScope scope, CoordinateCell<BattlefieldCell> target) {
183 1
            return maxScore(scope, target.cell()) - target.distance(scope.fighter().cell());
184
        }
185
186
        /**
187
         * Compute the max spell score from the given cell
188
         */
189
        private static <F extends ActiveFighter> double maxScore(MoveToCast<F>.GenerationScope scope, BattlefieldCell cell) {
190 1
            return scope.computePossibleCasts(cell).stream()
191 1
                .mapToDouble(scope::castScore)
192 1
                .max().orElse(0)
193
            ;
194
        }
195
    }
196
197
    /**
198
     * Select the nearest cell where a cast is possible
199
     *
200
     * Note: This selected cell is not the best cell for perform a cast, but the nearest cell.
201
     *       So, it does not perform the best move for maximize damage.
202
     */
203 1
    public static final class NearestStrategy<F extends ActiveFighter> implements TargetSelectionStrategy<F> {
204
        @Override
205
        public double score(MoveToCast<F>.GenerationScope scope, CoordinateCell<BattlefieldCell> target) {
206 1
            return -target.distance(scope.fighter().cell()) + sigmoid(BestTargetStrategy.maxScore(scope, target.cell()));
207
        }
208
209
        /**
210
         * Transform score value in interval [-inf; +inf] to bounded value [0; 1]
211
         *
212
         * @param value Score to transform
213
         */
214
        private double sigmoid(double value) {
215 1
            return 0.5 + value / (2 * (1 + Math.abs(value)));
216
        }
217
    }
218
}
219