Skip to content

A parked CR 616.1 replacement choice is destroyed by the spell's own CR 608.2n graveyard move #7509

Description

@lgray

🤖 AI text below 🤖

A parked CR 616.1 replacement choice is destroyed by the spell's own CR 608.2n graveyard move

pending_replacement is a single slot. When a spell's effect parks a CR 616.1 replacement choice mid-resolution, the spell's own "final part of resolution" move to its owner's graveyard is proposed while that choice is still parked. If that move also raises an optional replacement, it overwrites the parked choice — and the card the effect had already selected is silently dropped. It never leaves the hand, and nothing records it.

This is a data-loss defect. It is pre-existing and independent of the paused-discard-batch repair (PR #7494); it was found while instrumenting a fixture for that work.

Measured signature

Instrumenting a random-discard fixture to dump pending_replacement.proposed, the discard batch, and per-card zones at every prompt. At prompt #0:

pending = ZoneChange { object_id: ObjectId(2), from: Stack, to: Graveyard, cause: Some(ObjectId(2)), … }   <- the spell itself
batch   = (PlayerId(1), paused_card ObjectId(6), Random { pool: [3,4,5], remaining: 1 })
zones   = [(3,Hand),(4,Hand),(5,Hand),(6,Hand)]
end     = spell_zone=Graveyard  p1_gy=1  p1_hand=3  zones=[(3,Hand),(4,Graveyard),(5,Hand),(6,Hand)]

ObjectId(6) — the victim the effect actually selected first — never left the hand, for the rest of the game. ObjectId(4), selected after the resume, did.

Rules frame

CR 608.2n: "As the final part of an instant or sorcery spell's resolution, the spell is put into its owner's graveyard." That move is proposed while a CR 616.1 choice for a different object is already parked. CR 616.1 gives the affected player the choice among applicable replacement effects; the engine can hold only one such pending choice at a time.

Independence from PR #7494 — the load-bearing claim

The overwrite is already in place at prompt #0, before any resume code from that PR runs. Its drain executes only on a ChooseReplacement action, and prompt #0 is the first prompt. The only code from that PR on the path to this dump is the park, which writes the batch field and never touches pending_replacement. When the drain did run, it behaved correctly — it made the second pick, and that card reached the graveyard.

Smallest known repro

Two-player GameScenario. Give P1 a permanent hosting ReplacementDefinition::new(ReplacementEvent::Moved).destination_zone(Zone::Graveyard).mode(ReplacementMode::Optional { decline: None }) with valid_card: None. Give P0 a sorcery reading "Target player discards two cards at random." and P1 four cards in hand. Cast it targeting P1 and decline every prompt. Two prompts are raised; only one card leaves P1's hand.

Why PR #7494's own fixtures do not hit it

Its Windfall arm gates on ReplacementEvent::Discard (Library of Leng class). A spell going to its owner's graveyard is not a discard, so the spell's own move raises no choice there and there is nothing to clobber. The random-discard fixture that exposed this has since been narrowed with Not { SpecificObject { id: … } }, with the measurement recorded in its doc comment so a future reader sees why the narrowing exists.

Suggested repair shape

The parked-choice slot needs to either queue rather than overwrite, or refuse a second park while one is outstanding and defer the spell's own move until the first choice resolves. Note that the sacrifice family already solves an adjacent ordering problem with per-batch completion bookkeeping (PendingPlayerScopeSacrificeCompletion), which documents surviving "every replacement-choice pause" — that is the nearest precedent to trace before designing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions