Skip to content

feat(parser): support "whenever you discover, discover again for the same value"#5515

Merged
matthewevans merged 1 commit into
phase-rs:mainfrom
davion-knight:fix/curator-discover-again
Jul 10, 2026
Merged

feat(parser): support "whenever you discover, discover again for the same value"#5515
matthewevans merged 1 commit into
phase-rs:mainfrom
davion-knight:fix/curator-discover-again

Conversation

@davion-knight

Copy link
Copy Markdown
Contributor

Fixes #5270 (Curator of Sun's Creation). CR 701.57a.

"Whenever you discover, discover again for the same value. This ability triggers only once each turn." parsed to an Unknown trigger with an Unimplemented effect — the ability did nothing. The discover trigger (TriggerMode::Discover + match_discover) and effect (Effect::Discover) both already exist; this wires the two missing parser paths plus the "same value" quantity.

1. Trigger — "whenever you discover"

whenever/when <player> discover[s] (bare) → TriggerMode::Discover (matched on the EffectResolved{Discover} event). The discover keyword action is not a PlayerActionKind, so a dedicated is_bare_discover_subject gate routes it before the parse_player_action_list path. A compound discover, investigate, scry, … subject (Val, Marooned Surveyor) is deliberately left to the action-list path.

2. Effect — "discover again for the same value"

Effect::Discover { mana_value_limit: QuantityRef::TriggeringDiscoverValue, player: Controller } (composed from tags, not a verbatim match).

3. QuantityRef::TriggeringDiscoverValue (new)

The mana-value limit N of the discover that fired the current "whenever you discover" trigger. Threaded via a transient GameState::last_discover_value, set in the discover resolver before the EffectResolved event fires; resolves to 0 outside a discover context. Classified per the engine-variant checklist:

  • ability-scan: Axes::NONE (reads a bounded game-state scalar; no growing/sibling/projected axis).
  • read/write profiler: empty() (written only by discover resolution, never a sibling trigger).
  • cast-time board-state classifier: falls to the _ => false trigger-event-context arm (correctly non-evaluable before activation).
  • coverage + resolver arms added.

The OncePerTurn constraint already parsed, so Curator no longer loops on itself.

Verification

  • New tests: trigger_curator_discover_again_for_the_same_value (parser), triggering_discover_value_reads_last_discover_scalar (resolver).
  • Full engine lib suite: 16026 passed / 0 failed (touches shared game/quantity, ability_scan, ability_rw, layers, triggers, coverage).
  • Combinator gate clean (exit 0); clippy clean; parser gap analysis unchanged.

…same value"

Curator of Sun's Creation (phase-rs#5270): "Whenever you discover, discover again for the
same value. This ability triggers only once each turn." parsed to an Unknown
trigger mode with an Unimplemented effect — the ability did nothing. Both the
discover TRIGGER and the discover EFFECT already exist in the engine; this wires
the two missing parser paths and the "same value" quantity.

1. Trigger: "whenever/when <player> discover[s]" (bare) → `TriggerMode::Discover`
   (matched on the `EffectResolved{Discover}` event by `match_discover`). The
   discover keyword action is NOT a `PlayerActionKind`, so a dedicated
   `is_bare_discover_subject` gate routes it before the action-list path. A
   compound "discover, investigate, …" subject (Val) is left to that path.

2. Effect: "discover again for the same value" → `Effect::Discover` with
   `mana_value_limit = QuantityRef::TriggeringDiscoverValue`.

3. `QuantityRef::TriggeringDiscoverValue` (new): the mana-value limit N of the
   discover that fired the current "whenever you discover" trigger. Threaded via
   `GameState::last_discover_value`, set in the discover resolver (discover.rs)
   before the EffectResolved event fires; resolves to 0 outside a discover
   context. Classified conservatively in the ability-scan (Axes::NONE),
   read/write profiler (empty — written only by discover resolution, never a
   sibling trigger), and coverage sites; the cast-time board-state classifier
   correctly treats it as trigger-event context (non-evaluable before activation).

CR 701.57a. The `OncePerTurn` constraint ("triggers only once each turn") already
parsed, so `Curator` no longer loops.

