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
21 changes: 21 additions & 0 deletions crates/engine/src/game/effects/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6857,6 +6857,27 @@ fn previous_effect_amount_from_events(
// event slice that may contain result-table branch effects or nested
// rolls interleaved with the outer dice.
Effect::RollDie { .. } => return state.die_result_this_resolution,
// CR 121.2 + CR 121.2a + CR 608.2c: `draw::resume_draw_sequence` is the
// single authority for how many cards a draw instruction delivered — it
// commits the whole instruction's post-replacement total to
// `state.last_effect_count` once the sequence completes (a unit replaced
// by something else contributes 0; one doubled by a count modifier
// contributes its post-replacement count). Read that committed total
// instead of re-summing draw events, exactly as the `RollDie` arm above
// defers to `die_result_this_resolution`, so "draw N cards, then discard
// that many" (Varina, Lich Queen; Hordewing Skaab; Horrid Shadowspinner;
// Laquatus's Creativity; Last Stand) reads the true total rather than a
// per-unit or pre-replacement count. The same stamp feeds the condition
// peer `AbilityCondition::PreviousEffectAmount` — Transcendent Archaic's
// "if you draw one or more cards this way, discard two cards".
//
// Returns early rather than falling through the `> 0` filter below: a
// draw that delivered zero cards is a real zero result and must stamp
// `Some(0)`. "Draw a card for each Island you control, then discard that
// many cards" (Last Stand) controlling no Islands has to discard 0, not
// inherit the life-gain amount its preceding chain step left behind in
// `last_effect_amount`.
Effect::Draw { .. } => return state.last_effect_count,
_ => 0,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,29 @@ fn hand_len(runner: &GameRunner, player: PlayerId) -> usize {
.unwrap_or(0)
}

fn library_len(runner: &GameRunner, player: PlayerId) -> usize {
runner
.state()
.players
.iter()
.find(|p| p.id == player)
.map(|p| p.library.len())
.unwrap_or(0)
}

/// Drive the trigger to the point where its "you may draw" decision is live and
/// accept it.
///
/// The `Priority`-arm-then-`break` shape this replaces exited before ever
/// dispatching `DecideOptionalEffect`: after `run_combat` the trigger is on the
/// stack under `WaitingFor::Priority`, so the very first iteration took the
/// priority arm, ran `advance_until_stack_empty` (which stops as soon as
/// `PassPriority` is rejected under `OptionalEffectChoice`) and broke out. The
/// draw therefore never happened and the "net hand size unchanged" assertion
/// held vacuously (issue #6858). Keep advancing and accepting until neither is
/// possible so the optional draw is genuinely taken.
fn accept_optional_effect(runner: &mut GameRunner) {
loop {
for _ in 0..8 {
match &runner.state().waiting_for {
WaitingFor::OptionalEffectChoice { .. } => {
runner
Expand All @@ -38,12 +59,14 @@ fn accept_optional_effect(runner: &mut GameRunner) {
}
WaitingFor::Priority { .. } if !runner.state().stack.is_empty() => {
runner.advance_until_stack_empty();
break;
}
_ => break,
_ => return,
}
}
runner.advance_until_stack_empty();
panic!(
"optional draw never settled; stuck on {:?}",
runner.state().waiting_for
);
}

#[test]
Expand All @@ -69,14 +92,46 @@ fn hordewing_skaab_discards_only_as_many_as_drawn_not_entire_hand() {

let mut runner = scenario.build();
let hand_before = hand_len(&runner, P0);
let library_before = library_len(&runner, P0);
assert_eq!(hand_before, 7, "precondition: seven cards in hand");

run_combat(&mut runner, vec![zombie], vec![]);
accept_optional_effect(&mut runner);

let hand_after = hand_len(&runner, P0);
// Reach-guard (issue #6858): the net-hand-size assertion below is satisfied
// just as well by a trigger that drew nothing and discarded nothing, so it
// cannot stand alone. Pin the draw against the library and the discard
// against the live prompt before reading hand size.
assert_eq!(
library_len(&runner, P0),
library_before - 1,
"one opponent was damaged: the optional draw must have taken a card"
);
let WaitingFor::DiscardChoice {
player,
count,
cards,
..
} = runner.state().waiting_for.clone()
else {
panic!(
"\"If you do, discard that many cards\" must prompt for one discard, got {:?}",
runner.state().waiting_for
);
};
assert_eq!(player, P0);
assert_eq!(count, 1, "discard exactly as many as were drawn");

runner
.act(GameAction::SelectCards {
cards: cards.iter().copied().take(1).collect(),
})
.expect("submitting the discard selection must succeed");
accept_optional_effect(&mut runner);

assert_eq!(
hand_after, hand_before,
hand_len(&runner, P0),
hand_before,
"one opponent was damaged: draw 1, then discard 1 — net hand size unchanged"
);
}
Loading
Loading