Skip to content

feat(parser): support Me, the Immortal + The Master, Mesmerist - #3509

Merged
matthewevans merged 7 commits into
phase-rs:mainfrom
ntindle:fix/who-misparse-34-unsupported-cluster-play-cast
Jun 17, 2026
Merged

feat(parser): support Me, the Immortal + The Master, Mesmerist#3509
matthewevans merged 7 commits into
phase-rs:mainfrom
ntindle:fix/who-misparse-34-unsupported-cluster-play-cast

Conversation

@ntindle

@ntindle ntindle commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements full engine support for two previously-unsupported Doctor Who precon cards.

Cards supported

  • Me, the Immortal — counter-persistence across zones (CountersPersistAcrossZones, excluded Hand/Library; runtime guard in zones.rs::counters_persist_on_move).
  • The Master, Mesmerist — the ~'s power possessive no longer swallows the trailing period, so the activated ability grants Skulk (until EOT) to an opponent creature filtered by PtComparison Power LE Ref{Power,Source}, and the trailing Goad it. chains correctly.

Note

These were two cards from a WHO triage cluster that turned out heterogeneous; the other three (The Fourth/Eighth/Second Doctor) need distinct play-from-zone / attack-restriction primitives and are handled in separate follow-up PRs.

Verification

cargo fmt clean · cargo clippy -p engine --all-targets -D warnings clean · cargo test -p engine 12,176 pass / 0 fail · both cards re-parse with zero Unimplemented in the fixed clauses.

CR refs verified against docs/MagicCompRules.txt.

🤖 Generated with Claude Code

@ntindle
ntindle requested a review from matthewevans as a code owner June 16, 2026 14:38

@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 implements counter persistence across zone changes (CR 122.2 and CR 113.6b) to support cards like Me, the Immortal and Skullbriar, the Walking Grave, including parser support, state tracking, and corresponding unit tests. It also fixes a parser issue where possessive apostrophes could prevent sentence-boundary splitting. Feedback on this PR highlights a critical rules-fidelity issue in zones.rs where querying the raw GameObject directly for active static definitions bypasses Layer 6 ability-removal effects (such as Humility or Yixlid Jailer), meaning counters would incorrectly persist even if the object has lost its abilities in the source zone.

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.

Comment on lines +43 to +54
fn counters_persist_on_move(state: &GameState, object_id: ObjectId, to: Zone) -> bool {
let Some(obj) = state.objects.get(&object_id) else {
return false;
};
super::functioning_abilities::active_static_definitions(state, obj).any(|def| {
matches!(
&def.mode,
StaticMode::CountersPersistAcrossZones { excluded_zones }
if !excluded_zones.contains(&to)
)
})
}

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.

high

[HIGH] Bypassing the layers pipeline in counters_persist_on_move violates CR 113.6b and official rulings when ability-removal effects (such as Humility or Yixlid Jailer) are active in the from-zone.

Why it matters

According to official rulings for Skullbriar, if it loses its abilities (e.g., due to Humility on the battlefield or Yixlid Jailer in the graveyard), its counter-persistence ability does not function when it changes zones, and its counters must be cleared. By querying active_static_definitions directly on the raw GameObject from state.objects, Layer 6 ability-removal effects are completely bypassed, leading to incorrect rules behavior where counters are incorrectly preserved.

Suggested Fix

Instead of reading static definitions directly from the raw object, query the layered/effective characteristics of the object in the from-zone. If the engine has an effective_object or similar layered state accessor, use that to check for the presence of the CountersPersistAcrossZones static ability.

Additionally, please add a unit test in zones.rs verifying that if the object loses its abilities (simulating Humility/Yixlid Jailer), its counters are correctly cleared upon zone transition.

References
  1. Strict fidelity to the MTG Comprehensive Rules (CR) — every game rule, validation, and computed value matches the CR exactly. Convenience shortcuts that get rules wrong are not simpler; they are wrong. (link)

@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 review — CHANGES REQUESTED (one rules-fidelity gap + rebase)

Reviewed at head. The architecture here is genuinely class-based, not two name-matched special cases, and most of it is excellent. One open rules-fidelity gap (raised by Gemini, confirmed below) needs addressing before enqueue, and the branch is BEHIND main.

What's right (keep all of this)

  • StaticMode::CountersPersistAcrossZones { excluded_zones: Vec<Zone> } parameterizes the exclusion set as a typed zone list — covers the whole "Counters remain on ~ as it moves to any zone other than [zones]" class. Parser anchors on the normalized ~ glyph (name-agnostic; Me, the Immortal and Skullbriar share the verbatim phrase), and the zone-list tail is an alt combinator (parse_excluded_zone_list) with a word-order variant arm — sibling phrasings slot in without a new variant. Tests assert on the ~ form and exercise the full destination matrix, so the parameter is verified, not one card.
  • The clause-splitter fix in sequence.rs (a ~'s possessive apostrophe no longer opens a phantom single-quote that swallows a . sentence boundary) is a real building-block fix for the whole "[filter referencing ~'s P/T] gains [kw] until EOT. [imperative on it]" class, with a discriminating splitter-level test. Good reasoning that Oracle quoted-ability text always uses double quotes.
  • All new exhaustive-match sites (Hash, is_resettable/grouping, Display, is_data_carrying_static) updated.
  • CR annotations verified against docs/MagicCompRules.txt: CR 122.2 (counters cease on zone change), CR 113.6b (ability functions only from stated zones), CR 701.15a (goad) — all confirmed.

