Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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.

39 changes: 38 additions & 1 deletion crates/engine/src/game/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5758,6 +5758,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 @@ -5781,6 +5782,7 @@ pub(crate) fn drain_pending_cost_move_resume(
| PendingCostMoveResume::ActivationMillPayment { .. }
| PendingCostMoveResume::LoyaltyActivation { .. }
| PendingCostMoveResume::CounterAdditionUnlessPayment { .. }
| PendingCostMoveResume::RandomDiscardUnlessPayment { .. }
)
),
CostMoveDrainBoundary::PriorityBoundary => matches!(
Expand Down Expand Up @@ -5861,6 +5863,19 @@ 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 701.9b + CR 616.1: same Delivered/Prevented -> Paid/Failed mapping
// as the counter-addition sibling directly above; a delivered (possibly
// redirected) discard counts as paid, a fully prevented one cannot pay
// a cost (CR 118.3).
engine_payment_choices::resume_random_discard_unless_payment(
state,
events,
matches!(boundary, CostMoveDrainBoundary::ReplacementDelivered { .. }),
)?
} else {
unreachable!("eligible cost-move root must remain parked")
};
Expand Down Expand Up @@ -16087,6 +16102,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`/`:5785`/
// `:5865` (+1/+1/+13 = +15, zero deletions), i.e. entirely ABOVE this
// producer; predicted `12004+15` equals the observed coordinate exactly.
// They add the `RandomDiscardUnlessPayment` resume to the two drain
// eligibility lists 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 @@ -16426,7 +16463,7 @@ mod stage2_injector_tests {
//
// SET PRESERVATION: unchanged. Upstream adds no line matching the needle to this file and
// neither does this branch — total still 37, partition still 5/7/25.
"game/engine.rs:12004".to_string(),
"game/engine.rs:12019".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