fix(parser): UNSUPPORTED seam: Once-per-turn {0} alternative cost gated on a dynamic mana-value thre#5755
Conversation
…ed on a dynamic mana-value thre
…2-unsupported-seam-once-per
There was a problem hiding this comment.
Code Review
This pull request implements support for once-per-turn alternative cost grants (such as As Foretold) by introducing a frequency axis to alternative cost definitions and tracking used permissions in the game state. It also adds a parser mechanism to rebind bare X mana-value gates from trailing 'where X is...' clauses to dynamic quantities like counters on the source. The implementation is highly idiomatic, adheres strictly to the MTG Comprehensive Rules (CR 118.9, CR 202.3, CR 601.2b), and uses composable building blocks with thorough test coverage. No review comments were provided to assess.
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.
Parse changes introduced by this PR · 1 card(s), 2 signature(s) (baseline: main
|
matthewevans
left a comment
There was a problem hiding this comment.
Approved — the current ported head cleanly parameterizes the existing alternative-cost seam.
🔴 Blocker
- None.
🟡 Non-blocking
- None.
✅ Clean
crates/engine/src/game/casting.rsandcasting_costs.rscarry the selected once-per-turn source through the existing cast choice and consume it only at finalized casts; decline and copied-spell paths leave the permission unspent.- The current parse artifact matches the stated scope exactly: As Foretold loses
unknownand gains the zero-cost alternative-cost static with the source-counter mana-value filter. - Registered runtime coverage reaches the cast pipeline for above-threshold exclusion, acceptance, and decline; current Rust lint/tests, card-data, and platform checks are green.
Recommendation: enqueue via the merge queue.
Summary
Fixes a parser misparse affecting 1 card(s) in the Doctor Who Commander precons.
Root cause: UNSUPPORTED seam: Once-per-turn {0} alternative cost gated on a dynamic mana-value threshold
Cards corrected
Fix
Implemented Cluster 112 (As Foretold): the once-per-turn, dynamic-mana-value alternative-cost grant class, as a pure parameterization of the existing
StaticMode::CastWithAlternativeCostseam (no new enum variant). Two class-level axes were added over the existing unlimited grants (Fist of Suns / Rooftop Storm / Jodah / Primal Prayers):frequency: CastFrequencyfield onCastWithAlternativeCost(reusing the existing enum; defaultUnlimited, skip-serialized so existing cards' JSON is byte-identical).FilterProp::Cmc { LE, QuantityRef::CountersOn { Source, Time } }machinery.Runtime once-per-turn enforcement mirrors the established
hand_cast_free_permissions_usedlifecycle: a newGameState::alt_cost_grant_permissions_usedHashSet is skipped-when-used ingranted_spell_alternative_cost_for, consumed atfinalize_cast, and cleared at turn start inturns.rs. Provenance flows via newOption<ObjectId>fields onSpellContextandPendingCastacross theOptionalCostChoiceround-trip. The parser gained aparse_alt_cost_frequency_prefixcombinator (single authority shared by the classifier pre-filter and the lowering), a relaxed classifier admitting the "Once each turn," prefix + singular "spell you cast", a bare-article ("a"/"an") widening to any-spell, a strict-fail on unconsumed mana-value tails, and the building-block win:parse_mana_value_suffixnow binds a trailing ", where X is " clause onto a bare-X gate so every consumer inherits dynamic-MV support.VERIFICATION (worktree not under Tilt; cargo run directly): cargo fmt clean; targeted parser diff-gate vs HEAD clean (zero string dispatch in my parser diffs);
cargo clippy -p engine -p phase-ai -p engine-wasm -- -D warnings(NO --all-targets) exit 0 — cross-crate exhaustiveness satisfied;cargo test -p engine -p phase-ai --no-fail-fastexit 0 (all lib + integration pass, including 5 new As Foretold tests: parser building-block, full-parse, runtime offer/accept/consume, runtime decline-not-consumed, runtime MV-gate). oracle-gen export confirms As Foretold now emits the OncePerTurn CastWithAlternativeCost static with Cmc LE CountersOn(Source,time) and ZERO Unimplemented, while Fist of Suns/Rooftop Storm/Jodah/Conspiracy Unraveler stay byte-identical (frequency unset = Unlimited).JUDGEMENT CALLS: (1) Finalize consumption reads
ability.context.alt_cost_grant_sourceBEFORE the placeholder branch, not fromstack_ability— vanilla permanent spells carrystack_ability=Nonebut their alt cost was still applied and must consume the slot (caught by the first integration run). (2) Split the MV-gate runtime check into its own test because a no-mana cast leaves a spell on the stack, blocking a later sorcery-speed cast. (3) Article handling viamatches!(type_prefix_lower.trim(), "" | "a" | "an")— a tokenized equality check, not substring dispatch.STOP-AND-RETURN: none — no CR ambiguity, seam fit cleanly.
DEVIATIONS FROM PLAN: plan cited CR 707.10c for the copy reset; correct rule is CR 707.10 ("a copy of a spell isn't cast") — used 707.10. Plan's finalize capture read
stack_ability.context; corrected toability.contextpre-placeholder. Scope expansion to keep the workspace compiling after the type changes (see scopeExpansion).RISKS: full-corpus gen-card-data.sh + cargo semantic-audit/coverage were NOT run (disk ceiling ~27Gi, worktree not the card-data source of truth). Changes are narrow and guarded and the full 16502-test lib suite passes, but recommend the orchestrator run a corpus semantic-audit under Tilt to confirm no other "...spell you cast with mana value N ..." card now strict-fails to honest Unimplemented (by-design, not a silent misparse). Known SHARED pre-existing limitation (not introduced):
payable_spell_alternative_cost_detailsis Zone::Hand-gated, so As Foretold applies to hand-cast spells only — same as every existing grant; out of scope, note in PR body.Files changed
CR references
Verification
cargo fmt --all— passcheck-parser-combinators.sh <merge-base>— passcargo clippy -p engine -p phase-ai -p engine-wasm -- -D warnings— passcargo test -p engine -p phase-ai— passoracle-gen data --filter "as foretold"— passCards confirmed re-parsed correctly: As Foretold
🤖 Generated with Claude Code