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);
+ }
+}