diff --git a/src/main/java/fr/quatrevieux/araknemu/game/GameModule.java b/src/main/java/fr/quatrevieux/araknemu/game/GameModule.java index e89da6ed..b4f93110 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/GameModule.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/GameModule.java @@ -143,6 +143,7 @@ import fr.quatrevieux.araknemu.game.fight.ai.factory.type.Aggressive; import fr.quatrevieux.araknemu.game.fight.ai.factory.type.Blocking; import fr.quatrevieux.araknemu.game.fight.ai.factory.type.Fixed; +import fr.quatrevieux.araknemu.game.fight.ai.factory.type.Lunatic; import fr.quatrevieux.araknemu.game.fight.ai.factory.type.Runaway; import fr.quatrevieux.araknemu.game.fight.ai.factory.type.Support; import fr.quatrevieux.araknemu.game.fight.ai.factory.type.Tactical; @@ -1092,7 +1093,8 @@ private void configureServices(ContainerConfigurator configurator) { new Support(simulator), new Tactical(simulator), new Fixed(simulator), - new Blocking() + new Blocking(), + new Lunatic(simulator) ); } ); 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 56de121e..064ff20c 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 @@ -35,9 +35,11 @@ import fr.quatrevieux.araknemu.game.fight.ai.action.MoveToAttack; import fr.quatrevieux.araknemu.game.fight.ai.action.MoveToAttractEnemy; import fr.quatrevieux.araknemu.game.fight.ai.action.MoveToBoost; +import fr.quatrevieux.araknemu.game.fight.ai.action.MoveToCast; import fr.quatrevieux.araknemu.game.fight.ai.action.TeleportNearEnemy; import fr.quatrevieux.araknemu.game.fight.ai.action.logic.GeneratorAggregate; import fr.quatrevieux.araknemu.game.fight.ai.action.logic.NullGenerator; +import fr.quatrevieux.araknemu.game.fight.ai.action.util.CastSpell; import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; import java.util.ArrayList; @@ -306,6 +308,35 @@ public final GeneratorBuilder debuff(Simulator simulator) { return add(new Debuff(simulator)); } + /** + * Try to cast a spell, using the given selector + * + * @param simulator Simulator used by AI + * @param selector The spell cast selector. If a lambda is used, it will be used as a score function. + * + * @return The builder instance + * @see CastSpell The used action generator + */ + public final GeneratorBuilder cast(Simulator simulator, CastSpell.SimulationSelector selector) { + return add(new CastSpell(simulator, selector)); + } + + /** + * Move to the best cell for cast a spell, and then cast it. + * + * @param simulator Simulator used by AI + * @param selector The spell cast selector. If a lambda is used, it will be used as a score function. + * + * @return The builder instance + * @see CastSpell The used action generator + */ + public final GeneratorBuilder castFromBestCell(Simulator simulator, CastSpell.SimulationSelector selector) { + add(new MoveToCast(simulator, selector, new MoveToCast.BestTargetStrategy())); + cast(simulator, selector); + + return this; + } + /** * Try to move near the selected enemy * diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/util/CastSpell.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/util/CastSpell.java index 0c97df07..b11a58ee 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/util/CastSpell.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/util/CastSpell.java @@ -76,7 +76,9 @@ public interface SimulationSelector { /** * Check if the simulation is valid */ - public boolean valid(CastSimulation simulation); + public default boolean valid(CastSimulation simulation) { + return score(simulation) > 0; + } /** * Compare the two simulation diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/Lunatic.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/Lunatic.java new file mode 100644 index 00000000..2b6aca1e --- /dev/null +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/Lunatic.java @@ -0,0 +1,81 @@ +/* + * 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.factory.type; + +import fr.arakne.utils.value.helper.RandomUtil; +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.memory.MemoryKey; +import fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation; +import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; + +/** + * AI for monster which can randomly attack allies or enemies + * Used by Chaferfu + */ +public final class Lunatic extends AbstractAiBuilderFactory { + private static final MemoryKey TARGET_ALLIES = new TargetAlliesMemory(); + private final Simulator simulator; + + public Lunatic(Simulator simulator) { + this.simulator = simulator; + } + + @Override + protected void configure(GeneratorBuilder builder) { + builder.when(ai -> Boolean.TRUE.equals(ai.get(TARGET_ALLIES)), cb -> cb + .success(b -> b + .castFromBestCell(simulator, simulation -> castScore(simulation, true)) + .moveNearAllies() + ) + .otherwise(b -> b + .castFromBestCell(simulator, simulation -> castScore(simulation, false)) + .moveNearEnemy() + ) + ); + } + + private static double castScore(CastSimulation simulation, boolean targetAllies) { + final double alliesScore = simulation.alliesBoost() + simulation.alliesLife() - 100 * simulation.killedAllies(); + final double enemiesScore = simulation.enemiesBoost() + simulation.enemiesLife() - 100 * simulation.killedEnemies(); + final double selfScore = simulation.selfBoost() + simulation.selfLife() - 100 * simulation.suicideProbability() + simulation.invocation(); + + final double baseScore = targetAllies + ? -alliesScore + enemiesScore + selfScore + : alliesScore - enemiesScore + selfScore + ; + + return baseScore / simulation.actionPointsCost(); + } + + private static final class TargetAlliesMemory implements MemoryKey { + private static final RandomUtil RANDOM = RandomUtil.createShared(); + + @Override + public Boolean defaultValue() { + return RANDOM.nextBoolean(); + } + + @Override + public Boolean refresh(Boolean value) { + return RANDOM.nextBoolean(); + } + } +} diff --git a/src/test/java/fr/quatrevieux/araknemu/game/GameModuleTest.java b/src/test/java/fr/quatrevieux/araknemu/game/GameModuleTest.java index 680f59ae..8054eea6 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/GameModuleTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/GameModuleTest.java @@ -215,5 +215,6 @@ void availableAiTypes() throws Exception { assertTrue(aiFactory.create(fighter, "SUPPORT").isPresent()); assertTrue(aiFactory.create(fighter, "TACTICAL").isPresent()); assertTrue(aiFactory.create(fighter, "BLOCKING").isPresent()); + assertTrue(aiFactory.create(fighter, "LUNATIC").isPresent()); } } 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 26ec7249..d2a1bda8 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 @@ -35,10 +35,12 @@ import fr.quatrevieux.araknemu.game.fight.ai.action.MoveToAttack; import fr.quatrevieux.araknemu.game.fight.ai.action.MoveToAttractEnemy; import fr.quatrevieux.araknemu.game.fight.ai.action.MoveToBoost; +import fr.quatrevieux.araknemu.game.fight.ai.action.MoveToCast; import fr.quatrevieux.araknemu.game.fight.ai.action.TeleportNearEnemy; import fr.quatrevieux.araknemu.game.fight.ai.action.logic.ConditionalGenerator; import fr.quatrevieux.araknemu.game.fight.ai.action.logic.GeneratorAggregate; import fr.quatrevieux.araknemu.game.fight.ai.action.logic.NullGenerator; +import fr.quatrevieux.araknemu.game.fight.ai.action.util.CastSpell; import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; import fr.quatrevieux.araknemu.game.fight.turn.action.util.BaseCriticalityStrategy; import org.junit.jupiter.api.BeforeEach; @@ -197,6 +199,16 @@ void debuff() { assertInstanceOf(Debuff.class, builder.debuff(simulator).build()); } + @Test + void cast() { + assertInstanceOf(CastSpell.class, builder.cast(simulator, simulation -> 0).build()); + } + + @Test + void castFromBestCell() throws NoSuchFieldException, IllegalAccessException { + assertActions(builder.castFromBestCell(simulator, simulation -> 0).build(), MoveToCast.class, CastSpell.class); + } + @Test void blockNearestEnemy() { assertInstanceOf(BlockNearestEnemy.class, builder.blockNearestEnemy().build()); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/LunaticTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/LunaticTest.java new file mode 100644 index 00000000..1f885485 --- /dev/null +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/LunaticTest.java @@ -0,0 +1,156 @@ +/* + * 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.factory.type; + +import fr.quatrevieux.araknemu.game.fight.ai.AiBaseCase; +import fr.quatrevieux.araknemu.game.fight.ai.memory.AiMemory; +import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; +import fr.quatrevieux.araknemu.game.fight.turn.action.cast.Cast; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class LunaticTest extends AiBaseCase { + @BeforeEach + @Override + public void setUp() throws Exception { + super.setUp(); + + actionFactory = new Lunatic(container.get(Simulator.class)); + dataSet.pushFunctionalSpells(); + } + + @Test + void name() { + assertEquals("LUNATIC", actionFactory.name()); + } + + @Test + void shouldAttackAllyOrEnemy() throws NoSuchFieldException, IllegalAccessException { + final Map targetCounts = new HashMap<>(); + + for (int i = 0; i < 100; ++i) { + configureFight(b -> b + .addSelf(fb -> fb.cell(342)) + .addEnemy(fb -> fb.cell(327)) + .addAlly(fb -> fb.cell(328)) + ); + removeSpell(6); + + Cast cast = generateCast(); + + assertEquals(3, cast.spell().id()); + targetCounts.put(cast.target().id(), targetCounts.getOrDefault(cast.target().id(), 0) + 1); + } + + assertBetween(40, 60, targetCounts.get(327)); + assertBetween(40, 60, targetCounts.get(328)); + } + + @Test + void shouldMoveNearEnemyOrAllyIfCantAttack() throws NoSuchFieldException, IllegalAccessException { + final Map targetCounts = new HashMap<>(); + + for (int i = 0; i < 100; ++i) { + configureFight(b -> b + .addSelf(fb -> fb.cell(342)) + .addEnemy(fb -> fb.cell(312)) + .addAlly(fb -> fb.cell(370)) + ); + removeAllAP(); + + generateAndPerformMove(); + + targetCounts.put(fighter.cell().id(), targetCounts.getOrDefault(fighter.cell().id(), 0) + 1); + } + + assertBetween(40, 60, targetCounts.get(356)); + assertBetween(40, 60, targetCounts.get(327)); + } + + @Test + void shouldDefineTargetAtStartTurn() throws NoSuchFieldException, IllegalAccessException { + int targetAllyCount = 0; + + for (int i = 0; i < 100; ++i) { + configureFight(b -> b + .addSelf(fb -> fb.cell(342)) + .addEnemy(fb -> fb.cell(312)) + .addAlly(fb -> fb.cell(370)) + ); + removeSpell(6); + + Cast cast = generateCast(); + + assertEquals(3, cast.spell().id()); + boolean targetAlly = cast.target().id() == 370; + + if (targetAlly) { + ++targetAllyCount; + turn.perform(cast); + turn.terminate(); + + generateAndPerformMove(); + assertEquals(356, fighter.cell().id()); + } else { + assertEquals(312, cast.target().id()); + turn.perform(cast); + turn.terminate(); + + generateAndPerformMove(); + assertEquals(327, fighter.cell().id()); + } + } + + assertBetween(40, 60, targetAllyCount); + } + + @Test + void shouldChangeTargetOnMemoryRefresh() throws NoSuchFieldException, IllegalAccessException { + int targetAllyCount = 0; + + configureFight(b -> b + .addSelf(fb -> fb.cell(342)) + .addEnemy(fb -> fb.cell(312)) + .addAlly(fb -> fb.cell(370)) + ); + removeSpell(6); + + Field memoryField = ai.getClass().getDeclaredField("memory"); + memoryField.setAccessible(true); + AiMemory memory = (AiMemory) memoryField.get(ai); + + for (int i = 0; i < 100; ++i) { + memory.refresh(); + Cast cast = generateCast(); + + if (cast.target().id() == 370) { + ++targetAllyCount; + } + } + + assertBetween(40, 60, targetAllyCount); + } +}