diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 35b666e0781..36bbf6542b8 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -51,6 +51,7 @@ import forge.game.player.PlayerActionConfirmMode; import forge.game.replacement.ReplaceMoved; import forge.game.replacement.ReplacementEffect; import forge.game.spellability.*; +import forge.game.staticability.StaticAbility; import forge.game.trigger.Trigger; import forge.game.trigger.TriggerType; import forge.game.trigger.WrappedAbility; @@ -670,7 +671,7 @@ public class AiController { // This is for playing spells regularly (no Cascade/Ripple etc.) private AiPlayDecision canPlayAndPayFor(final SpellAbility sa) { - boolean xCost = sa.getPayCosts().hasXInAnyCostPart(sa); + boolean xCost = sa.getPayCosts().hasXInAnyCostPart(); if (!xCost && !ComputerUtilCost.canPayCost(sa, player)) { // for most costs, it's OK to check if they can be paid early in order to avoid running a heavy API check @@ -707,6 +708,13 @@ public class AiController { return canPlaySa(((WrappedAbility) sa).getWrappedAbility()); } + // Trying to play a card that has Buyback without a Buyback cost, look for possible additional considerations + if (getBooleanProperty(AiProps.TRY_TO_PRESERVE_BUYBACK_SPELLS)) { + if (card.hasKeyword(Keyword.BUYBACK) && !sa.isBuyBackAbility() && !canPlaySpellWithoutBuyback(card, sa)) { + return AiPlayDecision.NeedsToPlayCriteriaNotMet; + } + } + // When processing a new SA, clear the previously remembered cards that have been marked to avoid re-entry // which might potentially cause a stack overflow. AiCardMemory.clearMemorySet(this, AiCardMemory.MemorySet.MARKED_TO_AVOID_REENTRY); @@ -801,10 +809,69 @@ public class AiController { if ("True".equals(card.getSVar("NonStackingEffect")) && isNonDisabledCardInPlay(card.getName())) { return AiPlayDecision.NeedsToPlayCriteriaNotMet; } + // add any other necessary logic to play a basic spell here return ComputerUtilCard.checkNeedsToPlayReqs(card, sa); } + private boolean canPlaySpellWithoutBuyback(Card card, SpellAbility sa) { + boolean wasteBuybackAllowed = false; + + // About to lose game : allow + if (ComputerUtil.aiLifeInDanger(player, true, 0)) { + wasteBuybackAllowed = true; + } + + int copies = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(card.getName())).size(); + // Have two copies : allow + if (copies >= 2) { + wasteBuybackAllowed = true; + } + + int neededMana = 0; + boolean dangerousRecurringCost = false; + for (SpellAbility sa2 : GameActionUtil.getOptionalCosts(sa)) { + if (sa2.isOptionalCostPaid(OptionalCost.Buyback)) { + Cost sac = sa2.getPayCosts(); + CostAdjustment.adjust(sac, sa2); + if (sac.getCostMana() != null) { + neededMana = sac.getCostMana().getMana().getCMC(); + } + if (sac.hasSpecificCostType(CostPayLife.class) + || sac.hasSpecificCostType(CostDiscard.class) + || sac.hasSpecificCostType(CostSacrifice.class)) { + dangerousRecurringCost = true; + } + } + } + + // won't be able to afford buyback any time soon + // if Buyback cost includes sacrifice, life, discard + if (dangerousRecurringCost) { + wasteBuybackAllowed = true; + } + + // Memory Crystal-like effects need special handling + for (Card c : game.getCardsIn(ZoneType.Battlefield)) { + for (StaticAbility s : c.getStaticAbilities()) { + if ("ReduceCost".equals(s.getParam("Mode")) + && "Spell.Buyback".equals(s.getParam("ValidSpell"))) { + neededMana -= AbilityUtils.calculateAmount(c, s.getParam("Amount"), s); + } + } + } + if (neededMana < 0) { + neededMana = 0; + } + + int hasMana = ComputerUtilMana.getAvailableManaEstimate(player, false); + if (hasMana < neededMana - 1) { + wasteBuybackAllowed = true; + } + + return wasteBuybackAllowed; + } + // not sure "playing biggest spell" matters? private final static Comparator saComparator = new Comparator() { @Override diff --git a/forge-ai/src/main/java/forge/ai/AiProps.java b/forge-ai/src/main/java/forge/ai/AiProps.java index 23b3b791fe6..1f8b56a9720 100644 --- a/forge-ai/src/main/java/forge/ai/AiProps.java +++ b/forge-ai/src/main/java/forge/ai/AiProps.java @@ -58,6 +58,7 @@ public enum AiProps { /** */ THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER ("135"), /** */ THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER ("110"), /** */ CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL ("true"), /** */ + TRY_TO_PRESERVE_BUYBACK_SPELLS ("true"), /** */ MIN_SPELL_CMC_TO_COUNTER ("0"), /** */ CHANCE_TO_COUNTER_CMC_1 ("50"), /** */ CHANCE_TO_COUNTER_CMC_2 ("75"), /** */ diff --git a/forge-game/src/main/java/forge/game/cost/Cost.java b/forge-game/src/main/java/forge/game/cost/Cost.java index 5a48859876a..1d7606922c0 100644 --- a/forge-game/src/main/java/forge/game/cost/Cost.java +++ b/forge-game/src/main/java/forge/game/cost/Cost.java @@ -935,19 +935,17 @@ public class Cost implements Serializable { return true; } - public boolean hasXInAnyCostPart(SpellAbility sa) { + public boolean hasXInAnyCostPart() { boolean xCost = false; - if (sa.getPayCosts() != null) { - for (CostPart p : sa.getPayCosts().getCostParts()) { - if (p instanceof CostPartMana) { - if (((CostPartMana) p).getAmountOfX() > 0) { - xCost = true; - break; - } - } else if (p.getAmount().equals("X")) { + for (CostPart p : this.getCostParts()) { + if (p instanceof CostPartMana) { + if (((CostPartMana) p).getAmountOfX() > 0) { xCost = true; break; } + } else if (p.getAmount().equals("X")) { + xCost = true; + break; } } return xCost; diff --git a/forge-gui/res/ai/Cautious.ai b/forge-gui/res/ai/Cautious.ai index 0e732f97963..05b1d3460d7 100644 --- a/forge-gui/res/ai/Cautious.ai +++ b/forge-gui/res/ai/Cautious.ai @@ -244,6 +244,10 @@ BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF=3 # library to put some into the graveyard. INTUITION_ALTERNATIVE_LOGIC=true +# If enabled, the AI will run some additional checks in order to try to preserve spells that have Buyback and not +# use them unless absolutely necessary (or unless multiple copies are in hand). +TRY_TO_PRESERVE_BUYBACK_SPELLS=true + # How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the # card on top of the library EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=2 diff --git a/forge-gui/res/ai/Default.ai b/forge-gui/res/ai/Default.ai index 8e03095a670..e00c218cdf6 100644 --- a/forge-gui/res/ai/Default.ai +++ b/forge-gui/res/ai/Default.ai @@ -245,6 +245,10 @@ BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF=3 # library to put some into the graveyard. INTUITION_ALTERNATIVE_LOGIC=true +# If enabled, the AI will run some additional checks in order to try to preserve spells that have Buyback and not +# use them unless absolutely necessary (or unless multiple copies are in hand). +TRY_TO_PRESERVE_BUYBACK_SPELLS=true + # How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the # card on top of the library EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=2 diff --git a/forge-gui/res/ai/Experimental.ai b/forge-gui/res/ai/Experimental.ai index b5f8624f8a2..5b9048770f5 100644 --- a/forge-gui/res/ai/Experimental.ai +++ b/forge-gui/res/ai/Experimental.ai @@ -245,6 +245,10 @@ BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF=5 # library to put some into the graveyard. INTUITION_ALTERNATIVE_LOGIC=true +# If enabled, the AI will run some additional checks in order to try to preserve spells that have Buyback and not +# use them unless absolutely necessary (or unless multiple copies are in hand). +TRY_TO_PRESERVE_BUYBACK_SPELLS=true + # How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the # card on top of the library EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=2 diff --git a/forge-gui/res/ai/Reckless.ai b/forge-gui/res/ai/Reckless.ai index 188cf8199a9..4641fe37b0b 100644 --- a/forge-gui/res/ai/Reckless.ai +++ b/forge-gui/res/ai/Reckless.ai @@ -245,6 +245,10 @@ BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF=3 # library to put some into the graveyard. INTUITION_ALTERNATIVE_LOGIC=true +# If enabled, the AI will run some additional checks in order to try to preserve spells that have Buyback and not +# use them unless absolutely necessary (or unless multiple copies are in hand). +TRY_TO_PRESERVE_BUYBACK_SPELLS=false + # How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the # card on top of the library EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=1 diff --git a/forge-gui/src/main/java/forge/match/input/InputAttack.java b/forge-gui/src/main/java/forge/match/input/InputAttack.java index cf46d7c3e17..d8955dcfafe 100644 --- a/forge-gui/src/main/java/forge/match/input/InputAttack.java +++ b/forge-gui/src/main/java/forge/match/input/InputAttack.java @@ -84,7 +84,7 @@ public class InputAttack extends InputSyncronizedBase { } private void updatePrompt() { - String alphaLabel = canCallBackAttackers() ? "Call Back" : "AlphaStrike"; + String alphaLabel = canCallBackAttackers() ? "Call Back" : "Alpha Strike"; getController().getGui().updateButtons(getOwner(), "OK", alphaLabel, true, true, true); }