fix(parser): parse the leading negated-turn scope on static cost modifiers (Geyser Drake, Naiad of Hidden Coves)#6198
Conversation
…fiers (Geyser Drake, Naiad of Hidden Coves)
"During turns other than yours, spells you cast cost {1} less to cast" (Geyser
Drake, Naiad of Hidden Coves) parsed with condition: None -- the leading
negated-turn timing clause was not recognized by the cost-modifier parser, so
the reducer applied on EVERY turn, including the controller's own.
Root cause: the cost-modifier leading-condition handling recognized only the
affirmative "During your turn," prefix (Tithe Taker). A leading "During turns
other than yours," clause -- the form these reducers print -- fell through and
dropped the timing restriction.
Fix: add `parse_leading_turn_scope`, mapping the leading clause to the same
`StaticCondition` the CDA / dispatch / type-change static parsers already use for
this vocabulary -- `Not(DuringYourTurn)` for "turns other than yours",
`DuringYourTurn` for the affirmative form -- and route the shared leading-clause
fallback in `try_parse_cost_modification` through it. A single authority for every
cost-mod branch (self-spell, first-qualified, generic), so the negated form is
handled symmetrically with the affirmative one. Parse-only: the cost-mod
condition resolver already evaluates `StaticCondition::Not` and `DuringYourTurn`
(`evaluate_cost_mod_static_condition`), gating against the source controller
(CR 102.1).
CR 604.1 (continuous-effect duration/condition) / CR 601.2f (total cost
determination) / CR 102.1 (active player).
Tests: parser unit test over the leading negated + affirmative forms, a
plain-unconditioned regression, and a self-spell branch-symmetry case; registered
runtime cast-pipeline regression (negated_turn_spell_cost_reduction) proving a {2}
spell stays {2} on the controller's own turn and is reduced to {1} on other turns
-- the cost is read at `WaitingFor::TargetSelection`, and reverting the parser fix
reduces it on the controller's turn too, failing the test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces support for parsing leading turn-scope clauses, specifically handling both affirmative ('during your turn, ') and negated ('during turns other than yours, ') conditions for cost-modification static abilities. It extracts this logic into a reusable helper parse_leading_turn_scope using nom combinators, updates try_parse_cost_modification to utilize it, and adds comprehensive integration tests to verify correct parsing and runtime behavior. The implementation adheres perfectly to the repository's architectural guidelines, including proper nom combinator usage and Magic Comprehensive Rules (CR) annotations. No review comments were provided, and there is no additional feedback to address.
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.
|
Maintainer update for
Held pending current-head CI, a fresh parse-diff artifact, and the now- |
Parse changes introduced by this PR · 4 card(s), 4 signature(s) (baseline: main
|
# Conflicts: # crates/engine/src/parser/oracle_static/static_helpers.rs
|
Maintainer branch update for
Held pending the new current-head CI, parse-diff artifact, and GitHub's transient |
The hurkyls_final_meditation_self_cost_tracks_controller_turn regression
cast Hurkyl's Final Meditation with CastPaymentMode::Auto but seeds no
mana source, so Auto payment failed with "Cannot pay mana cost" before the
helper could read the locked pending-cast cost. Hurkyl's takes no target,
so the cast never pauses at TargetSelection the way the Geyser helper does.
Manual mode surfaces WaitingFor::ManaPayment and pauses at the locked-cost
boundary without auto-tapping, matching the helper's own
assert!(matches!(waiting_for, ManaPayment { .. })).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed The helper cast Hurkyl's Final Meditation with Verified locally (Tilt down): all 4 tests in |
|
Acknowledged the |
matthewevans
left a comment
There was a problem hiding this comment.
Approved after current-head review: the leading turn-scope parser uses the shared static-condition vocabulary, composes with the ordinal gate, and the registered runtime regressions distinguish both controller-turn outcomes.
Summary
"During turns other than yours, spells you cast cost {1} less to cast" (Geyser
Drake, Naiad of Hidden Coves) parsed with
condition: None— the leadingnegated-turn timing clause was not recognized by the cost-modifier scope stripper,
so the reducer applied on every turn, including the controller's own.
Root cause:
strip_cost_mod_during_your_turn_scope(static_helpers.rs) onlyhandled a trailing " during your turn" suffix (Paladin Class). A leading
"During turns other than yours," / "During your turn," clause — the form these
reducers print — fell through and dropped the timing restriction.
Fix: add
parse_leading_turn_scope, mapping the leading clause to the sameStaticConditionthe CDA / dispatch / type-change static parsers already use forthis vocabulary —
Not(DuringYourTurn)for "turns other than yours",DuringYourTurnfor the affirmative form — and route the single sharedleading-clause fallback in
try_parse_cost_modificationthrough it, so everycost-mod branch (self-spell, first-qualified, generic) gates the negated form
symmetrically with the affirmative one (previously only the affirmative
"During your turn," was recognized). Parse-only: the cost-mod condition
resolver already evaluates
StaticCondition::NotandDuringYourTurn(
evaluate_cost_mod_static_condition), gating against the source controller(CR 102.1).
Files changed
crates/engine/src/parser/oracle_static/static_helpers.rs— newparse_leading_turn_scopecombinator; the shared leading-clause fallback now recognizes both the affirmative and negated forms (single authority for all branches).crates/engine/src/parser/oracle_static/tests.rs— parser unit test over the leading negated + affirmative forms, a plain-unconditioned regression, and a self-spell branch-symmetry case.crates/engine/tests/integration/negated_turn_spell_cost_reduction.rs(new, registered) — runtime cast-pipeline regression.CR references
Implementation method (required)
Found in the same fresh cost-modifier live-parse audit that surfaced the ordinal
fix; live-verified against HEAD (
condition: None). TheNot(DuringYourTurn)mapping reuses the negated-turn vocabulary already present in cda.rs / dispatch.rs
/ type_change.rs — a building-block reuse, not a new condition.
Track
Developer / misparse fix.
LLM
Claude Opus 4.8 (1M context).
Verification
cargo fmt --all— clean.cargo clippy -p engine --all-targets --features proptest -- -D warnings— 0 warnings.cargo test -p engine --lib parser::oracle_static— green (incl. the new leading-scope unit test).cargo test -p engine --test integration negated_turn_spell_cost_reduction— 3/3 pass: a {2} spell stays {2} on the controller's own turn and is reduced to {1} on other turns (cost read atWaitingFor::TargetSelection; reverting the parser fix reduces on the controller's turn too and fails the test).Gate A
Tilt was down; ran checks directly. clippy 0 warnings; parser lib suite green;
integration regression 3/3.
Anchored on
upstream/main @
11cc01581.Final review-impl
Independent fresh-context review (diff + CLAUDE.md + review-impl skill only, no
prior conversation) ran clean on all lenses — correct seam, nom-mandate (pure
alt/value/tag), regression safety (trailing "during your turn" suffix andplain cost mods unchanged; the comma in
tag("during your turn, ")can't mis-fireon the comma-less trailing form), realizability (
evaluate_cost_mod_static_conditionhandles
Not+DuringYourTurn, gated against the source controller), and adiscriminating runtime test (flips on revert). It raised one LOW finding: the
negated form was initially handled only in the generic branch, leaving the
self-spell / first-qualified branches asymmetric. Addressed in this PR by
routing the negated form through the single shared leading-clause authority so all
three branches are symmetric (covered by the added self-spell unit-test case). No
real card exercised the asymmetric subclass, but the fix is now correct for it.
Claimed parse impact
2 cards move from an unconditioned every-turn reducer to a correctly negated-turn-
gated reducer: Geyser Drake, Naiad of Hidden Coves. No card regresses — the
trailing " during your turn" suffix path (Paladin Class) and plain unconditioned
cost mods are unchanged (covered by the parser unit test's regression asserts).
Validation Failures
None.
CI Failures
None expected.