Skip to content

Let the AI use Venser, the Sojourner (and two other mass-unblockable cards) - #11396

Open
liamiak wants to merge 5 commits into
Card-Forge:masterfrom
liamiak:let-ai-use-venser
Open

Let the AI use Venser, the Sojourner (and two other mass-unblockable cards)#11396
liamiak wants to merge 5 commits into
Card-Forge:masterfrom
liamiak:let-ai-use-venser

Conversation

@liamiak

@liamiak liamiak commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Venser, the Sojourner is AI:RemoveDeck:All. Two of his three abilities already work — the flag was covering for one dead one, and that one is not his alone.

What was dead

The -1 ("Creatures can't be blocked this turn") returned CantPlayAi on every board, including ones where it wins on the spot — EffectAi's no-AILogic branch is a bare return CantPlayAi, so nothing here narrows existing behaviour.

AILogic$ UnblockableForLethal holds it until attacking with everything that can attack would take an opponent to zero with nothing able to block — the state the ability itself creates, so blockers are not counted.

AI power Opponent life Before After
18 18 CantPlayAi activates
18 19 CantPlayAi declines
18 18, opponent holds a Fog CantPlayAi declines

Same tag on the other two cards where this is an optional choice, and both argue for it harder than Venser: Jace, Arcane Strategist spends seven loyalty, Tezzeret's Gatebreaker sacrifices itself. The rest are mandatory triggers, static, or wrong for a lethal-only gate — Glaring Spotlight also grants hexproof, and Reverse the Polarity is a Charm mode CharmAi reaches another way.

The check, and its second caller

It lives in ComputerUtilCombat beside sumDamageIfUnblocked, with the guards doAssault() already has: ai.cantWin() and hasAFogEffect, fog last since it walks several zones.

RestrictBlocking (War Cadence) asked the same question with its own loop — raw getCurrentPower(), no cantWin, no cantLoseForZeroOrLessLife, no Fog — so it pays {X} into a Fog where these three now decline. It calls the helper instead, keeping getStrongestOpponent() so its defender choice is unchanged, and loses a possibleBlockers.size() > 1 condition that had suppressed the lethal branch against a lone blocker.

Testing

362 tests, 0 failures. Both tests play the phase out through AiController rather than calling the ability AI directly. Removing either half of the Venser fix fails it; so does reverting the War Cadence change, on the Fog case.

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

@liamiak
liamiak force-pushed the let-ai-use-venser branch from 36e0699 to c5c4944 Compare August 6, 2026 11:42
@liamiak

liamiak commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto master and cut this down. The lethal check now lives in ComputerUtilCombat next to sumDamageIfUnblocked rather than as a private method here, and picks up the two guards AiAttackController.doAssault() already has — ai.cantWin() and an opponent Fog — so the AI no longer spends Venser's loyalty, or sacrifices Tezzeret's Gatebreaker, into a Fog.

+98/-4, of which 61 is the test. Description updated to match.

liamiak1 and others added 5 commits August 25, 2026 07:41
Making everything unblockable is worth nothing unless the attack it enables
wins the game, and EffectAi had no way to ask that, so every card with the
effect declined on every board - including boards where it was lethal.

Add an UnblockableForLethal logic that holds the ability until attacking
with everything that can attack would take an opponent to zero with nothing
able to block, which is precisely the state the ability creates. Blockers
are therefore not counted.

Applied to the three cards where using it is an optional choice - Venser,
the Sojourner's -1, Jace, Arcane Strategist's -7 and Tezzeret's Gatebreaker.
All three went from declining on every board to firing only on lethal ones.

The others with the same effect are not decisions: Keeper of Keys and
Veiling Oddity are mandatory triggers, Bedlam, Tazeem and Tanglewalker are
static. Glaring Spotlight grants hexproof alongside it, so it is also a
protection card and a lethal-only gate would be wrong for it. Reverse the
Polarity offers it as a Charm mode, which CharmAi picks through a different
path and would need its own verification.

Venser also loses AI:RemoveDeck:All. His other two abilities already work -
ChangeZoneAi's blink logic handles the +2 (skips tokens, prefers ETB
triggers, waits for post-combat, and takes back anything an opponent has
gained control of), and the -8 is already AILogic$ Always.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both directions on Venser's -1: fires at exactly lethal, holds one damage
short. Calls the AI directly rather than playing a game out, so it does not
depend on MyRandom.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Move the check next to sumDamageIfUnblocked, where the rest of the
combat maths lives, and give it the two guards
AiAttackController.doAssault already has: ai.cantWin() and an
opponent Fog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The earlier version of this test called EffectAi directly to keep MyRandom
out of it. That skipped the part most likely to break: AiController strips
every ability of an AI:RemoveDeck card from the candidate list, so Venser's
-1 was unreachable in a real game no matter what EffectAi returned, and a
direct call could not tell.

Play the phase out instead, the way BlightAiTest does, and assert on
loyalty: 5 to start, so 4 means the -1 was activated, 7 the +2, and 5 that
Venser was left alone. Each case asserts an exact number rather than an
absence, so a setup that quietly stopped working cannot pass.

Both halves of the fix now fail the test when removed - restoring
AI:RemoveDeck:All, or dropping the AILogic tag, each give 5 where 4 is
expected. Neither was covered before.

MyRandom is not a factor here: the branch sets randomReturn = true, so the
decision does not consult it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RestrictBlocking asked the same question this branch just extracted - would
my whole team connecting kill them - but answered it with its own loop:
raw getCurrentPower() summed over creatures that can attack, against the
strongest opponent's life.

That loop has no ai.cantWin() check, no opp.cantLoseForZeroOrLessLife()
check and no Fog check, so War Cadence would pay {X} into a Fog where the
three cards above it now decline. getCurrentPower() also misreads double
strike and prevention, which damageIfUnblocked handles.

Point it at unblockedAttackIsLethal instead. Keeping getStrongestOpponent()
rather than the all-opponents overload leaves its choice of defender alone,
so the only changes are the three guards, the damage figure, and the
possibleBlockers.size() > 1 condition, which had been suppressing the
lethal branch against a lone blocker - the case where taxing that blocker
is most clearly right.

War Cadence is the only card using this logic, and had no test. The Fog
case fails without this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@liamiak
liamiak force-pushed the let-ai-use-venser branch from c5c4944 to 4b683e7 Compare August 25, 2026 15:20
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.

2 participants