Repository navigation
Finish #9484: a single-card leaves-the-battlefield trigger checks look-back for itself - #12198
Open
haksanlulz wants to merge 2 commits into
Open
haksanlulz wants to merge 2 commits into
haksanlulz wants to merge 2 commits into
Conversation
…e battlefield before the event A card exiled "until this leaves the battlefield" returns inside the host's own zone change, so its triggers were registered before the host's dies trigger collected its watchers. A returned Slaughter Specialist therefore saw Brutal Cathar die and got a counter (Card-Forge#9484), although the two are never on the battlefield together (Brutal Cathar ruling) and leaves-the-battlefield triggers look back to the game state before the event (CR 603.10a). TriggerChangesZone now applies the check TriggerChangesZoneAll already makes: a battlefield trigger of a permanent in play now but absent from the event's last-state battlefield does not fire. The check needs that snapshot in the run params, which effects and state-based actions already supply. Fixes Card-Forge#9484 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hanmac
reviewed
Oct 9, 2026
Comment on lines
+99
to
+101
| CardCollectionView lastStateBattlefield = (CardCollectionView) runParams.get(AbilityKey.LastStateBattlefield); | ||
| if (lastStateBattlefield != null && !lastStateBattlefield.contains(getHostCard())) { | ||
| return false; |
Contributor
There was a problem hiding this comment.
should probably be combined with the block below at line 111-117?
Contributor
Author
There was a problem hiding this comment.
I folded them in, thanks.
tool4ever
reviewed
Oct 9, 2026
| game.copyLastState(); | ||
| Map<AbilityKey, Object> params = AbilityKey.newMap(); | ||
| AbilityKey.addCardZoneTableParams(params, new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard())); | ||
| game.getAction().destroy(game.getCardState(c[0]), null, false, params); |
Contributor
There was a problem hiding this comment.
I don't know, this isn't how it'd happen in the engine normally and that's the only reason you get this test to fail...
Contributor
Author
There was a problem hiding this comment.
That’s fair. I think it's still worth keeping the route tests, #9491 has none and they’d be the only thing that notices if the prune ever regresses. The check itself I can keep as is now that it’s folded, or drop, either works. The original issue can be closed in anycase.
Review feedback: TriggerChangesZone had two blocks of the same shape, the new battlefield one and the graveyard one for Boneyard Scourge. One block now keys on the event's origin and picks the last-state zone by where the trigger works. The graveyard branch keeps its condition on the trigger's own Origin parameter and gains the null guard the battlefield branch has. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
haksanlulz
force-pushed
the
fix/9484-until-leaves-lookback
branch
from
October 9, 2026 19:30
30ec120 to
a001228
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Finish #9484: a single-card leaves-the-battlefield trigger checks look-back for itself
Fixes #9484
Summary
A card exiled "until this leaves the battlefield" comes back during the host's own zone change (
Card.runLeavesPlayCommands()insideGameAction.changeZone), so its triggers are registered before the host's dies trigger finds its watchers. In #9484 a Slaughter Specialist exiled by Brutal Cathar returned when Brutal Cathar died, saw it die, and got a +1/+1 counter.It shouldn't. Brutal Cathar's ruling (2021-09-24): "The exiled card returns to the battlefield immediately after this creature leaves the battlefield. Nothing happens between the two events, including state-based actions. The two creatures aren't on the battlefield at the same time." A dies trigger looks back in time (CR 603.10a), so it sees the game as it was just before the event, with the Specialist still in exile.
#9491 ("Part for #9484") handles this wherever the death goes through a zone table's batch event:
CardZoneTable.triggerChangesZoneAllrebuilds the active triggers from the event's last-state battlefield, so a permanent that is in play now but absent from that snapshot is left out. This covers the other part, aChangesZonetrigger evaluated without that batch event, where the trigger itself had no look-back check whileTriggerChangesZoneAll.performTestalready has one.What changed
TriggerChangesZone.performTestmakes the checkTriggerChangesZoneAllalready makes: a trigger that works on the battlefield, for an event whose origin is the battlefield, doesn't fire when its host is in play now but isn't in the event's last-state battlefield. A delayed trigger (no active zone) is left alone, and a host that has itself left is not in play, so its own leaves trigger, registered from its last known information, still fires.AbilityKey.LastStateBattlefieldfrom the run params, which effects (AbilityKey.addCardZoneTableParams), costs and state-based actions already pass, and does nothing when the params carry none.Verification
UntilHostLeavesLookBackTest(forge-gui-desktop, 3 tests): Brutal Cathar exiles a watcher, a second copy of the watcher enters as the control, Brutal Cathar dies. The copy on the battlefield gets 1 counter, the returned one gets 0. (1) The death arrives throughdestroywith a zone table and no batch event: onmasterit fails withexpected:<0> but was:<1>, here it passes. (2) Murder cast from hand, then state-based actions, for both Slaughter Specialist (ChangesZone) and Sengir Connoisseur (ChangesZoneAll): guards Only register LTB trigger that are in lastStateBattlefield #9491's prune. (3) The other direction: Zombie Apocalypse returns Midnight Reaper and then destroys a Human in the same resolution, and the Reaper still takes its 1 damage, so the check does not suppress a permanent that entered after the resolution's snapshot was taken.mvn -U -B clean -P windows-linux install, Java 17, on this branch (based onmasterbb00615): BUILD SUCCESS, 0 Checkstyle violations, 773 tests, 0 failures, 6 skipped (NetworkPlayIntegrationTest).Size
136+ / 0- across 2 files, one commit; 126 of the additions are the test.
Disclosure
Written with an AI coding agent (Claude Code); tested as described above. The commit carries the co-author line.
🤖 Generated with Claude Code