From f16a7ff0b0be6f71e1bea401b29b17fe2d1b4e68 Mon Sep 17 00:00:00 2001 From: Seravy Date: Sun, 18 Feb 2018 15:32:14 +0100 Subject: [PATCH 01/11] incomplete --- .../src/main/java/forge/ai/AiController.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 65a73a93cda..464a05b9bed 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -703,6 +703,36 @@ public class AiController { if ("True".equals(card.getSVar("NonStackingEffect")) && isNonDisabledCardInPlay(card.getName())) { return AiPlayDecision.NeedsToPlayCriteriaNotMet; } + // Trying to play a card that has Buyback without a Buyback cost + if (card.hasKeyword("Buyback")) { // This line does not work? + if (!sa.isBuyBackAbility()) { + boolean wastebuybackallowed = false; + // About to lose game : allow + if (ComputerUtil.aiLifeInDanger(player, true, 0)) { + wastebuybackallowed = true; + } + int copies = 0; + // Have two copies : allow + for (Card hand : player.getCardsIn(ZoneType.Hand)) { + if (hand.getName().equals(card.getName())) { + copies++; + } + } + if (copies >= 2) { + wastebuybackallowed = true; + } + // Won't be able to afford buyback any time soon + /* if ( ComputerUtilMana.getAvailableManaEstimate(player) >= + sa. + + + ) { wastebuybackallowed = true; } */ + + // If Buyback cost includes sacrifice, life, discard + + if (!wastebuybackallowed) return AiPlayDecision.NeedsToPlayCriteriaNotMet; + } + } // add any other necessary logic to play a basic spell here return ComputerUtilCard.checkNeedsToPlayReqs(card, sa); } From 537259989f659d292b484097583c09fd5bdb2fd0 Mon Sep 17 00:00:00 2001 From: Seravy Date: Sun, 18 Feb 2018 16:47:29 +0100 Subject: [PATCH 02/11] AI will avoid wasting a BUYBACK card without paying the buyback cost when able --- .../src/main/java/forge/ai/AiController.java | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 464a05b9bed..7e9c6d31502 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -704,7 +704,9 @@ public class AiController { return AiPlayDecision.NeedsToPlayCriteriaNotMet; } // Trying to play a card that has Buyback without a Buyback cost - if (card.hasKeyword("Buyback")) { // This line does not work? + + if (card.hasStartOfKeyword("Buyback")) { + //if (card.getBuybackAbility()!=null) { if (!sa.isBuyBackAbility()) { boolean wastebuybackallowed = false; // About to lose game : allow @@ -722,13 +724,23 @@ public class AiController { wastebuybackallowed = true; } // Won't be able to afford buyback any time soon - /* if ( ComputerUtilMana.getAvailableManaEstimate(player) >= - sa. - - - ) { wastebuybackallowed = true; } */ - // If Buyback cost includes sacrifice, life, discard + int neededMana = 0; + for (SpellAbility sa2 : GameActionUtil.getOptionalCosts(sa)) { + if (sa2.isOptionalCostPaid(OptionalCost.Buyback)) { + neededMana = sa2.getPayCosts().getCostMana().getMana().getCMC(); + if (sa2.getPayCosts().hasSpecificCostType(CostPayLife.class) + || (sa2.getPayCosts().hasSpecificCostType(CostDiscard.class)) || + (sa2.getPayCosts().hasSpecificCostType(CostSacrifice.class)) + ) { + neededMana = 999; + } + } + } + int hasmana = ComputerUtilMana.getAvailableManaEstimate(player, false); + if (hasmana < neededMana - 1) { + wastebuybackallowed = true; + } if (!wastebuybackallowed) return AiPlayDecision.NeedsToPlayCriteriaNotMet; } From cbc0335d901972ac6e099536b363e3113dd60936 Mon Sep 17 00:00:00 2001 From: Seravy Date: Sun, 18 Feb 2018 18:43:23 +0100 Subject: [PATCH 03/11] Trying to get effective cost instead of original cost but doesn't seem to recognize Memory Crystal anyway. I guess it's not that important. --- forge-ai/src/main/java/forge/ai/AiController.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 7e9c6d31502..836bc320731 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -728,10 +728,12 @@ public class AiController { int neededMana = 0; for (SpellAbility sa2 : GameActionUtil.getOptionalCosts(sa)) { if (sa2.isOptionalCostPaid(OptionalCost.Buyback)) { - neededMana = sa2.getPayCosts().getCostMana().getMana().getCMC(); - if (sa2.getPayCosts().hasSpecificCostType(CostPayLife.class) - || (sa2.getPayCosts().hasSpecificCostType(CostDiscard.class)) || - (sa2.getPayCosts().hasSpecificCostType(CostSacrifice.class)) + Cost sac = sa2.getPayCosts(); + CostAdjustment.adjust(sac, sa2); // Does not recognize Memory Crystal anyway??? + neededMana = sac.getCostMana().getMana().getCMC(); + if (sac.hasSpecificCostType(CostPayLife.class) + || (sac.hasSpecificCostType(CostDiscard.class)) || + (sac.hasSpecificCostType(CostSacrifice.class)) ) { neededMana = 999; } From e3b4c59b9f7f51307a63c6064a40f8882c708dfb Mon Sep 17 00:00:00 2001 From: Seravy Date: Mon, 19 Feb 2018 12:50:15 +0000 Subject: [PATCH 04/11] Update AiController.java --- forge-ai/src/main/java/forge/ai/AiController.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 836bc320731..3e86c849cfb 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -733,8 +733,7 @@ public class AiController { neededMana = sac.getCostMana().getMana().getCMC(); if (sac.hasSpecificCostType(CostPayLife.class) || (sac.hasSpecificCostType(CostDiscard.class)) || - (sac.hasSpecificCostType(CostSacrifice.class)) - ) { + (sac.hasSpecificCostType(CostSacrifice.class))) { neededMana = 999; } } From 3346cbf0c5dc05a74ec7eb130f27135281265ad5 Mon Sep 17 00:00:00 2001 From: Seravy Date: Mon, 19 Feb 2018 17:05:56 +0000 Subject: [PATCH 05/11] Update AiController.java --- forge-ai/src/main/java/forge/ai/AiController.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 3e86c849cfb..0a9f51bd98d 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -708,10 +708,10 @@ public class AiController { if (card.hasStartOfKeyword("Buyback")) { //if (card.getBuybackAbility()!=null) { if (!sa.isBuyBackAbility()) { - boolean wastebuybackallowed = false; + boolean wasteBuybackAllowed = false; // About to lose game : allow if (ComputerUtil.aiLifeInDanger(player, true, 0)) { - wastebuybackallowed = true; + wasteBuybackAllowed = true; } int copies = 0; // Have two copies : allow @@ -721,7 +721,7 @@ public class AiController { } } if (copies >= 2) { - wastebuybackallowed = true; + wasteBuybackAllowed = true; } // Won't be able to afford buyback any time soon // If Buyback cost includes sacrifice, life, discard @@ -740,10 +740,10 @@ public class AiController { } int hasmana = ComputerUtilMana.getAvailableManaEstimate(player, false); if (hasmana < neededMana - 1) { - wastebuybackallowed = true; + wasteBuybackAllowed = true; } - if (!wastebuybackallowed) return AiPlayDecision.NeedsToPlayCriteriaNotMet; + if (!wasteBuybackAllowed) return AiPlayDecision.NeedsToPlayCriteriaNotMet; } } // add any other necessary logic to play a basic spell here From b3c8901aa1a7aa488d6f38c30b6d08dd5c31b05c Mon Sep 17 00:00:00 2001 From: Seravy Date: Mon, 19 Feb 2018 17:08:09 +0000 Subject: [PATCH 06/11] Update AiController.java --- forge-ai/src/main/java/forge/ai/AiController.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 0a9f51bd98d..4cba38a474f 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -713,13 +713,8 @@ public class AiController { if (ComputerUtil.aiLifeInDanger(player, true, 0)) { wasteBuybackAllowed = true; } - int copies = 0; + int copies = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(card.getName())).size(); // Have two copies : allow - for (Card hand : player.getCardsIn(ZoneType.Hand)) { - if (hand.getName().equals(card.getName())) { - copies++; - } - } if (copies >= 2) { wasteBuybackAllowed = true; } From b54e8461b8948323d9fda2e617758d1163b1dfe0 Mon Sep 17 00:00:00 2001 From: Seravy Date: Mon, 19 Feb 2018 17:10:13 +0000 Subject: [PATCH 07/11] Update AiController.java --- forge-ai/src/main/java/forge/ai/AiController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 4cba38a474f..aece8718126 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -733,8 +733,8 @@ public class AiController { } } } - int hasmana = ComputerUtilMana.getAvailableManaEstimate(player, false); - if (hasmana < neededMana - 1) { + int hasMana = ComputerUtilMana.getAvailableManaEstimate(player, false); + if (hasMana < neededMana - 1) { wasteBuybackAllowed = true; } From ae5090a69a05d46a96e85e2282d3a3747ca9fec7 Mon Sep 17 00:00:00 2001 From: Agetian Date: Tue, 4 Dec 2018 14:33:46 +0300 Subject: [PATCH 08/11] - More careful use of Buyback spells without Buyback. --- .../src/main/java/forge/ai/AiController.java | 86 +++++++++++-------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 4b708ec5863..537a7666e93 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -801,48 +801,62 @@ public class AiController { if ("True".equals(card.getSVar("NonStackingEffect")) && isNonDisabledCardInPlay(card.getName())) { return AiPlayDecision.NeedsToPlayCriteriaNotMet; } + // Trying to play a card that has Buyback without a Buyback cost - - if (card.hasStartOfKeyword("Buyback")) { - //if (card.getBuybackAbility()!=null) { - if (!sa.isBuyBackAbility()) { - 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; - } - // Won't be able to afford buyback any time soon - // If Buyback cost includes sacrifice, life, discard - int neededMana = 0; - for (SpellAbility sa2 : GameActionUtil.getOptionalCosts(sa)) { - if (sa2.isOptionalCostPaid(OptionalCost.Buyback)) { - Cost sac = sa2.getPayCosts(); - CostAdjustment.adjust(sac, sa2); // Does not recognize Memory Crystal anyway??? - neededMana = sac.getCostMana().getMana().getCMC(); - if (sac.hasSpecificCostType(CostPayLife.class) - || (sac.hasSpecificCostType(CostDiscard.class)) || - (sac.hasSpecificCostType(CostSacrifice.class))) { - neededMana = 999; - } - } - } - int hasMana = ComputerUtilMana.getAvailableManaEstimate(player, false); - if (hasMana < neededMana - 1) { - wasteBuybackAllowed = true; - } - - if (!wasteBuybackAllowed) return AiPlayDecision.NeedsToPlayCriteriaNotMet; - } + if (card.hasKeyword(Keyword.BUYBACK) && !sa.isBuyBackAbility() && !canPlaySpellWithoutBuyback(card, sa)) { + 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); // TODO: Does not recognize Memory Crystal + 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; + } + + 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 From 4d1b7a9aec47634647320d46aa917544d607a3b3 Mon Sep 17 00:00:00 2001 From: Agetian Date: Tue, 4 Dec 2018 14:51:30 +0300 Subject: [PATCH 09/11] - Logic improvement (Buyback) --- .../src/main/java/forge/ai/AiController.java | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 537a7666e93..68a5cc08e52 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; @@ -707,6 +708,11 @@ public class AiController { return canPlaySa(((WrappedAbility) sa).getWrappedAbility()); } + // Trying to play a card that has Buyback without a Buyback cost + 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); @@ -802,11 +808,6 @@ public class AiController { return AiPlayDecision.NeedsToPlayCriteriaNotMet; } - // Trying to play a card that has Buyback without a Buyback cost - if (card.hasKeyword(Keyword.BUYBACK) && !sa.isBuyBackAbility() && !canPlaySpellWithoutBuyback(card, sa)) { - return AiPlayDecision.NeedsToPlayCriteriaNotMet; - } - // add any other necessary logic to play a basic spell here return ComputerUtilCard.checkNeedsToPlayReqs(card, sa); } @@ -823,7 +824,6 @@ public class AiController { // Have two copies : allow if (copies >= 2) { wasteBuybackAllowed = true; - } int neededMana = 0; @@ -831,7 +831,7 @@ public class AiController { for (SpellAbility sa2 : GameActionUtil.getOptionalCosts(sa)) { if (sa2.isOptionalCostPaid(OptionalCost.Buyback)) { Cost sac = sa2.getPayCosts(); - CostAdjustment.adjust(sac, sa2); // TODO: Does not recognize Memory Crystal + CostAdjustment.adjust(sac, sa2); if (sac.getCostMana() != null) { neededMana = sac.getCostMana().getMana().getCMC(); } @@ -849,6 +849,19 @@ public class AiController { 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; From a2ca811136f038185117f5cea9356aeaaeeb857d Mon Sep 17 00:00:00 2001 From: Agetian Date: Tue, 4 Dec 2018 16:11:08 +0300 Subject: [PATCH 10/11] - Minor logic tweak (Buyback) - hasXInAnyCostPart doesn't need a SA parameter now that it's in Cost. - Fixed a minor mistype on mobile Forge. --- .../src/main/java/forge/ai/AiController.java | 4 ++-- .../src/main/java/forge/game/cost/Cost.java | 16 +++++++--------- .../main/java/forge/match/input/InputAttack.java | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 68a5cc08e52..cdc86dd4c0f 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -671,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 @@ -708,7 +708,7 @@ public class AiController { return canPlaySa(((WrappedAbility) sa).getWrappedAbility()); } - // Trying to play a card that has Buyback without a Buyback cost + // Trying to play a card that has Buyback without a Buyback cost, look for possible additional considerations if (card.hasKeyword(Keyword.BUYBACK) && !sa.isBuyBackAbility() && !canPlaySpellWithoutBuyback(card, sa)) { return AiPlayDecision.NeedsToPlayCriteriaNotMet; } 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/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); } From 1d7188b8f5471202bcc9c5918378dadb6750d7ea Mon Sep 17 00:00:00 2001 From: Agetian Date: Tue, 4 Dec 2018 16:17:03 +0300 Subject: [PATCH 11/11] - Minor logic tweak (Buyback) + AI property - hasXInAnyCostPart doesn't need a SA parameter now that it's in Cost. - Fixed a minor mistype on mobile Forge. --- forge-ai/src/main/java/forge/ai/AiController.java | 6 ++++-- forge-ai/src/main/java/forge/ai/AiProps.java | 1 + forge-gui/res/ai/Cautious.ai | 4 ++++ forge-gui/res/ai/Default.ai | 4 ++++ forge-gui/res/ai/Experimental.ai | 4 ++++ forge-gui/res/ai/Reckless.ai | 4 ++++ 6 files changed, 21 insertions(+), 2 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index cdc86dd4c0f..36bbf6542b8 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -709,8 +709,10 @@ public class AiController { } // Trying to play a card that has Buyback without a Buyback cost, look for possible additional considerations - if (card.hasKeyword(Keyword.BUYBACK) && !sa.isBuyBackAbility() && !canPlaySpellWithoutBuyback(card, sa)) { - return AiPlayDecision.NeedsToPlayCriteriaNotMet; + 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 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-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