Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/engine/src/ai_support/payment_continuation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,11 @@ fn classify_parked_cost_move_root(state: &GameState) -> PaymentContinuationState
| PendingCostMoveResume::Foretell { .. }
| PendingCostMoveResume::UnlessBouncePayment { .. }
| PendingCostMoveResume::CounterAdditionUnlessPayment { .. }
// CR 701.9b: a parked random unless-discard holds no pending cast and
// no mana-ability cursor — the game picks the cards with no player
// input — so like its counter-addition sibling it affiliates with no
// payment-continuation root.
| PendingCostMoveResume::RandomDiscardUnlessPayment(..)
| PendingCostMoveResume::LoyaltyActivation { .. } => {
PaymentContinuationState::NotAffiliated
}
Expand Down Expand Up @@ -659,6 +664,8 @@ fn pending_cost_move_contains_root(
| Some(PendingCostMoveResume::DelveManaPayment { .. })
| Some(PendingCostMoveResume::UnlessBouncePayment { .. })
| Some(PendingCostMoveResume::CounterAdditionUnlessPayment { .. })
// CR 701.9b: holds no pending cast, so it can contain no root.
| Some(PendingCostMoveResume::RandomDiscardUnlessPayment(..))
| Some(PendingCostMoveResume::LoyaltyActivation { .. })
| None => false,
}
Expand Down
466 changes: 445 additions & 21 deletions crates/engine/src/game/effects/discard.rs

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion crates/engine/src/game/elimination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ fn abandon_pending_spell_casts(
| PendingCostMoveResume::UnlessBouncePayment { .. }
| PendingCostMoveResume::ManaAbilityPayment { .. }
| PendingCostMoveResume::LoyaltyActivation { .. }
| PendingCostMoveResume::CounterAdditionUnlessPayment { .. } => false,
| PendingCostMoveResume::CounterAdditionUnlessPayment { .. }
| PendingCostMoveResume::RandomDiscardUnlessPayment(..) => false,
};
if !abandons_spell {
state.pending_cost_move_resume = Some(resume);
Expand Down
37 changes: 36 additions & 1 deletion crates/engine/src/game/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6448,6 +6448,7 @@ pub(crate) fn drain_pending_cost_move_resume(
| PendingCostMoveResume::ActivationMillPayment { .. }
| PendingCostMoveResume::LoyaltyActivation { .. }
| PendingCostMoveResume::CounterAdditionUnlessPayment { .. }
| PendingCostMoveResume::RandomDiscardUnlessPayment(..)
)
),
// CR 606.4 + CR 616.1: a fully-prevented loyalty counter add (e.g. an
Expand All @@ -6471,6 +6472,7 @@ pub(crate) fn drain_pending_cost_move_resume(
| PendingCostMoveResume::ActivationMillPayment { .. }
| PendingCostMoveResume::LoyaltyActivation { .. }
| PendingCostMoveResume::CounterAdditionUnlessPayment { .. }
| PendingCostMoveResume::RandomDiscardUnlessPayment(..)
)
),
CostMoveDrainBoundary::PriorityBoundary => matches!(
Expand Down Expand Up @@ -6551,6 +6553,14 @@ pub(crate) fn drain_pending_cost_move_resume(
events,
matches!(boundary, CostMoveDrainBoundary::ReplacementDelivered { .. }),
)?
} else if matches!(
state.pending_cost_move_resume,
Some(PendingCostMoveResume::RandomDiscardUnlessPayment(..))
) {
// CR 118.12: random discard pauses only after its Moved replacement
// returns a replacement choice; the delivered boundary resumes the
// payment through its already-authorized paid epilogue.
engine_payment_choices::resume_random_discard_unless_payment(state, events)?
} else {
unreachable!("eligible cost-move root must remain parked")
};
Expand Down Expand Up @@ -18166,6 +18176,28 @@ mod stage2_injector_tests {
// with a delegation; it sits above this producer and below the first two.
// The merge tree therefore retains main's first two coordinates
// (`:6177`/`:6254`) and shifts this one by −16 to `:9442`.
// Random-discard-as-a-cost (#7320, review round 1): `engine.rs:12004 ⇒
// :12019`, +15, and ONLY the engine.rs entry moved — the four
// effects/mod.rs + scoped_library_search entries did not, which is the
// set-preservation evidence. `git diff -U0` on this file has exactly three
// hunks, ALL inside `drain_pending_cost_move_resume` at `:5761`/
// `:5865` (+1/+13 = +14, zero deletions), i.e. entirely ABOVE this
// producer; predicted `12004+14` equals the observed coordinate exactly.
// They add the `RandomDiscardUnlessPayment` delivery resume and its
// dispatch arm — a cost-payment continuation, not
// a prompt mint: it RESUMES an already-minted `UnlessPayment` rather than
// creating a recipient, so it is correctly absent from this census.
// Identity re-established, not assumed: the producer at `:12019` is the
// same announcement-time modal mint this row NAMES — an `Ok(Some(..))` of
// the optional-effect prompt over `player` / `source_id` /
// `trigger_description` / `may_trigger_key` — still inside
// `begin_pending_trigger_target_selection`. (Spelled out rather than
// quoted: the needle above is ASSEMBLED so this row cannot be counted by
// its own instrument, and a verbatim quote here re-introduces exactly the
// self-count that defends against — it inflates `in_test` and reds the
// TOTAL assert instead of this one.) The two asserts
// above this one fired GREEN on the run that caught it — total still 37,
// partition still 5/7/25 — so no producer was added or lost.
//
// ⚠ THIS ROW FAILS IN CI BEFORE IT FAILS LOCALLY, and that is not a bug in the
// row. CI checks out `refs/pull/<n>/merge` — this branch merged with CURRENT
Expand Down Expand Up @@ -18846,7 +18878,10 @@ mod stage2_injector_tests {
// This rebase raised the literal as a CONFLICT twice and then drifted it SILENTLY a
// third time at the tip; only the offset control caught the silent one. That is the
// drift class FU-4 (content-hash coordinate anchor) exists to end.
"game/engine.rs:12763".to_string(),
// #7320's random-discard continuation adds ten lines above this producer in the
// merged tree. Re-derived by the exact producer text at `:12773`, not by carrying
// the prior coordinate.
"game/engine.rs:12773".to_string(),
],
"the five production producers, NAMED: the CR 603.5 gate in `resolve_chain_body` \
plus the two repeated-optional-payment drivers, the per-player acceptance cursor \
Expand Down
Loading
Loading