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

[AI] Implements missing effects simulators #308

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
72b8b06
feat(ai): Handle invocation effect
vincent4vx Sep 3, 2023
c371ee7
feat(ai): Handle double effect
vincent4vx Sep 5, 2023
0c2c7c6
feat(ai): Handle move back simulator
vincent4vx Sep 9, 2023
d9b528e
feat(ai): Handle sacrifice simulator
vincent4vx Sep 11, 2023
8ade684
fix(ai): Register sacrifice simulator
vincent4vx Sep 13, 2023
8fe44a4
feat(ai): Handle heal or multiply damage simulator
vincent4vx Sep 14, 2023
fd73c80
feat(ai): Handle attraction effect
vincent4vx Dec 2, 2023
3009136
feat(ai): Add giver percent life simulator
vincent4vx Dec 2, 2023
20fff4d
feat(ai): Add return spell simulator
vincent4vx Dec 9, 2023
cb3dcc1
feat(ai): Add steal characteristic simulator
vincent4vx Dec 9, 2023
c95204c
feat(ai): Add damage on action point use simulator
vincent4vx Dec 11, 2023
f966e07
feat(ai): Add fixed damage simulator
vincent4vx Dec 12, 2023
a581c6f
feat(ai): Add fixed steal life simulator
vincent4vx Dec 16, 2023
686f832
feat(ai): Add percent life damage simulator
vincent4vx Dec 16, 2023
a5c9be3
ci(checkerframework): add a cast to NonNegative
vincent4vx Dec 16, 2023
318a0d3
feat(ai): Add percent life lost damage simulator
vincent4vx Dec 23, 2023
291ab52
feat(ai): Add fixed heal simulator
vincent4vx Dec 23, 2023
ee11cab
feat(ai): Add kill simulator
vincent4vx Dec 23, 2023
3aae063
feat(ai): Add invisibility simulator
vincent4vx Dec 23, 2023
7efe968
feat(ai): Add skip turn simulator
vincent4vx Dec 26, 2023
0a55f13
feat(ai): Add multiply damage simulator
vincent4vx Dec 26, 2023
8f1886e
feat(ai): Add add characteristic on damage simulator
vincent4vx Dec 26, 2023
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
16 changes: 15 additions & 1 deletion src/main/java/fr/quatrevieux/araknemu/game/GameModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,22 @@
import fr.quatrevieux.araknemu.game.fight.ai.factory.type.Support;
import fr.quatrevieux.araknemu.game.fight.ai.factory.type.Tactical;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.AddMaxSummonedCreatureSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.AlterActionPointsSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.AlterCharacteristicSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.ArmorSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.DamageSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.FixedCasterDamage;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.HealOrMultiplyDamageSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.HealSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.InvokeDoubleSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.InvokeMonsterSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.MoveBackSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.PunishmentSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.RemovePointsSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.SetStateSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.StealLifeSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.SwitchPositionOnAttackSimulator;
import fr.quatrevieux.araknemu.game.fight.builder.ChallengeBuilderFactory;
import fr.quatrevieux.araknemu.game.fight.builder.PvmBuilderFactory;
import fr.quatrevieux.araknemu.game.fight.castable.effect.Element;
Expand Down Expand Up @@ -937,7 +944,7 @@ private void configureServices(ContainerConfigurator configurator) {
simulator.register(126, new AlterCharacteristicSimulator()); // intelligence
simulator.register(138, new AlterCharacteristicSimulator(2)); // percent damage
simulator.register(178, new AlterCharacteristicSimulator(8)); // heal
simulator.register(182, new AlterCharacteristicSimulator(10)); // summoned creature
simulator.register(182, new AddMaxSummonedCreatureSimulator(10)); // summoned creature
simulator.register(606, new AlterCharacteristicSimulator()); // Wisdom not dispellable
simulator.register(607, new AlterCharacteristicSimulator()); // Strength not dispellable
simulator.register(608, new AlterCharacteristicSimulator()); // Luck not dispellable
Expand Down Expand Up @@ -967,6 +974,13 @@ private void configureServices(ContainerConfigurator configurator) {
.state(50, -500) // Altruiste
);
simulator.register(150, new PunishmentSimulator());
simulator.register(109, new FixedCasterDamage());
simulator.register(180, new InvokeDoubleSimulator());
simulator.register(181, new InvokeMonsterSimulator(container.get(MonsterService.class), simulator));
simulator.register(405, new InvokeMonsterSimulator(container.get(MonsterService.class), simulator));
simulator.register(5, new MoveBackSimulator());
simulator.register(765, new SwitchPositionOnAttackSimulator());
simulator.register(79, new HealOrMultiplyDamageSimulator(100));

return simulator;
});
Expand Down
109 changes: 109 additions & 0 deletions src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Invoke.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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-2023 Vincent Quatrevieux
*/

