- Add basic Airbending AI. (#9200)

- Slightly improve Earthbending AI.
This commit is contained in:
Agetian
2025-11-18 10:20:24 +03:00
committed by GitHub
parent fd1dcba0aa
commit d85c1467b6
3 changed files with 87 additions and 10 deletions

View File

@@ -24,6 +24,7 @@ public enum SpellApiToAi {
.put(ApiType.AddPhase, AddPhaseAi.class) .put(ApiType.AddPhase, AddPhaseAi.class)
.put(ApiType.AddTurn, AddTurnAi.class) .put(ApiType.AddTurn, AddTurnAi.class)
.put(ApiType.AdvanceCrank, AdvanceCrankAi.class) .put(ApiType.AdvanceCrank, AdvanceCrankAi.class)
.put(ApiType.Airbend, AirbendAi.class)
.put(ApiType.AlterAttribute, AlterAttributeAi.class) .put(ApiType.AlterAttribute, AlterAttributeAi.class)
.put(ApiType.Amass, AmassAi.class) .put(ApiType.Amass, AmassAi.class)
.put(ApiType.Animate, AnimateAi.class) .put(ApiType.Animate, AnimateAi.class)

View File

@@ -0,0 +1,53 @@
package forge.ai.ability;
import forge.ai.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.combat.Combat;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
public class AirbendAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
// Check own cards that need saving, non-token, above CMC 2 so that it's hopefully worth saving this one
final Combat combat = aiPlayer.getGame().getCombat();
final CardCollection threatenedTgts = CardLists.filter(aiPlayer.getCreaturesInPlay(),
card -> !card.isToken() && card.getCMC() > 2 &&
(ComputerUtil.predictThreatenedObjects(aiPlayer, null, true).contains(card)
|| (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(aiPlayer, card, combat))));
if (!threatenedTgts.isEmpty()) {
Card bestSaved = ComputerUtilCard.getBestAI(threatenedTgts);
sa.getTargets().add(bestSaved);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// Check opponent's cards that need bouncing (only in the AI's own turn, main phase 1, or at the end of opponent's
// turn, to get rid of potential blockers)
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
if (ph.is(PhaseType.MAIN1, aiPlayer) || (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer)) {
final CardCollection opposingThreats = aiPlayer.getOpponents().getCreaturesInPlay();
if (!opposingThreats.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestAI(opposingThreats));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
// TODO: add logic to use it to remove threatening spells when the ability allows to target spells?
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
AiAbilityDecision decision = canPlay(aiPlayer, sa);
if (decision.willingToPlay() || mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}

View File

@@ -1,27 +1,50 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.AiAbilityDecision; import forge.ai.*;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostSacrifice;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
public class EarthbendAi extends SpellAbilityAi { public class EarthbendAi extends SpellAbilityAi {
@Override @Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) { protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
CardCollection nonAnimatedLands = CardLists.filter(aiPlayer.getLandsInPlay(), CardPredicates.NON_CREATURES); CardCollection lands = aiPlayer.getLandsInPlay();
if (lands.isEmpty()) {
if (nonAnimatedLands.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
} }
CardCollection fetchLands = CardLists.filter(lands, c -> {
for (final SpellAbility ability : c.getAllSpellAbilities()) {
if (ability.isActivatedAbility()) {
final Cost cost = ability.getPayCosts();
for (final CostPart part : cost.getCostParts()) {
if (!(part instanceof CostSacrifice)) {
continue;
}
CostSacrifice sacCost = (CostSacrifice) part;
if (sacCost.payCostFromSource() && ComputerUtilCost.canPayCost(ability, c.getController(), false)) {
return true;
}
}
}
}
return false;
});
Card bestToAnimate = ComputerUtilCard.getBestLandToAnimate(nonAnimatedLands); Card tgtLand = null;
sa.getTargets().add(bestToAnimate);
if (!fetchLands.isEmpty()) {
// Prioritize fetchlands as they can be reused later
tgtLand = ComputerUtilCard.getBestLandToAnimate(fetchLands);
} else {
tgtLand = ComputerUtilCard.getBestLandToAnimate(lands);
}
sa.getTargets().add(tgtLand);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }