From 9c41f3a00b30b6d11fe57e1f047023b251504c60 Mon Sep 17 00:00:00 2001 From: Vanja Date: Tue, 25 Aug 2026 12:32:13 +0200 Subject: [PATCH 1/2] Improve reveal-count AI decisions --- .../src/main/java/forge/ai/AiController.java | 33 ++++++++-------- .../main/java/forge/ai/SpellAbilityAi.java | 4 ++ .../main/java/forge/ai/ability/RevealAi.java | 38 +++++++++++++++++++ forge-gui/res/cardsfolder/b/brine_seer.txt | 2 +- forge-gui/res/cardsfolder/c/cinder_seer.txt | 2 +- forge-gui/res/cardsfolder/i/ivy_seer.txt | 2 +- forge-gui/res/cardsfolder/j/jasmine_seer.txt | 2 +- .../res/cardsfolder/n/nightshade_seer.txt | 2 +- .../cardsfolder/p/phosphorescent_feast.txt | 2 +- .../res/cardsfolder/r/rofelloss_gift.txt | 2 +- .../res/cardsfolder/s/scent_of_brine.txt | 2 +- .../res/cardsfolder/s/scent_of_cinder.txt | 2 +- forge-gui/res/cardsfolder/s/scent_of_ivy.txt | 2 +- .../res/cardsfolder/s/scent_of_jasmine.txt | 2 +- .../res/cardsfolder/s/scent_of_nightshade.txt | 4 +- 15 files changed, 73 insertions(+), 28 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index c45a4720d908..b1ce3d274715 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -820,27 +820,30 @@ private AiPlayDecision canPlayAndPayFor(final SpellAbility sa) { if (altHost == null) { return AiPlayDecision.CantPlaySa; } - altHost.setCastSA(sa); } else if (!sa.canPlay()) { return AiPlayDecision.CantPlaySa; } - // state needs to be switched here so API checks evaluate the right face - if (host != altHost) { - sa.setHostCard(altHost); - } - - AiPlayDecision decision = canPlayAndPayForFace(sa); - - if (host != altHost) { - sa.setHostCard(host); + if (sa.getApi() != null) { + altHost = SpellApiToAi.Converter.get(sa).getAiEvaluationHost(player, sa, altHost); } - - if (sa.isSpell()) { - altHost.setCastSA(null); + try { + if (sa.isSpell()) { + altHost.setCastSA(sa); + } + // state needs to be switched here so API checks evaluate the right face + if (host != altHost) { + sa.setHostCard(altHost); + } + return canPlayAndPayForFace(sa); + } finally { + if (host != altHost) { + sa.setHostCard(host); + } + if (sa.isSpell()) { + altHost.setCastSA(null); + } } - - return decision; } // This is for playing spells regularly (no Cascade/Ripple etc.) diff --git a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java index ebd9307587af..d6e9ec5fff1f 100644 --- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java @@ -65,6 +65,10 @@ public final AiAbilityDecision canPlayWithSubs(final Player aiPlayer, final Spel return chkDrawbackWithSubs(aiPlayer, subAb); } + protected Card getAiEvaluationHost(final Player ai, final SpellAbility sa, final Card host) { + return host; + } + /** * Handles the AI decision to play a "main" SpellAbility */ diff --git a/forge-ai/src/main/java/forge/ai/ability/RevealAi.java b/forge-ai/src/main/java/forge/ai/ability/RevealAi.java index 2740efd4b51e..ddbbea5bf43d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RevealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RevealAi.java @@ -6,15 +6,23 @@ import forge.ai.PlayerControllerAi; import forge.game.ability.AbilityUtils; import forge.game.card.Card; +import forge.game.card.CardCollection; +import forge.game.card.CardCopyService; +import forge.game.card.CardLists; import forge.game.cost.Cost; import forge.game.player.Player; import forge.game.spellability.Spell; import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; public class RevealAi extends RevealAiBase { @Override protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { + if (isRememberedSelfRevealAnyNumber(sa) && getRevealableCards(ai, sa).isEmpty()) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + if (!revealHandTargetAI(ai, sa, false)) { return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } @@ -26,6 +34,36 @@ protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa return super.checkApiLogic(ai, sa); } + @Override + protected Card getAiEvaluationHost(final Player ai, final SpellAbility sa, final Card host) { + if (!isRememberedSelfRevealAnyNumber(sa)) { + return host; + } + + final CardCollection cards = getRevealableCards(ai, sa); + if (cards.isEmpty()) { + return host; + } + + final Card projectedHost = CardCopyService.getLKICopy(host); + projectedHost.addRemembered(cards); + return projectedHost; + } + + private static boolean isRememberedSelfRevealAnyNumber(final SpellAbility sa) { + return sa.hasParam("AnyNumber") && sa.hasParam("RememberRevealed") && !sa.usesTargeting() + && (!sa.hasParam("Defined") || "You".equals(sa.getParam("Defined"))); + } + + private static CardCollection getRevealableCards(final Player ai, final SpellAbility sa) { + final CardCollection cards = sa.hasParam("RevealValid") + ? CardLists.getValidCards(ai.getCardsIn(ZoneType.Hand), sa.getParam("RevealValid"), + ai, sa.getHostCard(), sa) + : new CardCollection(ai.getCardsIn(ZoneType.Hand)); + cards.remove(sa.getHostCard()); + return cards; + } + @Override protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) { // logic to see if it should reveal Miracle Card diff --git a/forge-gui/res/cardsfolder/b/brine_seer.txt b/forge-gui/res/cardsfolder/b/brine_seer.txt index 1a8b0375055b..9e120acb6d41 100644 --- a/forge-gui/res/cardsfolder/b/brine_seer.txt +++ b/forge-gui/res/cardsfolder/b/brine_seer.txt @@ -6,5 +6,5 @@ A:AB$ Reveal | Cost$ 2 U T | Defined$ You | RevealValid$ Card.Blue | AnyNumber$ SVar:DBCounter:DB$ Counter | TargetType$ Spell | TgtPrompt$ Select target spell | ValidTgts$ Card | UnlessCost$ Y | SubAbility$ DBCleanup SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:Y:Remembered$Amount -AI:RemoveDeck:All +AI:RemoveDeck:Random Oracle:{2}{U}, {T}: Reveal any number of blue cards in your hand. Counter target spell unless its controller pays {1} for each card revealed this way. diff --git a/forge-gui/res/cardsfolder/c/cinder_seer.txt b/forge-gui/res/cardsfolder/c/cinder_seer.txt index 2d35bdba1f57..98a05dc52190 100644 --- a/forge-gui/res/cardsfolder/c/cinder_seer.txt +++ b/forge-gui/res/cardsfolder/c/cinder_seer.txt @@ -6,5 +6,5 @@ A:AB$ Reveal | Cost$ 2 R T | Defined$ You | RevealValid$ Card.Red | AnyNumber$ T SVar:DBCinderDamage:DB$ DealDamage | ValidTgts$ Any | NumDmg$ X | SubAbility$ DBCinderCleanup SVar:X:Remembered$Amount SVar:DBCinderCleanup:DB$ Cleanup | ClearRemembered$ True -AI:RemoveDeck:All +AI:RemoveDeck:Random Oracle:{2}{R}, {T}: Reveal any number of red cards in your hand. Cinder Seer deals X damage to any target, where X is the number of cards revealed this way. diff --git a/forge-gui/res/cardsfolder/i/ivy_seer.txt b/forge-gui/res/cardsfolder/i/ivy_seer.txt index 7f045a36f988..e847af04b6a0 100644 --- a/forge-gui/res/cardsfolder/i/ivy_seer.txt +++ b/forge-gui/res/cardsfolder/i/ivy_seer.txt @@ -6,5 +6,5 @@ A:AB$ Reveal | Cost$ 2 G T | Defined$ You | RevealValid$ Card.Green | AnyNumber$ SVar:DBIvyPump:DB$ Pump | ValidTgts$ Creature | NumAtt$ +IvyX | NumDef$ +IvyX | SubAbility$ DBIvyCleanup SVar:IvyX:Remembered$Amount SVar:DBIvyCleanup:DB$ Cleanup | ClearRemembered$ True -AI:RemoveDeck:All +AI:RemoveDeck:Random Oracle:{2}{G}, {T}: Reveal any number of green cards in your hand. Target creature gets +X/+X until end of turn, where X is the number of cards revealed this way. diff --git a/forge-gui/res/cardsfolder/j/jasmine_seer.txt b/forge-gui/res/cardsfolder/j/jasmine_seer.txt index e5cca11fe556..6dd9c8c7977a 100644 --- a/forge-gui/res/cardsfolder/j/jasmine_seer.txt +++ b/forge-gui/res/cardsfolder/j/jasmine_seer.txt @@ -6,5 +6,5 @@ A:AB$ Reveal | Cost$ 2 W T | Defined$ You | RevealValid$ Card.White | AnyNumber$ SVar:DBJasmineLife:DB$ GainLife | LifeAmount$ JasmineX | SubAbility$ DBJasmineCleanup SVar:JasmineX:Remembered$Amount/Twice SVar:DBJasmineCleanup:DB$ Cleanup | ClearRemembered$ True -AI:RemoveDeck:All +AI:RemoveDeck:Random Oracle:{2}{W}, {T}: Reveal any number of white cards in your hand. You gain 2 life for each card revealed this way. diff --git a/forge-gui/res/cardsfolder/n/nightshade_seer.txt b/forge-gui/res/cardsfolder/n/nightshade_seer.txt index 1780877b6b65..19c227a072de 100644 --- a/forge-gui/res/cardsfolder/n/nightshade_seer.txt +++ b/forge-gui/res/cardsfolder/n/nightshade_seer.txt @@ -6,5 +6,5 @@ A:AB$ Reveal | Cost$ 2 B T | Defined$ You | RevealValid$ Card.Black | AnyNumber$ SVar:DBNightshadePump:DB$ Pump | ValidTgts$ Creature | NumAtt$ -NightshadeX | NumDef$ -NightshadeX | IsCurse$ True | SubAbility$ DBNightshadeCleanup SVar:DBNightshadeCleanup:DB$ Cleanup | ClearRemembered$ True SVar:NightshadeX:Remembered$Amount -AI:RemoveDeck:All +AI:RemoveDeck:Random Oracle:{2}{B}, {T}: Reveal any number of black cards in your hand. Target creature gets -X/-X until end of turn, where X is the number of cards revealed this way. diff --git a/forge-gui/res/cardsfolder/p/phosphorescent_feast.txt b/forge-gui/res/cardsfolder/p/phosphorescent_feast.txt index 4a5a335a73ee..7fc455a32bc5 100644 --- a/forge-gui/res/cardsfolder/p/phosphorescent_feast.txt +++ b/forge-gui/res/cardsfolder/p/phosphorescent_feast.txt @@ -5,5 +5,5 @@ A:SP$ Reveal | AnyNumber$ True | RememberRevealed$ True | SubAbility$ DBGainLife SVar:DBGainLife:DB$ GainLife | LifeAmount$ X | SubAbility$ DBCleanup SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:X:Remembered$ChromaSource.Green/Twice -AI:RemoveDeck:All +AI:RemoveDeck:Random Oracle:Chroma — Reveal any number of cards in your hand. You gain 2 life for each green mana symbol in those cards' mana costs. diff --git a/forge-gui/res/cardsfolder/r/rofelloss_gift.txt b/forge-gui/res/cardsfolder/r/rofelloss_gift.txt index f042be618d25..73d0b2c18c64 100644 --- a/forge-gui/res/cardsfolder/r/rofelloss_gift.txt +++ b/forge-gui/res/cardsfolder/r/rofelloss_gift.txt @@ -5,5 +5,5 @@ A:SP$ Reveal | RevealValid$ Card.Green+YouCtrl | AnyNumber$ True | RememberRevea SVar:DBChangeZone:DB$ ChangeZone | Hidden$ True | Mandatory$ True | ChangeType$ Card.Enchantment+YouOwn | ChangeNum$ X | Origin$ Graveyard | Destination$ Hand | SubAbility$ DBCleanup SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:X:Remembered$Amount -AI:RemoveDeck:All +AI:RemoveDeck:Random Oracle:Reveal any number of green cards in your hand. Return an enchantment card from your graveyard to your hand for each card revealed this way. diff --git a/forge-gui/res/cardsfolder/s/scent_of_brine.txt b/forge-gui/res/cardsfolder/s/scent_of_brine.txt index 6bf2d96495a2..d4a78a996411 100644 --- a/forge-gui/res/cardsfolder/s/scent_of_brine.txt +++ b/forge-gui/res/cardsfolder/s/scent_of_brine.txt @@ -5,5 +5,5 @@ A:SP$ Reveal | Defined$ You | RevealValid$ Card.Blue | AnyNumber$ True | Remembe SVar:DBScentOfBrineCounter:DB$ Counter | TargetType$ Spell | TgtPrompt$ Select target spell | ValidTgts$ Card | UnlessCost$ ScentOfBrineX | SubAbility$ DBScentOfBrineCleanup SVar:DBScentOfBrineCleanup:DB$ Cleanup | ClearRemembered$ True SVar:ScentOfBrineX:Remembered$Amount -AI:RemoveDeck:All +AI:RemoveDeck:Random Oracle:Reveal any number of blue cards in your hand. Counter target spell unless its controller pays {1} for each card revealed this way. diff --git a/forge-gui/res/cardsfolder/s/scent_of_cinder.txt b/forge-gui/res/cardsfolder/s/scent_of_cinder.txt index 0e00be7cf2d9..30ba9e40d234 100644 --- a/forge-gui/res/cardsfolder/s/scent_of_cinder.txt +++ b/forge-gui/res/cardsfolder/s/scent_of_cinder.txt @@ -5,5 +5,5 @@ A:SP$ Reveal | Defined$ You | RevealValid$ Card.Red | AnyNumber$ True | Remember SVar:DBScentOfCinderDamage:DB$ DealDamage | ValidTgts$ Any | NumDmg$ ScentOfCinderX | SubAbility$ DBScentOfCinderCleanup SVar:ScentOfCinderX:Remembered$Amount SVar:DBScentOfCinderCleanup:DB$ Cleanup | ClearRemembered$ True -AI:RemoveDeck:All +AI:RemoveDeck:Random Oracle:Reveal any number of red cards in your hand. Scent of Cinder deals X damage to any target, where X is the number of cards revealed this way. diff --git a/forge-gui/res/cardsfolder/s/scent_of_ivy.txt b/forge-gui/res/cardsfolder/s/scent_of_ivy.txt index 4d2fd9ea2644..f8714871473c 100644 --- a/forge-gui/res/cardsfolder/s/scent_of_ivy.txt +++ b/forge-gui/res/cardsfolder/s/scent_of_ivy.txt @@ -5,5 +5,5 @@ A:SP$ Reveal | Defined$ You | RevealValid$ Card.Green | AnyNumber$ True | Rememb SVar:DBScentOfIvyPump:DB$ Pump | ValidTgts$ Creature | NumAtt$ +ScentOfIvyX | NumDef$ +ScentOfIvyX | SubAbility$ DBScentOfIvyCleanup SVar:ScentOfIvyX:Remembered$Amount SVar:DBScentOfIvyCleanup:DB$ Cleanup | ClearRemembered$ True -AI:RemoveDeck:All +AI:RemoveDeck:Random Oracle:Reveal any number of green cards in your hand. Target creature gets +X/+X until end of turn, where X is the number of cards revealed this way. diff --git a/forge-gui/res/cardsfolder/s/scent_of_jasmine.txt b/forge-gui/res/cardsfolder/s/scent_of_jasmine.txt index 230fff3c3d37..24c6490948f4 100644 --- a/forge-gui/res/cardsfolder/s/scent_of_jasmine.txt +++ b/forge-gui/res/cardsfolder/s/scent_of_jasmine.txt @@ -5,5 +5,5 @@ A:SP$ Reveal | Defined$ You | RevealValid$ Card.White | AnyNumber$ True | Rememb SVar:DBScentOfJasmineLife:DB$ GainLife | LifeAmount$ ScentOfJasmineX | SubAbility$ DBScentOfJasmineCleanup SVar:ScentOfJasmineX:Remembered$Amount/Twice SVar:DBScentOfJasmineCleanup:DB$ Cleanup | ClearRemembered$ True -AI:RemoveDeck:All +AI:RemoveDeck:Random Oracle:Reveal any number of white cards in your hand. You gain 2 life for each card revealed this way. diff --git a/forge-gui/res/cardsfolder/s/scent_of_nightshade.txt b/forge-gui/res/cardsfolder/s/scent_of_nightshade.txt index 56da31dce2ca..b947eee3755e 100644 --- a/forge-gui/res/cardsfolder/s/scent_of_nightshade.txt +++ b/forge-gui/res/cardsfolder/s/scent_of_nightshade.txt @@ -2,8 +2,8 @@ Name:Scent of Nightshade ManaCost:1 B Types:Instant A:SP$ Reveal | Defined$ You | RevealValid$ Card.Black | AnyNumber$ True | RememberRevealed$ True | SubAbility$ DBScentOfNightshadePump | SpellDescription$ Reveal any number of black cards in your hand. Target creature gets -X/-X until end of turn, where X is the number of cards revealed this way. -SVar:DBScentOfNightshadePump:DB$ Pump | ValidTgts$ Creature | NumAtt$ -ScentOfNightshadeX | NumDef$ -ScentOfNightshadeX | SubAbility$ DBScentOfNightshadeCleanup +SVar:DBScentOfNightshadePump:DB$ Pump | ValidTgts$ Creature | NumAtt$ -ScentOfNightshadeX | NumDef$ -ScentOfNightshadeX | IsCurse$ True | SubAbility$ DBScentOfNightshadeCleanup SVar:DBScentOfNightshadeCleanup:DB$ Cleanup | ClearRemembered$ True SVar:ScentOfNightshadeX:Remembered$Amount -AI:RemoveDeck:All +AI:RemoveDeck:Random Oracle:Reveal any number of black cards in your hand. Target creature gets -X/-X until end of turn, where X is the number of cards revealed this way. From 2b4c86434dc5c9facd2739eb9bac53059ddd52ad Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sat, 29 Aug 2026 17:51:14 +0200 Subject: [PATCH 2/2] Clean up --- docs/Card-scripting-API/AbilityFactory.md | 4 +-- .../main/java/forge/ai/AiAbilityDecision.java | 9 +++---- .../src/main/java/forge/ai/AiController.java | 23 +++++++---------- .../src/main/java/forge/ai/ComputerUtil.java | 14 +++-------- .../main/java/forge/ai/SpellAbilityAi.java | 25 +++++++++++++++---- .../java/forge/ai/ability/CountersPutAi.java | 2 +- .../java/forge/ai/ability/LifeGainAi.java | 8 ++---- .../main/java/forge/ai/ability/RevealAi.java | 25 +++++-------------- .../java/forge/game/cost/CostAdjustment.java | 4 +++ .../StaticAbilityCantTarget.java | 5 ---- 10 files changed, 51 insertions(+), 68 deletions(-) diff --git a/docs/Card-scripting-API/AbilityFactory.md b/docs/Card-scripting-API/AbilityFactory.md index bb90cde961cd..925abe484cca 100644 --- a/docs/Card-scripting-API/AbilityFactory.md +++ b/docs/Card-scripting-API/AbilityFactory.md @@ -83,8 +83,8 @@ Only needed if the card text doesn't contain the type/color words but the abilit ## Generic AI params - `IsCurse$ True` - for effects that are normally treated positive e.g. Pump - `AICheckSVar$ {Count}` -- `AILogic$ {String}` -- `AITgts$ BetterThanEvalRating.130` -Normally the AI will only prefer targeting cards that satisfy the constraint. However, you can add `AITgtsStrict$ True` if playing it should only happen when enough of these cards are available, e.g. *Rootwater Matriarch*. +- `AILogic$ {String}` - this is a special param since it supports some globally available values, however many AI API classes also have very individual logic paths which will be mentioned in their sections +- `AITgts$ BetterThanEvalRating.130` - Normally the AI will only prefer targeting cards that satisfy the constraint. However, you can add `AITgtsStrict$ True` if playing it should only happen when enough of these cards are available, e.g. *Rootwater Matriarch*. # Factories (in Alphabetical Order) diff --git a/forge-ai/src/main/java/forge/ai/AiAbilityDecision.java b/forge-ai/src/main/java/forge/ai/AiAbilityDecision.java index f3434385801d..952aff8d3201 100644 --- a/forge-ai/src/main/java/forge/ai/AiAbilityDecision.java +++ b/forge-ai/src/main/java/forge/ai/AiAbilityDecision.java @@ -6,14 +6,13 @@ import forge.game.player.Player; import forge.game.spellability.SpellAbility; -public record AiAbilityDecision(int rating, AiPlayDecision decision, SpellAbility sa) { +public record AiAbilityDecision(int rating, AiPlayDecision decision) { private static int MIN_RATING = 30; - public AiAbilityDecision(int rating, AiPlayDecision decision) { - this(rating, decision, null); - } - public boolean willingToPlay() { + return willingToPlay(null); + } + public boolean willingToPlay(SpellAbility sa) { if (!decision.willingToPlay()) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index b1ce3d274715..93d105302da0 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -813,35 +813,30 @@ public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean private AiPlayDecision canPlayAndPayFor(final SpellAbility sa) { final Card host = sa.getHostCard(); - Card altHost = host; if (sa instanceof Spell sp) { - altHost = sp.canPlayFromHost(); + Card altHost = sp.canPlayFromHost(); if (altHost == null) { return AiPlayDecision.CantPlaySa; } + // state needs to be switched here so API checks evaluate the right face + if (host != altHost) { + sa.setHostCard(altHost); + } + altHost.setCastSA(sa); } else if (!sa.canPlay()) { return AiPlayDecision.CantPlaySa; } - if (sa.getApi() != null) { - altHost = SpellApiToAi.Converter.get(sa).getAiEvaluationHost(player, sa, altHost); - } try { - if (sa.isSpell()) { - altHost.setCastSA(sa); - } - // state needs to be switched here so API checks evaluate the right face - if (host != altHost) { - sa.setHostCard(altHost); - } return canPlayAndPayForFace(sa); } finally { - if (host != altHost) { + // in addition to engine some AI api can also switch host + if (sa.getHostCard() != host) { sa.setHostCard(host); } if (sa.isSpell()) { - altHost.setCastSA(null); + host.setCastSA(null); } } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index 2646a5458ec6..695eb256c46b 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -1381,7 +1381,8 @@ public static boolean activateForCost(SpellAbility sa, final Player ai) { } if (abCost.hasTapCost() && source.hasSVar("AITapDown")) { return true; - } else if (sa.getRootAbility().isPwAbility() && ai.getGame().getPhaseHandler().is(PhaseType.MAIN2)) { + } + if (sa.getRootAbility().isPwAbility() && ai.getGame().getPhaseHandler().is(PhaseType.MAIN2)) { for (final CostPart part : sa.getRootAbility().getPayCosts().getCostParts()) { if (part instanceof CostPutCounter) { return part.convertAmount() == null || part.convertAmount() > 0 || ai.isCardInPlay("Carth the Lion"); @@ -1390,16 +1391,7 @@ public static boolean activateForCost(SpellAbility sa, final Player ai) { } for (final CostPart part : abCost.getCostParts()) { if (part instanceof CostSacrifice sac) { - if (sac.payCostFromSource()) { - if (source.getSVar("SacMe").equals("6")) { - return true; - } else if (shouldSacrificeThreatenedCard(ai, source, sa)) { - return true; - } - continue; - } - - final CardCollection typeList = + final List typeList = sac.payCostFromSource() ? List.of(source) : CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), sac.getType(), source.getController(), source, sa); for (Card c : typeList) { if (c.getSVar("SacMe").equals("6")) { diff --git a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java index d6e9ec5fff1f..a3057b5115ba 100644 --- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java @@ -13,6 +13,7 @@ import forge.game.GameEntity; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; +import forge.game.card.CardCollection; import forge.game.card.CardCopyService; import forge.game.card.CardState; import forge.game.card.CounterType; @@ -65,10 +66,6 @@ public final AiAbilityDecision canPlayWithSubs(final Player aiPlayer, final Spel return chkDrawbackWithSubs(aiPlayer, subAb); } - protected Card getAiEvaluationHost(final Player ai, final SpellAbility sa, final Card host) { - return host; - } - /** * Handles the AI decision to play a "main" SpellAbility */ @@ -161,7 +158,8 @@ protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { if ("Never".equals(aiLogic)) { return false; - } else if ("Once".equals(aiLogic)) { + } + if ("Once".equals(aiLogic)) { return !sa.getHostCard().getAbilityActivatedThisTurn().getActivators(sa).contains(ai); } return true; @@ -276,6 +274,7 @@ public AiAbilityDecision chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) { } if (subAb == null) { + // TODO this should result in the average rating of each decision return decision; } @@ -507,4 +506,20 @@ protected static boolean playReusable(final Player ai, final SpellAbility sa) { return phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai); } + + protected boolean setAiEvaluationHost(final SpellAbility sa, final CardCollection remember) { + if (sa.isTrigger() || sa.isCastFromPlayEffect()) { + // reset not supported yet + return false; + } + Card host = sa.getHostCard(); + if (!host.isLKI()) { + host = CardCopyService.getLKICopy(host); + sa.getRootAbility().setHostCard(host); + } + if (remember != null) { + host.addRemembered(remember); + } + return true; + } } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java index 1e4ea9c27b43..5a38f62666b1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -776,7 +776,7 @@ protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean && amount == 0 // And counter amount wasn't set previously by something (e.g. Wildborn Preserver) && sa.hasSVar(amountStr) && sa.getSVar(amountStr).equals("Count$xPaid")) { // Spend all remaining mana to add X counters (eg. Hero of Leina Tower) - int payX = ComputerUtilCost.setMaxXValue(sa, ai, true); + ComputerUtilCost.setMaxXValue(sa, ai, true); } if (!mandatory) { diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java index c3af218e6f79..2518d1675921 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java @@ -79,7 +79,6 @@ protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final Game game = ai.getGame(); final int life = ai.getLife(); final String aiLogic = sa.getParamOrDefault("AILogic", ""); - boolean activateForCost = ComputerUtil.activateForCost(sa, ai); boolean lifeCritical = life <= 5; lifeCritical |= ph.getPhase().isBefore(PhaseType.COMBAT_DAMAGE) @@ -102,10 +101,7 @@ protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, } // Sacrificing in response to something dangerous is generally good in any phase - boolean isSacCost = false; - if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) { - isSacCost = true; - } + boolean isSacCost = sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class); // Don't use lifegain before main 2 if possible if (!lifeCritical && ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases") @@ -113,7 +109,7 @@ protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, return false; } - return lifeCritical || activateForCost + return lifeCritical || ComputerUtil.activateForCost(sa, ai) || (ph.getNextTurn().equals(ai) && !ph.getPhase().isBefore(PhaseType.END_OF_TURN)) || sa.hasParam("PlayerTurn") || isSorcerySpeed(sa, ai); } diff --git a/forge-ai/src/main/java/forge/ai/ability/RevealAi.java b/forge-ai/src/main/java/forge/ai/ability/RevealAi.java index ddbbea5bf43d..04b7a1db70a1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RevealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RevealAi.java @@ -7,7 +7,6 @@ import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardCollection; -import forge.game.card.CardCopyService; import forge.game.card.CardLists; import forge.game.cost.Cost; import forge.game.player.Player; @@ -19,8 +18,12 @@ public class RevealAi extends RevealAiBase { @Override protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { - if (isRememberedSelfRevealAnyNumber(sa) && getRevealableCards(ai, sa).isEmpty()) { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + if (isRememberedSelfRevealAnyNumber(sa)) { + CardCollection revealable = getRevealableCards(ai, sa); + if (revealable.isEmpty()) { + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); + } + setAiEvaluationHost(sa, revealable); } if (!revealHandTargetAI(ai, sa, false)) { @@ -34,22 +37,6 @@ protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa return super.checkApiLogic(ai, sa); } - @Override - protected Card getAiEvaluationHost(final Player ai, final SpellAbility sa, final Card host) { - if (!isRememberedSelfRevealAnyNumber(sa)) { - return host; - } - - final CardCollection cards = getRevealableCards(ai, sa); - if (cards.isEmpty()) { - return host; - } - - final Card projectedHost = CardCopyService.getLKICopy(host); - projectedHost.addRemembered(cards); - return projectedHost; - } - private static boolean isRememberedSelfRevealAnyNumber(final SpellAbility sa) { return sa.hasParam("AnyNumber") && sa.hasParam("RememberRevealed") && !sa.usesTargeting() && (!sa.hasParam("Defined") || "You".equals(sa.getParam("Defined"))); diff --git a/forge-game/src/main/java/forge/game/cost/CostAdjustment.java b/forge-game/src/main/java/forge/game/cost/CostAdjustment.java index 80c6147815f7..b836db573377 100644 --- a/forge-game/src/main/java/forge/game/cost/CostAdjustment.java +++ b/forge-game/src/main/java/forge/game/cost/CostAdjustment.java @@ -352,6 +352,10 @@ private static void adjustCostByConvokeOrImprovise(ManaCostBeingPaid cost, final untappedCards = CardLists.filter(untappedCards, CardPredicates.CREATURES); } + if (untappedCards.isEmpty()) { + return; + } + Map convokedCards = payer.getController().chooseCardsForConvokeOrImprovise(sa, cost.toManaCost(), untappedCards, artifacts, creatures, maxReduction); diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantTarget.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantTarget.java index f96c20095bb7..5330d2057ab1 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantTarget.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantTarget.java @@ -32,13 +32,8 @@ import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; -/** - * The Class StaticAbilityCantTarget. - */ public class StaticAbilityCantTarget { - static String MODE = "CantTarget"; - public static StaticAbility cantTarget(final GameEntity entity, final SpellAbility spellAbility) { final Game game = entity.getGame(); for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {