AI: make the untap-for-ramp logic fire, and point it at the mana source - #11373
AI: make the untap-for-ramp logic fire, and point it at the mana source#11373liamiak wants to merge 6 commits into
Conversation
doPoolExtraManaLogic gated on hasEnoughManaSourcesToCast, which counts potential mana sources including tapped ones, then asked whether the cost minus the freed mana was payable using only usable sources. Two notions of available mana in one test, so the case the logic exists for - plenty of lands, tapped out, untapping one reaches the spell - was the one it declined. untapPrefTargeting then fell through to getMostExpensivePermanentAI, so even once the decision was right the target was chosen by mana value rather than by the reason. It now asks getBestLandAI when the reason is mana. The opponent's-turn clause untapped unconditionally; it now checks that we are holding something castable at that timing, which is what its own comment always said it was for. That check deliberately stays off the mana solver: it runs on every step past declare blockers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With the ramp logic working for cards that untap any permanent, the same AILogic covers them, so AI:RemoveDeck:All comes off Kiora's Follower, Unbender Tine, Vizier of Tumbling Sands, Tidewater Minion and Krosan Restorer. That flag suppresses every ability of a card, not just the untap, so Krosan Restorer's threshold ability and Tidewater Minion's Debuff were checked separately - the threshold ability needed the logic too, or it fired and untapped lands for nothing. Argothian Elder and Ley Weaver were never flagged and untap two lands today whether or not it helps; they get the logic for the same reason. Aphetto Alchemist stays flagged: it untaps artifacts and creatures, never lands, so the ramp channel does not reach it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three of the four fail without the preceding two commits: ramping when tapped out but source rich, untapping the mana source rather than the biggest tapped permanent, and holding on the opponent's turn with nothing to hold up. The fourth guards the new gate against over-firing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
25bcafc to
cca159d
Compare
| PT:2/2 | ||
| A:AB$ Untap | Cost$ T | ValidTgts$ Permanent.Other | TgtPrompt$ Select another target permanent. | SpellDescription$ Untap another target permanent. | ||
| AI:RemoveDeck:All | ||
| A:AB$ Untap | Cost$ T | ValidTgts$ Permanent.Other | TgtPrompt$ Select another target permanent. | AILogic$ PoolExtraMana | SpellDescription$ Untap another target permanent. |
There was a problem hiding this comment.
not sure ignoring the other card types is better in general
also I don't really like how the logic just guesses AI wants to pay with the extra shard, I need to think if it makes sense to synchronize it with ComputerUtilMana somewhow
There was a problem hiding this comment.
You were right, and it's worse than it looked — thanks for pushing on it.
Two separate things were narrowing these cards.
"Mana source" was spelled "land". The target choice filtered on LANDS_PRODUCING_MANA, so a tapped Llanowar Elves or Signet was invisible even though reusing one is the same ramp. Now CardPredicates.PRODUCES_MANA with getBestAI, which detectPriorityUntapTargets a few lines up already uses for mixed lists.
The bigger one: the AILogic was vetoing untaps that are worth making anyway. checkAiLogic runs before checkApiLogic, so when doPoolExtraManaLogic declined, detectPriorityUntapTargets never ran at all. Measured: with AILogic$ PoolExtraMana a Kiora's Follower will not untap a tapped Time Vault; without the tag it untaps it and takes the extra turn. The gate is now additive — doPoolExtraManaLogic(...) || hasPriorityUntapTarget(...) — so mana is a reason to untap rather than the only one.
Timing note: my first version of that check cost about a fifth of whole chooseSpellAbilityToPlay on a wide board, because getTargetableCards ran over everything. Filtering on tapped first brings it back to the noise floor.
The eleven land-only carriers are unaffected — their ValidTgts re-imposes Land, and on an all-land list the new predicate and getBestAI select exactly what the old ones did.
On the shard guess: that arithmetic and its TODO are already on master, I only moved them — not defending it, and happy to leave it untouched while you think about the ComputerUtilMana side. One data point in case it helps: since it only shaves GENERIC, a {G} one-drop is never predicted; I had to use a two-drop to make the gate fire in a test.
There was a problem hiding this comment.
Still undecided but some notes:
- we would save the runtime cost of two extra payability checks per SA
- since priority order for these pool abilities should be rather low most likely other SA costs have been checked before them in that priority window
- might also make it easier to eventually remove the limit for permanent spells since we know AI considered the API playable (though some safety checks still run after
canPayCost)
- might also make it easier to eventually remove the limit for permanent spells since we know AI considered the API playable (though some safety checks still run after
- since priority order for these pool abilities should be rather low most likely other SA costs have been checked before them in that priority window
- but also lose the knowledge that running
checkApiLogicin isolation gives us a "fully aware" decision- then again it's not really a hard contract, might find a way to cover both
- there is already some precedent of API checks mixing things via
AiCardMemory - still need to compare with related
ManaAi#doManaRitualLogic
There was a problem hiding this comment.
I went and did the doManaRitualLogic comparison — the premise splits between the two.
ManaRitual is evaluated after the hand: saComparator exempts ApiType.Mana from the "cast 0 mana cost spells first (might be a Mox)" hoist, so the AI has genuinely costed the hand first (measured order: Sengir Vampire, Nightmare, Bog Wraith, Hill Giant, then Dark Ritual).
The untappers are hoisted to the front of the window — a {T} ability has total mana 0 and isn't ApiType.Mana. Measured, the untap is the only SA evaluated, so there's no prior knowledge yet to reuse.
Prototyped both directions (25 permanents, 5-card hand, configs interleaved in one JVM):
(a) order + memory — add PoolExtraMana to that ApiType.Mana exception; record CantAfford/CantAffordX hosts in a new AiCardMemory set cleared next to HELD_MANA_SOURCES_FOR_NEXT_SPELL; the scan only narrows on it — the payability check still decides, and an empty set falls back to the full scan, so checkApiLogic in isolation still computes its own answer. Gate 3.8 → 1.0 ms, 10 → 3 solver calls (reachable); 3.3 → 1.7 ms, 10 → 5 (unreachable).
(b) prefilter — no ordering change: skip a spell before any solver call when cmc > getAvailableManaEstimate(ai, true) + extraGeneric, i.e. ManaRitual's arithmetic as a pre-filter for this exact check. Gate → 1.7 ms / 4 calls and 0.7 ms / 2 calls. Doesn't touch the isolation question at all; the cost is that the estimate is colour-blind and can under-count, so it can miss a ramp.
UntapAiTest passes 7/7 under either as default; full suite 362/0/6, same as baseline. Whole-phase timing I can't call — two runs disagreed on the sign.
Happy to land either separately from this PR, or to leave it entirely while you think about the ComputerUtilMana side. (b) is five lines and independent of everything you're weighing; (a) is the one that gives each logic the half it's missing.
There was a problem hiding this comment.
thanks, admittedly I did not expect saComparator to also move all 0 cmc abilities to the front 🤦♂️
I will check if that's really the best default behaviour
There was a problem hiding this comment.
notably it's a 14 year old change which includes more than its comment claims :/
- getting a better option for an activated ability by casting a permanent first seems smart
- but AI would then also prefer using mana for Assassinate even if King's Assassin is available too
so which order is more useful seems more like a highly situational 50/50 instead of an obvious heuristic?
though maybe the downside of wasting mana is too high 🤔
There was a problem hiding this comment.
Tested the ordering question rather than argue it, and it goes against my own option (a). Same board as rampsWhenTappedOutButSourceRich, but with both a Centaur Courser {2}{G} (needs the untap) and a Grizzly Bears {1}{G} (doesn't) in hand:
| untapped? | cast | |
|---|---|---|
| today | yes | Centaur Courser |
| (a) untapper last | no | Grizzly Bears - Courser stranded |
| (b) prefilter | yes | Centaur Courser |
Evaluated after the hand, the AI takes the affordable 2-drop and the untap never happens. So the hoist is doing real work for this logic - please drop (a). (b) is the five-liner that doesn't touch ordering at all. None of the seven UntapAiTest cases holds both an affordable and an unaffordable spell, which is why this never surfaced.
For the wider question: AIActivateLast$ True is checked in compareEvaluator above the 0-cost hoist, so cards that lose under the default can already opt out - only Vivien of the Arkbow uses it today. And the priority loop (PhaseHandler ~1049) keeps asking until the player passes, so activating a free ability first doesn't forfeit the spell; it's cast on the next pass.
There was a problem hiding this comment.
the problem with such tests is that they're pretty arbitrary:
if AI had another one drop available you could instead have support to go wide which is another viable strategy (more bodies to overwhelm opponent defense)
@tehdiplomat
your opinion on always using 0 cmc activated abilities first?
There was a problem hiding this comment.
Your #11642 looks like it answered this: the 0 cmc hoist stayed, the comment moved to "use 0 cmc abilities first (might be a Mox)" which is what it actually does, and going wide is now a TODO alongside it. I am reading that as settled unless you meant otherwise.
On that basis option (a) is dropped - the ordering change and the memory set with it - which is also where the tests you called arbitrary were pointing, so those go too. That leaves (b), the five-line prefilter that does not touch ordering at all.
The PoolExtraMana target choice filtered on LANDS_PRODUCING_MANA, so a tapped mana creature or artifact was invisible to it even though reusing one is the same ramp. Add CardPredicates.PRODUCES_MANA and pick with getBestAI, which handles a mixed permanent list. The two battlefield lookups in the gate were the same filter twice, so they now share a helper. Land-only carriers are unaffected: their ValidTgts re-imposes Land, and on an all-land list the new predicate and getBestAI select what the old ones did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
checkAiLogic runs before checkApiLogic, so when doPoolExtraManaLogic declined, detectPriorityUntapTargets never ran at all. A Kiora's Follower would not untap a tapped Time Vault; without the AILogic it untaps it and takes the extra turn. Make the gate additive so mana is a reason to untap rather than the only one. Tapped-ness is checked before targetability: filtering the other way round put getTargetableCards over the whole board and cost about a fifth of chooseSpellAbilityToPlay on a wide one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One test per behaviour: reusing a mana creature when no land is in play, freeing a Time Vault with no mana reason, and holding the tap when there is no source to reuse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Separately, and independent of the ordering question above: there is a colour form of the reach check ready, which settles the TODO in Untapping a source hands back that source's colour, not a generic mana, so a spell held up on coloured pips never shows as reachable - I have not pushed it here, because it delegates to |
AILogic$ PoolExtraManais meant to let the AI tap a Voyaging Satyr to untap a land and reach abigger spell. Two things stop it working.
The gate counts the wrong mana.
doPoolExtraManaLogicaskshasEnoughManaSourcesToCast, whichcounts potential sources including tapped ones, and then asks whether the cost minus the freed mana
is payable using only sources it can actually use. Two notions of available mana in one test, so the
case the card exists for — plenty of lands, tapped out, untapping one reaches the spell — is exactly
the case it declines. Measured, Voyaging Satyr with a
{2}{G}in hand:The decision never reaches the target chooser.
untapPrefTargetingfalls through togetMostExpensivePermanentAI. On a land-only untapper you can't tell, because every legal target isa land; on one that untaps any permanent it takes the biggest creature instead of the land. It now
asks
getBestLandAIwhen the reason is mana.With both fixed the same AILogic covers cards that untap any permanent, so this drops
AI:RemoveDeck:Allfrom Kiora's Follower, Unbender Tine, Vizier of Tumbling Sands, Tidewater Minionand Krosan Restorer, and adds the logic to Argothian Elder and Ley Weaver, which are unflagged and
untap two lands today whether or not it helps. Aphetto Alchemist stays flagged: it untaps artifacts
and creatures, never lands.
The opponent's-turn clause untapped unconditionally. It now checks that we hold something castable
at that timing, which is what its own comment always said it was for.
Cost. The gate swap makes one of these cards' main-2 decision about 22% dearer — 10.6ms to
12.9ms for a whole
chooseSpellAbilityToPlayon a 40-permanent board, becausecanPayManaCostis1.6ms against
hasEnoughManaSourcesToCastat 1.1ms. The opponent's-turn check is deliberately keptoff the mana solver for the same reason: 4.1ms against 4.2ms before. One board, one machine.
Four tests, three of which fail without the change. Krosan Restorer's threshold ability and Tidewater
Minion's
{4}Debuff were checked separately, since the flag was suppressing those too. Vizier'scycling trigger I reasoned about rather than measured: it is an untargeted free untap, so it should
not be a loss.
🤖 Implemented with the assistance of Claude Code (Opus).