From 1ae45d3cf1b15f9f71713d55dc8372a20a5d230e Mon Sep 17 00:00:00 2001 From: liamiak Date: Sun, 2 Aug 2026 10:50:57 -0600 Subject: [PATCH 1/3] AI: don't let a RemoveCounterAll sub-ability veto its own parent RemoveCounterAll maps to CannotPlayAi, which refuses in chkDrawback too. As a sub-ability that takes the parent down with it: Oblivion Stone's wipe ends by clearing the fate counters it just checked, so DestroyAllAi said play it and the bookkeeping step said no. Alaundo the Seer's tap ability is dead the same way, and it is unflagged. 17 of the 19 cards reaching this API use it as a sub-ability, and each is a consequence of its parent rather than a decision. canPlay still refuses, so nothing gains a new main ability. Co-Authored-By: Claude Opus 5 --- .../src/main/java/forge/ai/SpellApiToAi.java | 2 +- .../forge/ai/ability/CountersRemoveAllAi.java | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 forge-ai/src/main/java/forge/ai/ability/CountersRemoveAllAi.java diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java index 7921f9b61600..998420cb2b4b 100644 --- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java @@ -158,7 +158,7 @@ public enum SpellApiToAi { .put(ApiType.Regenerate, RegenerateAi.class) .put(ApiType.Regeneration, AlwaysPlayAi.class) .put(ApiType.RemoveCounter, CountersRemoveAi.class) - .put(ApiType.RemoveCounterAll, CannotPlayAi.class) + .put(ApiType.RemoveCounterAll, CountersRemoveAllAi.class) .put(ApiType.RemoveFromCombat, RemoveFromCombatAi.class) .put(ApiType.RemoveFromGame, AlwaysPlayAi.class) .put(ApiType.RemoveFromMatch, AlwaysPlayAi.class) diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAllAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAllAi.java new file mode 100644 index 000000000000..38eaf7aa7f79 --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAllAi.java @@ -0,0 +1,26 @@ +package forge.ai.ability; + +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; +import forge.ai.SpellAbilityAi; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; + +/** + * The AI has no logic for choosing to remove all counters of a kind, so it still will not activate + * one on purpose. As a sub-ability it is a consequence of an effect the AI has already decided it + * wants - usually bookkeeping, like Oblivion Stone clearing the fate counters it just checked - so + * it must not veto its own parent. + */ +public class CountersRemoveAllAi extends SpellAbilityAi { + + @Override + protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + + @Override + public AiAbilityDecision chkDrawback(Player aiPlayer, SpellAbility sa) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } +} From a4f233b8f2caff81ada5c5586316e60d9838ac74 Mon Sep 17 00:00:00 2001 From: liamiak Date: Sun, 2 Aug 2026 10:50:58 -0600 Subject: [PATCH 2/3] AI: seed Oblivion Stone's fate counters at the opponent's end step Both abilities cost {T}, so seeding on the AI's own turn costs it the wipe. At the opponent's end step the tap is free - the AI untaps first - and the mana is spare by then. AITgts keeps it off the Stone itself, which is sacrificed as a cost and gone before the wipe checks fate counters, and off lands, which the wipe does not destroy. Without it the AI protected the Stone 6/6. Unflags Oblivion Stone. Co-Authored-By: Claude Opus 5 --- forge-gui/res/cardsfolder/o/oblivion_stone.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/forge-gui/res/cardsfolder/o/oblivion_stone.txt b/forge-gui/res/cardsfolder/o/oblivion_stone.txt index 49920c056fcc..33b49ee0901e 100644 --- a/forge-gui/res/cardsfolder/o/oblivion_stone.txt +++ b/forge-gui/res/cardsfolder/o/oblivion_stone.txt @@ -1,8 +1,7 @@ Name:Oblivion Stone ManaCost:3 Types:Artifact -A:AB$ PutCounter | Cost$ 4 T | ValidTgts$ Permanent | CounterType$ FATE | CounterNum$ 1 | SpellDescription$ Put a fate counter on target permanent. +A:AB$ PutCounter | Cost$ 4 T | ValidTgts$ Permanent | CounterType$ FATE | CounterNum$ 1 | AILogic$ AtOppEOT | AITgts$ Permanent.YouCtrl+Other+nonLand | SpellDescription$ Put a fate counter on target permanent. A:AB$ DestroyAll | Cost$ 5 T Sac<1/CARDNAME> | ValidCards$ Permanent.nonLand+counters_LT1_FATE | SubAbility$ DBRemove | SpellDescription$ Destroy each nonland permanent without a fate counter on it, then remove all fate counters from all permanents. SVar:DBRemove:DB$ RemoveCounterAll | ValidCards$ Permanent | CounterType$ FATE | AllCounters$ True -AI:RemoveDeck:All Oracle:{4}, {T}: Put a fate counter on target permanent.\n{5}, {T}, Sacrifice Oblivion Stone: Destroy each nonland permanent without a fate counter on it, then remove all fate counters from all permanents. From 58c27c51a62dcf1c48ab7053684f101a603ab2fc Mon Sep 17 00:00:00 2001 From: liamiak Date: Sun, 2 Aug 2026 10:50:58 -0600 Subject: [PATCH 3/3] Test for the AI changes above Co-Authored-By: Claude Opus 5 --- .../ability/RemoveCounterAllDrawbackTest.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 forge-gui-desktop/src/test/java/forge/ai/ability/RemoveCounterAllDrawbackTest.java diff --git a/forge-gui-desktop/src/test/java/forge/ai/ability/RemoveCounterAllDrawbackTest.java b/forge-gui-desktop/src/test/java/forge/ai/ability/RemoveCounterAllDrawbackTest.java new file mode 100644 index 000000000000..3c4f5e03f54a --- /dev/null +++ b/forge-gui-desktop/src/test/java/forge/ai/ability/RemoveCounterAllDrawbackTest.java @@ -0,0 +1,47 @@ +package forge.ai.ability; + +import forge.ai.AITest; +import forge.game.Game; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.zone.ZoneType; +import org.testng.annotations.Test; + +import static junit.framework.Assert.assertEquals; + +/** + * RemoveCounterAll has no AI, so as a sub-ability it used to refuse and take its parent down with + * it. Oblivion Stone's wipe ends by clearing the fate counters it just checked, which meant the + * wipe itself could never be activated. + */ +public class RemoveCounterAllDrawbackTest extends AITest { + + @Test + public void bookkeepingSubAbilityDoesNotVetoTheWipe() { + Game game = initAndCreateGame(); + Player ai = game.getPlayers().get(1); + Player opp = game.getPlayers().get(0); + ai.setTeam(0); + opp.setTeam(1); + + for (int i = 0; i < 8; i++) { + addCard("Wastes", ai); + } + fillLibrary(ai, 15); + fillLibrary(opp, 15); + // nothing of the AI's own is in range, so the wipe is pure upside + addCard("Grizzly Bears", opp); + addCard("Grizzly Bears", opp); + addCard("Grizzly Bears", opp); + addCard("Oblivion Stone", ai); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, ai); + game.getAction().checkStateEffects(true); + playUntilNextTurn(game); + + assertEquals("it sacrificed Oblivion Stone to wipe", 1, + countCardsWithName(game, "Oblivion Stone", ZoneType.Graveyard)); + assertEquals("the opponent's board is gone", 0, + countCardsWithName(game, "Grizzly Bears", ZoneType.Battlefield)); + } +}