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

Improve AI for sommoned creatures #342

Merged
merged 4 commits into from
Apr 15, 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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,11 @@ jobs:
run: mvn --batch-mode --update-snapshots -P '!checkerframework,!checkerframework-jdk8,!checkerframework-jdk9orlater' verify

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: target/site/jacoco/jacoco.xml
verbose: true

java_compatibility:
runs-on: ubuntu-latest
Expand Down
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>
15 changes: 15 additions & 0 deletions src/main/java/fr/quatrevieux/araknemu/game/fight/ai/AI.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@
*/
public Optional<? extends FighterData> enemy();

/**
* Get the main ally
* This method behavior can change, depending on the AI resolution strategy
*
* Unlike {@link #enemy()}, this method does not return a value most of the time,
* it is used to mark a given ally as the main target for buffs, heals, etc.
*
* In case of invocation, this method should return the invoker.
*
* An empty optional can be returned, no ally is preferred
*/
public default Optional<? extends FighterData> ally() {
return Optional.empty();

Check warning on line 76 in src/main/java/fr/quatrevieux/araknemu/game/fight/ai/AI.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/fr/quatrevieux/araknemu/game/fight/ai/AI.java#L76

Added line #L76 was not covered by tests
}

/**
* Get helper for the current AI
*/
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/fr/quatrevieux/araknemu/game/fight/ai/FighterAI.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import fr.quatrevieux.araknemu.game.fight.ai.action.ActionGenerator;
import fr.quatrevieux.araknemu.game.fight.ai.action.FightAiActionFactoryAdapter;
import fr.quatrevieux.araknemu.game.fight.ai.util.AIHelper;
import fr.quatrevieux.araknemu.game.fight.fighter.Fighter;
import fr.quatrevieux.araknemu.game.fight.fighter.FighterData;
import fr.quatrevieux.araknemu.game.fight.fighter.PlayableFighter;
import fr.quatrevieux.araknemu.game.fight.map.BattlefieldMap;
Expand Down Expand Up @@ -138,9 +139,22 @@ public Stream<? extends FighterData> fighters() {

@Override
public Optional<? extends FighterData> enemy() {
if (fighter.invoked()) {
final Fighter invoker = fighter.invoker();

if (invoker != null && !invoker.hidden()) {
return helper.enemies().nearestFrom(invoker.cell());
}
}

return helper.enemies().nearest();
}

@Override
public Optional<? extends FighterData> ally() {
return Optional.ofNullable(fighter.invoker()).filter(invoker -> !invoker.hidden());
}

@Override
public AIHelper helper() {
return helper;
Expand Down
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 All @@ -107,11 +112,12 @@ public double score(CastSimulation simulation) {
}

private double damageScore(CastSimulation simulation) {
return - simulation.enemiesLife() + simulation.alliesLife() + simulation.selfLife() * 2;
return - simulation.enemiesLife() - simulation.mainEnemyLife() + simulation.alliesLife() + simulation.selfLife() * 2;
}

private double killScore(CastSimulation simulation) {
final double killRatio = simulation.killedEnemies()
+ simulation.mainEnemyKill()
- 1.5 * simulation.killedAllies()
- 2 * simulation.suicideProbability()
;
Expand All @@ -128,7 +134,7 @@ private double boostScore(CastSimulation simulation) {
*
* @see CastSimulation#suicideProbability()
*/
enum SuicideStrategy {
public enum SuicideStrategy {
/**
* Always allow suicide
* Should be used on The Sacrificial Doll AI
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 @@ -61,6 +61,7 @@ public boolean valid(CastSimulation simulation) {
public double score(CastSimulation simulation) {
final double score =
- simulation.enemiesBoost()
- simulation.mainEnemyBoost()
+ simulation.alliesBoost()
+ simulation.selfBoost()
;
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
@@ -0,0 +1,46 @@
/*
* 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-2020 Vincent Quatrevieux
*/

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

import fr.quatrevieux.araknemu.game.fight.ai.AI;
import fr.quatrevieux.araknemu.game.fight.turn.action.Action;

import java.util.Optional;

/**
* Try to move near the selected ally
*/
public final class MoveNearAlly implements ActionGenerator {
private final ActionGenerator moveGenerator;

public MoveNearAlly() {
this.moveGenerator = new MoveNearFighter(AI::ally);
}

@Override
public void initialize(AI ai) {
moveGenerator.initialize(ai);
}

@Override
public <A extends Action> Optional<A> generate(AI ai, AiActionFactory<A> actions) {
return moveGenerator.generate(ai, actions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ public final class MoveToAttack implements ActionGenerator {
private final MoveToCast generator;
private final Attack attack;

private MoveToAttack(Simulator simulator, MoveToCast.TargetSelectionStrategy strategy) {
this.attack = new Attack(simulator);
private MoveToAttack(Simulator simulator, MoveToCast.TargetSelectionStrategy strategy, Attack.SuicideStrategy suicideStrategy) {
this.attack = new Attack(simulator, suicideStrategy);
this.generator = new MoveToCast(simulator, attack, strategy);
}

Expand All @@ -63,13 +63,30 @@ public <A extends Action> Optional<A> generate(AI ai, AiActionFactory<A> actions
* So, it do not perform the best move for maximize damage.
*/
public static MoveToAttack nearest(Simulator simulator) {
return new MoveToAttack(simulator, new MoveToCast.NearestStrategy());
return nearest(simulator, Attack.SuicideStrategy.IF_KILL_ENEMY);
}

/**
* Select the nearest cell where a cast is possible
*
* Note: This selected cell is not the best cell for perform an attack, but the nearest cell.
* So, it do not perform the best move for maximize damage.
*/
public static MoveToAttack nearest(Simulator simulator, Attack.SuicideStrategy suicideStrategy) {
return new MoveToAttack(simulator, new MoveToCast.NearestStrategy(), suicideStrategy);
}

/**
* Select the best target cell for cast a spell, and maximizing damage
*/
public static MoveToAttack bestTarget(Simulator simulator) {
return new MoveToAttack(simulator, new MoveToCast.BestTargetStrategy());
return bestTarget(simulator, Attack.SuicideStrategy.IF_KILL_ENEMY);
}

/**
* Select the best target cell for cast a spell, and maximizing damage
*/
public static MoveToAttack bestTarget(Simulator simulator, Attack.SuicideStrategy suicideStrategy) {
return new MoveToAttack(simulator, new MoveToCast.BestTargetStrategy(), suicideStrategy);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
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.MoveNearAlly;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveNearEnemy;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveToAttack;
import fr.quatrevieux.araknemu.game.fight.ai.action.MoveToAttractEnemy;
Expand Down Expand Up @@ -159,6 +160,27 @@ public final GeneratorBuilder moveToAttack(Simulator simulator) {
return add(MoveToAttack.bestTarget(simulator));
}

/**
* Try to move to the best cell for cast an attack spell
*
* To ensure that the move will be performed, add the attack action after this one.
* Otherwise, if an attack is possible from the current cell it will be performed,
* which will results to sub-optimal action.
*
* The action will not be performed if there is a tackle chance and if an attack is possible from the current cell
*
* @param simulator Simulator used by AI
* @param suicideStrategy Indicate if the fighter allow suicidal attack or not
*
* @return The builder instance
*
* @see GeneratorBuilder#attackFromBestCell(Simulator) For perform move and attack
* @see MoveToAttack#bestTarget(Simulator) The used action generator
*/
public final GeneratorBuilder moveToAttack(Simulator simulator, Attack.SuicideStrategy suicideStrategy) {
return add(MoveToAttack.bestTarget(simulator, suicideStrategy));
}

/**
* Try to attack from the nearest cell
*
Expand Down Expand Up @@ -196,6 +218,22 @@ public final GeneratorBuilder attack(Simulator simulator) {
return add(new Attack(simulator));
}

/**
* Try to attack from the current cell
*
* @param simulator Simulator used by AI
* @param suicideStrategy Indicate if the fighter allow suicidal attack or not
*
* @return The builder instance
*
* @see Attack The used action generator
* @see GeneratorBuilder#attackFromBestCell(Simulator) For perform move and attack action
* @see GeneratorBuilder#attackFromNearestCell(Simulator) For perform move and attack action
*/
public final GeneratorBuilder attack(Simulator simulator, Attack.SuicideStrategy suicideStrategy) {
return add(new Attack(simulator, suicideStrategy));
}

/**
* Try to boost oneself
*
Expand Down Expand Up @@ -346,6 +384,17 @@ public final GeneratorBuilder moveNearAllies() {
return add(new MoveNearAllies());
}

/**
* Try to move near the main ally (e.g. invoker in case of summoned creature)
*
* @return The builder instance
*
* @see MoveNearAlly The used action generator
*/
public final GeneratorBuilder moveNearAlly() {
return add(new MoveNearAlly());
}

/**
* Try to invoke a monster
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package fr.quatrevieux.araknemu.game.fight.ai.factory.type;

import fr.quatrevieux.araknemu.game.fight.ai.action.Attack;
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.simulation.Simulator;
Expand All @@ -42,17 +43,22 @@ public Aggressive(Simulator simulator) {

@Override
public void configure(GeneratorBuilder builder, PlayableFighter fighter) {
final Attack.SuicideStrategy suicideStrategy = fighter.invoked()
? Attack.SuicideStrategy.ALLOW
: Attack.SuicideStrategy.IF_KILL_ENEMY
;

builder
.boostSelf(simulator)
;

// Optimisation: do not execute "move to attack" is the fighter has only close combat spell
// because move near enemy will perform the correct movement action
if (hasDistanceSpell(fighter)) {
builder.moveToAttack(simulator);
builder.moveToAttack(simulator, suicideStrategy);
}

builder.attack(simulator);
builder.attack(simulator, suicideStrategy);

builder
.attractEnemy()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ public void configure(GeneratorBuilder builder, PlayableFighter fighter) {
.invoke(simulator)
.debuff(simulator)
.attack(simulator)
.moveNearAllies()
.when(ai -> ai.ally().isPresent(), nb -> nb
.success(GeneratorBuilder::moveNearAlly)
.otherwise(GeneratorBuilder::moveNearAllies)
)
)
.otherwise(sb -> aloneAi.configure(sb, fighter))
);
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
Loading
Loading