The gap (confirmed against head — must address)

counters_persist_on_move reads functioning_abilities::active_static_definitions, which iterates the object's raw obj.static_definitions (gated only by phasing / command-zone / condition). It does not reflect Layer-6 ability-removal (Humility / Yixlid Jailer's "Cards in graveyards lose all abilities"):

  • evaluate_layers (layers.rs:1244) only processes battlefield + hand objects; graveyard/exile objects never have RemoveAllAbilities applied to their static_definitions.
  • So with Yixlid Jailer out and Me/Skullbriar in a graveyard bearing counters, a graveyard→exile move would incorrectly persist the counters — the from-zone ability is rules-meant to be stripped (CR 113.6b read from the from-zone state). The doc-comment in zones.rs anticipates the active_zones axis but is silent on this ability-removal axis.

This is narrow, and graveyard ability-removal appears generally unmodeled engine-wide, so it is not a regression of existing behavior — but it is a known-wrong interaction introduced on a new path, and the PR's own doc comment gives a false sense of completeness by addressing only the active_zones caveat.

Requested changes

  1. Either guard the helper against from-zone ability-removal, or — given graveyard layer application is a broader engine limitation — extend the zones.rs doc-comment caveat to explicitly name the Humility/Yixlid (Layer-6 ability-removal) axis as the documented limitation, matching how the active_zones caveat is already called out, and add a // TODO-class follow-up note. Make the deferral honest rather than implicit.
  2. Rebase onto origin/main (mergeStateStatus: BEHIND).

Everything else is enqueue-quality. Once the deferral is documented honestly (or the guard added) and the branch is current, this is good to land.

@matthewevans matthewevans added the enhancement New feature or request label Jun 16, 2026
@matthewevans matthewevans self-assigned this Jun 17, 2026
Address maintainer review: the counters_persist_on_move doc-comment implied
that "any layer-applied ability removal (such as Yixlid Jailer in graveyards)
[is] evaluated from the correct zone", which overstated completeness. The helper
reads obj.static_definitions directly via active_static_definitions, and
evaluate_layers only applies layers to battlefield + hand objects — so a
graveyard/exile object never has its abilities stripped, and with Yixlid Jailer
in play the counters would INCORRECTLY persist on a graveyard->exile move.

Restructure the doc-comment into two explicitly-named documented limitations
(the existing active_zones caveat plus the Layer-6 ability-removal axis), make
the CR 113.6b paragraph claim only the condition gate (not ability removal), and
add a TODO to re-check persistence against the layer-resolved view once
evaluate_layers applies Layer-6 ability removal to non-battlefield zones. This
makes the deferral honest rather than implicit; graveyard ability-removal is
unmodeled engine-wide, so this is not a regression.

Verification: clippy -p engine --all-targets -D warnings clean;
cargo test -p engine green (0 failed).
@matthewevans

Copy link
Copy Markdown
Member

Maintainer follow-up — deferral documented honestly & enqueuing

Addressed the one open rules-fidelity gap by making the deferral explicit rather than implicit, and rebased onto current main.

The previous counters_persist_on_move doc-comment claimed the helper evaluates "any layer-applied ability removal (such as Yixlid Jailer in graveyards) … from the correct zone" — which overstated completeness, since active_static_definitions reads obj.static_definitions directly and evaluate_layers only applies layers to battlefield + hand objects. I restructured the comment into two explicitly-named documented limitations:

  1. active_zones (the existing caveat, kept).
  2. Layer-6 ability removal (Humility / Yixlid Jailer) — now named directly: with Yixlid Jailer in play and Me/Skullbriar in a graveyard, the helper still observes the persistence static and would incorrectly persist counters on a graveyard→exile move, because graveyard ability-removal is unmodeled engine-wide. Not a regression; flagged with a TODO to re-check persistence against the layer-resolved view once evaluate_layers applies Layer-6 removal to non-battlefield zones.

The CR 113.6b paragraph now claims only the condition gate (not ability removal). Chose the honest-deferral path over a one-off guard since the limitation is engine-wide, exactly as you suggested.

Verification: clippy -p engine --all-targets -D warnings clean · cargo test -p engine green (0 failed, including the Me/Skullbriar persistence matrix).

@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.

Deferral documented honestly (Layer-6 ability-removal axis named explicitly + TODO), rebased onto main. Engine suite green.

@matthewevans
matthewevans enabled auto-merge June 17, 2026 05:52
@matthewevans matthewevans removed their assignment Jun 17, 2026
@matthewevans
matthewevans added this pull request to the merge queue Jun 17, 2026
Merged via the queue into phase-rs:main with commit 1d8f22a Jun 17, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants