fix(engine): recognize bare (unlabeled) Strive-shaped cost surcharge (Fireball) - #5545
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
… gap Adds the discovered casting-route bug (X+distribute cost never re-derives after targets are chosen) as its own open item, per the scope-split decision. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbgwGxbU9NHN9kou9isp8K
|
Thanks for this — the bare (unlabeled) Strive-shaped surcharge is a real parser gap and the seam looks right at a glance. This branch is currently in conflict with
Could you rebase onto the latest |
|
Bonus finding, verified directly: while working on a separate follow-up fix (the {X}+distribute casting-route timing bug, unrelated files), I confirmed this PR's fix also incidentally resolves a second, previously-undocumented misparse for Fireball's exact clause. On Verified this PR's fix ( |
…ibute casting route (phase-rs#5556) * fix(engine): re-derive target-dependent cost surcharge after distribution Fireball ({X}{R}: "This spell costs {1} more to cast for each target beyond the first. Fireball deals X damage divided evenly, rounded down, among any number of targets.") never actually paid the surcharge at runtime, even with strive_cost correctly populated (separate parser fix, PR phase-rs#5545) — its specific casting route bypassed cost re-derivation entirely. Root cause: {X} is announced (CR 601.2b) and the cost locked in via apply_post_x_cost_modifiers BEFORE targets exist, so the per-target surcharge loop is a no-op at that point. Targets are chosen after (CR 601.2c), then the "divided evenly among any number of targets" shape pauses at WaitingFor::DistributeAmong (CR 601.2d) — and the GameAction::DistributeAmong handler resumed by calling casting_costs::finalize_cast directly with the stale, pre-target cost, skipping check_additional_cost_or_pay_with_distribute entirely (the only place apply_target_dependent_cost_modifiers, the CR 601.2f surcharge authority, actually runs). Fix: route through casting_costs::finish_pending_cast_cost_or_pay (the same authority every other casting route already uses to go from "targets known" to "cost determined, locked, paid") instead of calling finalize_cast directly. This is a general seam fix, not Fireball- specific — Fireball is currently the only real card combining an {X} cost, a strive-shaped per-target surcharge, and a distribute-among- targets effect (verified via Scryfall), so any future card with this shape is covered automatically. Requires a rollback wrapper: state.pending_cast is taken (set to None) before the call, and finish_pending_cast_cost_or_pay's downstream chain has no restore-on-error of its own — so if the recomputed (correctly higher) cost turns out unpayable, a bare Err would leave state.waiting_for stale as DistributeAmong while pending_cast is gone, so a resubmitted DistributeAmong action would silently fall through to an unrelated resolution-time continuation branch instead of being cleanly rejected. Mirrors finalize_mana_payment's existing pending_for_restore clone-and-restore-on-Err pattern (CR 601.2h: "Unpayable costs can't be paid"). Applied the identical fix to a second call site with the same anti-pattern, found during review: engine_resolution_choices.rs's NamedChoice handler's spell-cast resume branch (activation_ability_index == None) also called finalize_cast directly with a stale cost. No currently-shipped card was confirmed to reach it with a target- dependent cost modifier, but the fix is mechanical and the pattern is identical, so it's fixed defensively rather than left as a known latent gap. A second near-miss (finalize_mana_payment's two pending.distribute- gated blocks) was investigated and found provably unreachable for every real card today (git-verified: the phase-rs#2856 gate predates and is an ancestor of the commits shaping those blocks) — documented with a cross-reference comment, no functional change needed there. Also corrects DistributionUnit::EvenSplitDamage's doc comment, which incorrectly claimed it always bypasses WaitingFor::DistributeAmong — true only for the non-deferred-target-selection flow, not for a deferred-selection card like Fireball. Went through 3 rounds of independent plan review (closed: a fabricated test-file dependency later clarified as belonging to a separate not-yet-merged PR, a genuinely-investigated near-miss call site, a corrected card-class table, and a traced-and-fixed state-corruption risk in the naive version of this fix) plus a review-impl pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbgwGxbU9NHN9kou9isp8K * fix: address clippy findings on PR review (never_loop, manual_is_multiple_of) drive_choose_x's for-loop never actually looped (every match arm diverges), so clippy correctly flagged it as dead — replaced with a single match. Also replaced total % n == 0 with total.is_multiple_of(n) per clippy's suggestion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbgwGxbU9NHN9kou9isp8K --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Fireball ({X}{R}, Alpha 1993): "This spell costs {1} more to cast for
each target beyond the first." parses as a genuine CR 601.2f cost
increase, but parse_strive_cost_line silently dropped it — it
unconditionally required an em-dash "Strive — " ability-word label,
which Fireball's clause predates by 21 years. The clause vanished with
zero Unimplemented signal (a separate catch-all for "this spell costs "
lines suppresses the warning), exactly the "looks done, is wrong"
misparse-backlog class.
Officious Interrogation (Murders at Karlov Manor, 2024) has the
identical bare, unlabeled shape — printed nine years after Strive
existed, WotC just didn't apply the label — confirming this is a real,
recurring grammatical variant, not a one-off pre-2014 artifact.
Split parse_strive_cost_line into a labeled-first entry point (~17
existing Strive cards, unchanged) plus a shared parse_strive_cost_body
nom pipeline, falling back to the bare form when no label is present.
Every downstream consumer (IR lowering, CardFace/GameObject field
copy, apply_target_dependent_cost_modifiers/concrete_cost_for_x in
game/casting.rs) already generically consumes the resulting
strive_cost regardless of population path, so the parser is the sole
fix point.
Fireball's own runtime gameplay cost is NOT fixed by this change — its
specific casting route ({X} cost + "divided evenly among any number of
targets", which pauses at a pre-payment DistributeAmong step) bypasses
the cost-surcharge mechanism entirely via a separate, independent
engine bug, unrelated to this parser gap. That's scoped to its own
follow-up (tracked in WORKLIST.md) since it touches shared
casting-pipeline code affecting ~15 other cards' payment-mode/convoke/
assist access, not just Fireball's surcharge. The runtime discriminating
test here uses Officious Interrogation instead, whose casting route
does reach the surcharge seam, proving the parser fix reaches real
production payment.
List hygiene: removes Fireball, Officious Interrogation, and Ajani's
Presence (confirmed via direct test execution to already parse
correctly today — a stale entry, not touched by this fix) from
docs/parser-misparse-backlog.md's Category phase-rs#5.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XbgwGxbU9NHN9kou9isp8K
8158762 to
405a376
Compare
|
Maintainer rebase — I've pushed your branch onto current Heads-up that I force-pushed What was wrongThe branch had no merge base with What I didCherry-picked your one real commit onto current The only conflict was in Verified before pushingBuilt against current
That last group is the one I cared about: splitting Your writeup was unusually good — in particular, calling out that Fireball's own runtime cost is not fixed here (its Letting CI run now; I'll approve and enqueue once it's green. For next timeTo avoid the no-merge-base problem, re-point your fork at upstream: |
Parse changes introduced by this PR · 2 card(s), 1 signature(s) (baseline: main
|
matthewevans
left a comment
There was a problem hiding this comment.
Approved. Rebased onto current main by maintainer (see above); 13/13 checks green on the rebased head 405a376084.
✅ Clean
- Right seam. Splitting
parse_strive_cost_lineinto a labeled entry point plus a sharedparse_strive_cost_bodyfixes the class, not the card: the em-dashStrive —label was an unconditional requirement, and Fireball's clause predates the ability word by 21 years. Officious Interrogation (MKM, 2024) has the identical bare shape, which is the proof this is a recurring grammatical variant rather than a pre-2014 artifact. - Scope is exactly the claim. Parse-diff (baseline
400a18223a92): 2 cards, 1 signature — Fireball and Officious Interrogation, nothing else. The removedRaiseCost (affects=self)static corroborates the diagnosis: the bare clause was being swallowed into a bogus self-targeting static instead of astrive_cost, with noUnimplementedsignal to flag it. Textbook "looks done, is wrong." - Discriminating runtime test at the right seam.
officious_bare_strive_surcharge_adds_wu_per_extra_targetproves the parsed cost reaches real production payment. And the pre-existing labeled-Strive tests still pass, which is the evidence that the ~17 existing labeled Strive cards didn't regress on the way through. - Honest scoping. Calling out that Fireball's own runtime cost is not fixed here — its
{X}+DistributeAmongroute bypasses the surcharge seam via a separate engine bug — and then picking Officious Interrogation as the runtime test because its casting route actually reaches the seam. Choosing the test card by which path the fix genuinely touches, and saying so in the commit message, is the part most PRs get wrong.
Enqueueing.
Summary
Fixes a parser misparse in
docs/parser-misparse-backlog.md's Category #5 ("Dropped 'for each' / dynamic count collapsed to Fixed", 333 cards).[[Fireball]] ({X}{R}, Sorcery, Limited Edition Alpha 1993 — verified via Scryfall):
"This spell costs {1} more to cast for each target beyond the first.\nFireball deals X damage divided evenly, rounded down, among any number of targets."The first line is a genuine CR 601.2f cost increase — mechanically identical to the "Strive" ability word (CR 207.2c, introduced in Journey into Nyx 2014, purely flavor-text with no independent rules meaning).
parse_strive_cost_linealready handles this cost shape fully and correctly for ~17 Strive-labeled cards, but it unconditionally requires an em-dash"Strive — "label before attempting the cost-pattern match. Fireball's clause predates the label by 21 years, so it's bare — the function returnsNoneimmediately, and the clause is silently dropped (a separate, unrelated catch-all for lines starting with"this spell costs "suppresses anyUnimplementedwarning, so the bug produces zero signal — exactly the "looks done, is wrong" class this backlog exists to catch).[[Officious Interrogation]] (Murders at Karlov Manor, 2024 — verified via Scryfall) has the identical bare, unlabeled shape:
"This spell costs {W}{U} more to cast for each target beyond the first.\nChoose any number of target players. Investigate X times..."— printed nine years after Strive existed, WotC simply chose not to apply the label. This confirms the bare form is a real, recurring grammatical variant of the mechanic, not a one-off pre-2014 artifact, so the fix is scoped to the class, not just Fireball.Fix
parse_strive_cost_linenow tries the labeled form first (unchanged behavior for existing Strive cards), and on no label, falls back to running the identical nom pipeline directly on the un-stripped line (factored into a sharedparse_strive_cost_bodyhelper so neither path duplicates the cost-matching logic). Every downstream consumer of the resultingstrive_cost: Option<ManaCost>(IR lowering,CardFace/GameObjectfield-copy, andgame/casting.rs'sapply_target_dependent_cost_modifiers/concrete_cost_for_x) already consumes it generically regardless of population path, so the parser function is the sole fix point — confirmed by direct trace, not assumption.Important scope note — Fireball's own gameplay cost is not fully fixed by this PR
While implementing this, I found a separate, independent engine bug: Fireball's specific casting route ({X} cost + "divided evenly among any number of targets", which pauses at a pre-payment
DistributeAmongstep) bypasses the cost-surcharge mechanism entirely — the total cost gets locked in back atChooseXValuetime, before targets are known, and is never re-derived once targets are actually chosen. So even with this parser fix, Fireball's actual in-game cost still won't scale with target count — that's a shared casting-pipeline bug (affects ~15 cards' payment-mode/convoke/assist access on the{X}+ distribute-among-targets route, not just Fireball's surcharge), scoped to its own follow-up rather than this PR. The runtime discriminating test here uses Officious Interrogation instead, whose casting route does reach the surcharge seam correctly, to prove the parser fix reaches real production payment for at least one card in the class.Files changed
crates/engine/src/parser/oracle.rs— the fixcrates/engine/src/parser/oracle_tests.rs— 3 new unit tests (Fireball's real text, Officious Interrogation's real text as class-coverage proof, a precision guard against over-matching)crates/engine/tests/integration/officious_interrogation_bare_strive_surcharge.rs(new) — discriminating runtime test driving the real cast pipelinecrates/engine/tests/integration/main.rs— registers the new test moduledocs/parser-misparse-backlog.md— list hygiene: removes Fireball, Officious Interrogation, and Ajani's Presence (a Strive-labeled card also listed in this category — confirmed via direct test execution, not inference, that it already parses correctly today; a stale entry unrelated to this fix)CR references
CR 207.2c, CR 601.2c, CR 601.2f
Verification
cargo fmt --allcleancargo clippy -p engine --lib -- -D warningsclean./scripts/check-parser-combinators.sh(Gate A): 0 violationscargo test -p engine --lib: 15976 passed, 0 failedcargo test -p engine --test integration: 2569 passed, 0 failedCo-Authored-By: Claude Sonnet 5 noreply@anthropic.com
https://claude.ai/code/session_01XbgwGxbU9NHN9kou9isp8K