From c79ed8b609b1e2201cddb6b7d242e37db70bc3d4 Mon Sep 17 00:00:00 2001 From: Agetian Date: Mon, 10 Dec 2018 18:26:16 +0300 Subject: [PATCH] - Basic implementation for chaining two damage spells, supports damage+damage or damage+debuff. --- .../src/main/java/forge/ai/AiCardMemory.java | 5 ++ .../src/main/java/forge/ai/AiController.java | 48 +++++++++---- .../java/forge/ai/ComputerUtilAbility.java | 67 +++++++++++++++++++ .../main/java/forge/ai/ComputerUtilMana.java | 5 ++ .../java/forge/ai/ability/DamageDealAi.java | 25 ++++++- 5 files changed, 134 insertions(+), 16 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiCardMemory.java b/forge-ai/src/main/java/forge/ai/AiCardMemory.java index 47909f5e2e5..a94287e549e 100644 --- a/forge-ai/src/main/java/forge/ai/AiCardMemory.java +++ b/forge-ai/src/main/java/forge/ai/AiCardMemory.java @@ -51,6 +51,7 @@ public class AiCardMemory { HELD_MANA_SOURCES_FOR_MAIN2, // These mana sources will not be used before Main 2 HELD_MANA_SOURCES_FOR_DECLBLK, // These mana sources will not be used before Combat - Declare Blockers HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK, // These mana sources will not be used before the opponent's Combat - Declare Blockers + HELD_MANA_SOURCES_FOR_NEXT_PRIORITY, // These mana sources will not be used until the next time the AI receives priority ATTACHED_THIS_TURN, // These equipments were attached to something already this turn ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn BOUNCED_THIS_TURN, // These cards were bounced this turn @@ -65,6 +66,7 @@ public class AiCardMemory { private final Set memHeldManaSources; private final Set memHeldManaSourcesForCombat; private final Set memHeldManaSourcesForEnemyCombat; + private final Set memHeldManaSourcesForNextPriority; private final Set memAttachedThisTurn; private final Set memAnimatedThisTurn; private final Set memBouncedThisTurn; @@ -84,6 +86,7 @@ public class AiCardMemory { this.memTrickAttackers = new HashSet<>(); this.memChosenFogEffect = new HashSet<>(); this.memMarkedToAvoidReentry = new HashSet<>(); + this.memHeldManaSourcesForNextPriority = new HashSet<>(); } private Set getMemorySet(MemorySet set) { @@ -98,6 +101,8 @@ public class AiCardMemory { return memHeldManaSourcesForCombat; case HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK: return memHeldManaSourcesForEnemyCombat; + case HELD_MANA_SOURCES_FOR_NEXT_PRIORITY: + return memHeldManaSourcesForNextPriority; case ATTACHED_THIS_TURN: return memAttachedThisTurn; case ANIMATED_THIS_TURN: diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 5e777707bc8..bf60b900916 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -630,31 +630,48 @@ public class AiController { } public boolean reserveManaSources(SpellAbility sa) { - return reserveManaSources(sa, PhaseType.MAIN2, false); + return reserveManaSources(sa, PhaseType.MAIN2, false, false, null); + } + + public boolean reserveManaSourcesTillNextPriority(SpellAbility sa, SpellAbility exceptForSa) { + return reserveManaSources(sa, null, false, true, exceptForSa); } public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy) { + return reserveManaSources(sa, phaseType, enemy, true, null); + } + + public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy, boolean nextPriority, SpellAbility exceptForThisSa) { ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0); CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player); + // used for chained spells where two spells need to be cast in succession + if (exceptForThisSa != null) { + manaSources.removeAll(ComputerUtilMana.getManaSourcesToPayCost(ComputerUtilMana.calculateManaCost(exceptForThisSa, true, 0), exceptForThisSa, player)); + } + if (manaSources.isEmpty()) { return false; } AiCardMemory.MemorySet memSet; - switch (phaseType) { - case MAIN2: - memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2; - break; - case COMBAT_DECLARE_BLOCKERS: - memSet = enemy ? AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK - : AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK; - break; - default: - System.out.println("Warning: unsupported mana reservation phase specified for reserveManaSources: " - + phaseType.name() + ", reserving until Main 2 instead. Consider adding support for the phase if needed."); - memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2; - break; + if (phaseType == null && nextPriority) { + memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_NEXT_PRIORITY; + } else { + switch (phaseType) { + case MAIN2: + memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2; + break; + case COMBAT_DECLARE_BLOCKERS: + memSet = enemy ? AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK + : AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK; + break; + default: + System.out.println("Warning: unsupported mana reservation phase specified for reserveManaSources: " + + phaseType.name() + ", reserving until Main 2 instead. Consider adding support for the phase if needed."); + memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2; + break; + } } // This is a simplification, since one mana source can produce more than one mana, @@ -1317,6 +1334,9 @@ public class AiController { // re-created if needed and used for any AI logic that needs it. predictedCombat = null; + // Reset priority mana reservation + AiCardMemory.clearMemorySet(player, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_NEXT_PRIORITY); + if (useSimulation) { return singleSpellAbilityList(simPicker.chooseSpellAbilityToPlay(null)); } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java index b1df18b97ce..aaf6bbae40c 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java @@ -7,6 +7,7 @@ import com.google.common.base.Predicate; import com.google.common.collect.Lists; import forge.card.CardStateName; +import forge.card.mana.ManaCost; import forge.game.Game; import forge.game.GameActionUtil; import forge.game.ability.ApiType; @@ -15,10 +16,15 @@ import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; import forge.game.card.CardPredicates.Presets; +import forge.game.cost.Cost; +import forge.game.cost.CostPartMana; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbilityStackInstance; +import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; public class ComputerUtilAbility { public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) { @@ -182,4 +188,65 @@ public class ComputerUtilAbility { return targeted; } + + public static Pair getDamageAfterChainingSpells(Player ai, SpellAbility sa, String damage) { + if (sa.getSubAbility() != null || sa.getParent() != null) { + // Doesn't work yet for complex decisions where damage is only a part of the decision process + return null; + } + + // Try to chain damage/debuff effects + if (StringUtils.isNumeric(damage) || (damage.startsWith("-") && StringUtils.isNumeric(damage.substring(1)))) { + // currently only works for predictable numeric damage + CardCollection cards = new CardCollection(); + cards.addAll(ai.getCardsIn(ZoneType.Hand)); + cards.addAll(ai.getCardsIn(ZoneType.Battlefield)); + cards.addAll(ai.getCardsActivableInExternalZones(true)); + for (Card c : cards) { + for (SpellAbility ab : c.getSpellAbilities()) { + if (ab.equals(sa) || ab.getSubAbility() != null) { // decisions for complex SAs with subs are not supported yet + continue; + } + // currently works only with cards that don't have additional costs (only mana is supported) + if (ab.getPayCosts() != null && (ab.getPayCosts().hasNoManaCost() || ab.getPayCosts().hasOnlySpecificCostType(CostPartMana.class))) { + String dmgDef = "0"; + if (ab.getApi() == ApiType.DealDamage) { + dmgDef = ab.getParamOrDefault("NumDmg", "0"); + } else if (ab.getApi() == ApiType.Pump) { + dmgDef = ab.getParamOrDefault("NumDef", "0"); + if (dmgDef.startsWith("-")) { + dmgDef = dmgDef.substring(1); + } else { + continue; // not a toughness debuff + } + } + if (StringUtils.isNumeric(dmgDef) && ab.canPlay()) { // currently doesn't work for X and other dependent costs + if (sa.usesTargeting() && ab.usesTargeting()) { + // Ensure that the chained spell can target at least the same things (or more) as the current one + TargetRestrictions tgtSa = sa.getTargetRestrictions(); + TargetRestrictions tgtAb = sa.getTargetRestrictions(); + if (tgtSa.canTgtCreature() && !tgtAb.canTgtCreature()) { + continue; + } else if (tgtSa.canTgtPlaneswalker() && !tgtAb.canTgtPlaneswalker()) { + continue; + } + // FIXME: should it also check restrictions for targeting players? + ManaCost costSa = sa.getPayCosts() != null ? sa.getPayCosts().getTotalMana() : ManaCost.NO_COST; + ManaCost costAb = ab.getPayCosts().getTotalMana(); // checked for null above + ManaCost total = ManaCost.combine(costSa, costAb); + SpellAbility combinedAb = ab.copyWithDefinedCost(new Cost(total, false)); + // can we pay both costs? + if (ComputerUtilMana.canPayManaCost(combinedAb, ai, 0)) { + //aic.reserveManaSourcesTillNextPriority(ab); // reserve mana for the second spell + return Pair.of(ab, Integer.parseInt(dmgDef)); + } + } + } + } + } + } + } + + return null; + } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index 1330e32a0f3..9bbdc3a56a6 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -856,6 +856,11 @@ public class ComputerUtilMana { AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); int chanceToReserve = aic.getIntProperty(AiProps.RESERVE_MANA_FOR_MAIN2_CHANCE); + // Mana reserved for spell synchronization + if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_NEXT_PRIORITY)) { + return true; + } + PhaseType curPhase = ai.getGame().getPhaseHandler().getPhase(); // For combat tricks, always obey mana reservation diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java index f3ca5cecf38..9cc402cd28b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java @@ -21,6 +21,7 @@ import forge.game.spellability.TargetChoices; import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; import forge.util.Aggregates; +import org.apache.commons.lang3.tuple.Pair; import java.util.List; import java.util.Map; @@ -195,6 +196,9 @@ public class DamageDealAi extends DamageAiBase { return false; } + // Try to chain damage/debuff effects + Pair chainDmg = ComputerUtilAbility.getDamageAfterChainingSpells(ai, sa, damage); + // temporarily disabled until better AI if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; @@ -216,8 +220,24 @@ public class DamageDealAi extends DamageAiBase { return false; } - if (!this.damageTargetAI(ai, sa, dmg, false)) { - return false; + // test what happens if we chain this to another damaging spell + if (chainDmg != null && ai.getController().isAI()) { + int extraDmg = chainDmg.getValue(); + if (!this.damageTargetAI(ai, sa, dmg + extraDmg, false)) { + return false; // won't play it even in chain + } else { + // we are about to decide to play this damage spell; if there's something chained to it, reserve mana for + // the second spell so we don't misplay + if (chainDmg != null && ai.getController().isAI()) { + AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); + aic.reserveManaSourcesTillNextPriority(chainDmg.getKey(), sa); + } + } + } else { + // simple targeting when there is no spell chaining plan + if (!this.damageTargetAI(ai, sa, dmg, false)) { + return false; + } } if ((damage.equals("X") && source.getSVar(damage).equals("Count$xPaid")) || @@ -238,6 +258,7 @@ public class DamageDealAi extends DamageAiBase { source.setSVar("PayX", Integer.toString(actualPay)); } } + return true; }