Passed
Branch master (3c0c2d)
by Vincent
11:07
created

canAttackFromCell(FightCell)   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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