Skip to content

Commit

Permalink
feat(ai): Allow to resolve main ally on AI and add move near invoker …
Browse files Browse the repository at this point in the history
…on summoned creature
  • Loading branch information
vincent4vx committed Apr 14, 2024
1 parent 37827dc commit 2b73510
Show file tree
Hide file tree
Showing 10 changed files with 337 additions and 1 deletion.
15 changes: 15 additions & 0 deletions src/main/java/fr/quatrevieux/araknemu/game/fight/ai/AI.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ public interface AI {
*/
public Optional<? extends FighterData> enemy();

/**
* Get the main ally
* This method behavior can change, depending on the AI resolution strategy
*
* Unlike {@link #enemy()}, this method does not return a value most of the time,
* it is used to mark a given ally as the main target for buffs, heals, etc.
*
* In case of invocation, this method should return the invoker.
*
* An empty optional can be returned, no ally is preferred
*/
public default Optional<? extends FighterData> ally() {
return Optional.empty();
}

/**
* Get helper for the current AI
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ public Optional<? extends FighterData> enemy() {
return helper.enemies().nearest();
}

@Override
public Optional<? extends FighterData> ally() {
return Optional.ofNullable(fighter.invoker()).filter(invoker -> !invoker.hidden());
}

@Override
public AIHelper helper() {
return helper;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* This file is part of Araknemu.
*
* Araknemu is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Araknemu is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Araknemu. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (c) 2017-2020 Vincent Quatrevieux
*/

package fr.quatrevieux.araknemu.game.fight.ai.action;

import fr.quatrevieux.araknemu.game.fight.ai.AI;
import fr.quatrevieux.araknemu.game.fight.turn.action.Action;

import java.util.Optional;

/**
* Try to move near the selected ally
*/
public final class MoveNearAlly implements ActionGenerator {
private final ActionGenerator moveGenerator;

public MoveNearAlly() {
this.moveGenerator = new MoveNearFighter(AI::ally);
}

@Override
public void initialize(AI ai) {
moveGenerator.initialize(ai);
}

@Override
public <A extends Action> Optional<A> generate(AI ai, AiActionFactory<A> actions) {
return moveGenerator.generate(ai, actions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import fr.quatrevieux.araknemu.game.fight.ai.action.Invoke;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveFarEnemies;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveNearAllies;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveNearAlly;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveNearEnemy;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveToAttack;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveToAttractEnemy;
Expand Down Expand Up @@ -383,6 +384,17 @@ public final GeneratorBuilder moveNearAllies() {
return add(new MoveNearAllies());
}

/**
* Try to move near the main ally (e.g. invoker in case of summoned creature)
*
* @return The builder instance
*
* @see MoveNearAlly The used action generator
*/
public final GeneratorBuilder moveNearAlly() {
return add(new MoveNearAlly());
}

/**
* Try to invoke a monster
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ public void configure(GeneratorBuilder builder, PlayableFighter fighter) {
.invoke(simulator)
.debuff(simulator)
.attack(simulator)
.moveNearAllies()
.when(ai -> ai.ally().isPresent(), nb -> nb
.success(GeneratorBuilder::moveNearAlly)
.otherwise(GeneratorBuilder::moveNearAllies)
)
)
.otherwise(sb -> aloneAi.configure(sb, fighter))
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,8 @@ public double computeScore(int spellId, int spellLevel, int targetCell) {

return CastSpell.SimulationSelector.class.cast(action).score(simulation);
}

public int distance(Fighter fighter1, Fighter fighter2) {
return fighter1.cell().coordinate().distance(fighter2.cell().coordinate());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,37 @@ void enemyWhenInvokedButInvokerHiddenShouldReturnNearest() {
assertEquals(otherEnemy, ai.enemy().get());
}

@Test
void allyShouldBeEmptyForClassicFighter() {
FighterAI ai = new FighterAI(fighter, fight, NullGenerator.get());

assertFalse(ai.ally().isPresent());
}

@Test
void allyShouldInvokerOnInvocationFighter() {
DoubleFighter invoc = new DoubleFighter(-10, player.fighter());
fight.fighters().joinTurnList(invoc, fight.map().get(112)); // Adjacent to enemy 126
invoc.init();

FighterAI ai = new FighterAI(invoc, fight, NullGenerator.get());

assertEquals(player.fighter(), ai.ally().get());
}

@Test
void allyShouldBeEmptyIfInvokerIsHidden() {
DoubleFighter invoc = new DoubleFighter(-10, player.fighter());
fight.fighters().joinTurnList(invoc, fight.map().get(112)); // Adjacent to enemy 126
invoc.init();

FighterAI ai = new FighterAI(invoc, fight, NullGenerator.get());

player.fighter().setHidden(player.fighter(), true);

assertFalse(ai.ally().isPresent());
}

@RepeatedIfExceptionsTest
void startEmptyShouldStop() throws InterruptedException {
fight.turnList().start();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* This file is part of Araknemu.
*
* Araknemu is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Araknemu is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Araknemu. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (c) 2017-2024 Vincent Quatrevieux
*/

package fr.quatrevieux.araknemu.game.fight.ai.action;

import fr.quatrevieux.araknemu.game.fight.ai.AI;
import fr.quatrevieux.araknemu.game.fight.ai.AiBaseCase;
import fr.quatrevieux.araknemu.game.fight.fighter.Fighter;
import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter;
import fr.quatrevieux.araknemu.game.fight.map.FightCell;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.*;

class MoveNearAllyTest extends AiBaseCase {
@Override
@BeforeEach
public void setUp() throws Exception {
super.setUp();

action = new MoveNearAlly();
}

@Test
void generateNotInitialized() {
assertFalse(action.generate(Mockito.mock(AI.class), Mockito.mock(AiActionFactory.class)).isPresent());
}

@Test
void success() {
configureFight(fb -> fb
.addSelf(builder -> builder.cell(125))
.addEnemy(builder -> builder.cell(254))
);

Fighter invoc = createInvocation(122);
assertEquals(6, distance(invoc, player.fighter()));

generateAndPerformMove();

assertEquals(3, distance(invoc, player.fighter()));
assertEquals(138, invoc.cell().id());
assertEquals(0, turn.points().movementPoints());
}

@Test
void withAllyOnPathShouldBeCircumvented() {
configureFight(fb -> fb
.addSelf(builder -> builder.cell(181))
.addAlly(builder -> builder.cell(166))
.addEnemy(builder -> builder.cell(325))
);

Fighter invoc = createInvocation(151);
assertEquals(2, distance(invoc, player.fighter()));

generateAndPerformMove();

assertEquals(1, distance(invoc, player.fighter()));
assertEquals(195, invoc.cell().id());
assertEquals(0, turn.points().movementPoints());
}

@Test
void whenAllyBlockAccess() {
configureFight(fb -> fb
.addSelf(builder -> builder.cell(341))
.addAlly(builder -> builder.cell(284))
.addEnemy(builder -> builder.cell(45))
);

Fighter invoc = createInvocation(211);
assertEquals(9, distance(invoc, player.fighter()));

generateAndPerformMove();

assertEquals(6, distance(invoc, player.fighter()));
assertEquals(256, invoc.cell().id());
assertEquals(0, turn.points().movementPoints());
}

// See: https://github.com/Arakne/Araknemu/issues/94
@Test
void notAccessibleCellShouldTruncateToNearestCell() {
configureFight(fb -> fb
.map(10342)
.addSelf(builder -> builder.cell(69))
.addEnemy(builder -> builder.cell(75))
);

Fighter invoc = createInvocation(155);
assertEquals(6, distance(invoc, player.fighter()));

generateAndPerformMove();

assertEquals(4, distance(invoc, player.fighter()));
assertEquals(126, invoc.cell().id());
assertEquals(1, turn.points().movementPoints());
}

@Test
void noMP() {
configureFight(fb -> fb
.addSelf(builder -> builder.cell(125))
.addEnemy(builder -> builder.cell(321))
);

Fighter invoc = createInvocation(122);

removeAllMP();

assertDotNotGenerateAction();
}

@Test
void onAdjacentCell() {
configureFight(fb -> fb
.addSelf(builder -> builder.cell(125))
.addEnemy(builder -> builder.cell(321))
);

Fighter invoc = createInvocation(110);

assertDotNotGenerateAction();
}

@Test
void noAllyDefined() {
configureFight(fb -> fb
.addSelf(builder -> builder.cell(125))
.addEnemy(builder -> builder.cell(321))
);

assertDotNotGenerateAction();
}

@Test
void allyHidden() {
configureFight(fb -> fb
.addSelf(builder -> builder.cell(125))
.addEnemy(builder -> builder.cell(254))
);

player.fighter().setHidden(player.fighter(), true);

Fighter invoc = createInvocation(122);
assertDotNotGenerateAction();
}

private Fighter createInvocation(FightCell cell) {
DoubleFighter invoc = new DoubleFighter(-10, player.fighter());
fight.fighters().joinTurnList(invoc, cell);
invoc.init();

configureFighterAi(invoc);

return invoc;
}

private Fighter createInvocation(int cell) {
return createInvocation(fight.map().get(cell));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import fr.quatrevieux.araknemu.game.fight.ai.action.Invoke;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveFarEnemies;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveNearAllies;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveNearAlly;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveNearEnemy;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveToAttack;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveToAttractEnemy;
Expand Down Expand Up @@ -176,6 +177,11 @@ void moveNearAllies() {
assertInstanceOf(MoveNearAllies.class, builder.moveNearAllies().build());
}

@Test
void moveNearAlly() {
assertInstanceOf(MoveNearAlly.class, builder.moveNearAlly().build());
}

@Test
void moveToBoost() throws NoSuchFieldException, IllegalAccessException {
assertActions(builder.moveToBoost(simulator).build(), MoveToBoost.class, Boost.class);
Expand Down
Loading

0 comments on commit 2b73510

Please sign in to comment.