Skip to content

AI: only untap a permanent when it makes a concrete difference - #11373

Open
liamiak wants to merge 4 commits into
Card-Forge:masterfrom
liamiak:improve-untap-ai
Open

AI: only untap a permanent when it makes a concrete difference#11373
liamiak wants to merge 4 commits into
Card-Forge:masterfrom
liamiak:improve-untap-ai

Conversation

@liamiak

@liamiak liamiak commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

The problem

When no priority target (Time Vault-style "won't auto-untap") is present, UntapAi fell through to getMostExpensivePermanentAI — untapping the priciest tapped permanent whether or not untapping it achieved anything. For a card like Kiora's Follower that meant tapping it every turn to untap a land that untaps itself next turn regardless: motion with no value. That crudeness is why ~40 cards with a plain {T}: untap ability carry AI:RemoveDeck:All, which removes them from AI decks entirely.

The change

For free ({T}-only) untap abilities, pick a benefit-directed target — untap as late as possible, only for a concrete reason, and untap the exact card that reason points to:

  • Ramp (own main): untap a mana source if and only if untapping that specific source turns a spell in hand we can't currently afford into one we can — verified with the real mana-payment routine (canPayManaCost), so it respects the actual cost, colours included, not just mana value.
  • Blocker (opponent's turn): untap our best creature only while actually under attack.
  • Otherwise: hold.

Costed / triggered / Always-logic untaps are a deliberate investment and keep their existing behaviour byte-for-byte.

Behaviour, free untappers (before → after)

situation before after
tapped land, nothing to cast untap (waste) hold
untapping a source affords a spell untap untap that source
ramp justified, but a creature is also tapped untap the creature untap the source
source untapped still can't afford the spell untap (waste) hold
idle tapped creature, own turn untap (waste) hold
under attack untap untap (blocker)
Time Vault / won't-auto-untap untap untap (unchanged)

Cards taken off AI:RemoveDeck:All

The improved logic makes six clean {T}-untap cards playable, so this drops the flag on all of them: Aphetto Alchemist, Kiora's Follower, Unbender Tine, Vizier of Tumbling Sands, Krosan Restorer, Tidewater Minion. Each was verified to untap only for genuine ramp/blocker benefit and hold otherwise.

Kept flagged: combo/return-cost pieces, and Ith, High Arcanist, whose untap is a combat damage-prevention trick rather than the ramp/blocker pattern.

Validation

Full desktop suite green (286). A characterization pass across free and costed untappers confirms every free-untap waste case now holds, every genuine benefit still fires, and costed untappers are unchanged.

Follow-up (not in this PR)

Two things are deliberately deferred to a follow-up:

  • Costed untap cards ({X}{T}, {1}{T}, and other non-free untappers still flagged). This change only affects free {T}-only untaps, so their flags are left in place until the follow-up gives them their own benefit logic — untapping is worth real mana under a different, tighter bar than a free tap.
  • A general "untap to re-use this permanent's own tap-ability" scan (e.g. untap a Merfolk Looter to loot again) — the natural generalization of the ramp case, but it needs faithful reuse of each ability's own AI evaluation (its combat caution, timing and hand-state), which warrants its own change and test suite.

🤖 Implemented with the assistance of Claude Code (Opus).

When no priority target (Time Vault-style "won't auto-untap") was present,
UntapAi fell through to getMostExpensivePermanentAI - untapping the priciest
tapped permanent whether or not untapping it achieved anything. For a card like
Kiora's Follower that meant tapping it every turn to untap a land that untaps
itself next turn regardless: motion with no value, which is why ~40 plain
{T}: untap cards carry AI:RemoveDeck:All.

For free ({T}-only) untap abilities, pick a benefit-directed target instead, and
untap the exact card the benefit points to:

 - ramp (own main): untap a mana source iff untapping that specific source turns
   a spell in hand we cannot currently afford into one we can - checked with the
   real mana-payment routine (canPayManaCost), so it respects the actual cost,
   colours included, not just mana value;
 - blocker (opponent's turn): untap our best creature only while under attack;
 - otherwise: hold.

Costed / triggered / Always-logic untaps are a deliberate investment and keep
their existing behaviour unchanged.

With the crude waste gone, drop AI:RemoveDeck:All from six clean {T}-untap cards
verified to untap only for genuine benefit: Aphetto Alchemist, Kiora's Follower,
Unbender Tine, Vizier of Tumbling Sands, Krosan Restorer and Tidewater Minion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* check ({@link ComputerUtilMana#canPayManaCost}) against the source temporarily untapped, so it
* accounts for the actual cost - colours included - not just mana value.
*/
private static Card pickRampUntap(final Player ai, final CardCollection untapList) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code looks too unclean
also just redundant to doPoolExtraManaLogic...?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good calls, both — addressed in the last three commits.

  • Unclean: the smell was a board poke — it flipped a card's tapped state to probe affordability and restored it in a finally. Replaced with a small shared helper, untappingEnablesACast, that models the freed mana by shaving generic off a copy of the cost (ManaCostBeingPaid#decreaseShard) and asking canPayManaCost against usable sources only. No tap/untap of real permanents.
  • Redundant: you were right that it overlapped doPoolExtraManaLogic, so I routed the dorks through the same helper. That also surfaced a latent bug on the dork side: its old !hasEnoughManaSourcesToCast gate counts tapped sources, so when you're tapped out but source-rich it decided it had "enough" and declined to ramp — exactly the spot where untapping a land is what lets the spell resolve. The helper asks "can I pay with usable sources now, and would one more mana close the gap?" instead, which is a strict superset of the old firing (nothing that ramped before stops), and the whole forge.ai suite stays green.

Split into three commits — helper + generic path, then the dork routing/fix, then an optional test — so the fix and the test are separable.

liamiak1 and others added 3 commits July 25, 2026 06:26
The free ({T}-only) untap's ramp check temporarily flipped a card's tapped
state to probe affordability, restoring it in a finally block. Poking live game
state to answer a hypothetical is fragile, and it re-implemented a question the
mana logic already answers by arithmetic.

Replace it with a shared helper, untappingEnablesACast, that models the freed
mana by shaving generic off a copy of the cost (ManaCostBeingPaid#decreaseShard)
and asking ComputerUtilMana#canPayManaCost against usable sources only - no
tap/untap of the board. The generic ramp path now calls it; the mana-dorks keep
their own inline copy for now (routed through the helper in the next commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
doPoolExtraManaLogic now shares the untappingEnablesACast helper instead of
duplicating the check, removing the redundancy the free-untap path already
resolved. Sharing it also fixes a latent bug the dork side never exposed.

The old gate was !hasEnoughManaSourcesToCast, which counts *all* mana sources,
tapped included. So with, say, five lands (three already tapped from casting
this turn) and a four-drop in hand, it decided it had "enough sources" and
declined to untap - exactly the tapped-out spot where untapping a land is what
lets the four-drop resolve. The helper instead asks "can I pay with my *usable*
sources now, and would one more mana close the gap?", so the dorks fire in that
case. It is a strict superset of the old firing (source-poor still can't pay
now), so nothing that ramped before stops; whole forge.ai suite stays green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Optional guard for the shared ramp check: Kiora's Follower untaps a tapped land
when one more mana reaches a spell in hand it otherwise can't afford, and holds
when nothing becomes castable. Kept as its own commit so it can be dropped
without touching the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants