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

generate(AI,AiActionFactory)   B

Complexity

Conditions 8

Size

Total Lines 38
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 8.0093

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 38
ccs 18
cts 19
cp 0.9474
rs 7.3333
c 0
b 0
f 0
cc 8
crap 8.0093
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.util.SpellCaster;
25
import fr.quatrevieux.araknemu.game.fight.ai.util.SpellsHelper;
26
import fr.quatrevieux.araknemu.game.fight.castable.Castable;
27
import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter;
28
import fr.quatrevieux.araknemu.game.fight.fighter.FighterData;
29
import fr.quatrevieux.araknemu.game.fight.map.BattlefieldCell;
30
import fr.quatrevieux.araknemu.game.fight.map.BattlefieldMap;
31
import fr.quatrevieux.araknemu.game.fight.turn.action.Action;
32
import fr.quatrevieux.araknemu.game.spell.Spell;
33
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
34
35
import java.util.Collections;
36
import java.util.Comparator;
37
import java.util.List;
38
import java.util.Optional;
39
import java.util.stream.Collectors;
40
41
/**
42
 * Try to teleport near enemy
43
 */
44 1
public final class TeleportNearEnemy<F extends ActiveFighter> implements ActionGenerator<F> {
45 1
    private List<Spell> teleportSpells = Collections.emptyList();
46
47
    @Override
48
    public void initialize(AI<F> ai) {
49 1
        final SpellsHelper helper = ai.helper().spells();
50
51 1
        teleportSpells = helper
52 1
            .withEffect(4)
53 1
            .sorted(Comparator.comparingInt(Castable::apCost))
54 1
            .collect(Collectors.toList())
55
        ;
56 1
    }
57
58
    @Override
59
    public Optional<Action> generate(AI<F> ai, AiActionFactory actions) {
60 1
        if (teleportSpells.isEmpty()) {
61 1
            return Optional.empty();
62
        }
63
64 1
        final int actionPoints = ai.turn().points().actionPoints();
65
66 1
        if (actionPoints < 1) {
67 1
            return Optional.empty();
68
        }
69
70 1
        final Optional<? extends FighterData> enemy = ai.enemy();
71
72 1
        if (!enemy.isPresent()) {
73
            return Optional.empty();
74
        }
75
76 1
        final SpellCaster caster = ai.helper().spells().caster(actions.castSpellValidator());
77 1
        final Selector selector = new Selector(enemy.get().cell(), ai.fighter().cell());
78
79
        // Already at adjacent cell of the enemy
80 1
        if (selector.adjacent()) {
81 1
            return Optional.empty();
82
        }
83
84
        // Spells are ordered by AP cost : the first spell which can reach an accessible adjacent is necessarily the best spell
85 1
        for (Spell spell : teleportSpells) {
86 1
            if (spell.apCost() > actionPoints) {
87 1
                break; // Following spells have an higher cost
88
            }
89
90 1
            if (selectBestTeleportTargetForSpell(caster, selector, ai.map(), spell)) {
91 1
                return selector.action(actions);
92
            }
93 1
        }
94
95 1
        return selector.action(actions);
96
    }
97
98
    /**
99
     * Select the best possible target for the given spell
100
     * The result will be push()'ed into selector
101
     *
102
     * @return true if the spell can reach an adjacent cell
103
     */
104
    private boolean selectBestTeleportTargetForSpell(SpellCaster caster, Selector selector, BattlefieldMap map, Spell spell) {
105 1
        for (BattlefieldCell cell : map) {
106
            // Target or launch is not valid
107 1
            if (!cell.walkable() || !caster.validate(spell, cell)) {
108 1
                continue;
109
            }
110
111
            // Adjacent cell found : no need to continue
112 1
            if (selector.push(spell, cell)) {
113 1
                return true;
114
            }
115 1
        }
116
117 1
        return false;
118
    }
119
120
    /**
121
     * Select the best spell and cell couple for teleport
122
     */
123
    private class Selector {
124
        private final CoordinateCell<BattlefieldCell> enemyCell;
125
        private int distance;
126
        private @MonotonicNonNull BattlefieldCell cell;
127
        private @MonotonicNonNull Spell spell;
128
129 1
        public Selector(BattlefieldCell enemyCell, BattlefieldCell currentCell) {
130 1
            this.enemyCell = enemyCell.coordinate();
131 1
            this.distance = this.enemyCell.distance(currentCell);
132 1
        }
133
134
        /**
135
         * Check if the current cell is adjacent to the enemy cell
136
         */
137
        public boolean adjacent() {
138 1
            return distance == 1;
139
        }
140
141
        /**
142
         * Push the teleport parameters and check if there are better than the previous
143
         *
144
         * @return true if the new cell is adjacent to the target
145
         */
146
        public boolean push(Spell spell, BattlefieldCell cell) {
147 1
            final int currentDistance = enemyCell.distance(cell);
148
149 1
            if (currentDistance < distance) {
150 1
                this.spell = spell;
151 1
                this.cell = cell;
152 1
                this.distance = currentDistance;
153
            }
154
155 1
            return adjacent();
156
        }
157
158
        /**
159
         * Get the best cast action
160
         * May returns an empty optional if no teleport spell can be found, or if the fighter is already on the best cell
161
         */
162
        public Optional<Action> action(AiActionFactory actions) {
163 1
            if (spell == null || cell == null) {
164 1
                return Optional.empty();
165
            }
166
167 1
            return Optional.of(actions.cast(spell, cell));
168
        }
169
    }
170
}
171