Skip to content

fix(parser): scope enchanted-creature's-controller phase triggers (#5275)#5317

Merged
matthewevans merged 2 commits into
phase-rs:mainfrom
philluiz2323:fix/super-intelligence-upkeep-controller-scope-5275
Jul 7, 2026
Merged

fix(parser): scope enchanted-creature's-controller phase triggers (#5275)#5317
matthewevans merged 2 commits into
phase-rs:mainfrom
philluiz2323:fix/super-intelligence-upkeep-controller-scope-5275

Conversation

@philluiz2323

Copy link
Copy Markdown
Contributor

Summary

[[Super Intelligence]] — "Enchant creature\nAt the beginning of the upkeep of enchanted creature's controller, that player draws a card." — behaved like a Howling Mine, giving the extra card on every player's upkeep instead of only the enchanted creature's controller's upkeep.

Root cause: try_parse_phase_trigger scopes "enchanted player's <phase>" (player-enchant Auras) to AttachedTo, but had no handling for the creature-enchant "the <phase> of enchanted creature's controller" form. That left valid_target unset, and match_phase treats an unset player scope as "any active player" — so the trigger fired on every upkeep.

Fixes #5275

Fix

Two small, localized changes — no new engine variant:

  1. Parser (try_parse_phase_trigger): recognize "enchanted creature's controller" / "enchanted permanent's controller" (ASCII + ) in the phase text and set valid_target = TargetFilter::ParentTargetController, mirroring the existing "enchanted player's"AttachedTo arm.
  2. Matcher (player_matches_filter, trigger side): add a ParentTargetController arm that resolves to the controller of the permanent the source Aura is attached to and compares it to the active player.

Per CR 303.4e, the Aura's controller and the enchanted permanent's controller need not be the same, so this can't reuse the source-controller turn constraint (OnlyDuringYourTurn). ParentTargetController's documented resolution already covers "the AttachedTo host on source for Aura phase triggers," so it's the intended representation here; no trigger currently used it as valid_target, so the new matcher arm is additive.

