Skip to content

Commit

Permalink
feat(ai): Take in account main ally to compute heal and boost score
Browse files Browse the repository at this point in the history
  • Loading branch information
vincent4vx committed Apr 15, 2024
1 parent 2b73510 commit 97e40b2
Show file tree
Hide file tree
Showing 15 changed files with 421 additions and 16 deletions.
6 changes: 6 additions & 0 deletions checkstyle.xml
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,10 @@
<property name="checks" value="AnonInnerLength" />
<property name="files" value="fr.quatrevieux.araknemu.network.game.out.game.UpdateCells" />
</module>

<!-- Ignore CastSimulation -->
<module name="SuppressionSingleFilter">
<property name="checks" value="MethodCountCheck" />
<property name="files" value="(fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation)" />
</module>
</module>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ public Optional<? extends FighterData> enemy() {
return ai.enemy().map(this::getProxyFighter);
}

@Override
public Optional<? extends FighterData> ally() {
return ai.ally().map(this::getProxyFighter);
}

/**
* Change the current cell of the handled fighter
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
*/
Expand Down Expand Up @@ -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();
}
}

/**
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ private CastSimulation simulate(Spell spell, AI ai, CastScope<FighterData, Battl
final CastSimulation simulation = new CastSimulation(spell, scope.caster(), scope.target());

ai.enemy().ifPresent(simulation::setMainEnemy);
ai.ally().ifPresent(simulation::setMainAlly);

for (CastScope.EffectScope<FighterData, BattlefieldCell> effect : scope.effects()) {
final EffectSimulator simulator = simulators.get(effect.effect().effect());
Expand All @@ -142,6 +143,7 @@ private CastSimulation simulate(Spell spell, AI ai, CastScope<FighterData, Battl
if (effect.effect().probability() > 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());
Expand Down
3 changes: 2 additions & 1 deletion src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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));
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,9 +35,9 @@ public class DummyGenerator implements ActionGenerator {
public void initialize(AI ai) {}

@Override
public Optional<Action> generate(AI ai, AiActionFactory actions) {
public <A extends Action> Optional<A> generate(AI ai, AiActionFactory<A> actions) {
return Optional.of(
new Action() {
(A) new FightAction() {
@Override
public Fighter performer() {
return null;
Expand All @@ -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;
}
}
);
}
Expand Down
Loading

0 comments on commit 97e40b2

Please sign in to comment.