Skip to content

Commit

Permalink
Merge pull request #355 from vincent4vx/feature-ai-chaferfu
Browse files Browse the repository at this point in the history
feat(ai): Implement AI for chaferfu
  • Loading branch information
vincent4vx authored Jul 6, 2024
2 parents f3530ae + ae490c3 commit 6fbd3bb
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 2 deletions.
4 changes: 3 additions & 1 deletion src/main/java/fr/quatrevieux/araknemu/game/GameModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
);
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*
* 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<Boolean> 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<Boolean> {
private static final RandomUtil RANDOM = RandomUtil.createShared();

@Override
public Boolean defaultValue() {
return RANDOM.nextBoolean();
}

@Override
public Boolean refresh(Boolean value) {
return RANDOM.nextBoolean();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*
* 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<Integer, Integer> 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<Integer, Integer> 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);
}
}

0 comments on commit 6fbd3bb

Please sign in to comment.