package fr.quatrevieux.araknemu.game.fight.ai.action;

import fr.quatrevieux.araknemu.game.fight.ai.AI;
import fr.quatrevieux.araknemu.game.fight.ai.action.util.CastSpell;
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.FighterData;
import fr.quatrevieux.araknemu.game.fight.team.Team;
import fr.quatrevieux.araknemu.game.fight.turn.action.Action;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.util.NullnessUtil;

import java.util.Iterator;
import java.util.Optional;

/**
* Invoke a creature
*
* This generator will prioritize healing when allies are low life.
* It will also prioritize killing enemies.
*/
public final class Invoke implements ActionGenerator, CastSpell.SimulationSelector {
private final CastSpell cast;
private @Nullable AI ai;
private double alliesLifeRatio = 1.0;

@SuppressWarnings({"assignment", "argument"})
public Invoke(Simulator simulator) {
this.cast = new CastSpell(simulator, this);
}

@Override
public void initialize(AI ai) {
this.ai = ai;
this.alliesLifeRatio = computeAlliesLifeRatio(ai);
}

@Override
public <A extends Action> Optional<A> generate(AI ai, AiActionFactory<A> actions) {
return cast.generate(ai, actions);
}

@Override
public boolean valid(CastSimulation simulation) {
return simulation.invocation() > 0
&& simulation.killedAllies() == 0
&& simulation.suicideProbability() == 0
;
}

@Override
public double score(CastSimulation simulation) {
double score = simulation.invocation();

// Can kill an enemy : increase invocation score by the max life of the enemy
if (simulation.killedEnemies() >= 0.99) {
score += NullnessUtil.castNonNull(ai).enemy().map(fighter -> fighter.life().max()).orElse(0);
}

// Prioritize healing when allies are low life
score += (2 - alliesLifeRatio) * (simulation.alliesLife() + simulation.selfLife());

// Add direct damage to enemies
score -= simulation.enemiesLife();

return score / simulation.actionPointsCost();
}

private double computeAlliesLifeRatio(AI ai) {
final Team<?> team = ai.fighter().team();

double totalCurrentLife = 0;
double totalMaxLife = 0;

for (Iterator<? extends FighterData> it = ai.fighters().iterator(); it.hasNext();) {
final FighterData fighter = it.next();

if (fighter.team().equals(team)) {
totalCurrentLife += fighter.life().current();
totalMaxLife += fighter.life().max();
}
}

if (totalMaxLife == 0) {
return 1.0;

Check warning on line 104 in src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Invoke.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/Invoke.java#L104

Added line #L104 was not covered by tests
Copy link
Contributor

@moonlight83340 moonlight83340 Sep 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never test ? (Code coverage report)

}

return totalCurrentLife / totalMaxLife;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import fr.quatrevieux.araknemu.game.fight.ai.action.Boost;
import fr.quatrevieux.araknemu.game.fight.ai.action.Debuff;
import fr.quatrevieux.araknemu.game.fight.ai.action.Heal;
import fr.quatrevieux.araknemu.game.fight.ai.action.Invoke;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveFarEnemies;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveNearAllies;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveNearEnemy;
Expand Down Expand Up @@ -343,6 +344,17 @@ public final GeneratorBuilder moveNearAllies() {
return add(new MoveNearAllies());
}

/**
* Try to invoke a monster
*
* @return The builder instance
*
* @see Invoke The used action generator
*/
public final GeneratorBuilder invoke(Simulator simulator) {
return add(new Invoke(simulator));
}

/**
* Build the action generator object
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public void configure(GeneratorBuilder builder, PlayableFighter fighter) {

builder
.moveOrTeleportNearEnemy()
.invoke(simulator)
.boostAllies(simulator)
.heal(simulator)
;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public Runaway(Simulator simulator) {
public void configure(GeneratorBuilder builder) {
builder
.boostSelf(simulator)
.attack(simulator)
.invoke(simulator)
.attackFromNearestCell(simulator)
.boostAllies(simulator)
.moveFarEnemies()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public void configure(GeneratorBuilder builder, PlayableFighter fighter) {
.success(sb -> sb
.moveToBoost(simulator)
.heal(simulator)
.invoke(simulator)
.debuff(simulator)
.attack(simulator)
.moveNearAllies()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public void configure(GeneratorBuilder builder) {
.boostSelf(simulator)
.attackFromBestCell(simulator)
.when(Predicates.hasLessThanPercentLife(50), cond -> cond.success(gb -> gb.heal(simulator)))
.invoke(simulator)
.debuff(simulator)
.boostAllies(simulator)
.heal(simulator)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public final class CastSimulation {
private double enemiesBoost;
private double alliesBoost;
private double selfBoost;
private double invocation;

private double killedAllies;
private double killedEnemies;
Expand Down Expand Up @@ -231,6 +232,13 @@ public double selfBoost() {
return selfBoost;
}

/**
* The score of the invoked creature
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Score ?

*/
public double invocation() {
return invocation;
}

/**
* Add a boost to the target
*
Expand All @@ -246,6 +254,15 @@ public double boost() {
}, target);
}

/**
* Add an invocation score
*
* @param score The score of the invoked creature
*/
public void addInvocation(double score) {
invocation += score;
}

/**
* Get the simulated spell caster
*/
Expand Down Expand Up @@ -302,6 +319,7 @@ public void merge(CastSimulation simulation, double percent) {
suicide += simulation.suicide * percent / 100d;

actionPointsModifier += simulation.actionPointsModifier * percent / 100d;
invocation += simulation.invocation * percent / 100d;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@

package fr.quatrevieux.araknemu.game.fight.ai.simulation;

import fr.quatrevieux.araknemu.game.fight.ai.AI;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.EffectSimulator;
import fr.quatrevieux.araknemu.game.fight.castable.CastScope;
import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter;
import fr.quatrevieux.araknemu.game.fight.fighter.FighterData;
import fr.quatrevieux.araknemu.game.fight.map.BattlefieldCell;
import fr.quatrevieux.araknemu.game.fight.turn.action.util.CriticalityStrategy;
import fr.quatrevieux.araknemu.game.spell.Spell;
import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect;
import fr.quatrevieux.araknemu.game.world.creature.characteristics.Characteristics;

import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -55,20 +58,21 @@ public void register(int effectId, EffectSimulator simulator) {
* Simulate the spell cast
*
* @param spell Spell to cast
* @param ai The AI of the current fighter
* @param caster The caster (current fighter)
* @param target The cell target
*
* @return The simulation result
*/
public CastSimulation simulate(Spell spell, ActiveFighter caster, BattlefieldCell target) {
final CastSimulation normalSimulation = simulate(spell, new SimulationCastScope(spell, caster, target, spell.effects()));
public CastSimulation simulate(Spell spell, AI ai, ActiveFighter caster, BattlefieldCell target) {
final CastSimulation normalSimulation = simulate(spell, ai, new SimulationCastScope(spell, caster, target, spell.effects()));
final int hitRate = spell.criticalHit();

if (hitRate < 2) {
return normalSimulation;
}

final CastSimulation criticalSimulation = simulate(spell, new SimulationCastScope(spell, caster, target, spell.criticalEffects()));
final CastSimulation criticalSimulation = simulate(spell, ai, new SimulationCastScope(spell, caster, target, spell.criticalEffects()));
final CastSimulation simulation = new CastSimulation(spell, caster, target);

final int criticalRate = 100 / criticalityStrategy.hitRate(caster, hitRate);
Expand All @@ -79,12 +83,44 @@ public CastSimulation simulate(Spell spell, ActiveFighter caster, BattlefieldCel
return simulation;
}

/**
* Compute the theoretical score of a spell
*
* Unlike {@link #simulate(Spell, AI, ActiveFighter, BattlefieldCell)} no simulation is performed, so this score will
* not take in account the target resistance, placement, etc...
*
* @param spell The spell to score
* @param characteristics The characteristics of the caster
*
* @return The spell score
*/
public SpellScore score(Spell spell, Characteristics characteristics) {
final SpellScore score = new SpellScore(spell.constraints().range().max());

for (SpellEffect effect : spell.effects()) {
// Ignore probable effects
if (effect.probability() > 0) {
continue;
}

final EffectSimulator simulator = simulators.get(effect.effect());

if (simulator != null) {
simulator.score(score, effect, characteristics);
}
}

return score;
}

/**
* Simulate a cast result
*
* @param spell The spell to simulate
* @param scope The cast scope
* @param ai The AI of the current fighter
*/
private CastSimulation simulate(Spell spell, CastScope<FighterData, BattlefieldCell> scope) {
private CastSimulation simulate(Spell spell, AI ai, CastScope<FighterData, BattlefieldCell> scope) {
// Remove invisible fighters from simulation
scope.targets().forEach(target -> {
if (target.hidden()) {
Expand All @@ -104,10 +140,10 @@ private CastSimulation simulate(Spell spell, CastScope<FighterData, BattlefieldC
if (effect.effect().probability() > 0) {
final CastSimulation probableSimulation = new CastSimulation(spell, scope.caster(), scope.target());

simulator.simulate(probableSimulation, effect);
simulator.simulate(probableSimulation, ai, effect);
simulation.merge(probableSimulation, effect.effect().probability());
} else {
simulator.simulate(simulation, effect);
simulator.simulate(simulation, ai, effect);
}
}

Expand Down
Loading
Loading