Skip to content

fix(parser): graveyard-exile replacement accepts counter rider + prefix word order (Dauthi Voidwalker #5245)#5443

Merged
matthewevans merged 2 commits into
phase-rs:mainfrom
shin-core:pr-graveyard-exile-counter-5245
Jul 9, 2026
Merged

fix(parser): graveyard-exile replacement accepts counter rider + prefix word order (Dauthi Voidwalker #5245)#5443
matthewevans merged 2 commits into
phase-rs:mainfrom
shin-core:pr-graveyard-exile-counter-5245

Conversation

@shin-core

Copy link
Copy Markdown
Contributor

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 (Unimplemented replacement_structure).

parse_graveyard_exile_replacement recognized its exile outcome with a hardcoded tag("exile it instead"), which matched neither:

  • the "instead exile it" prefix word order, nor
  • the "with a void counter on it" counter rider (CR 122.1).

Fix

Delegate the exile outcome to the shared parse_exile_anaphor_clause building block — already used by the self-die (~ would die) and non-self creature-die exile branches. It 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_abilityextract_etb_counters already 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

  • CR 614.1a — replacement effects that exile instead of moving to the graveyard.
  • CR 122.1 — the exiled object enters with the specified counter(s).
  • CR 730.3e / CR 111.1 — "a card" subject is token-excluding (NonToken), unchanged.

Tests

  • dauthi_voidwalker_exile_with_void_counter — asserts opponent-owned + NonToken scope 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.
  • All 336 replacement-parser tests pass; leyline_of_the_void_opponent_scoped and rest_in_peace_graveyard_exile unchanged.

Verification

  • cargo check -p engine — clean
  • cargo clippy -p engine — clean
  • Confirmed end-to-end: the full Dauthi card no longer emits a replacement_structure Unimplemented.

Scope note: Dauthi's activated ability also has a separate "Choose an exiled card …" gap; that's an independent parser issue and is intentionally out of scope for this focused fix.

🤖 Generated with Claude Code

…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>
@shin-core shin-core requested a review from matthewevans as a code owner July 9, 2026 14:44

@gemini-code-assist gemini-code-assist Bot left a comment

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.

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.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 6 card(s), 9 signature(s) (baseline: main e8244c78d4a9)

3 card(s) · replacement/Moved · added: Moved

Examples: Anafenza, the Foremost, Dauthi Voidwalker, Forbidden Crypt

2 card(s) · ability/ChangeZone · removed: ChangeZone (target=parent target, to=exile)

Examples: Anafenza, the Foremost, Forbidden Crypt

1 card(s) · ability/Attach · field conditional: target is EquipmentEquipment changed zones this way

Examples: The Invincible Iron Man

1 card(s) · static/Continuous · added: Continuous (affects=equipped by self creature, mods=add dynamic power, toughness +1)

Examples: Cranial Ram

1 card(s) · ability/PutCounter · field duration: until end of turn

Examples: Machine Man, Model X-51

1 card(s) · ability/grant Flying · added: grant Flying (affects=self, duration=until end of turn, grants=grant Flying)

Examples: Machine Man, Model X-51

1 card(s) · ability/he · removed: he

Examples: Machine Man, Model X-51

1 card(s) · ability/replacement_structure · removed: replacement_structure

Examples: Dauthi Voidwalker

1 card(s) · ability/static_structure · removed: static_structure

Examples: Cranial Ram

1 card(s) had Oracle-text changes (errata/reprint) — excluded as non-parser.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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>
@shin-core

Copy link
Copy Markdown
Contributor Author

Thanks @matthewevans — good catch. Fixed in 9111a55.

Added Scope::You so "your graveyard" (Forbidden Crypt) maps to FilterProp::Owned { controller: You }, symmetric with the existing opponent's-graveyard scope (Leyline → Owned { Opponent }). It's recognized before the generic graveyard fallthrough, so the possessive owner is captured rather than swallowed as unscoped. Added a forbidden_crypt_your_graveyard_scopes_to_you test asserting You-owned + NonToken and not opponent-scoped; Dauthi/Leyline/Rest in Peace are unchanged (337 replacement tests pass, clippy clean).

@matthewevans matthewevans self-assigned this Jul 9, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainer re-review: current head addresses the Forbidden Crypt owner-scope finding; parser diff and tests are in scope.

@matthewevans matthewevans added the bug Bug fix label Jul 9, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 9, 2026
@matthewevans matthewevans removed their assignment Jul 9, 2026
Merged via the queue into phase-rs:main with commit f179d78 Jul 9, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dauthi's Scamming me — [[Dauthi Voidwalker]]'s sac ability doesnt resolve properly.

2 participants