Skip to content

Fix AI cloning creatures that shrink under its own control - #11675

Open
liamiak wants to merge 3 commits into
Card-Forge:masterfrom
liamiak:ai-clone-perspective
Open

Fix AI cloning creatures that shrink under its own control#11675
liamiak wants to merge 3 commits into
Card-Forge:masterfrom
liamiak:ai-clone-perspective

Conversation

@liamiak

@liamiak liamiak commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

CloneAi ranks candidates with evaluateCreature, which reads live power and toughness. For a characteristic-defining creature those are computed under whoever controls it now, so an opponent's Nightmare scores as the 8/8 it is for them rather than the 0/0 it would be for us.

With Cryptoplasm out and the opponent holding a Nightmare and eight Swamps, the AI copies it, becomes a 0/0 and dies on the spot. Six cards reach this with nothing keeping them out of AI decks: Artisan of Forms, Cephalid Facetaker, Cryptoplasm, Crystalline Resonance, Hulkling and Protean Thaumaturge. The one flagged card on the path, Mindlink Mech, only ever sees creatures that crewed it.

The check

#11690 already answers this for token copies: take an LKI copy, put it under our control, recheck statics on that card alone, and see whether a toughness-setting CDA leaves it alive. That is now ComputerUtilCard.filterOutFatalCopies, called from both CopyPermanentAi and CloneAi.

An earlier version of this PR claimed the value could not be worked out ahead of the fact and priced candidates with a one-play simulation instead. That was wrong on both halves — the LKI route works, and the simulation was gated behind usesHybridSimulation/usesFullSimulation, so on the default profile it skipped every CDA candidate rather than pricing one.

Board master with this
Their Nightmare, only target copies it, Cryptoplasm dies keeps ours
Their Nightmare + a Serra Angel copies Nightmare, dies Serra Angel
Their Nightmare, we hold Swamps Nightmare 8/8 unchanged

Known gap: cards choosing through Choices$ rather than targeting — Vesuvan Doppelganger — resolve via CloneEffect and chooseSingleEntityForEffect, a path this does not touch.

Three tests, all driving AiController on the default profile; two fail without the fix. Full desktop suite: 360 tests, 0 failures.

Written with Claude Opus 5 (also recorded in the commit co-authors).

@tool4ever

Copy link
Copy Markdown
Contributor

The idea is nice, but over-engineered in typical AI fashion :P

See my related implementation here: #11690
When that's merged feel free to turn it into a generic helper for such clone scenarios too

liamiak1 and others added 3 commits August 25, 2026 11:15
CloneAi ranks candidates with evaluateCreature, which reads live power and toughness.
For a characteristic-defining creature those are computed under whoever controls it
now, so an opponent's Nightmare scores as the 8/8 it is for them rather than the 0/0
it would be for us. With Cryptoplasm out and the opponent holding a Nightmare and
eight Swamps, the AI copied it, became a 0/0 and died on the spot.

The value cannot be worked out ahead of the fact. Power and toughness come from the
continuous effects layer, which only runs for cards on the battlefield: an LKI copy
with its controller reassigned still reports the opponent's numbers.

So a candidate carrying a CharacteristicDefining static that an opponent controls is
now left alone unless the AI can actually price it. Everything else is taken on the
evaluation as before, which is most of the time and costs nothing. The check is
deliberately broader than the cards that are really controller-dependent, because a
false positive passes over one candidate while a false negative only leaves the
previous behaviour in place.

Where the profile already pays for simulation, it can do better than pass over them:
the candidates are walked in evaluation order and the first one a one-play simulation
agrees is an improvement is taken, so an opponent's Nightmare is copied when we have
Swamps of our own too. No simulation is run outside those profiles - every other
entry to the simulator is behind usesHybridSimulation or usesFullSimulation and this
keeps to that.

That path needed an engine fix. GameSimulator.findSaInSimGame matches on description
against the host card's spell abilities, and a trigger's ability is in neither, so it
returned the "could not be simulated" sentinel - which OnePlaySafetyChecker treats as
approval. Trigger ids are reassigned by the copy, but the order is rebuilt from the
same script, so it now falls back to matching on position and confirms the API.

Six cards reach this with nothing keeping them out of AI decks: Artisan of Forms,
Cephalid Facetaker, Cryptoplasm, Crystalline Resonance, Hulkling and Protean
Thaumaturge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plays until Cryptoplasm's upkeep trigger fires of its own accord. Two of the default
profile cases fail without the fix: copying an opponent's Nightmare and dying as a
0/0, and the same board with a Serra Angel alongside, where it should reach the
honest second best instead. A third has no controller-dependent creature in reach and
covers the ordinary path.

The two simulation cases cover the other half: with Swamps of our own the Nightmare is
worth copying and is taken, without them it is refused. The second of those fails if
the GameSimulator trigger lookup is dropped, since the simulation then reports that it
could not run and OnePlaySafetyChecker reads that as approval.

Drop this commit if you would rather not carry the tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Card-Forge#11690 answers this far more cheaply than this branch did: take an LKI copy,
put it under our control, recheck statics on that one card, and see whether
the toughness-setting CDA leaves it alive. No simulation, and it works on
every AI profile.

The version here ranked candidates and then paid for up to three one-play
simulations, gated on usesHybridSimulation() || usesFullSimulation(). On the
default profile that gate is false, so it never priced a CDA creature at all
- it just skipped past every one of them. Avoidance, not an answer.

Lift the check out of CopyPermanentAi into
ComputerUtilCard.filterOutFatalCopies and call it from both, as suggested on
the PR. CopyPermanentAi keeps its own SetToughness guard, since that is about
the copy effect overriding toughness rather than the candidate.

The GameSimulator trigger-matching hunk and its test go with the simulation
approach - nothing calls that path now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@liamiak
liamiak force-pushed the ai-clone-perspective branch from 70d9c31 to e4faae4 Compare August 25, 2026 19:09
@liamiak

liamiak commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Done — thanks, your version is better than mine on two counts, not just smaller.

Lifted the check out of CopyPermanentAi into ComputerUtilCard.filterOutFatalCopies, and both call it now. CopyPermanentAi keeps its own SetToughness guard, since that one is about the copy effect overriding toughness rather than the candidate. Source went from 101 lines to 30, and the GameSimulator trigger-matching hunk went with the simulation it existed for.

The old version also had a hole I had not spotted: it was gated on usesHybridSimulation() || usesFullSimulation(), and on the default profile that is false, so it skipped every CDA candidate rather than pricing one. Measured on a board where their Nightmare would be a live 8/8 for us — old code left Cryptoplasm a 2/2, yours copies it correctly.

What is left on top of master is one behaviour: don't clone into a body that dies under our control. Their 8/8 Nightmare while we hold Islands is a 0/0 for us, and getBestCreatureAI ranks it top because it evaluates under their board.

Board master with this
Their Nightmare, only target copies it, Cryptoplasm dies keeps ours
Their Nightmare + a Serra Angel copies Nightmare, dies Serra Angel
Their Nightmare, we hold Swamps Nightmare 8/8 unchanged

Three tests, all through AiController on the default profile. 360 in the module, 0 failures.

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