CR references

  • CR 303.4e — an Aura's controller is separate from the enchanted object's controller; the two need not be the same
  • CR 109.4 — controller of an object (the enchanted permanent's current controller)
  • CR 503.1 — the upkeep step

Verification

  • cargo fmt --all — clean
  • scripts/check-parser-combinators.sh — clean
  • New parser unit tests in parser::oracle::tests:
    • super_intelligence_upkeep_scoped_to_enchanted_creature_controller — asserts phase == Upkeep, valid_target == ParentTargetController, and the card fully parses (no Unimplemented).
    • each_player_upkeep_phase_trigger_stays_unscoped — regression: a genuine "each player's upkeep" Howling-Mine trigger keeps valid_target == None (fires every turn).
  • Full parser::oracle::tests suite green.

Scope note

The "enchanted player's <phase>" player-enchant path (AttachedTo) is unchanged; the new arm is else if, so player-enchant Auras are unaffected. The existing Aggression card ("the end step of enchanted creature's controller") now also gains correct turn-owner scoping as a member of the same class.

…ase-rs#5275)

Super Intelligence -- "At the beginning of the upkeep of enchanted
creature's controller, that player draws a card." -- behaved like a
Howling Mine: the phase trigger carried no player scope, so it fired on
EVERY player's upkeep instead of only the enchanted creature's
controller's.

try_parse_phase_trigger scoped "enchanted player's <phase>" (player-
enchant Auras) to AttachedTo but had no handling for the creature-enchant
"the <phase> of enchanted creature's controller" form, leaving valid_target
unset (any player). Recognize that form and scope valid_target to
ParentTargetController, and add a trigger-side player_matches_filter arm
that resolves it to the controller of the permanent the Aura is attached
to. Per CR 303.4e the Aura's controller and the enchanted permanent's
controller may differ, so this cannot reuse the source-controller turn
constraint. No new engine variant -- reuses the existing
ParentTargetController filter whose documented resolution already covers
the AttachedTo host of an Aura phase trigger.

Adds tests: the Super Intelligence upkeep scope (ParentTargetController)
and a Howling-Mine regression ("each player's upkeep" stays unscoped).

CR 303.4e (Aura vs enchanted-permanent controller) + CR 109.4 (controller
of an object) + CR 503.1 (upkeep step).

@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 scoping for Aura phase triggers to the controller of the enchanted permanent or creature (e.g., "enchanted creature's controller") by introducing TargetFilter::ParentTargetController and parsing the corresponding Oracle phrases. However, the parser implementation in oracle_trigger.rs uses fragile substring checks (scan_contains) instead of idiomatic nom combinators, which violates the repository's strict architectural rule (R1) requiring combinators for parsing dispatch.

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 +11590 to +11593
else if scan_contains(phase_text, "enchanted creature's controller")
|| scan_contains(phase_text, "enchanted creature\u{2019}s controller")
|| scan_contains(phase_text, "enchanted permanent's controller")
|| scan_contains(phase_text, "enchanted permanent\u{2019}s controller")

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

Avoid using fragile substring checks like scan_contains for parsing Oracle phrases, as this bypasses the robust nom-based parser and creates fragile matches. Instead, decompose compound phrases (such as "enchanted 's controller") into modular, reusable parsers for constituent parts and compose them using idiomatic combinator aggregates (like nested alt and tag sequences) to prevent combinatorial explosion and improve maintainability.

Suggested change
else if scan_contains(phase_text, "enchanted creature's controller")
|| scan_contains(phase_text, "enchanted creature\u{2019}s controller")
|| scan_contains(phase_text, "enchanted permanent's controller")
|| scan_contains(phase_text, "enchanted permanent\u{2019}s controller")
let mut enchanted_controller = preceded(
tag("enchanted "),
terminated(card_type, alt((tag("'s controller"), tag("’s controller"))))
);
References
  1. Avoid verbatim string equality for parsing Oracle phrases as it bypasses the robust nom-based parser and creates fragile matches. Instead, decompose compound phrases into modular, reusable parsers for constituent parts and compose them using idiomatic combinator aggregates.

@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] Parse the enchanted-permanent controller phrase with composed nom axes instead of literal phrase scans. Evidence: crates/engine/src/parser/oracle_trigger.rs:11590 adds four scan_contains checks for the creature/permanent and ASCII/curly-apostrophe variants. Why it matters: this is new Oracle parsing dispatch, and the project rule is to compose the grammar dimensions (enchanted + object kind + possessive controller) with nom combinators so future sibling forms do not require another cartesian list of literals. Suggested fix: add a small phase-trigger helper that parses enchanted {creature|permanent}{'s|’s} controller with tag/alt/preceded/terminated (or an existing possessive helper if one fits), then set ParentTargetController from that parsed result.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 27 card(s), 1 signature(s) (baseline: main 1c3d1e1a9dd3)

27 card(s) · trigger/Phase · field valid target: parent target's controller

Examples: Aggression, Apathy, Dance of the Dead (+24 more)

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

…ators

Addresses review feedback (R1) on phase-rs#5317. The enchanted-permanent
controller scope was matched with four scan_contains literals (creature/
permanent x ASCII/curly apostrophe) — a cartesian list. Replace it with a
single composed combinator, parse_enchanted_controller_phrase, that parses
the axes with tag/alt ("enchanted " + {creature|permanent} + {'s|’s} +
" controller"), matched at any word boundary via scan_at_word_boundaries
(the phrase trails the phase noun, e.g. "the upkeep of …"). A new object-
kind sibling is now one alt() arm. Behavior-preserving; no logic change.
@philluiz2323

Copy link
Copy Markdown
Contributor Author

Thanks — addressed in f5e2c75. Replaced the four scan_contains literals with a single composed combinator parse_enchanted_controller_phrase that parses the grammar axes (tag("enchanted ") + alt((tag("creature"), tag("permanent"))) + alt((tag("'s"), tag("’s"))) + tag(" controller")), matched at any word boundary via scan_at_word_boundaries since the phrase trails the phase noun ("the upkeep of …"). A new object-kind sibling is now one alt() arm rather than another pair of literals. Behavior is unchanged; fmt and the parser-combinator gate pass and cargo check is clean.

@matthewevans matthewevans self-assigned this Jul 7, 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 review: current head resolves the prior nom-combinator finding; parse diff is scoped to enchanted permanent controller phase triggers.

@matthewevans matthewevans added the bug Bug fix label Jul 7, 2026
@matthewevans matthewevans enabled auto-merge July 7, 2026 20:23
@matthewevans matthewevans removed their assignment Jul 7, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 7, 2026
Merged via the queue into phase-rs:main with commit 757d7d4 Jul 7, 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.

Super Intelligence is a howling mine effect not only for the player — [[Super Intelligence]] draws an extra card on eac…

2 participants