Skip to content

Let the AI pay for converge and sunburst when X is the colour knob - #11485

Closed
liamiak wants to merge 1 commit into
Card-Forge:masterfrom
liamiak:ai-engineered-explosives
Closed

Let the AI pay for converge and sunburst when X is the colour knob#11485
liamiak wants to merge 1 commit into
Card-Forge:masterfrom
liamiak:ai-engineered-explosives

Conversation

@liamiak

@liamiak liamiak commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

On a converge or sunburst card whose colour count lives in a second SVar, X has one job: buy colours. The AI was not announcing it, so those cards were cast for the minimum.

ComputerUtilCost.setMaxXValue ends with sa.setXManaCostPaid(x), leaving X at the maximum. PermanentAi then measured its converge baseline there:

final int xPay = ComputerUtilCost.setMaxXValue(sa, ai, false);   // X is now the max
if (source.hasConverge()) {
    int nColors = ComputerUtilMana.getConvergeCount(sa, ai);     // measured at max X
    for (int i = 1; i <= xPay; i++) {
        sa.setXManaCostPaid(i);
        int newColors = ComputerUtilMana.getConvergeCount(sa, ai);
        if (newColors > nColors) { nColors = newColors; }
        else { sa.setXManaCostPaid(i - 1); break; }
    }
}

The baseline is already the best colour count available, so the first step of the walk always compared worse and X collapsed to 0. TokenAi had a different miss: its X block is guarded by "X".equals(tokenAmount), so a card counting colours in Y never reached any X handling at all.

Both want the same thing, so it is now named once - ComputerUtilMana.setXForBestConverge - and PermanentAi shrinks by 10 lines calling it.

Effect

All five cards in the pool whose cost has X and for which Card.hasConverge() is true, with five differently coloured lands untapped:

X before X after
Engineered Explosives 0 5 0 charge counters, so it could only ever destroy 0-drops
Chamber Sentry 0 5 entered as a 0/0 and died
Skyrider Elf 0 3 entered as a 0/0 and died
Sweep the Skies unannounced 4 converge 1 -> 5, so five thopters rather than one
Prismatic Ending unannounced unannounced not fixed, see below

Only Engineered Explosives is flagged. The other three that this fixes are in AI decks today - two of them entering as 0/0s and dying.

Edge cases behave: five Wastes gives X=0, and Plains/Plains/Island gives X=2 rather than paying for a colour already covered.

Engineered Explosives unflagged

Casting it for X=0 was the only thing wrong with it. The activated half needs nothing: DestroyAll routes it through the same DestroyAllAi.doMassRemovalLogic as Wrath of God, and it matched Wrath's decision on every board I tried. Two charge counters, everything at cmc 2 so both cards hit the same permanents:

AI creatures opponent creatures Engineered Explosives Wrath of God
161 307 declines declines
161 468 plays plays
322 629 plays plays

It does wipe at a cost to itself when the swing justifies it, on the shared CREATURE_EVAL_THRESHOLD - not only when it risks nothing.

The flag was also self-fulfilling: AiController.getSpellAbilityToPlayImpl strips every ability of a flagged card before evaluating anything, so "the AI never plays it" held regardless of whether its AI worked.

Prismatic Ending, not fixed

Announcing X takes its converge from 1 to 5 and it still declines, so X is necessary but not sufficient there. Its condition is ConditionPresent$ Permanent.nonLand+cmcLEY, and Count$Converge reads the colours actually paid:

if (sq[0].contains("Converge")) {
    SpellAbility castSA = c.getCastSA();
    return doXMath(castSA == null ? 0 : castSA.getPayingColors().countColors(), expr, c, ctb);
}

Before the spell is cast getCastSA() is null, so Y is 0 and cmcLEY matches nothing above a 0-drop. The AI's getConvergeCount predicts converge by simulating payment, but the condition path uses the rules-side count; bridging the two is a separate piece of work and I have left the card flagged.

DamageAllAi:30 and DestroyAllAi:64 have the same X-only assumption, but no card in the pool currently reaches them through it.

Testing

Two tests. Engineered Explosives plays a real turn and lands with five charge counters - that one has to go through the full AiController path, since it is the unflagging that needs proving. Sweep the Skies asserts the announced X instead, because TokenAi gates token spells behind a random roll and a played-turn assertion on it is flaky; both assertions were run repeatedly each way.

Each test fails without its own half of the change. 337 tests, 0 failures, checkstyle clean.

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

On a card whose colour count lives in a second SVar, X has one job: buy
colours. The AI was not announcing it, so those cards were cast for the
minimum.

ComputerUtilCost.setMaxXValue ends by setting X on the ability, so it
leaves X at the maximum. PermanentAi then measured its converge baseline
there, which is already the best colour count available - so the first
step of its walk always compared worse and X collapsed to 0. TokenAi
missed differently: its X block is guarded by "X".equals(tokenAmount),
so a card counting colours in Y never reached any X handling at all.

Both want the same thing, so name it once as
ComputerUtilMana.setXForBestConverge; PermanentAi loses ten lines to it.

Effect on every card in the pool whose cost has X and where
Card.hasConverge() is true, with five differently coloured lands:

  Engineered Explosives   X=0 -> 5          (0 charge counters -> 5)
  Chamber Sentry          X=0 -> 5          (entered as a 0/0 and died)
  Skyrider Elf            X=0 -> 3          (entered as a 0/0 and died)
  Sweep the Skies         none -> 4         (converge 1 -> 5)

Only Engineered Explosives was flagged; the other three are in AI decks
today. It is unflagged here, since casting it for X=0 was the only thing
wrong with it - DestroyAllAi already matched Wrath of God's decision on
every board tried.

Prismatic Ending is the fifth and stays flagged. Announcing X takes its
converge to 5 and it still declines: its condition reads Count$Converge,
which returns the colours actually paid and so is 0 before the spell is
cast. Predicting that in the condition path is separate work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SVar:X:Count$xPaid
SVar:Y:Count$CardCounters.CHARGE
SVar:NonStackingEffect:True
AI:RemoveDeck:All

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.

I don't think the reasoning for this was zero X since that's another regression and more like AI unable to coordinate which one is actually a smart choice

@liamiak

liamiak commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

You're right — closing into #11486.

Engineered Explosives needs board-aware counter selection rather than most-colours: its counters
pick which mana value the wipe hits, so five is usually the worst live choice. It stays flagged.
#11486 carries the other three cards and unifies the duplicated simulation you flagged there.

@liamiak liamiak closed this Aug 2, 2026
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