Tests: `trigger_curator_discover_again_for_the_same_value` (parser),
`triggering_discover_value_reads_last_discover_scalar` (resolver). Full engine lib
suite green (16026 passed / 0 failed), combinator gate clean, clippy clean,
parser gap analysis unchanged.

@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 support for MTG Comprehensive Rule 701.57a (discovering again for the same value, as seen on Curator of Sun's Creation) by introducing a new QuantityRef::TriggeringDiscoverValue variant and tracking the transient last_discover_value in GameState. The parser is also updated to recognize bare discover triggers. Feedback on this PR highlights a potential race condition and stale read vulnerability from using a global GameState field for transient discover values, recommending retrieving this from the triggering event context instead. Additionally, a violation of Rule R1 was identified in the parser where .starts_with() is used instead of nom combinators.

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 +37 to +40
// CR 701.57a: record this discover's mana-value limit so a "whenever you
// discover" trigger's effect can reference "the same value" (Curator of
// Sun's Creation). Set before the trigger fires on the EffectResolved event.
state.last_discover_value = Some(limit as i32);

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] Global transient state last_discover_value is prone to race conditions and stale reads. Evidence: crates/engine/src/game/effects/discover.rs:40.
Why it matters: A single global field in GameState will be overwritten if multiple discover events occur before the triggers resolve, leading to incorrect values during resolution.
Suggested fix: Retrieve the discover value from the triggering event context (e.g., state.current_trigger_events) instead of a global GameState field.

Comment on lines +13198 to +13199
let rest = rest.trim_start();
rest.is_empty() || rest.starts_with(['.', ';'])

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.

medium

[MEDIUM] Use of starts_with in parser dispatch violates repository rules. Avoid redundant string prefix checks (such as '.starts_with()') before calling parsing functions that already perform leading character validation and whitespace trimming. Replace starts_with with a character check or a nom combinator.

Suggested change
let rest = rest.trim_start();
rest.is_empty() || rest.starts_with(['.', ';'])
let rest = rest.trim_start();
rest.is_empty() || rest.chars().next().map_or(false, |c| c == '.' || c == ';')
References
  1. R1. Nom combinators on the first pass — no exceptions. Every new parser dispatch under crates/engine/src/parser/ must use nom 8.0 combinators... any new .starts_with() used for parsing dispatch in non-test parser code is a finding. (link)
  2. Avoid redundant string prefix checks (such as '.starts_with()') before calling parsing functions that already perform leading character validation and whitespace trimming.

@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR · 1 card(s), 2 signature(s) (baseline: main 42a1057cccea)

1 card(s) · trigger/Discover · added: Discover (active in=battlefield, constraint=once per turn, valid target=controller)

Examples: Curator of Sun's Creation

1 card(s) · trigger/Whenever you discover · removed: Whenever you discover (active in=battlefield, constraint=once per turn)

Examples: Curator of Sun's Creation

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.

Approving. The architecture pass recorded on this head (1d2aa09) stands: new QuantityRef::TriggeringDiscoverValue + transient GameState::last_discover_value is the defensible seam given EffectResolved{Discover} carries no value, the variant is justified (no sibling cluster to parameterize), it's wired exhaustively, and the once-per-turn loop guard is both parsed and asserted (CR 701.57a). All 13 checks are green and the branch is clean.

One non-blocking watch item for a follow-up (not gating this PR): the discover value is read at re-discover resolution time rather than snapshotted at trigger time, so two overlapping discovers in the same on-stack window could feed the wrong value. It's narrow and fails safe (0 outside a discover context); the proper fix is a value-carrying EffectResolved{Discover} event, which is a separate change. Thanks for the clean fix and the loop-guard test.

@matthewevans matthewevans added bug Bug fix quality For high-quality minimal to no-churn PRs labels Jul 10, 2026
@matthewevans
matthewevans added this pull request to the merge queue Jul 10, 2026
Merged via the queue into phase-rs:main with commit 1f875c9 Jul 10, 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 quality For high-quality minimal to no-churn PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Curator of Sun's Creation — After discovering during my turn with [[Pantlaza, Sun-Favored]], my [[Curator of Sun's Crea…

2 participants