fix(parser): Final Fortune loses at the extra turn's end step, not this one (#4231) - #8901
rykerwilliams wants to merge 2 commits into
Conversation
…d step (phase-rs#4231) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eation turn (phase-rs#4231) Final Fortune / Last Chance / Warrior's Oath / Chance for Glory: "that turn" is the extra turn granted by the parent clause (CR 500.7), so the delayed loss must skip the casting turn's own end step. Both temporal recognizers now use TurnGate::AfterCreationTurn, the existing floor for "the end step on your next turn". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review. 📝 WalkthroughWalkthroughThe parser now delays “that turn’s end step” triggers until after the creation turn. New integration tests verify Final Fortune resolves its loss at the granted extra turn’s end step. ChangesFinal Fortune timing
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Bug fix · Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to The delayed-loss timing fix is covered for casts during either player’s turn, with no concrete unresolved merge risk identified. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files. (1 skipped: 1 too large.)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
matthewevans
left a comment
There was a problem hiding this comment.
Changes requested
This fixes the immediate-turn timing, but the representation is still not rules-correct for that specific extra turn.
-
lower.rs:6853-6866andlower.rs:7044-7061selectController + AfterCreationTurn, butdelayed_trigger.rs:235-265persists onlyAfter(turn_number)andtriggers.rs:13175-13192matches any later controller end step. It must bind the delayed loss to the actual enqueued extra turn, or leave this form unimplemented until that provenance exists. In particular, add runtime coverage for a skipped extra turn and a reordered/LIFO extra-turn queue; the present tests cover only one ordinary extra turn. -
The generic syntax arms have no antecedent discriminator but force
Controller. Oracle en-Vec uses the same phrase after "that player's next turn," where the referent is the target player's turn rather than the controller's extra turn. Carry the antecedent authority through lowering, or reject/preserve the non-extra-turn form, with a discriminating parser/runtime test.
Required CI and the exact-head parse-diff receipt are still pending/missing. Those are merge gates after the correctness fixes, not the basis of these requested changes.
|
Generated for head Parse changes introduced by this PR✓ No card-parse changes detected. |
|
Both review points are correct, and I'm handing the design decision back rather than pushing a fix, because the rules-correct version turns out to reach the wire protocol. Everything below is measured; file:line references are at Why "bind it to the actual enqueued extra turn" is not a small change
The blocker that makes this your call, not mineThe id is wire-visible. And compatibility here is exact-match: Other registration surfaces this touches (found in review, listed so they aren't rediscovered)
A measured scope gap that the CR 614.10a purge would inheritStranglehold's "If an opponent would begin an extra turn" loses its subject in the engine: A correction to my own recordI earlier measured "Chance for Glory lowers to zero abilities" and would have used it to shrink the class. That was a dead instrument, not a finding — the committed test Where that leaves this PRThe commit here is a strict improvement — it stops the loss firing at the casting turn's end step — but it is not the representation you asked for, and it is wrong in the two ways you named: a skipped extra turn still kills the caster, and a LIFO-reordered queue fires it a turn early. Oracle en-Vec is confirmed as your second point: it already produces a controller-bound Three ways forward, and I'll take whichever you pick:
I have the full design, the gate verdict, and a verification matrix (skipped turn, LIFO reorder, zero-referent grant, non-controller grant, legacy-save decode, both serialized |
|
I re-read the follow-up on this head. The requested changes remain: the delayed-loss trigger must bind the specific granted extra turn and be discarded when that turn is skipped, and the generic temporal anaphor cannot force the controller without an antecedent authority. No code changed to resolve either issue, so this remains blocked. |
|
Understood, and agreed on both points — I'm not disputing the requirements, and I've deliberately not pushed a partial fix. To unblock it I need one decision from you, because it isn't mine to make: Is a
Either answer unblocks me immediately; I just don't want to spend a peer-evicting protocol bump on your behalf without you choosing it. |
|
I’ve captured the protocol-version tradeoff. The current PR remains blocked; no protocol bump or coverage rollback is authorized in this sweep. Please leave the branch unchanged pending an explicit maintainer decision on the supported-wire compatibility cost. |
Closes #4231.
The bug
Final Fortune:
This reproduces on current
main. Cast on your own turn, the delayed loss fires at this turn's end step, so the game ends (GameOver { winner: P1 }) before the extra turn is ever taken. Cast at instant speed on an opponent's turn, it happened to work, because that end step isn't the caster's.Root cause
Both temporal recognizers in
crates/engine/src/parser/oracle_effect/lower.rsmap "at the beginning of that turn's end step" toAtNextPhaseForPlayer { phase: End, gate: TurnGate::None, binding: Controller }:strip_temporal_prefix;strip_temporal_suffixtable.Their comments called this "identical to the 'your next end step' arm". It isn't. "Your next end step" may be the current turn's. "That turn" is the extra turn from the first sentence, which CR 500.7 adds after the current turn, so the current turn's end step must never fire it. With no turn floor, the matcher fired at the first end step where the caster was the active player.
The fix
Both arms now use the existing
TurnGate::AfterCreationTurn, which is already the floor for the Kav Landseeker "the end step on your next turn" arm next to them. It is stamped toAfter(creation_turn)ineffects::delayed_trigger::resolve, and theAtNextPhaseForPlayermatcher ingame/triggers.rsskips every matching phase up to and including that turn.It is a parser-only change: no new types, fields, or variants, and no runtime change.
Class
Scryfall lists four printed cards with exactly this sentence after an extra turn: Final Fortune, Last Chance, Warrior's Oath, Chance for Glory. Alchemist's Gambit carries the same sentence inside its cleave brackets. Oracle en-Vec also says "that turn's end step", but its anaphor is "that player's next turn". The parse-diff below will show whether it reaches this arm.
Tests
crates/engine/tests/integration/issue_4231_final_fortune_loses_at_extra_turns_end_step.rsdrives the real cast and priority loop (GameRunner::cast(..).resolve(), thenPassPrioritythrough each step). It uses Final Fortune's verbatim Oracle text.Reach-guards run before any timing assertion: exactly one extra turn is queued and exactly one delayed trigger is scheduled.
final_fortune_cast_on_your_turn_loses_at_the_extra_turns_end_step_not_this_one: the caster must still be in the game once the casting turn is over. The next turn must be theirs (turn_number + 1), and they must lose, with P1 winning, at that turn's end step.final_fortune_cast_on_an_opponents_turn_loses_at_the_extra_turns_end_step: the instant-speed sibling. It already passed before the fix and pins that the gate doesn't delay the loss past the caster's extra turn.The existing unit test
that_turns_end_step_temporal_resolves_to_controller_next_end_stepnow expectsAfterCreationTurnfrom both recognizers.Revert-proof: with
lower.rsreverted toupstream/main, the own-turn test fails withFinal Fortune's caster must still be in the game after the turn Final Fortune was cast in; waiting_for = GameOver { winner: Some(PlayerId(1)) }.Verification
All results are on the committed head
e33d78a4a:that_turns_end_step_temporal_resolves_to_controller_next_end_steptest andextra_turn_then_lose_parses_delayed_lose_the_game.cargo test -p phase-engine --lib: 21381 passed, 0 failed (8 ignored).Rust testsjob passed one33d78a4a. Locally, two full runs were cut off by the 580s cap on a loaded machine, and a four-shard run completed two shards: 3571 passed, 0 failed (1743 + 1828). The other two shards (3484 tests) hit the cap while a concurrent build held the CPU. I'm not claiming a local full pass; CI's is the complete one.cargo fmt --allis clean. Clippy was not run locally; CI's lint job owns it.docs/MagicCompRules.txt: 500.7, 603.7a, 104.3e, 614.10a.Residuals, stated rather than hidden
Both of these are equally wrong on
maintoday; this PR neither causes nor fixes them. Fixing either properly needs a delayed trigger bound to a specific extra turn, and extra turns carry no identity today.BeginTurnreplacement): the ruling and CR 614.10a say you don't lose, but the loss now waits for your next natural turn's end step.🤖 Generated with Claude Code