From 37827dcdd6eaabe5ee014dd9a8af7bdae420b10d Mon Sep 17 00:00:00 2001 From: Vincent Quatrevieux Date: Fri, 12 Apr 2024 17:54:37 +0200 Subject: [PATCH 1/4] feat(ai): Allow summon AI to perform suicidal attack and prioritize invoker enemy as target --- .../araknemu/game/fight/ai/FighterAI.java | 9 ++ .../araknemu/game/fight/ai/action/Attack.java | 5 +- .../araknemu/game/fight/ai/action/Debuff.java | 1 + .../game/fight/ai/action/MoveToAttack.java | 25 +++- .../ai/action/builder/GeneratorBuilder.java | 37 +++++ .../fight/ai/factory/type/Aggressive.java | 10 +- .../fight/ai/simulation/CastSimulation.java | 60 ++++++++ .../game/fight/ai/simulation/Simulator.java | 3 + .../game/fight/ai/util/FightersHelper.java | 12 +- .../araknemu/game/fight/ai/FighterAITest.java | 27 ++++ .../game/fight/ai/action/AttackTest.java | 27 ++++ .../game/fight/ai/action/DebuffTest.java | 25 ++++ .../action/builder/GeneratorBuilderTest.java | 16 ++ .../fight/ai/factory/type/AggressiveTest.java | 30 ++++ .../ai/simulation/CastSimulationTest.java | 140 +++++++++++++++++- .../fight/ai/simulation/SimulatorTest.java | 41 +++++ .../fight/ai/util/FightersHelperTest.java | 22 +++ 17 files changed, 480 insertions(+), 10 deletions(-) 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 864faa890..afd18375d 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 @@ -23,6 +23,7 @@ import fr.quatrevieux.araknemu.game.fight.ai.action.ActionGenerator; import fr.quatrevieux.araknemu.game.fight.ai.action.FightAiActionFactoryAdapter; import fr.quatrevieux.araknemu.game.fight.ai.util.AIHelper; +import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; import fr.quatrevieux.araknemu.game.fight.fighter.FighterData; import fr.quatrevieux.araknemu.game.fight.fighter.PlayableFighter; import fr.quatrevieux.araknemu.game.fight.map.BattlefieldMap; @@ -138,6 +139,14 @@ public Stream fighters() { @Override public Optional enemy() { + if (fighter.invoked()) { + final Fighter invoker = fighter.invoker(); + + if (invoker != null && !invoker.hidden()) { + return helper.enemies().nearestFrom(invoker.cell()); + } + } + return helper.enemies().nearest(); } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Attack.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Attack.java index c10b4864e..de9e40d9e 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Attack.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Attack.java @@ -107,11 +107,12 @@ public double score(CastSimulation simulation) { } private double damageScore(CastSimulation simulation) { - return - simulation.enemiesLife() + simulation.alliesLife() + simulation.selfLife() * 2; + return - simulation.enemiesLife() - simulation.mainEnemyLife() + simulation.alliesLife() + simulation.selfLife() * 2; } private double killScore(CastSimulation simulation) { final double killRatio = simulation.killedEnemies() + + simulation.mainEnemyKill() - 1.5 * simulation.killedAllies() - 2 * simulation.suicideProbability() ; @@ -128,7 +129,7 @@ private double boostScore(CastSimulation simulation) { * * @see CastSimulation#suicideProbability() */ - enum SuicideStrategy { + public enum SuicideStrategy { /** * Always allow suicide * Should be used on The Sacrificial Doll AI diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Debuff.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Debuff.java index 5fe58ccd5..7e069dc2a 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Debuff.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Debuff.java @@ -61,6 +61,7 @@ public boolean valid(CastSimulation simulation) { public double score(CastSimulation simulation) { final double score = - simulation.enemiesBoost() + - simulation.mainEnemyBoost() + simulation.alliesBoost() + simulation.selfBoost() ; diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveToAttack.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveToAttack.java index fe3e7f374..651b1d157 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveToAttack.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveToAttack.java @@ -40,8 +40,8 @@ public final class MoveToAttack implements ActionGenerator { private final MoveToCast generator; private final Attack attack; - private MoveToAttack(Simulator simulator, MoveToCast.TargetSelectionStrategy strategy) { - this.attack = new Attack(simulator); + private MoveToAttack(Simulator simulator, MoveToCast.TargetSelectionStrategy strategy, Attack.SuicideStrategy suicideStrategy) { + this.attack = new Attack(simulator, suicideStrategy); this.generator = new MoveToCast(simulator, attack, strategy); } @@ -63,13 +63,30 @@ public Optional generate(AI ai, AiActionFactory actions * So, it do not perform the best move for maximize damage. */ public static MoveToAttack nearest(Simulator simulator) { - return new MoveToAttack(simulator, new MoveToCast.NearestStrategy()); + return nearest(simulator, Attack.SuicideStrategy.IF_KILL_ENEMY); + } + + /** + * Select the nearest cell where a cast is possible + * + * Note: This selected cell is not the best cell for perform an attack, but the nearest cell. + * So, it do not perform the best move for maximize damage. + */ + public static MoveToAttack nearest(Simulator simulator, Attack.SuicideStrategy suicideStrategy) { + return new MoveToAttack(simulator, new MoveToCast.NearestStrategy(), suicideStrategy); } /** * Select the best target cell for cast a spell, and maximizing damage */ public static MoveToAttack bestTarget(Simulator simulator) { - return new MoveToAttack(simulator, new MoveToCast.BestTargetStrategy()); + return bestTarget(simulator, Attack.SuicideStrategy.IF_KILL_ENEMY); + } + + /** + * Select the best target cell for cast a spell, and maximizing damage + */ + public static MoveToAttack bestTarget(Simulator simulator, Attack.SuicideStrategy suicideStrategy) { + return new MoveToAttack(simulator, new MoveToCast.BestTargetStrategy(), suicideStrategy); } } 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 9cb12271f..3e71bff0a 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 @@ -159,6 +159,27 @@ public final GeneratorBuilder moveToAttack(Simulator simulator) { return add(MoveToAttack.bestTarget(simulator)); } + /** + * Try to move to the best cell for cast an attack spell + * + * To ensure that the move will be performed, add the attack action after this one. + * Otherwise, if an attack is possible from the current cell it will be performed, + * which will results to sub-optimal action. + * + * The action will not be performed if there is a tackle chance and if an attack is possible from the current cell + * + * @param simulator Simulator used by AI + * @param suicideStrategy Indicate if the fighter allow suicidal attack or not + * + * @return The builder instance + * + * @see GeneratorBuilder#attackFromBestCell(Simulator) For perform move and attack + * @see MoveToAttack#bestTarget(Simulator) The used action generator + */ + public final GeneratorBuilder moveToAttack(Simulator simulator, Attack.SuicideStrategy suicideStrategy) { + return add(MoveToAttack.bestTarget(simulator, suicideStrategy)); + } + /** * Try to attack from the nearest cell * @@ -196,6 +217,22 @@ public final GeneratorBuilder attack(Simulator simulator) { return add(new Attack(simulator)); } + /** + * Try to attack from the current cell + * + * @param simulator Simulator used by AI + * @param suicideStrategy Indicate if the fighter allow suicidal attack or not + * + * @return The builder instance + * + * @see Attack The used action generator + * @see GeneratorBuilder#attackFromBestCell(Simulator) For perform move and attack action + * @see GeneratorBuilder#attackFromNearestCell(Simulator) For perform move and attack action + */ + public final GeneratorBuilder attack(Simulator simulator, Attack.SuicideStrategy suicideStrategy) { + return add(new Attack(simulator, suicideStrategy)); + } + /** * Try to boost oneself * diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/Aggressive.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/Aggressive.java index d504269de..b647da3ee 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/Aggressive.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/Aggressive.java @@ -19,6 +19,7 @@ package fr.quatrevieux.araknemu.game.fight.ai.factory.type; +import fr.quatrevieux.araknemu.game.fight.ai.action.Attack; import fr.quatrevieux.araknemu.game.fight.ai.action.builder.GeneratorBuilder; import fr.quatrevieux.araknemu.game.fight.ai.factory.AbstractAiBuilderFactory; import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; @@ -42,6 +43,11 @@ public Aggressive(Simulator simulator) { @Override public void configure(GeneratorBuilder builder, PlayableFighter fighter) { + final Attack.SuicideStrategy suicideStrategy = fighter.invoked() + ? Attack.SuicideStrategy.ALLOW + : Attack.SuicideStrategy.IF_KILL_ENEMY + ; + builder .boostSelf(simulator) ; @@ -49,10 +55,10 @@ public void configure(GeneratorBuilder builder, PlayableFighter fighter) { // Optimisation: do not execute "move to attack" is the fighter has only close combat spell // because move near enemy will perform the correct movement action if (hasDistanceSpell(fighter)) { - builder.moveToAttack(simulator); + builder.moveToAttack(simulator, suicideStrategy); } - builder.attack(simulator); + builder.attack(simulator, suicideStrategy); builder .attractEnemy() diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulation.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulation.java index 266542dc4..eff22467b 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulation.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulation.java @@ -24,6 +24,7 @@ import fr.quatrevieux.araknemu.game.fight.map.BattlefieldCell; import fr.quatrevieux.araknemu.game.spell.Spell; import org.checkerframework.checker.index.qual.Positive; +import org.checkerframework.checker.nullness.qual.Nullable; /** * The simulation result of a cast @@ -37,18 +38,22 @@ public final class CastSimulation { private final Spell spell; private final FighterData caster; private final BattlefieldCell target; + private @Nullable FighterData mainEnemy = null; private double enemiesLife; private double alliesLife; private double selfLife; + private double mainEnemyLife; private double enemiesBoost; private double alliesBoost; private double selfBoost; + private double mainEnemyBoost; private double invocation; private double killedAllies; private double killedEnemies; + private double mainEnemyKill; private double suicide; private double actionPointsModifier = 0; @@ -206,6 +211,12 @@ public void apply(EffectValueComputer values, FighterData target) { killedEnemies += values.killProbability(); enemiesBoost += values.boost(); } + + if (target.equals(mainEnemy)) { + mainEnemyLife += values.lifeChange(); + mainEnemyKill += values.killProbability(); + mainEnemyBoost += values.boost(); + } } /** @@ -239,6 +250,42 @@ public double invocation() { return invocation; } + /** + * Get the boost value applied to the main enemy + * If no main enemy is defined, this value will be 0 + * + * @return Negative value for malus, and positive for bonus + * + * @see #setMainEnemy(FighterData) to define the main enemy + */ + public double mainEnemyBoost() { + return mainEnemyBoost; + } + + /** + * The probability to kill the main enemy + * If no main enemy is defined, this value will be 0 + * + * @return The value is between 0 (no chance to kill) and 1 (sure to kill) + * + * @see #setMainEnemy(FighterData) to define the main enemy + */ + public double mainEnemyKill() { + return mainEnemyKill; + } + + /** + * The life change of the main enemy + * If no main enemy is defined, this value will be 0 + * + * @return Negative value for damage, and positive for heal + * + * @see #setMainEnemy(FighterData) to define the main enemy + */ + public double mainEnemyLife() { + return mainEnemyLife; + } + /** * Add a boost to the target * @@ -320,6 +367,10 @@ public void merge(CastSimulation simulation, double percent) { actionPointsModifier += simulation.actionPointsModifier * percent / 100d; invocation += simulation.invocation * percent / 100d; + + mainEnemyLife += simulation.mainEnemyLife * percent / 100d; + mainEnemyBoost += simulation.mainEnemyBoost * percent / 100d; + mainEnemyKill += simulation.mainEnemyKill * percent / 100d; } /** @@ -402,6 +453,15 @@ private double computeCappedEffect(Interval value, double maxValue) { return computeCappedEffect(value, maxValue, cappedProbability(value, maxValue)); } + /** + * Define the main enemy of the current fighter + * + * When set, a score will be computed with this particular enemy, allowing to prioritize cast that targets this enemy + */ + public void setMainEnemy(FighterData mainEnemy) { + this.mainEnemy = mainEnemy; + } + /** * Structure for compute applied effects values */ diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/Simulator.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/Simulator.java index a0b221456..b8bba0863 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/Simulator.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/Simulator.java @@ -130,6 +130,8 @@ private CastSimulation simulate(Spell spell, AI ai, CastScope effect : scope.effects()) { final EffectSimulator simulator = simulators.get(effect.effect().effect()); @@ -139,6 +141,7 @@ private CastSimulation simulate(Spell spell, AI ai, CastScope 0) { final CastSimulation probableSimulation = new CastSimulation(spell, scope.caster(), scope.target()); + ai.enemy().ifPresent(probableSimulation::setMainEnemy); simulator.simulate(probableSimulation, ai, effect); simulation.merge(probableSimulation, effect.effect().probability()); diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/util/FightersHelper.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/util/FightersHelper.java index 27d94a17a..b4d7fe219 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/util/FightersHelper.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/util/FightersHelper.java @@ -96,7 +96,17 @@ public Stream adjacent() { * @return The nearest fighter */ public Optional nearest() { - final CoordinateCell currentCell = ai.fighter().cell().coordinate(); + return nearestFrom(ai.fighter().cell()); + } + + /** + * Get the nearest fighter from the given cell + * If multiple fighters have the same distance, the fighter with lower HP will be returned + * + * @return The nearest fighter + */ + public Optional nearestFrom(BattlefieldCell cell) { + final CoordinateCell currentCell = cell.coordinate(); return stream() .filter(fighter -> !fighter.hidden()) 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 ef2281081..0e54106a2 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 @@ -28,6 +28,7 @@ import fr.quatrevieux.araknemu.game.fight.ai.util.AIHelper; import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; import fr.quatrevieux.araknemu.game.fight.fighter.PlayableFighter; +import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter; import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighter; import fr.quatrevieux.araknemu.game.fight.state.PlacementState; import fr.quatrevieux.araknemu.game.fight.turn.FightTurn; @@ -65,6 +66,8 @@ public void setUp() throws Exception { otherEnemy = new PlayerFighter(makeSimpleGamePlayer(10)); fight.state(PlacementState.class).joinTeam(otherEnemy, enemy.team()); + otherEnemy.move(fight.map().get(126)); + fight.nextState(); } @@ -93,6 +96,30 @@ void enemyShouldFilterDeadFighters() { assertEquals(otherEnemy, ai.enemy().get()); } + @Test + void enemyWhenInvokedShouldReturnNearestOfInvoker() { + 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(enemy, ai.enemy().get()); + } + + @Test + void enemyWhenInvokedButInvokerHiddenShouldReturnNearest() { + DoubleFighter invoc = new DoubleFighter(-10, player.fighter()); + fight.fighters().joinTurnList(invoc, fight.map().get(112)); // Adjacent to enemy 126 + invoc.init(); + + player.fighter().setHidden(player.fighter(), true); + + FighterAI ai = new FighterAI(invoc, fight, NullGenerator.get()); + + assertEquals(otherEnemy, ai.enemy().get()); + } + @RepeatedIfExceptionsTest void startEmptyShouldStop() throws InterruptedException { fight.turnList().start(); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/AttackTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/AttackTest.java index 222b521aa..5e28dbf2e 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/AttackTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/AttackTest.java @@ -24,6 +24,7 @@ import fr.quatrevieux.araknemu.game.fight.ai.AiBaseCase; import fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation; import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; +import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter; import fr.quatrevieux.araknemu.game.fight.turn.action.cast.Cast; import fr.quatrevieux.araknemu.game.spell.Spell; import org.junit.jupiter.api.BeforeEach; @@ -367,13 +368,35 @@ void score() throws SQLException { configureFight(fb -> fb .addSelf(builder -> builder.cell(122)) .addEnemy(builder -> builder.player(other).cell(125)) + .addEnemy(builder -> builder.cell(150)) // Will be the main enemy ); assertEquals(5.05, computeScore(183, 125), 0.001); // 20 * 98% + 30 * 2% / 4 AP + assertEquals(10.1, computeScore(183, 150), 0.001); // *2 (main enemy) assertEquals(-10.1, computeScore(183, 122), 0.001); // ^ * -2 (self damage) assertEquals(0, computeScore(183, 110), 0.001); // no target } + @Test + void scoreWithInvocShouldPrioritizeEnemyNearInvoker() throws SQLException { + dataSet.pushFunctionalSpells(); + + configureFight(fb -> fb + .addSelf(builder -> builder.cell(122)) + .addEnemy(builder -> builder.player(other).cell(125)) + .addEnemy(builder -> builder.cell(150)) + ); + + DoubleFighter invoc = new DoubleFighter(-10, player.fighter()); + fight.fighters().joinTurnList(invoc, fight.map().get(110)); // Adjacent to enemy 125 + invoc.init(); + + configureFighterAi(invoc); + + assertEquals(5.05, computeScore(183, 125), 0.001); // 20 * 98% + 30 * 2% / 4 AP + assertEquals(10.1, computeScore(183, 150), 0.001); // *2 (main enemy) + } + @Test void scoreShouldLimitBoostAndDebuff() throws SQLException { dataSet.pushFunctionalSpells(); @@ -384,9 +407,13 @@ void scoreShouldLimitBoostAndDebuff() throws SQLException { .charac(Characteristic.STRENGTH, 0) ) .addEnemy(builder -> builder.player(other).cell(125)) + .addEnemy(builder -> builder.cell(150)) // Will be the main enemy ); assertEquals(0.816, computeScore(3, 1, 125), 0.001); // 4 * 98% + 8 * 2% / 5 AP assertEquals(0.68, computeScore(2, 1, 125), 0.001); // (1 + 1) * 98% + (2 + 2) * 2% / 3 AP + + assertEquals(2*0.816, computeScore(3, 1, 150), 0.001); // 4 * 98% + 8 * 2% / 5 AP + assertEquals(2*0.68, computeScore(2, 1, 150), 0.001); // (1 + 1) * 98% + (2 + 2) * 2% / 3 AP } } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/DebuffTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/DebuffTest.java index 080dcfd54..04446ac9a 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/DebuffTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/DebuffTest.java @@ -22,6 +22,7 @@ import fr.quatrevieux.araknemu.game.fight.ai.AiBaseCase; import fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation; import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; +import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter; import fr.quatrevieux.araknemu.game.spell.Spell; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -166,10 +167,34 @@ void score() throws SQLException { configureFight(fb -> fb .addSelf(builder -> builder.cell(122)) .addEnemy(builder -> builder.player(other).cell(125)) + .addEnemy(builder -> builder.cell(150)) // Will be the main enemy ); + // Not main enemy assertEquals(222.222, computeScore(81, 125), 0.001); // 200 / 0.9 AP (1 bonus AP 10%) assertEquals(-222.222, computeScore(81, 122), 0.001); // Self target assertEquals(30, computeScore(168, 125), 0.001); // 90 / 3 AP + + // Main enemy + assertEquals(444.444, computeScore(81, 150), 0.001); // 200 / 0.9 AP (1 bonus AP 10%) + assertEquals(60, computeScore(168, 180), 0.001); // 90 / 3 AP + } + + @Test + void scoreWithInvocShouldPrioritizeEnemyNearInvoker() throws SQLException { + configureFight(fb -> fb + .addSelf(builder -> builder.cell(122)) + .addEnemy(builder -> builder.player(other).cell(125)) + .addEnemy(builder -> builder.cell(150)) + ); + + DoubleFighter invoc = new DoubleFighter(-10, player.fighter()); + fight.fighters().joinTurnList(invoc, fight.map().get(110)); // Adjacent to enemy 125 + invoc.init(); + + configureFighterAi(invoc); + + assertEquals(222.222, computeScore(81, 125), 0.001); // 200 / 0.9 AP (1 bonus AP 10%) + assertEquals(444.444, computeScore(81, 150), 0.001); // 200 / 0.9 AP (1 bonus AP 10%) } } 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 b3b08c015..1164f416b 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 @@ -120,11 +120,27 @@ void moveToAttack() { assertInstanceOf(MoveToAttack.class, builder.moveToAttack(simulator).build()); } + @Test + void moveToAttackWithSuicideStrategy() { + assertInstanceOf(MoveToAttack.class, builder.moveToAttack(simulator, Attack.SuicideStrategy.ALLOW).build()); + } + @Test void attack() { assertInstanceOf(Attack.class, builder.attack(simulator).build()); } + @Test + void attackWithSuicideStrategy() throws NoSuchFieldException, IllegalAccessException { + ActionGenerator generator = builder.attack(simulator, Attack.SuicideStrategy.ALLOW).build(); + assertInstanceOf(Attack.class, generator); + + Field strategy = Attack.class.getDeclaredField("suicideStrategy"); + strategy.setAccessible(true); + + assertSame(Attack.SuicideStrategy.ALLOW, strategy.get(generator)); + } + @Test void boostSelf() { assertInstanceOf(Boost.class, builder.boostSelf(simulator).build()); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/AggressiveTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/AggressiveTest.java index 74ce76fcb..d57b770ab 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/AggressiveTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/AggressiveTest.java @@ -22,6 +22,9 @@ 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 fr.quatrevieux.araknemu.game.fight.fighter.invocation.InvocationFighter; +import fr.quatrevieux.araknemu.game.monster.MonsterService; import fr.quatrevieux.araknemu.game.player.spell.SpellBook; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -42,6 +45,33 @@ public void setUp() throws Exception { dataSet.pushFunctionalSpells(); } + @Test + void allowSuicideWhenInvoked() throws SQLException { + dataSet + .pushMonsterTemplateInvocations() + .pushMonsterSpellsInvocations() + ; + + configureFight(b -> b + .addSelf(fb -> fb.cell(342)) + .addEnemy(fb -> fb.cell(327)) + ); + + action = null; + + InvocationFighter invoc = new InvocationFighter( + -10, + container.get(MonsterService.class).load(116).get(1), // Sacrifiée + fighter.team(), + fighter + ); + fight.fighters().joinTurnList(invoc, fight.map().get(341)); // Near 327 + + configureFighterAi(invoc); + + assertCast(2006, 327); + } + @Test void shouldBoostFirst() { configureFight(b -> b diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulationTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulationTest.java index 213bb03cd..725d5f1f3 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulationTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulationTest.java @@ -34,6 +34,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; +import java.sql.SQLException; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -76,6 +77,32 @@ void heal() { assertEquals(5, simulation.selfLife()); assertEquals(4, simulation.alliesLife()); assertEquals(3, simulation.enemiesLife()); + assertEquals(0, simulation.mainEnemyLife()); + } + + @Test + void healWithMainEnemy() throws SQLException { + PlayerFighter mainEnemy = new PlayerFighter(makeSimpleGamePlayer(11)); + mainEnemy.setTeam(ennemy.team()); + mainEnemy.joinFight(fight, fight.map().get(127)); + mainEnemy.init(); + + simulation.setMainEnemy(mainEnemy); + + fighter.life().damage(fighter, 10); + allie.life().damage(fighter, 10); + ennemy.life().damage(fighter, 10); + mainEnemy.life().damage(fighter, 10); + + simulation.addHeal(new Interval(5, 5), fighter); + simulation.addHeal(new Interval(4, 4), allie); + simulation.addHeal(new Interval(3, 3), ennemy); + simulation.addHeal(new Interval(6, 6), mainEnemy); + + assertEquals(5, simulation.selfLife()); + assertEquals(4, simulation.alliesLife()); + assertEquals(9, simulation.enemiesLife()); + assertEquals(6, simulation.mainEnemyLife()); } @ParameterizedTest @@ -107,6 +134,27 @@ void addDamage() { assertEquals(-5, simulation.selfLife()); assertEquals(-4, simulation.alliesLife()); assertEquals(-3, simulation.enemiesLife()); + assertEquals(0, simulation.mainEnemyLife()); + } + + @Test + void addDamageWithMainEnemy() throws SQLException { + PlayerFighter mainEnemy = new PlayerFighter(makeSimpleGamePlayer(11)); + mainEnemy.setTeam(ennemy.team()); + mainEnemy.joinFight(fight, fight.map().get(127)); + mainEnemy.init(); + + simulation.setMainEnemy(mainEnemy); + + simulation.addDamage(new Interval(5, 5), fighter); + simulation.addDamage(new Interval(4, 4), allie); + simulation.addDamage(new Interval(3, 3), ennemy); + simulation.addDamage(new Interval(6, 6), mainEnemy); + + assertEquals(-5, simulation.selfLife()); + assertEquals(-4, simulation.alliesLife()); + assertEquals(-9, simulation.enemiesLife()); + assertEquals(-6, simulation.mainEnemyLife()); } @Test @@ -146,27 +194,65 @@ void addBoost() { assertEquals(5, simulation.selfBoost()); assertEquals(4, simulation.alliesBoost()); assertEquals(3, simulation.enemiesBoost()); + assertEquals(0, simulation.mainEnemyBoost()); + } + + @Test + void addBoostWithMainEnemy() throws SQLException { + PlayerFighter mainEnemy = new PlayerFighter(makeSimpleGamePlayer(11)); + mainEnemy.setTeam(ennemy.team()); + mainEnemy.joinFight(fight, fight.map().get(127)); + mainEnemy.init(); + + simulation.setMainEnemy(mainEnemy); + + simulation.addBoost(5, fighter); + simulation.addBoost(4, allie); + simulation.addBoost(3, ennemy); + simulation.addBoost(6, mainEnemy); + + assertEquals(5, simulation.selfBoost()); + assertEquals(4, simulation.alliesBoost()); + assertEquals(9, simulation.enemiesBoost()); + assertEquals(6, simulation.mainEnemyBoost()); } @Test - void killDamage() { + void killDamage() throws SQLException { + PlayerFighter mainEnemy = new PlayerFighter(makeSimpleGamePlayer(11)); + mainEnemy.setTeam(ennemy.team()); + mainEnemy.joinFight(fight, fight.map().get(127)); + mainEnemy.init(); + + simulation.setMainEnemy(mainEnemy); + simulation.addDamage(new Interval(1000, 1000), allie); assertEquals(1, simulation.killedAllies()); assertEquals(0, simulation.killedEnemies()); assertEquals(0, simulation.suicideProbability()); + assertEquals(0, simulation.mainEnemyKill()); simulation.addDamage(new Interval(1000, 1000), ennemy); assertEquals(1, simulation.killedAllies()); assertEquals(1, simulation.killedEnemies()); assertEquals(0, simulation.suicideProbability()); + assertEquals(0, simulation.mainEnemyKill()); simulation.addDamage(new Interval(1000, 1000), fighter); assertEquals(1, simulation.killedAllies()); assertEquals(1, simulation.killedEnemies()); assertEquals(1, simulation.suicideProbability()); + assertEquals(0, simulation.mainEnemyKill()); + + simulation.addDamage(new Interval(1000, 1000), mainEnemy); + + assertEquals(1, simulation.killedAllies()); + assertEquals(2, simulation.killedEnemies()); + assertEquals(1, simulation.suicideProbability()); + assertEquals(1, simulation.mainEnemyKill()); } @ParameterizedTest @@ -229,6 +315,36 @@ void merge() { assertEquals(13, simulation.enemiesBoost()); } + @Test + void mergeWithMainEnemy() throws SQLException { + PlayerFighter mainEnemy = new PlayerFighter(makeSimpleGamePlayer(11)); + mainEnemy.setTeam(ennemy.team()); + mainEnemy.joinFight(fight, fight.map().get(127)); + mainEnemy.init(); + + simulation.setMainEnemy(mainEnemy); + + simulation.addDamage(new Interval(15, 15), ennemy); + simulation.addBoost(15, ennemy); + simulation.addDamage(new Interval(10, 10), mainEnemy); + simulation.addBoost(-5, mainEnemy); + + CastSimulation other = new CastSimulation(Mockito.mock(Spell.class), fighter, fight.map().get(123)); + other.setMainEnemy(mainEnemy); + + other.addDamage(new Interval(25, 25), ennemy); + other.addBoost(-10, ennemy); + other.addDamage(new Interval(15, 15), mainEnemy); + other.addBoost(-2, mainEnemy); + + simulation.merge(other, 20); + + assertEquals(-33, simulation.enemiesLife()); + assertEquals(-13, simulation.mainEnemyLife()); + assertEquals(7.6, simulation.enemiesBoost()); + assertEquals(-5.4, simulation.mainEnemyBoost()); + } + @Test void mergeKill() { simulation.addDamage(new Interval(15, 15), ennemy); @@ -242,6 +358,28 @@ void mergeKill() { assertEquals(.2, simulation.killedEnemies()); } + @Test + void mergeKillMainEnemy() throws SQLException { + PlayerFighter mainEnemy = new PlayerFighter(makeSimpleGamePlayer(11)); + mainEnemy.setTeam(ennemy.team()); + mainEnemy.joinFight(fight, fight.map().get(127)); + mainEnemy.init(); + + simulation.setMainEnemy(mainEnemy); + + simulation.addDamage(new Interval(15, 15), mainEnemy); + + CastSimulation other = new CastSimulation(Mockito.mock(Spell.class), fighter, fight.map().get(123)); + other.setMainEnemy(mainEnemy); + + other.addDamage(new Interval(500, 500), mainEnemy); + + simulation.merge(other, 20); + + assertEquals(.2, simulation.killedEnemies()); + assertEquals(.2, simulation.mainEnemyKill()); + } + @Test void mergeSuicide() { simulation.addDamage(new Interval(0, 500), fighter); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/SimulatorTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/SimulatorTest.java index 81f8c1c7b..d5cf3fc43 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/SimulatorTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/SimulatorTest.java @@ -25,6 +25,7 @@ import fr.quatrevieux.araknemu.game.fight.ai.FighterAI; import fr.quatrevieux.araknemu.game.fight.ai.action.logic.NullGenerator; import fr.quatrevieux.araknemu.game.fight.ai.factory.ChainAiFactory; +import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter; import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighter; import fr.quatrevieux.araknemu.game.fight.module.AiModule; import fr.quatrevieux.araknemu.game.fight.turn.action.util.BaseCriticalityStrategy; @@ -81,10 +82,29 @@ void simulateAttack() { assertEquals(0, simulation.alliesLife()); assertEquals(-10.2, simulation.enemiesLife(), 0.1); + assertEquals(-10.2, simulation.mainEnemyLife(), 0.1); assertEquals(0, simulation.selfLife()); assertEquals(0, simulation.alliesBoost()); assertEquals(0, simulation.enemiesBoost()); assertEquals(0, simulation.selfBoost()); + assertEquals(0, simulation.mainEnemyBoost()); + } + + @Test + void simulateAttackNotMainEnemy() { + DoubleFighter invoc = new DoubleFighter(-10, other.fighter()); + fight.fighters().join(invoc, fight.map().get(142)); + + CastSimulation simulation = simulator.simulate(fighter.spells().get(3), ai, fighter, invoc.cell()); + + assertEquals(0, simulation.alliesLife()); + assertEquals(-10.2, simulation.enemiesLife(), 0.1); + assertEquals(0, simulation.mainEnemyLife(), 0.1); + assertEquals(0, simulation.selfLife()); + assertEquals(0, simulation.alliesBoost()); + assertEquals(0, simulation.enemiesBoost()); + assertEquals(0, simulation.selfBoost()); + assertEquals(0, simulation.mainEnemyBoost()); } @Test @@ -119,6 +139,8 @@ void simulateBoost() { assertEquals(0, simulation.alliesBoost()); assertEquals(0, simulation.enemiesBoost()); assertEquals(79, simulation.selfBoost(), .1); + assertEquals(0, simulation.mainEnemyBoost()); + assertEquals(0, simulation.mainEnemyLife()); } @Test @@ -127,6 +149,23 @@ void simulateWithProbableEffects() { assertEquals(0, simulation.alliesLife()); assertEquals(-23.5, simulation.enemiesLife(), 0.1); + assertEquals(-23.5, simulation.mainEnemyLife(), 0.1); + assertEquals(0, simulation.selfLife()); + assertEquals(0, simulation.alliesBoost()); + assertEquals(0, simulation.enemiesBoost()); + assertEquals(0, simulation.selfBoost()); + } + + @Test + void simulateWithProbableEffectsNotMainEnemy() { + DoubleFighter invoc = new DoubleFighter(-10, other.fighter()); + fight.fighters().join(invoc, fight.map().get(142)); + + CastSimulation simulation = simulator.simulate(getSpell(109, 5), ai, fighter, invoc.cell()); + + assertEquals(0, simulation.alliesLife()); + assertEquals(-23.5, simulation.enemiesLife(), 0.1); + assertEquals(0, simulation.mainEnemyLife(), 0.1); assertEquals(0, simulation.selfLife()); assertEquals(0, simulation.alliesBoost()); assertEquals(0, simulation.enemiesBoost()); @@ -136,9 +175,11 @@ void simulateWithProbableEffects() { @Test void simulateShouldHandleCriticality() { assertEquals(-23.5, simulator.simulate(getSpell(109, 5), ai, fighter, other.fighter().cell()).enemiesLife(), 0.1); + assertEquals(-23.5, simulator.simulate(getSpell(109, 5), ai, fighter, other.fighter().cell()).mainEnemyLife(), 0.1); fighter.characteristics().alter(Characteristic.CRITICAL_BONUS, 100); assertEquals(-36.5, simulator.simulate(getSpell(109, 5), ai, fighter, other.fighter().cell()).enemiesLife(), 0.1); + assertEquals(-36.5, simulator.simulate(getSpell(109, 5), ai, fighter, other.fighter().cell()).mainEnemyLife(), 0.1); } @Test diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/util/FightersHelperTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/util/FightersHelperTest.java index b153d8fa9..84c834b67 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/util/FightersHelperTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/util/FightersHelperTest.java @@ -152,6 +152,28 @@ void nearest() { assertEquals(getEnemy(1), helper().nearest().get()); } + @Test + void nearestFrom() { + configureFight(fb -> fb + .addSelf(b -> b.cell(123)) + .addEnemy(b -> b.cell(125)) + .addEnemy(b -> b.cell(135)) + ); + + assertEquals(getEnemy(0), helper().nearestFrom(fight.map().get(123)).get()); + assertEquals(getEnemy(1), helper().nearestFrom(fight.map().get(121)).get()); + + configureFight(fb -> fb + .addSelf(b -> b.cell(210)) + .addEnemy(b -> b.cell(195).currentLife(50)) + .addEnemy(b -> b.cell(196).currentLife(15)) + .addEnemy(b -> b.cell(123)) + ); + + assertEquals(getEnemy(1), helper().nearestFrom(fight.map().get(210)).get()); + assertEquals(getEnemy(2), helper().nearestFrom(fight.map().get(109)).get()); + } + @Test void nearestWithHiddenFighterShouldBeIgnored() { configureFight(fb -> fb From 2b7351075576a929605561db3ef95be33ee019d2 Mon Sep 17 00:00:00 2001 From: Vincent Quatrevieux Date: Sun, 14 Apr 2024 13:32:49 +0200 Subject: [PATCH 2/4] 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 From 97e40b2f4dc7ce65cca7326dd6c22227505f7f0e Mon Sep 17 00:00:00 2001 From: Vincent Quatrevieux Date: Mon, 15 Apr 2024 18:25:23 +0200 Subject: [PATCH 3/4] feat(ai): Take in account main ally to compute heal and boost score --- checkstyle.xml | 6 + .../araknemu/game/fight/ai/action/Attack.java | 5 + .../araknemu/game/fight/ai/action/Boost.java | 1 + .../araknemu/game/fight/ai/action/Heal.java | 2 +- .../araknemu/game/fight/ai/proxy/ProxyAI.java | 5 + .../fight/ai/simulation/CastSimulation.java | 77 ++++++++-- .../game/fight/ai/simulation/Simulator.java | 2 + .../araknemu/game/GameDataSet.java | 3 +- .../game/fight/ai/action/AttackTest.java | 28 ++++ .../game/fight/ai/action/BoostTest.java | 58 ++++++++ .../game/fight/ai/action/DummyGenerator.java | 17 ++- .../game/fight/ai/action/HealTest.java | 53 +++++++ .../game/fight/ai/proxy/ProxyAITest.java | 1 + .../ai/simulation/CastSimulationTest.java | 136 +++++++++++++++++- .../fight/ai/simulation/SimulatorTest.java | 43 ++++++ 15 files changed, 421 insertions(+), 16 deletions(-) diff --git a/checkstyle.xml b/checkstyle.xml index 1681453e5..4128135c6 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -287,4 +287,10 @@ + + + + + + diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Attack.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Attack.java index de9e40d9e..30f224967 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Attack.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Attack.java @@ -82,6 +82,11 @@ public boolean valid(CastSimulation simulation) { return false; } + // Disallow killing the main ally + if (simulation.mainAllyKill() > 0.1) { + return false; + } + // Kill more allies than enemies if (simulation.killedAllies() > simulation.killedEnemies()) { return false; diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Boost.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Boost.java index 0d7568f5a..2e84af718 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Boost.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Boost.java @@ -85,6 +85,7 @@ public boolean valid(CastSimulation simulation) { public double score(CastSimulation simulation) { double score = + simulation.alliesBoost() * alliesBoostRate + + simulation.mainAllyBoost() * alliesBoostRate + simulation.selfBoost() * selfBoostRate - simulation.enemiesBoost() ; diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Heal.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Heal.java index 79a80a3c2..ebf21ca8a 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Heal.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Heal.java @@ -73,7 +73,7 @@ public double score(CastSimulation simulation) { } private double healScore(CastSimulation simulation) { - return simulation.alliesLife() + simulation.selfLife() - simulation.enemiesLife(); + return simulation.alliesLife() + simulation.mainAllyLife() + simulation.selfLife() - simulation.enemiesLife(); } private double boostScore(CastSimulation simulation) { diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyAI.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyAI.java index 238d1280c..0b75de570 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyAI.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyAI.java @@ -85,6 +85,11 @@ public Optional enemy() { return ai.enemy().map(this::getProxyFighter); } + @Override + public Optional ally() { + return ai.ally().map(this::getProxyFighter); + } + /** * Change the current cell of the handled fighter * diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulation.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulation.java index eff22467b..a79e52803 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulation.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulation.java @@ -39,21 +39,25 @@ public final class CastSimulation { private final FighterData caster; private final BattlefieldCell target; private @Nullable FighterData mainEnemy = null; + private @Nullable FighterData mainAlly = null; private double enemiesLife; private double alliesLife; private double selfLife; private double mainEnemyLife; + private double mainAllyLife; private double enemiesBoost; private double alliesBoost; private double selfBoost; private double mainEnemyBoost; + private double mainAllyBoost; private double invocation; private double killedAllies; private double killedEnemies; private double mainEnemyKill; + private double mainAllyKill; private double suicide; private double actionPointsModifier = 0; @@ -64,6 +68,24 @@ public CastSimulation(Spell spell, FighterData caster, BattlefieldCell target) { this.target = target; } + /** + * Define the main enemy of the current fighter + * + * When set, a score will be computed with this particular enemy, allowing to prioritize cast that targets this enemy + */ + public void setMainEnemy(FighterData mainEnemy) { + this.mainEnemy = mainEnemy; + } + + /** + * Define the main ally of the current fighter + * + * When set, a score will be computed with this particular ally, allowing to prioritize boost or heal that targets this ally + */ + public void setMainAlly(FighterData mainAlly) { + this.mainAlly = mainAlly; + } + /** * The enemies life diff (negative value for damage, positive for heal) */ @@ -217,6 +239,12 @@ public void apply(EffectValueComputer values, FighterData target) { mainEnemyKill += values.killProbability(); mainEnemyBoost += values.boost(); } + + if (target.equals(mainAlly)) { + mainAllyLife += values.lifeChange(); + mainAllyKill += values.killProbability(); + mainAllyBoost += values.boost(); + } } /** @@ -286,6 +314,42 @@ public double mainEnemyLife() { return mainEnemyLife; } + /** + * Get the boost value applied to the main enemy + * If no main enemy is defined, this value will be 0 + * + * @return Negative value for malus, and positive for bonus + * + * @see #setMainAlly(FighterData) to define the main enemy + */ + public double mainAllyBoost() { + return mainAllyBoost; + } + + /** + * The probability to kill the main enemy + * If no main enemy is defined, this value will be 0 + * + * @return The value is between 0 (no chance to kill) and 1 (sure to kill) + * + * @see #setMainAlly(FighterData) to define the main enemy + */ + public double mainAllyKill() { + return mainAllyKill; + } + + /** + * The life change of the main enemy + * If no main enemy is defined, this value will be 0 + * + * @return Negative value for damage, and positive for heal + * + * @see #setMainAlly(FighterData) to define the main enemy + */ + public double mainAllyLife() { + return mainAllyLife; + } + /** * Add a boost to the target * @@ -371,6 +435,10 @@ public void merge(CastSimulation simulation, double percent) { mainEnemyLife += simulation.mainEnemyLife * percent / 100d; mainEnemyBoost += simulation.mainEnemyBoost * percent / 100d; mainEnemyKill += simulation.mainEnemyKill * percent / 100d; + + mainAllyLife += simulation.mainAllyLife * percent / 100d; + mainAllyBoost += simulation.mainAllyBoost * percent / 100d; + mainAllyKill += simulation.mainAllyKill * percent / 100d; } /** @@ -453,15 +521,6 @@ private double computeCappedEffect(Interval value, double maxValue) { return computeCappedEffect(value, maxValue, cappedProbability(value, maxValue)); } - /** - * Define the main enemy of the current fighter - * - * When set, a score will be computed with this particular enemy, allowing to prioritize cast that targets this enemy - */ - public void setMainEnemy(FighterData mainEnemy) { - this.mainEnemy = mainEnemy; - } - /** * Structure for compute applied effects values */ diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/Simulator.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/Simulator.java index b8bba0863..9908210f2 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/Simulator.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/Simulator.java @@ -131,6 +131,7 @@ private CastSimulation simulate(Spell spell, AI ai, CastScope effect : scope.effects()) { final EffectSimulator simulator = simulators.get(effect.effect().effect()); @@ -142,6 +143,7 @@ private CastSimulation simulate(Spell spell, AI ai, CastScope 0) { final CastSimulation probableSimulation = new CastSimulation(spell, scope.caster(), scope.target()); ai.enemy().ifPresent(probableSimulation::setMainEnemy); + ai.ally().ifPresent(probableSimulation::setMainAlly); simulator.simulate(probableSimulation, ai, effect); simulation.merge(probableSimulation, effect.effect().probability()); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java b/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java index feda1a932..9a0b0b675 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java @@ -979,7 +979,8 @@ public GameDataSet pushMonsterTemplateInvocations() throws SQLException, Contain "(158, 'Coquille Explosive', 1096, '-1,-1,-1', 'AGGRESSIVE', '1@a:2g;f:2g;d:2g;8:2;9:-1;|1@a:2l;f:2l;d:2l;8:2;9:-1;|1@a:2q;f:2q;d:2q;8:2;9:-1;|1@a:2v;f:2v;d:2v;8:2;9:-1;|1@a:34;f:34;d:34;8:2;9:-1;', '1000|1000|1000|1000|1000', '1|1|1|1|1', '261@1;265@1|261@2;265@2|261@3;265@3|261@4;265@4|261@5;265@5')," + "(43, 'Tofu', 1558, '-1,-1,-1', 'RUNAWAY', '1@13:6;1f:-c;17:6;1b:-c;s:b;t:b;a:2g;c:1d;f:2g;d:2g;e:2g;8:5;9:a;|2@13:7;1f:-a;17:7;1b:-a;s:c;t:c;a:2l;c:1i;f:2l;d:2l;e:2l;8:5;9:a;|3@13:8;1f:-9;17:8;1b:-9;s:d;t:d;a:2q;c:1n;f:2q;d:2q;e:2q;8:5;9:a;|4@13:9;1f:-8;17:9;1b:-8;s:f;t:f;a:2v;c:1s;f:2v;d:2v;e:2v;8:5;9:a;|5@13:a;1f:-7;17:a;1b:-7;s:g;t:g;a:34;c:21;f:34;d:34;e:34;8:5;9:a;|6@13:c;1f:-6;17:c;1b:-6;s:h;t:h;a:3o;c:26;f:3e;d:3o;e:3o;8:5;9:c;', '8|10|12|14|16|18', '1|1|1|1|1|1', '201@1|201@2|201@3|201@4|201@5|201@6')," + "(44, 'Bwork Mage', 1012, '-1,-1,-1', 'RUNAWAY', '1@v:-n;13:-1i;1f:5;17:5;1b:p;s:m;t:m;a:2g;c:2q;f:2g;d:2g;e:p;8:7;9:4;|2@v:-k;13:-1d;1f:9;17:9;1b:u;s:n;t:n;a:2l;c:2v;f:2l;d:2l;e:u;8:7;9:4;|3@v:-h;13:-18;1f:d;17:d;1b:13;s:p;t:p;a:2q;c:34;f:2q;d:2q;e:13;8:7;9:4;|4@v:-d;13:-13;1f:h;17:h;1b:18;s:q;t:q;a:2v;c:39;f:2v;d:2v;e:13;8:7;9:5;|5@v:-9;13:-u;1f:k;17:k;1b:1d;s:r;t:r;a:34;c:3e;f:34;d:34;e:18;8:7;9:5;|6@v:-5;13:-p;1f:n;17:n;1b:1i;s:u;t:u;a:3o;c:3o;f:3e;d:3o;e:1i;8:7;9:5;', '60|70|80|90|100|125', '1|1|1|1|1|1', '2004@1;2003@1|2004@2;2003@2|2004@3;2003@3|2004@4;2003@4|2004@5;2003@5|2004@6;2003@6')," + - "(117, 'La Gonflable', 1182, '-1,-1,-1', 'SUPPORT', '1@s:34;t:34;a:2g;f:2g;d:2g;8:6;9:3;|2@s:34;t:34;a:2l;f:2l;d:2l;8:6;9:3;|3@s:34;t:34;a:2q;f:2q;d:2q;8:6;9:3;|4@s:34;t:34;a:2v;f:2v;d:2v;8:6;9:3;|5@s:34;t:34;a:34;f:34;d:34;8:6;9:3;|6@s:34;t:34;a:3o;f:3e;d:3o;8:6;9:3;', '40|50|60|70|80|80', '1|1|1|1|1|1', '284@1;587@1|284@2;587@2|284@3;587@3|284@4;587@4|284@5;587@5|284@6;587@6')" + "(117, 'La Gonflable', 1182, '-1,-1,-1', 'SUPPORT', '1@s:34;t:34;a:2g;f:2g;d:2g;8:6;9:3;|2@s:34;t:34;a:2l;f:2l;d:2l;8:6;9:3;|3@s:34;t:34;a:2q;f:2q;d:2q;8:6;9:3;|4@s:34;t:34;a:2v;f:2v;d:2v;8:6;9:3;|5@s:34;t:34;a:34;f:34;d:34;8:6;9:3;|6@s:34;t:34;a:3o;f:3e;d:3o;8:6;9:3;', '40|50|60|70|80|80', '1|1|1|1|1|1', '284@1;587@1|284@2;587@2|284@3;587@3|284@4;587@4|284@5;587@5|284@6;587@6')," + + "(10001, 'Test épée divine', 1012, '-1,-1,-1', 'AGGRESSIVE', '1@v:-n;13:-1i;1f:5;17:5;1b:p;s:m;t:m;a:2g;c:2q;f:2g;d:2g;e:p;8:7;9:4;|2@v:-k;13:-1d;1f:9;17:9;1b:u;s:n;t:n;a:2l;c:2v;f:2l;d:2l;e:u;8:7;9:4;|3@v:-h;13:-18;1f:d;17:d;1b:13;s:p;t:p;a:2q;c:34;f:2q;d:2q;e:13;8:7;9:4;|4@v:-d;13:-13;1f:h;17:h;1b:18;s:q;t:q;a:2v;c:39;f:2v;d:2v;e:13;8:7;9:5;|5@v:-9;13:-u;1f:k;17:k;1b:1d;s:r;t:r;a:34;c:3e;f:34;d:34;e:18;8:7;9:5;|6@v:-5;13:-p;1f:n;17:n;1b:1i;s:u;t:u;a:3o;c:3o;f:3e;d:3o;e:1i;8:7;9:5;', '200|300|400|500|600|700', '1|1|1|1|1|1', '145@1|145@2|145@3|145@4|145@5|145@6')" ); use(MonsterRewardData.class, MonsterRewardItem.class); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/AttackTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/AttackTest.java index 5e28dbf2e..dfcbb025c 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/AttackTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/AttackTest.java @@ -25,7 +25,9 @@ import fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation; import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter; +import fr.quatrevieux.araknemu.game.fight.fighter.invocation.InvocationFighter; import fr.quatrevieux.araknemu.game.fight.turn.action.cast.Cast; +import fr.quatrevieux.araknemu.game.monster.MonsterService; import fr.quatrevieux.araknemu.game.spell.Spell; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -174,6 +176,32 @@ void disallowAttackAlliesIfItKilledMoreAlliesThanEnemies() throws SQLException, assertDotNotGenerateAction(); } + @Test + void disallowKillMainAlly() throws SQLException, NoSuchFieldException, IllegalAccessException { + dataSet + .pushFunctionalSpells() + .pushMonsterTemplateInvocations() + ; + + configureFight(fb -> fb + .addSelf(builder -> builder.cell(167).currentLife(5)) + .addEnemy(builder -> builder.cell(137).currentLife(5)) + .addEnemy(builder -> builder.cell(138).currentLife(5)) + .addEnemy(builder -> builder.cell(166).currentLife(500)) + ); + + InvocationFighter invoc = new InvocationFighter( + -15, + container.get(MonsterService.class).load(10001).get(5), + player.fighter().team(), + player.fighter() + ); + fight.fighters().joinTurnList(invoc, fight.map().get(152)); + configureFighterAi(invoc); + + assertDotNotGenerateAction(); + } + @Test void allowAttackAlliesIfItKilledMoreEnemies() throws SQLException, NoSuchFieldException, IllegalAccessException { dataSet.pushFunctionalSpells(); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/BoostTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/BoostTest.java index f67fac476..ddc5dd3a9 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/BoostTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/BoostTest.java @@ -23,11 +23,16 @@ import fr.quatrevieux.araknemu.game.fight.ai.AiBaseCase; import fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation; import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; +import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter; +import fr.quatrevieux.araknemu.game.fight.fighter.invocation.InvocationFighter; +import fr.quatrevieux.araknemu.game.monster.MonsterService; import fr.quatrevieux.araknemu.game.spell.Spell; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import java.sql.SQLException; + import static org.junit.jupiter.api.Assertions.assertEquals; class BoostTest extends AiBaseCase { @@ -51,6 +56,32 @@ void success() { assertCast(126, 122); } + @Test + void successShouldBoostMainAlly() throws SQLException { + dataSet + .pushMonsterTemplateInvocations() + .pushMonsterSpellsInvocations() + ; + action = Boost.self(container.get(Simulator.class)); + + configureFight(fb -> fb + .addSelf(builder -> builder.cell(122)) + .addAlly(builder -> builder.cell(127)) + .addEnemy(builder -> builder.cell(125)) + ); + + InvocationFighter invoc = new InvocationFighter( + -10, + container.get(MonsterService.class).load(39).get(5), // Lapino + player.fighter().team(), + player.fighter() + ); + fight.fighters().joinTurnList(invoc, fight.map().get(126)); + configureFighterAi(invoc); + + assertCast(582, 122); + } + @Test void successWithAllEnemiesInvisible() { action = Boost.self(container.get(Simulator.class)); @@ -239,4 +270,31 @@ void scoreShouldHandleSpellAPCost() { simulation.alterActionPoints(1); assertEquals(5, action.score(simulation), 0.01); } + + @Test + void scoreShouldPrioritizeMainAlly() { + Boost action = Boost.allies(container.get(Simulator.class)); + + Spell spell = Mockito.mock(Spell.class); + + configureFight(fb -> fb + .addSelf(builder -> builder.cell(122)) + .addEnemy(builder -> builder.player(other).cell(125)) + ); + + DoubleFighter invoc = new DoubleFighter(-10, player.fighter()); + fight.fighters().joinTurnList(invoc, fight.map().get(126)); + configureFighterAi(invoc); + + CastSimulation simulation = new CastSimulation(spell, invoc, fight.map().get(122)); + simulation.setMainAlly(player.fighter()); + simulation.addBoost(5, player.fighter()); + Mockito.when(spell.apCost()).thenReturn(3); + assertEquals(6.66, action.score(simulation), 0.01); + + simulation = new CastSimulation(spell, invoc, fight.map().get(122)); + simulation.addBoost(5, player.fighter()); + Mockito.when(spell.apCost()).thenReturn(3); + assertEquals(3.33, action.score(simulation), 0.01); + } } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/DummyGenerator.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/DummyGenerator.java index f27ff3ee9..3ca8a097f 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/DummyGenerator.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/DummyGenerator.java @@ -21,12 +21,11 @@ import fr.quatrevieux.araknemu.game.fight.ai.AI; import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; -import fr.quatrevieux.araknemu.game.fight.fighter.PlayableFighter; -import fr.quatrevieux.araknemu.game.fight.turn.FightTurn; import fr.quatrevieux.araknemu.game.fight.turn.Turn; import fr.quatrevieux.araknemu.game.fight.turn.action.Action; import fr.quatrevieux.araknemu.game.fight.turn.action.ActionResult; import fr.quatrevieux.araknemu.game.fight.turn.action.ActionType; +import fr.quatrevieux.araknemu.game.fight.turn.action.FightAction; import java.time.Duration; import java.util.Optional; @@ -36,9 +35,9 @@ public class DummyGenerator implements ActionGenerator { public void initialize(AI ai) {} @Override - public Optional generate(AI ai, AiActionFactory actions) { + public Optional generate(AI ai, AiActionFactory actions) { return Optional.of( - new Action() { + (A) new FightAction() { @Override public Fighter performer() { return null; @@ -53,6 +52,16 @@ public ActionType type() { public Duration duration() { return Duration.ZERO; } + + @Override + public boolean validate(Turn turn) { + return true; + } + + @Override + public ActionResult start() { + return null; + } } ); } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/HealTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/HealTest.java index adb232766..72b88be93 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/HealTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/HealTest.java @@ -23,7 +23,9 @@ import fr.quatrevieux.araknemu.game.fight.ai.AiBaseCase; import fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation; import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; +import fr.quatrevieux.araknemu.game.fight.fighter.invocation.InvocationFighter; import fr.quatrevieux.araknemu.game.fight.turn.action.cast.Cast; +import fr.quatrevieux.araknemu.game.monster.MonsterService; import fr.quatrevieux.araknemu.game.spell.Spell; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -88,6 +90,31 @@ void shouldIgnoreIfHealMostlyEnemies() throws SQLException { assertDotNotGenerateAction(); } + @Test + void successShouldHealMainAlly() throws SQLException { + dataSet + .pushMonsterTemplateInvocations() + .pushMonsterSpellsInvocations() + ; + + configureFight(fb -> fb + .addSelf(builder -> builder.cell(97).currentLife(10)) + .addAlly(builder -> builder.cell(127).currentLife(10)) + .addEnemy(builder -> builder.cell(125)) + ); + + InvocationFighter invoc = new InvocationFighter( + -10, + container.get(MonsterService.class).load(39).get(5), // Lapino + player.fighter().team(), + player.fighter() + ); + fight.fighters().joinTurnList(invoc, fight.map().get(126)); + configureFighterAi(invoc); + + assertCast(210, 97); + } + @Test void shouldIgnoreIfCanKillAllyOrItself() { configureFight(fb -> fb @@ -153,4 +180,30 @@ void score() throws SQLException { assertEquals(0.5, computeScore(121, 123), 0.001); // 2 (LP lost) / 4 AP assertEquals(1.126, computeScore(131, 123), 0.001); // (2 + (6 * 98% + 12.5 * 2%) * (3*0.075)) / 3 AP : buff heal is used as buff, so it's not capped by lost LP } + + @Test + void scoreShouldPrioritizeMainAlly() throws SQLException { + dataSet + .pushMonsterTemplateInvocations() + .pushMonsterSpellsInvocations() + ; + + configureFight(fb -> fb + .addSelf(builder -> builder.cell(97).currentLife(10)) + .addAlly(builder -> builder.cell(123).currentLife(10)) + .addEnemy(builder -> builder.player(other).cell(125)) + ); + + InvocationFighter invoc = new InvocationFighter( + -10, + container.get(MonsterService.class).load(39).get(5), // Lapino + player.fighter().team(), + player.fighter() + ); + fight.fighters().joinTurnList(invoc, fight.map().get(126)); + configureFighterAi(invoc); + + assertEquals(7.99, computeScore(210, 123), 0.001); + assertEquals(15.98, computeScore(210, 97), 0.001); + } } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyAITest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyAITest.java index aa8752be9..d9bfe0ec4 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyAITest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyAITest.java @@ -53,6 +53,7 @@ void baseProxy() { assertInstanceOf(ProxyPassiveFighter.class, proxy.enemy().get()); assertEquals(ai.enemy().get(), proxy.enemy().get()); + assertEquals(ai.ally(), proxy.ally()); List fighters = proxy.fighters().collect(Collectors.toList()); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulationTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulationTest.java index 725d5f1f3..c85a87af4 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulationTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/CastSimulationTest.java @@ -105,6 +105,31 @@ void healWithMainEnemy() throws SQLException { assertEquals(6, simulation.mainEnemyLife()); } + @Test + void healWithMainAlly() throws SQLException { + PlayerFighter mainAlly = new PlayerFighter(makeSimpleGamePlayer(11)); + mainAlly.setTeam(fighter.team()); + mainAlly.joinFight(fight, fight.map().get(127)); + mainAlly.init(); + + simulation.setMainAlly(mainAlly); + + fighter.life().damage(fighter, 10); + allie.life().damage(fighter, 10); + ennemy.life().damage(fighter, 10); + mainAlly.life().damage(fighter, 10); + + simulation.addHeal(new Interval(5, 5), fighter); + simulation.addHeal(new Interval(4, 4), allie); + simulation.addHeal(new Interval(3, 3), ennemy); + simulation.addHeal(new Interval(6, 6), mainAlly); + + assertEquals(5, simulation.selfLife()); + assertEquals(10, simulation.alliesLife()); + assertEquals(3, simulation.enemiesLife()); + assertEquals(6, simulation.mainAllyLife()); + } + @ParameterizedTest @MethodSource("provideHeal") void healLimitByLostLife(Interval value, double expectedValue) { @@ -157,6 +182,26 @@ void addDamageWithMainEnemy() throws SQLException { assertEquals(-6, simulation.mainEnemyLife()); } + @Test + void addDamageWithMainAlly() throws SQLException { + PlayerFighter mainAlly = new PlayerFighter(makeSimpleGamePlayer(11)); + mainAlly.setTeam(fighter.team()); + mainAlly.joinFight(fight, fight.map().get(127)); + mainAlly.init(); + + simulation.setMainAlly(mainAlly); + + simulation.addDamage(new Interval(5, 5), fighter); + simulation.addDamage(new Interval(4, 4), allie); + simulation.addDamage(new Interval(3, 3), ennemy); + simulation.addDamage(new Interval(6, 6), mainAlly); + + assertEquals(-5, simulation.selfLife()); + assertEquals(-10, simulation.alliesLife()); + assertEquals(-3, simulation.enemiesLife()); + assertEquals(-6, simulation.mainAllyLife()); + } + @Test void addDamageLimitByLife() { simulation.addDamage(new Interval(100, 100), ennemy); @@ -217,21 +262,47 @@ void addBoostWithMainEnemy() throws SQLException { assertEquals(6, simulation.mainEnemyBoost()); } + @Test + void addBoostWithMainAlly() throws SQLException { + PlayerFighter mainAlly = new PlayerFighter(makeSimpleGamePlayer(11)); + mainAlly.setTeam(fighter.team()); + mainAlly.joinFight(fight, fight.map().get(127)); + mainAlly.init(); + + simulation.setMainAlly(mainAlly); + + simulation.addBoost(5, fighter); + simulation.addBoost(4, allie); + simulation.addBoost(3, ennemy); + simulation.addBoost(6, mainAlly); + + assertEquals(5, simulation.selfBoost()); + assertEquals(10, simulation.alliesBoost()); + assertEquals(3, simulation.enemiesBoost()); + assertEquals(6, simulation.mainAllyBoost()); + } + @Test void killDamage() throws SQLException { PlayerFighter mainEnemy = new PlayerFighter(makeSimpleGamePlayer(11)); mainEnemy.setTeam(ennemy.team()); mainEnemy.joinFight(fight, fight.map().get(127)); mainEnemy.init(); - simulation.setMainEnemy(mainEnemy); + PlayerFighter mainAlly = new PlayerFighter(makeSimpleGamePlayer(12)); + mainAlly.setTeam(fighter.team()); + mainAlly.joinFight(fight, fight.map().get(128)); + mainAlly.init(); + simulation.setMainAlly(mainAlly); + simulation.addDamage(new Interval(1000, 1000), allie); assertEquals(1, simulation.killedAllies()); assertEquals(0, simulation.killedEnemies()); assertEquals(0, simulation.suicideProbability()); assertEquals(0, simulation.mainEnemyKill()); + assertEquals(0, simulation.mainAllyKill()); simulation.addDamage(new Interval(1000, 1000), ennemy); @@ -239,6 +310,7 @@ void killDamage() throws SQLException { assertEquals(1, simulation.killedEnemies()); assertEquals(0, simulation.suicideProbability()); assertEquals(0, simulation.mainEnemyKill()); + assertEquals(0, simulation.mainAllyKill()); simulation.addDamage(new Interval(1000, 1000), fighter); @@ -246,6 +318,7 @@ void killDamage() throws SQLException { assertEquals(1, simulation.killedEnemies()); assertEquals(1, simulation.suicideProbability()); assertEquals(0, simulation.mainEnemyKill()); + assertEquals(0, simulation.mainAllyKill()); simulation.addDamage(new Interval(1000, 1000), mainEnemy); @@ -253,6 +326,15 @@ void killDamage() throws SQLException { assertEquals(2, simulation.killedEnemies()); assertEquals(1, simulation.suicideProbability()); assertEquals(1, simulation.mainEnemyKill()); + assertEquals(0, simulation.mainAllyKill()); + + simulation.addDamage(new Interval(1000, 1000), mainAlly); + + assertEquals(2, simulation.killedAllies()); + assertEquals(2, simulation.killedEnemies()); + assertEquals(1, simulation.suicideProbability()); + assertEquals(1, simulation.mainEnemyKill()); + assertEquals(1, simulation.mainAllyKill()); } @ParameterizedTest @@ -345,6 +427,36 @@ void mergeWithMainEnemy() throws SQLException { assertEquals(-5.4, simulation.mainEnemyBoost()); } + @Test + void mergeWithMainAlly() throws SQLException { + PlayerFighter mainAlly = new PlayerFighter(makeSimpleGamePlayer(11)); + mainAlly.setTeam(fighter.team()); + mainAlly.joinFight(fight, fight.map().get(127)); + mainAlly.init(); + + simulation.setMainAlly(mainAlly); + + simulation.addDamage(new Interval(15, 15), ennemy); + simulation.addBoost(15, ennemy); + simulation.addDamage(new Interval(10, 10), mainAlly); + simulation.addBoost(-5, mainAlly); + + CastSimulation other = new CastSimulation(Mockito.mock(Spell.class), fighter, fight.map().get(123)); + other.setMainAlly(mainAlly); + + other.addDamage(new Interval(25, 25), ennemy); + other.addBoost(-10, ennemy); + other.addDamage(new Interval(15, 15), mainAlly); + other.addBoost(-2, mainAlly); + + simulation.merge(other, 20); + + assertEquals(-20, simulation.enemiesLife()); + assertEquals(-13, simulation.mainAllyLife()); + assertEquals(13, simulation.enemiesBoost()); + assertEquals(-5.4, simulation.mainAllyBoost()); + } + @Test void mergeKill() { simulation.addDamage(new Interval(15, 15), ennemy); @@ -380,6 +492,28 @@ void mergeKillMainEnemy() throws SQLException { assertEquals(.2, simulation.mainEnemyKill()); } + @Test + void mergeKillMainAlly() throws SQLException { + PlayerFighter mainAlly = new PlayerFighter(makeSimpleGamePlayer(11)); + mainAlly.setTeam(fighter.team()); + mainAlly.joinFight(fight, fight.map().get(127)); + mainAlly.init(); + + simulation.setMainAlly(mainAlly); + + simulation.addDamage(new Interval(15, 15), mainAlly); + + CastSimulation other = new CastSimulation(Mockito.mock(Spell.class), fighter, fight.map().get(123)); + other.setMainAlly(mainAlly); + + other.addDamage(new Interval(500, 500), mainAlly); + + simulation.merge(other, 20); + + assertEquals(.2, simulation.killedAllies()); + assertEquals(.2, simulation.mainAllyKill()); + } + @Test void mergeSuicide() { simulation.addDamage(new Interval(0, 500), fighter); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/SimulatorTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/SimulatorTest.java index d5cf3fc43..899cc2b71 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/SimulatorTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/SimulatorTest.java @@ -141,6 +141,28 @@ void simulateBoost() { assertEquals(79, simulation.selfBoost(), .1); assertEquals(0, simulation.mainEnemyBoost()); assertEquals(0, simulation.mainEnemyLife()); + assertEquals(0, simulation.mainAllyBoost()); + assertEquals(0, simulation.mainAllyLife()); + } + + @Test + void simulateBoostMainAlly() { + DoubleFighter invoc = new DoubleFighter(-10, fighter); + fight.fighters().join(invoc, fight.map().get(142)); + ai = new FighterAI(invoc, fight, new NullGenerator()); + + CastSimulation simulation = simulator.simulate(getSpell(42, 1), ai, invoc, fighter.cell()); + + assertEquals(0, simulation.alliesLife()); + assertEquals(0, simulation.enemiesLife()); + assertEquals(0, simulation.selfLife()); + assertEquals(79, simulation.alliesBoost(), .1); + assertEquals(0, simulation.enemiesBoost()); + assertEquals(0, simulation.selfBoost()); + assertEquals(0, simulation.mainEnemyBoost()); + assertEquals(0, simulation.mainEnemyLife()); + assertEquals(79, simulation.mainAllyBoost(), .1); + assertEquals(0, simulation.mainAllyLife()); } @Test @@ -154,6 +176,27 @@ void simulateWithProbableEffects() { assertEquals(0, simulation.alliesBoost()); assertEquals(0, simulation.enemiesBoost()); assertEquals(0, simulation.selfBoost()); + assertEquals(0, simulation.mainAllyBoost()); + assertEquals(0, simulation.mainAllyLife()); + } + + @Test + void simulateWithProbableEffectsMainAlly() { + DoubleFighter invoc = new DoubleFighter(-10, fighter); + fight.fighters().join(invoc, fight.map().get(142)); + ai = new FighterAI(invoc, fight, new NullGenerator()); + + CastSimulation simulation = simulator.simulate(getSpell(109, 5), ai, invoc, fighter.cell()); + + assertEquals(-23.5, simulation.alliesLife(), .1); + assertEquals(0, simulation.enemiesLife()); + assertEquals(0, simulation.mainEnemyLife()); + assertEquals(0, simulation.selfLife()); + assertEquals(0, simulation.alliesBoost()); + assertEquals(0, simulation.enemiesBoost()); + assertEquals(0, simulation.selfBoost()); + assertEquals(0, simulation.mainAllyBoost()); + assertEquals(-23.5, simulation.mainAllyLife(), .1); } @Test From bea7f2aed61e651d484da3d6a92f6ff1ab580a1b Mon Sep 17 00:00:00 2001 From: Vincent Quatrevieux Date: Mon, 15 Apr 2024 18:35:52 +0200 Subject: [PATCH 4/4] ci: update codecov GA --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b5ef8b1fe..d8dc07f5f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -45,10 +45,11 @@ jobs: run: mvn --batch-mode --update-snapshots -P '!checkerframework,!checkerframework-jdk8,!checkerframework-jdk9orlater' verify - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} files: target/site/jacoco/jacoco.xml + verbose: true java_compatibility: runs-on: ubuntu-latest