Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ai): Implement AI for chaferfu #355

Merged
merged 1 commit into from
Jul 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
Loading