🤖 AI text below 🤖
Summary
Declining an optional discard replacement (Library of Leng) during a forced-count multi-card discard (Balance) causes every discard after the first to be silently dropped. The player is instructed to discard 2 cards; 1 leaves the hand. Only one replacement prompt fires where two are owed.
This is pre-existing on main and independent of any in-flight PR — measured below on c0da2db9c (v0.57.0).
CR 701.9a: "To discard a card, move it from its owner's hand to that player's graveyard." Balance instructs each player to discard down to the fewest cards in a hand; with hands of 3 and 1, P0 owes 2 discards and must finish at 1. It finishes at 2.
Note the replacement here is declined, so CR 614.6 ("If an event is replaced, it never happens") should not be engaged at all — the discard should proceed unmodified. The card is lost from the batch rather than replaced.
Measured
Same tree, same test, one variable — c0da2db9c, 2-player, P0 hand 3, P1 hand 1, P0 controls Library of Leng, Balance cast for free, all replacements declined:
| fixture |
prompts |
P0 hand |
verdict |
| Balance + Library of Leng, declined |
1 |
2 |
wrong — one discard lost |
| Balance alone, no Library of Leng |
0 |
1 |
correct |
The optional replacement is the only variable. A save/restore round-trip inserted at the pause changes nothing (measured separately on a PR head: identical prompts=1 p0=2), so this is not a persistence issue.
Instrumented pause trace, same fixture:
[0] DiscardChoice(count=2) batch=false snap=true p0=3 p1=1
[1] ReplacementChoice batch=false snap=true p0=3 p1=1
[2] Priority batch=false snap=true p0=2 p1=1
After the single ReplacementChoice is declined, the flow proceeds to Priority with P0 at 2 cards and never returns to discard the second selected card. Also visible: Balance's selected-card discard never parks a pending_discard_batch (batch=false at every pause), unlike a forced whole-hand discard, which may be where the remaining-count state is lost.
Reproduction
Append to crates/engine/tests/integration/balance_equalization.rs (uses that file's existing BALANCE_ORACLE and hand_len). It currently panics with the observed values; delete the add_creature_from_oracle line to see the correct control.
#[test]
fn repro_optional_discard_replacement_drops_remaining_discard() {
use engine::game::scenario::{GameScenario, P0 as SEAT0, P1 as SEAT1};
use engine::types::actions::GameAction;
use engine::types::mana::ManaCost;
use engine::types::phase::Phase;
const LENG: &str = "You have no maximum hand size.\nIf an effect causes you to discard a card, discard it, but you may put it on top of your library instead of into your graveyard.";
let mut scenario = GameScenario::new();
scenario.at_phase(Phase::PreCombatMain);
for i in 0..3 {
scenario.add_card_to_hand(SEAT0, &format!("P0 card {i}"));
}
scenario.add_card_to_hand(SEAT1, "P1 card");
scenario
.add_creature_from_oracle(SEAT0, "Library of Leng", 1, 1, LENG)
.as_artifact();
let balance = scenario
.add_spell_to_hand_from_oracle(SEAT0, "Balance", false, BALANCE_ORACLE)
.with_mana_cost(ManaCost::zero())
.id();
let mut runner = scenario.build();
runner.cast(balance).resolve();
if let WaitingFor::DiscardChoice { cards, count, .. } = runner.state().waiting_for.clone() {
let picked: Vec<ObjectId> = cards.into_iter().take(count).collect();
runner
.act(GameAction::SelectCards { cards: picked })
.expect("selecting Balance's discards must succeed");
}
let mut prompts = 0;
while let WaitingFor::ReplacementChoice { candidates, .. } = runner.state().waiting_for.clone()
{
let index = candidates
.iter()
.position(|c| c.description == "Decline")
.expect("Library of Leng must offer Decline");
runner
.act(GameAction::ChooseReplacement { index })
.expect("declining must resume");
prompts += 1;
}
runner.advance_until_stack_empty();
panic!(
"MAINREPRO prompts={} p0={} p1={}",
prompts,
hand_len(runner.state(), SEAT0),
hand_len(runner.state(), SEAT1),
);
}
Class, not card
Any optional replacement that applies to discarding, over any effect that discards a chosen count greater than one. Library of Leng and Balance are the reproduction, not the scope.
Provenance
Found while diagnosing a test fixture on #7494. Reproduced on main at c0da2db9c with no PR content in the tree, so it is not caused by that PR. Filing separately rather than expanding its scope.
🤖 AI text below 🤖
Summary
Declining an optional discard replacement (Library of Leng) during a forced-count multi-card discard (Balance) causes every discard after the first to be silently dropped. The player is instructed to discard 2 cards; 1 leaves the hand. Only one replacement prompt fires where two are owed.
This is pre-existing on
mainand independent of any in-flight PR — measured below onc0da2db9c(v0.57.0).CR 701.9a: "To discard a card, move it from its owner's hand to that player's graveyard." Balance instructs each player to discard down to the fewest cards in a hand; with hands of 3 and 1, P0 owes 2 discards and must finish at 1. It finishes at 2.
Note the replacement here is declined, so CR 614.6 ("If an event is replaced, it never happens") should not be engaged at all — the discard should proceed unmodified. The card is lost from the batch rather than replaced.
Measured
Same tree, same test, one variable —
c0da2db9c, 2-player, P0 hand 3, P1 hand 1, P0 controls Library of Leng, Balance cast for free, all replacements declined:The optional replacement is the only variable. A save/restore round-trip inserted at the pause changes nothing (measured separately on a PR head: identical
prompts=1 p0=2), so this is not a persistence issue.Instrumented pause trace, same fixture:
After the single
ReplacementChoiceis declined, the flow proceeds toPrioritywith P0 at 2 cards and never returns to discard the second selected card. Also visible: Balance's selected-card discard never parks apending_discard_batch(batch=falseat every pause), unlike a forced whole-hand discard, which may be where the remaining-count state is lost.Reproduction
Append to
crates/engine/tests/integration/balance_equalization.rs(uses that file's existingBALANCE_ORACLEandhand_len). It currently panics with the observed values; delete theadd_creature_from_oracleline to see the correct control.Class, not card
Any optional replacement that applies to discarding, over any effect that discards a chosen count greater than one. Library of Leng and Balance are the reproduction, not the scope.
Provenance
Found while diagnosing a test fixture on #7494. Reproduced on
mainatc0da2db9cwith no PR content in the tree, so it is not caused by that PR. Filing separately rather than expanding its scope.