Skip to content

Fix AI implementation of Toxic Deluge and Flowstone Slide#11337

Open
liamiak wants to merge 1 commit into
Card-Forge:masterfrom
liamiak:fix-ai-toxic-deluge-flowstone-slide
Open

Fix AI implementation of Toxic Deluge and Flowstone Slide#11337
liamiak wants to merge 1 commit into
Card-Forge:masterfrom
liamiak:fix-ai-toxic-deluge-flowstone-slide

Conversation

@liamiak

@liamiak liamiak commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Toxic Deluge and Flowstone Slide both carry AI:RemoveDeck:All. The reason is that the AI cannot cast either of them at all — not that it plays them badly.

Why

Their X is announced as a cost, and nothing chooses it.

PumpAll has an AI class, so the generic X handling in AiController (which only fires for abilities with no AI class, and only for X in the mana cost) is skipped. Toxic Deluge's X lives in a PayLife<X> cost, which that path would not have covered anyway. With X unset, both pump amounts read 0 in PumpAllAi.checkApiLogic, the sweep evaluates as -0/-0, and it is always declined.

Toxic Deluge was also missing IsCurse$ True. SpellAbility.isCurse() is just hasParam("IsCurse"), so without it PumpAllAi never reached its sweeper branch and instead asked whether to pump the AI's own creatures. The majority of negative-NumDef PumpAll cards already carry the param.

What changed

PumpAllAi chooses X for a -X/-X sweep when the ability announces its own X, scores candidates by what each value destroys, and records it via setXManaCostPaid. The existing curse evaluation still makes the final call. The X ceiling already worked — ComputerUtilCost.setMaxXValue reaches CostPayLife.getMaxAmountX — so nothing new was needed in forge-game.

Candidates are the toughnesses actually on the battlefield rather than every point up to the maximum, so the AI does not walk a 40 point life total one step at a time.

Also:

  • Life spent is priced against how much of the total is going, and a sweep is skipped if what survives it could then kill the AI.
  • +X/-X hands survivors power until end of turn, so Flowstone Slide is measured against whoever still has a combat coming. On an opponent's turn their survivors can swing back; on the AI's own turn that boost expires long before they untap, so it is not counted against the cast.
  • If some X clears an opponent's blockers and leaves enough power to finish them off this turn, that X is taken regardless of card value — mirroring the shortcut already in DamageAllAi.
  • A mass pump lands on every opponent at once, so the evaluation weighs all of them rather than just the strongest. Two player games are unaffected, since there the two are the same list.

The "dies to -X/-X" filter existed in three copies; they now share one predicate on PumpAiBase, which is why that file shows more deletions than additions.

Deliberately out of scope

Triggers and sub-abilities are excluded from X selection, because X was already paid when the spell was cast. Without that guard The Meathook Massacre gets refused outright; there is a test for it.

Bane of the Living has the same underlying problem and is not fixed here. Its X belongs to a morph cost handled by SetStateAi, which returns early for TurnFaceUp before reaching any X selection, and sizing it properly means evaluating the turn-face-up trigger's effect from there. That is a different subsystem and belongs in its own change.

Testing

To see the original bug: put Toxic Deluge in an AI deck, give the opponent a few creatures, and watch it never get cast.

PumpAllAiTest covers the four seams — that the AI casts the card at all, that a mass sweep counts every opponent's board, that an expiring power boost does not scare it off a good sweep, and that an already-paid X on a trigger is left alone. Each was confirmed to fail without its corresponding fix.

Full run against current master: 244 tests green across PumpAllAiTest, the simulation suites, and the AI ability and combat tests.


Written with Claude Code (Opus 4.8), per the AI coding agent guidance in CONTRIBUTING.md; also recorded as a commit co-author.

Both cards carried AI:RemoveDeck:All because the AI could not cast them
at all: their X is announced as a cost, and nothing ever chose it.

PumpAll has an AI class, so the generic X handling in AiController is
skipped, and Toxic Deluge's X lives in a PayLife<X> cost that path would
not have seen anyway. With X unset both pump amounts read 0, so the
sweep evaluated as -0/-0 and was always declined. Toxic Deluge was also
missing IsCurse$ True, which meant PumpAllAi never reached its sweeper
branch and instead asked whether to pump the AI's own creatures.

PumpAllAi now picks X for a -X/-X sweep when the ability announces its
own X, scoring candidates by the creatures each value destroys, and
records it with setXManaCostPaid. Candidates are the toughnesses
actually on the battlefield rather than every point up to the maximum,
so the AI does not walk a 40 point life total one step at a time.

Alongside that:

- Life spent is priced against how much of the total is going, and a
  sweep is skipped if what survives it could then kill the AI.
- +X/-X hands survivors power until end of turn, so Flowstone Slide is
  measured against whoever still has a combat coming: on an opponent's
  turn their survivors can swing back, on the AI's own turn that boost
  has expired long before they untap.
- If some X clears an opponent's blockers and leaves enough power to
  finish them off this turn, that X is taken regardless of card value,
  mirroring the existing shortcut in DamageAllAi.
- A mass pump lands on every opponent at once, so the evaluation now
  weighs all of them instead of just the strongest. Two player games are
  unaffected.

The "dies to -X/-X" test existed in three copies; they now share one
predicate on PumpAiBase.

Triggers and sub-abilities are deliberately excluded, since X was
already paid when the spell was cast. The Meathook Massacre would
otherwise be refused, and the test covers that.

Bane of the Living has the same underlying problem but is not fixed
here: its X belongs to a morph cost handled by SetStateAi, which returns
early for TurnFaceUp before choosing X, and sizing it needs the
trigger's effect to be evaluated from there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Hanmac Hanmac added the AI General AI tag label Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI General AI tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants