Passed
Pull Request — master (#193)
by Vincent
11:12
created

MoveToAttack(Simulator,TargetSelectionStrategy)   A

Complexity

Conditions 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 5
ccs 5
cts 5
cp 1
crap 1
rs 10
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.simulation.CastSimulation;
25
import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator;
26
import fr.quatrevieux.araknemu.game.fight.ai.util.AIHelper;
27
import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter;
28
import fr.quatrevieux.araknemu.game.fight.map.FightCell;
29
import fr.quatrevieux.araknemu.game.fight.turn.action.Action;
30
31
import java.util.Collection;
32
import java.util.HashMap;
33
import java.util.Map;
34
import java.util.Optional;
35
import java.util.stream.Collectors;
36
37
/**
38
 * Try to move for perform an attack
39
 *
40
 * The nearest cell for perform an attack is selected.
41
 * If the current cell permit attacking, the fighter will not perform any move.
42
 *
43
 * For select the cell, the generator will iterate over all reachable cells
44
 * with the current amount of MPs, sort them by distance,
45
 * and check all spells on all available cells.
46
 * The first matching cell is selected.
47
 */
48
public final class MoveToAttack implements ActionGenerator {
49
    private final Movement movement;
50
    private final Simulator simulator;
51
    private final Attack attack;
52
    private final TargetSelectionStrategy strategy;
53
54 1
    private final Map<FightCell, Collection<CastSimulation>> possibleActionsCache = new HashMap<>();
55
56
    private ActiveFighter fighter;
57
    private AIHelper helper;
58
59 1
    private MoveToAttack(Simulator simulator, TargetSelectionStrategy strategy) {
60 1
        this.simulator = simulator;
61 1
        this.attack = new Attack(simulator);
62 1
        this.strategy = strategy;
63 1
        this.movement = new Movement(this::score, this::isValidCell);
64 1
    }
65
66
    @Override
67
    public void initialize(AI ai) {
68 1
        movement.initialize(ai);
69 1
        attack.initialize(ai);
70
71 1
        this.fighter = ai.fighter();
72 1
    }
73
74
    @Override
75
    public Optional<Action> generate(AI ai) {
76 1
        helper = ai.helper();
77
78
        // Cannot move or attack
79 1
        if (!helper.canCast() || !helper.canMove()) {
80 1
            return Optional.empty();
81
        }
82
83
        try {
84
            // Can attack, but there is at least 1 enemy : do not perform move because of potential tackle
85 1
            if (canAttackFromCell(fighter.cell()) && helper.enemies().adjacent().findFirst().isPresent()) {
86 1
                return Optional.empty();
87
            }
88
89 1
            return movement.generate(ai);
90
        } finally {
91 1
            possibleActionsCache.clear();
92
        }
93
    }
94
95
    /**
96
     * Simulate possible attacks from the given cell
97
     *
98
     * - List available spells
99
     * - Combine with all accessible cells
100
     * - Check if the action is valid
101
     * - Simulate the action
102
     * - Keep only simulation results with an effective attack
103
     *
104
     * Note: Because the fighter should be move to the tested cell, values cannot be computed lazily, like with a stream
105
     *
106
     * @param cell The cell from which spells will be casted
107
     *
108
     * @see Attack#valid(CastSimulation) To check if the attack is effective
109
     */
110
    public Collection<CastSimulation> computePossibleCasts(FightCell cell) {
111 1
        Collection<CastSimulation> possibleCasts = possibleActionsCache.get(cell);
112
113 1
        if (possibleCasts != null) {
114 1
            return possibleCasts;
115
        }
116
117 1
        possibleCasts = helper.simulateMove(cell, fighter -> helper.spells()
0 ignored issues
show
Comprehensibility introduced by
The variable fightershadows a field with the same name declared in line 56. Consider renaming it.
Loading history...
118 1
            .simulate(simulator)
119 1
            .filter(attack::valid) // Keep only effective attacks
120 1
            .collect(Collectors.toList())
121
        );
122
123 1
        possibleActionsCache.put(cell, possibleCasts);
124
125 1
        return possibleCasts;
126
    }
127
128
    /**
129
     * Check if there is at least one attack possible from the given cell
130
     */
131
    private boolean canAttackFromCell(FightCell cell) {
132 1
        return !computePossibleCasts(cell).isEmpty();
133
    }
134
135
    /**
136
     * Compute the score of a cell
137
     * The lowest value will be selected
138
     */
139
    private double score(CoordinateCell<FightCell> coordinate) {
140 1
        return strategy.score(this, coordinate);
141
    }
142
143
    /**
144
     * Check if the cell is a valid movement
145
     *
146
     * It must have at least one possible attack
147
     */
148
    private boolean isValidCell(Movement.ScoredCell scoredCell) {
149 1
        return canAttackFromCell(scoredCell.coordinates().cell());
150
    }
151
152
    /**
153
     * Select the nearest cell where a cast is possible
154
     *
155
     * Note: This selected cell is not the best cell for perform an attack, but the nearest cell.
156
     *       So, it do not perform the best move for maximize damage.
157
     */
158
    public static MoveToAttack nearest(Simulator simulator) {
159 1
        return new MoveToAttack(simulator, new NearestStrategy());
160
    }
161
162
    /**
163
     * Select the best target cell for cast a spell, and maximizing damage
164
     */
165
    public static MoveToAttack bestTarget(Simulator simulator) {
166 1
        return new MoveToAttack(simulator, new BestTargetStrategy());
167
    }
168
169
    public interface TargetSelectionStrategy {
170
        /**
171
         * Compute the score of a given target cell
172
         *
173
         * @param generator The action generator
174
         * @param target The cell to check
175
         *
176
         * @return The score as double. The highest value will be selected
177
         */
178
        public double score(MoveToAttack generator, CoordinateCell<FightCell> target);
179
    }
180
181 1
    public static final class BestTargetStrategy implements TargetSelectionStrategy {
182
        @Override
183
        public double score(MoveToAttack generator, CoordinateCell<FightCell> target) {
184 1
            return maxScore(generator, target.cell()) - target.distance(generator.fighter.cell());
185
        }
186
187
        /**
188
         * Compute the max spell score from the given cell
189
         */
190
        private static double maxScore(MoveToAttack generator, FightCell cell) {
191 1
            return generator.computePossibleCasts(cell).stream()
192 1
                .mapToDouble(generator.attack::score)
193 1
                .max().orElse(0)
194
            ;
195
        }
196
    }
197
198
    private static final class NearestStrategy implements TargetSelectionStrategy {
199
        @Override
200
        public double score(MoveToAttack generator, CoordinateCell<FightCell> target) {
201 1
            return -target.distance(generator.fighter.cell()) + sigmoid(BestTargetStrategy.maxScore(generator, target.cell()));
202
        }
203
204
        /**
205
         * Transform score value in interval [-inf; +inf] to bounded value [0; 1]
206
         *
207
         * @param value Score to transform
208
         */
209
        private double sigmoid(double value) {
210 1
            return 0.5 + value / (2 * (1 + Math.abs(value)));
211
        }
212
    }
213
}
214