From 2b7351075576a929605561db3ef95be33ee019d2 Mon Sep 17 00:00:00 2001 From: Vincent Quatrevieux Date: Sun, 14 Apr 2024 13:32:49 +0200 Subject: [PATCH] feat(ai): Allow to resolve main ally on AI and add move near invoker on summoned creature --- .../araknemu/game/fight/ai/AI.java | 15 ++ .../araknemu/game/fight/ai/FighterAI.java | 5 + .../game/fight/ai/action/MoveNearAlly.java | 46 +++++ .../ai/action/builder/GeneratorBuilder.java | 12 ++ .../game/fight/ai/factory/type/Support.java | 5 +- .../araknemu/game/fight/ai/AiBaseCase.java | 4 + .../araknemu/game/fight/ai/FighterAITest.java | 31 +++ .../fight/ai/action/MoveNearAllyTest.java | 181 ++++++++++++++++++ .../action/builder/GeneratorBuilderTest.java | 6 + .../fight/ai/factory/type/SupportTest.java | 33 ++++ 10 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearAlly.java create mode 100644 src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearAllyTest.java diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/AI.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/AI.java index 564197563..41be8d04e 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/AI.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/AI.java @@ -61,6 +61,21 @@ public interface AI { */ public Optional 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 ally() { + return Optional.empty(); + } + /** * Get helper for the current AI */ diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/FighterAI.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/FighterAI.java index afd18375d..eced64593 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/FighterAI.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/FighterAI.java @@ -150,6 +150,11 @@ public Optional enemy() { return helper.enemies().nearest(); } + @Override + public Optional ally() { + return Optional.ofNullable(fighter.invoker()).filter(invoker -> !invoker.hidden()); + } + @Override public AIHelper helper() { return helper; diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearAlly.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearAlly.java new file mode 100644 index 000000000..5c4335c3c --- /dev/null +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearAlly.java @@ -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 . + * + * 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 Optional generate(AI ai, AiActionFactory actions) { + return moveGenerator.generate(ai, actions); + } +} diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilder.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilder.java index 3e71bff0a..56de121e9 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilder.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilder.java @@ -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; @@ -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 * diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/Support.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/Support.java index b89713f96..fe1e20ae0 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/Support.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/Support.java @@ -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)) ); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/AiBaseCase.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/AiBaseCase.java index 047560b0c..f97c9ce47 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/AiBaseCase.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/AiBaseCase.java @@ -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()); + } } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/FighterAITest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/FighterAITest.java index 0e54106a2..3b43906f5 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/FighterAITest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/FighterAITest.java @@ -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(); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearAllyTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearAllyTest.java new file mode 100644 index 000000000..1fd61d425 --- /dev/null +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearAllyTest.java @@ -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 . + * + * 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)); + } +} diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilderTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilderTest.java index 1164f416b..26ec72496 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilderTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilderTest.java @@ -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; @@ -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); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/SupportTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/SupportTest.java index 79cdf2f4b..a23c03125 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/SupportTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/SupportTest.java @@ -19,9 +19,11 @@ package fr.quatrevieux.araknemu.game.fight.ai.factory.type; +import fr.quatrevieux.araknemu.core.di.ContainerException; import fr.quatrevieux.araknemu.game.fight.ai.AiBaseCase; import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; +import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -39,6 +41,13 @@ public void setUp() throws Exception { dataSet.pushFunctionalSpells(); } + @Override + public void tearDown() throws ContainerException { + super.tearDown(); + + action = null; + } + @Test void shouldBoostAlliesFirst() throws NoSuchFieldException, IllegalAccessException { configureFight(b -> b @@ -144,6 +153,30 @@ void shouldMoveNearAlliesIfCantAttack() throws NoSuchFieldException, IllegalAcce assertEquals(2, distance(getAlly(1))); } + @Test + void shouldMoveNearInvokerIfCantAttack() throws NoSuchFieldException, IllegalAccessException { + configureFight(b -> b + .addSelf(fb -> fb.cell(198)) + .addAlly(fb -> fb.cell(225)) + .addEnemy(fb -> fb.cell(165)) + ); + + DoubleFighter invoc = new DoubleFighter(-10, player.fighter()); + fight.fighters().joinTurnList(invoc, fight.map().get(210)); + invoc.init(); + + action = null; + configureFighterAi(invoc); + + setAP(2); + + assertEquals(5, distance(player.fighter())); + + generateAndPerformMove(); + assertEquals(197, fighter.cell().id()); + assertEquals(2, distance(player.fighter())); + } + @Test void shouldDoNothingOtherwise() throws NoSuchFieldException, IllegalAccessException { configureFight(b -> b