Skip to content

fix(parser): scope 'creatures that died under your control' count to the controller (#1129) - #3154

Merged
matthewevans merged 1 commit into
mainfrom
fix/issue-1129-died-under-your-control
Jun 13, 2026
Merged

fix(parser): scope 'creatures that died under your control' count to the controller (#1129)#3154
matthewevans merged 1 commit into
mainfrom
fix/issue-1129-died-under-your-control

Conversation

@matthewevans

Copy link
Copy Markdown
Member

Summary

Closes #1129.

Priest of the Crossing's "for each creature that died under your control this turn" counted every player's creature deaths. creatures_died_this_turn_ref() (oracle_nom/quantity.rs) always emitted ZoneChangeCountThisTurn{ filter: TypedFilter::creature() } with no controller restriction, because parse_creatures_died_this_turn_tail collapsed both the controller-qualified ("under your control", "your graveyard") and unqualified ("creatures that died this turn", "a graveyard") phrasings into the same unit value.

Class fix (mirrors the already-correct parse_for_each_creature_left_battlefield_this_turn): thread Option<ControllerRef> through the tail so qualified forms emit .controller(ControllerRef::You) and unqualified forms keep no restriction (counts all). The runtime resolver already honors the controller filter (zone_change_filter_inner, CR 109.5) — only the parser was dropping it. ~16 "died under your control" + ~8 "your graveyard" cards corrected; unqualified all-players cards unchanged.

Tests

  • Runtime priest_of_the_crossing_died_under_control.rs: P0's + P1's creatures both die; Priest counts only P0's (1, not 2). Fails on revert.
  • Parser test: qualified forms → controller == Some(You), unqualified → None (both for each and the number of surfaces).

CR 700.4, 109.5 (grep-verified). Verified locally via the parser combinator gate; full verification deferred to CI (local main checkout was transiently stale).

@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 addresses issue #1129 by updating the parser to correctly scope the controller (e.g., Some(ControllerRef::You)) for phrases like "creatures that died under your control" or "put into your graveyard", while leaving unqualified phrases unscoped. It also adds corresponding unit and integration tests. The reviewer noted that parse_creatures_died_this_turn_tail violates the repository style guide by enumerating the Cartesian product of phrase variants as separate tag arms instead of composing them using alt() per axis. A refactoring suggestion was provided to compose these axes modularly.

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 +2750 to 2808
fn parse_creatures_died_this_turn_tail(input: &str) -> OracleResult<'_, Option<ControllerRef>> {
alt((
value((), tag("creatures that died under your control this turn")),
value((), tag("creatures that died under your control")),
value((), tag("creatures that died this turn")),
value((), tag("creatures that died")),
value((), tag("creature that died under your control this turn")),
value((), tag("creature that died under your control")),
value((), tag("creature that died this turn")),
value((), tag("creature that died")),
value(
Some(ControllerRef::You),
tag("creatures that died under your control this turn"),
),
value(
Some(ControllerRef::You),
tag("creatures that died under your control"),
),
value(None, tag("creatures that died this turn")),
value(None, tag("creatures that died")),
value(
Some(ControllerRef::You),
tag("creature that died under your control this turn"),
),
value(
Some(ControllerRef::You),
tag("creature that died under your control"),
),
value(None, tag("creature that died this turn")),
value(None, tag("creature that died")),
// CR 700.4: "creature put into [a/your] graveyard from the battlefield"
// is the long form of "died" — both reference the same battlefield→
// graveyard transition tracked in `zone_changes_this_turn`.
// graveyard transition tracked in `zone_changes_this_turn`. CR 109.5:
// "your" graveyard scopes the count to the source's controller.
value(
(),
Some(ControllerRef::You),
tag("creatures put into your graveyard from the battlefield this turn"),
),
value(
(),
Some(ControllerRef::You),
tag("creatures put into your graveyard from the battlefield"),
),
value(
(),
None,
tag("creatures put into a graveyard from the battlefield this turn"),
),
value(
(),
None,
tag("creatures put into a graveyard from the battlefield"),
),
value(
(),
Some(ControllerRef::You),
tag("creature put into your graveyard from the battlefield this turn"),
),
value(
(),
Some(ControllerRef::You),
tag("creature put into your graveyard from the battlefield"),
),
value(
(),
None,
tag("creature put into a graveyard from the battlefield this turn"),
),
value(
(),
None,
tag("creature put into a graveyard from the battlefield"),
),
))

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] Avoid enumerating the Cartesian product of phrase variants as separate tag arms.

Why it matters: The Repository Style Guide (Parser section) strictly prohibits enumerating N-dimensional patterns as separate tag("full string") arms. Instead, they should be composed using alt() per axis to prevent combinatorial explosion and maintain idiomatic, modular parsers.

Suggested fix: Refactor parse_creatures_died_this_turn_tail to compose the subject number, verb phrasing, controller qualification, and temporal qualification axes.

fn parse_creatures_died_this_turn_tail(input: &str) -> OracleResult<'_, Option<ControllerRef>> {
    preceded(
        alt((tag("creatures"), tag("creature"))),
        terminated(
            alt((
                map(
                    preceded(
                        tag(" that died"),
                        opt(tag(" under your control")),
                    ),
                    |opt_control| opt_control.map(|_| ControllerRef::You),
                ),
                preceded(
                    tag(" put into "),
                    terminated(
                        alt((
                            value(Some(ControllerRef::You), tag("your")),
                            value(None, tag("a")),
                        )),
                        tag(" graveyard from the battlefield"),
                    ),
                ),
            )),
            opt(tag(" this turn")),
        ),
    )
References
  1. Parser rules require N-dimensional patterns to compose alt() per axis instead of enumerating the Cartesian product as separate tag arms. (link)
  2. Avoid verbatim string equality for parsing Oracle phrases. Decompose compound phrases into modular, reusable parsers and compose them using idiomatic combinator aggregates to prevent combinatorial explosion.

@matthewevans
matthewevans force-pushed the fix/issue-1129-died-under-your-control branch from c491a88 to 8e66066 Compare June 13, 2026 16:51
@matthewevans
matthewevans force-pushed the fix/issue-1129-died-under-your-control branch from 8e66066 to 46bd78c Compare June 13, 2026 17:14
@matthewevans
matthewevans added this pull request to the merge queue Jun 13, 2026
Merged via the queue into main with commit c377bfe Jun 13, 2026
10 checks passed
@matthewevans
matthewevans deleted the fix/issue-1129-died-under-your-control branch June 13, 2026 17:56
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.

Priest of the Crossing distributing free +1/+1's — [[Priest of the Crossing]] gives out +1/+1 counters regardless of th…

1 participant