fix(parser): graveyard-exile replacement accepts counter rider + prefix word order (Dauthi Voidwalker #5245)#5443
Conversation
…ix word order (Dauthi Voidwalker phase-rs#5245) "If a card would be put into an opponent's graveyard from anywhere, instead exile it with a void counter on it." strict-failed the replacement parser (Unimplemented "replacement_structure"). parse_graveyard_exile_replacement recognized its exile outcome with a hardcoded `tag("exile it instead")` that matched neither the "instead exile it" prefix word order nor the "with a void counter on it" rider (CR 122.1). Delegate the exile outcome to the shared parse_exile_anaphor_clause building block — already used by the self-die and non-self creature-die exile branches — which handles both word orders and lifts an inline `with N <type> counter(s) on it` modifier into enter_with_counters. The counters ride on the exile redirect's ChangeZone, which event_modifiers_for_ability → extract_etb_counters already reads and applies as the card enters exile (CR 614.1a). This covers the whole class ("would be put into [any/opponent's] graveyard → [instead] exile it [with counters]"), not just Dauthi. The shuffle-back outcomes are untouched (they use distinct phrasings the anaphor recognizer never matches). Tests: Dauthi's void-counter exile (opponent-owned + NonToken scope, one void counter on the redirect); a regression guard that the plain "exile it instead" suffix (Leyline / Rest in Peace) stays counter-free. All 336 replacement-parser tests pass. Closes phase-rs#5245 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request updates the graveyard-to-exile replacement effect parser in crates/engine/src/parser/oracle_replacement.rs to support exiling cards with counters on them, such as Dauthi Voidwalker. The Outcome::Exile enum variant is parameterized to carry counter information, and the parser now delegates to parse_exile_anaphor_clause to handle both word orders and extract counter riders, which are then applied to the redirect's enter_with_counters field. Unit tests have been added to verify this behavior and guard against regressions. I have no feedback to provide as there are no review comments and the implementation is clean and idiomatic.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Parse changes introduced by this PR · 6 card(s), 9 signature(s) (baseline: main
|
matthewevans
left a comment
There was a problem hiding this comment.
[MED] Forbidden Crypt is now parsed as a global graveyard redirect. Evidence: crates/engine/src/parser/oracle_replacement.rs:5017 parses any non-opponent graveyard phrase through Scope::Any, and crates/engine/src/parser/oracle_replacement.rs:5075 only adds an ownership filter for Scope::Opponent; Scryfall Oracle for Forbidden Crypt says, "If a card would be put into your graveyard from anywhere, exile that card instead." Why it matters: this PR newly makes Forbidden Crypt parse as a supported Moved replacement, but that replacement would also exile opponents' cards going to their graveyards. Suggested fix: model the graveyard scope as Any | You | Opponent and add a FilterProp::Owned { controller: ControllerRef::You } assertion for the Forbidden Crypt class.
… (Forbidden Crypt) Review finding on phase-rs#5443 (matthewevans, MED): widening the graveyard-exile outcome recognizer newly made Forbidden Crypt ("If a card would be put into your graveyard from anywhere, exile that card instead.") parse as a Moved replacement — but the Scope model only had Any | Opponent, so "your graveyard" fell through to Scope::Any and the redirect would exile opponents' cards too. Add Scope::You, recognized from "your graveyard" (before the generic " graveyard" fallthrough) and mapped to FilterProp::Owned { You }. CR 400.3: cards go to their owner's graveyard, so ownership is the whose-graveyard discriminant — symmetric with the existing opponent's-graveyard scope (Leyline of the Void → Owned { Opponent }). Test: Forbidden Crypt scopes to You-owned + NonToken, and is NOT opponent-scoped. Dauthi/Leyline/Rest in Peace unchanged (337 replacement tests pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @matthewevans — good catch. Fixed in 9111a55. Added |
matthewevans
left a comment
There was a problem hiding this comment.
Maintainer re-review: current head addresses the Forbidden Crypt owner-scope finding; parser diff and tests are in scope.
Summary
Fixes #5245. Dauthi Voidwalker — "If a card would be put into an opponent's graveyard from anywhere, instead exile it with a void counter on it." — strict-failed the replacement parser (
Unimplementedreplacement_structure).parse_graveyard_exile_replacementrecognized its exile outcome with a hardcodedtag("exile it instead"), which matched neither:"instead exile it"prefix word order, nor"with a void counter on it"counter rider (CR 122.1).Fix
Delegate the exile outcome to the shared
parse_exile_anaphor_clausebuilding block — already used by the self-die (~ would die) and non-self creature-die exile branches. It handles both word orders and lifts an inlinewith N <type> counter(s) on itmodifier intoenter_with_counters.The counters ride on the exile redirect's
ChangeZone, whichevent_modifiers_for_ability→extract_etb_countersalready reads and applies as the card enters exile (CR 614.1a). No engine/type changes — pure parser reuse of existing infrastructure.This covers the whole class (
would be put into [any/opponent's] graveyard → [instead] exile it [with counters]), not just Dauthi. The shuffle-back outcomes are untouched — they use distinct phrasings the anaphor recognizer never matches.Rules
NonToken), unchanged.Tests
dauthi_voidwalker_exile_with_void_counter— asserts opponent-owned +NonTokenscope and one void counter on the exile redirect.graveyard_exile_no_counter_suffix_word_order_unchanged— regression guard that the plain"exile it instead"suffix (Leyline / Rest in Peace) stays counter-free.leyline_of_the_void_opponent_scopedandrest_in_peace_graveyard_exileunchanged.Verification
cargo check -p engine— cleancargo clippy -p engine— cleanreplacement_structureUnimplemented.🤖 Generated with Claude Code