From cdfb3a355f475c28feddf551c58b2d8c8fdbae17 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Sat, 8 Aug 2026 19:02:53 -0700 Subject: [PATCH 1/5] fix(engine): settle cleanup discard triggers --- .../src/game/engine_resolution_choices.rs | 35 +- .../cleanup_discard_trigger_pipeline.rs | 449 ++++++++++++++++++ crates/engine/tests/integration/main.rs | 1 + 3 files changed, 480 insertions(+), 5 deletions(-) create mode 100644 crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs diff --git a/crates/engine/src/game/engine_resolution_choices.rs b/crates/engine/src/game/engine_resolution_choices.rs index 5405522f18..5f184cfb0c 100644 --- a/crates/engine/src/game/engine_resolution_choices.rs +++ b/crates/engine/src/game/engine_resolution_choices.rs @@ -22,7 +22,7 @@ use super::effects; use super::engine::EngineError; use super::turns; use super::zones; -use super::{casting, casting_costs, mana_abilities}; +use super::{casting, casting_costs, engine_priority, mana_abilities, public_state}; /// CR 701.23a + CR 614.1: offer every found card as its own replaceable event. /// Original survivors remain in the printed search continuation; modified cards @@ -4451,14 +4451,39 @@ pub(super) fn handle_resolution_choice( } } + let event_start = events.len(); if turns::finish_cleanup_discard(state, player, &chosen, events) { return Ok(action_result_outcome(events, state.waiting_for.clone())); } - let _ = turns::advance_phase_once(state, events); - return Ok(ResolutionChoiceOutcome::WaitingFor(turns::auto_advance( - state, events, - ))); + // CR 514.3a + CR 603.3 + CR 117.5: cleanup-discard events must pass + // through the ordinary SBA/trigger settlement before cleanup can end. + // Synchronize the provisional priority first: this is the authority + // that normalizes legacy waiting states and derives the authorized + // priority submitter under turn control. + let provisional_cleanup_priority = WaitingFor::Priority { player }; + public_state::sync_waiting_for(state, &provisional_cleanup_priority); + let settled = engine_priority::run_post_action_pipeline_from( + state, + events, + event_start, + &provisional_cleanup_priority, + false, + false, + )?; + public_state::sync_waiting_for(state, &settled); + + if matches!(state.waiting_for, WaitingFor::Priority { .. }) && state.stack.is_empty() { + let _ = turns::advance_phase_once(state, events); + let advanced = turns::auto_advance(state, events); + public_state::sync_waiting_for(state, &advanced); + } + + // The suffix pipeline above already processed this action's discard + // events, including persistent delayed triggers. Return the completed + // action rather than entering apply_action's outer full-buffer pipeline, + // which would otherwise scan those discard events a second time. + return Ok(action_result_outcome(events, state.waiting_for.clone())); } ( WaitingFor::ConniveDiscard { diff --git a/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs b/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs new file mode 100644 index 0000000000..c783d4db45 --- /dev/null +++ b/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs @@ -0,0 +1,449 @@ +//! Regression for Discord thread 1535814760485093526: cleanup discards must +//! settle their discard triggers before the cleanup step can advance. + +use engine::game::game_object::AttachTarget; +use engine::game::scenario::{GameRunner, GameScenario, P0, P1}; +use engine::game::trigger_index::reindex_object_triggers; +use engine::game::zones::create_object; +use engine::types::ability::{ + DelayedTriggerCondition, Effect, QuantityExpr, ReplacementDefinition, ResolvedAbility, + TargetFilter, TriggerDefinition, TurnGate, WheneverEventExpiry, +}; +use engine::types::actions::GameAction; +use engine::types::game_state::{ActivePlayerControl, DelayedTrigger, WaitingFor}; +use engine::types::identifiers::CardId; +use engine::types::replacements::{ReplacementEvent, ReplacementMode}; +use engine::types::triggers::TriggerMode; +use engine::types::zones::Zone; + +const MAGMAKIN_ARTILLERIST: &str = "Whenever you discard one or more cards, this creature deals that much damage to each opponent.\nCycling {1}{R} ({1}{R}, Discard this card: Draw a card.)\nWhen you cycle this card, it deals 1 damage to each opponent."; +const CURIOSITY: &str = "Enchant creature\nWhenever enchanted creature deals damage to an opponent, you may draw a card."; + +fn setup_cleanup_discard( + hand_size: usize, + artillerist_count: usize, + with_curiosity: bool, +) -> ( + GameRunner, + Vec, + Vec, +) { + let mut scenario = GameScenario::new(); + scenario.at_phase(engine::types::phase::Phase::Cleanup); + + let artillerists = (0..artillerist_count) + .map(|index| { + scenario + .add_creature_from_oracle( + P0, + &format!("Magmakin Artillerist {index}"), + 4, + 4, + MAGMAKIN_ARTILLERIST, + ) + .id() + }) + .collect::>(); + let curiosity = with_curiosity.then(|| { + scenario + .add_enchantment_from_oracle(P0, "Curiosity", CURIOSITY) + .with_subtypes(vec!["Aura"]) + .id() + }); + let cards = (0..hand_size) + .map(|index| scenario.add_card_to_hand(P0, &format!("Hand Card {index}"))) + .collect(); + for index in 0..4 { + scenario.add_card_to_library_top(P0, &format!("Library Card {index}")); + } + + let mut runner = scenario.build(); + { + let state = runner.state_mut(); + state.active_player = P0; + state.priority_player = P0; + state.phase = engine::types::phase::Phase::Cleanup; + state.waiting_for = WaitingFor::DiscardToHandSize { + player: P0, + count: hand_size - 7, + cards: cards.clone(), + }; + if let (Some(&artillerist), Some(curiosity)) = (artillerists.first(), curiosity) { + state.objects.get_mut(&curiosity).unwrap().attached_to = + Some(AttachTarget::Object(artillerist)); + state + .objects + .get_mut(&artillerist) + .unwrap() + .attachments + .push(curiosity); + reindex_object_triggers(state, curiosity); + } + } + + (runner, cards, artillerists) +} + +fn resolve_until_optional_choice(runner: &mut GameRunner) { + for _ in 0..16 { + match runner.state().waiting_for { + WaitingFor::OptionalEffectChoice { .. } => return, + WaitingFor::Priority { .. } if !runner.state().stack.is_empty() => { + runner + .act(GameAction::PassPriority) + .expect("pass priority while resolving cleanup trigger"); + } + ref waiting => panic!("expected Curiosity optional choice, got {waiting:?}"), + } + } + panic!("cleanup trigger chain did not reach Curiosity"); +} + +#[test] +fn cleanup_discard_stacks_magmakin_then_curiosity_exactly_once() { + let (mut runner, cards, artillerists) = setup_cleanup_discard(8, 1, true); + let artillerist = artillerists[0]; + + let result = runner + .act(GameAction::SelectCards { + cards: vec![cards[0]], + }) + .expect("submit cleanup discard"); + + assert_eq!( + result + .events + .iter() + .filter(|event| matches!(event, engine::types::events::GameEvent::Discarded { .. })) + .count(), + 1, + "the cleanup selection must emit one discard event" + ); + assert_eq!(runner.state().objects[&cards[0]].zone, Zone::Graveyard); + assert_eq!(runner.state().phase, engine::types::phase::Phase::Cleanup); + assert_eq!( + runner + .state() + .stack + .iter() + .filter(|entry| entry.source_id == artillerist) + .count(), + 1, + "Magmakin's batched discard trigger must be placed exactly once" + ); + + resolve_until_optional_choice(&mut runner); + assert_eq!( + runner.state().players[1].life, + 19, + "Magmakin deals one damage once" + ); + let hand_before_draw = runner.state().players[0].hand.len(); + runner + .act(GameAction::DecideOptionalEffect { accept: true }) + .expect("accept Curiosity draw"); + assert_eq!( + runner.state().players[0].hand.len(), + hand_before_draw + 1, + "Curiosity draws exactly one card after the single damage event" + ); + assert!( + !matches!( + runner.state().waiting_for, + WaitingFor::OptionalEffectChoice { .. } + ), + "the single Curiosity trigger must not be duplicated" + ); + assert!( + runner.state().stack.is_empty(), + "accepting Curiosity must leave no duplicate Curiosity trigger on the stack" + ); + assert!(matches!( + runner.state().waiting_for, + WaitingFor::Priority { player: P0 } + )); + for _ in 0..2 { + if matches!( + runner.state().waiting_for, + WaitingFor::DiscardToHandSize { .. } + ) { + break; + } + runner + .act(GameAction::PassPriority) + .expect("pass priority to begin the repeated cleanup step"); + } + let second_cleanup_card = match &runner.state().waiting_for { + WaitingFor::DiscardToHandSize { + player, + count, + cards, + } => { + assert_eq!(*player, P0); + assert_eq!(*count, 1); + cards[0] + } + waiting => panic!("expected repeated cleanup discard choice, got {waiting:?}"), + }; + runner + .act(GameAction::SelectCards { + cards: vec![second_cleanup_card], + }) + .expect("submit the repeated cleanup discard"); + resolve_until_optional_choice(&mut runner); + assert_eq!( + runner.state().players[1].life, + 18, + "both cleanup discards must produce exactly one Magmakin trigger" + ); + runner + .act(GameAction::DecideOptionalEffect { accept: false }) + .expect("decline the second Curiosity opportunity"); + assert!( + !matches!( + runner.state().waiting_for, + WaitingFor::OptionalEffectChoice { .. } + ), + "the second cleanup chain must surface exactly one Curiosity opportunity" + ); + assert!(runner.state().stack.is_empty()); + for _ in 0..4 { + if runner.state().phase != engine::types::phase::Phase::Cleanup { + break; + } + runner + .act(GameAction::PassPriority) + .expect("pass priority through the repeated cleanup step"); + } + assert_ne!( + runner.state().phase, + engine::types::phase::Phase::Cleanup, + "after the trigger stack empties, cleanup must run again before the turn advances" + ); +} + +#[test] +fn cleanup_discard_batches_two_cards_into_one_magmakin_trigger() { + let (mut runner, cards, artillerists) = setup_cleanup_discard(9, 1, false); + let artillerist = artillerists[0]; + + runner + .act(GameAction::SelectCards { + cards: cards[..2].to_vec(), + }) + .expect("submit two-card cleanup discard"); + + assert_eq!( + runner + .state() + .stack + .iter() + .filter(|entry| entry.source_id == artillerist) + .count(), + 1, + "one-or-more discard trigger must be batched and collected exactly once" + ); + for _ in 0..8 { + if runner.state().stack.is_empty() { + break; + } + runner + .act(GameAction::PassPriority) + .expect("pass priority to resolve Magmakin"); + } + assert!( + runner.state().stack.is_empty(), + "Magmakin trigger must resolve" + ); + assert_eq!( + runner.state().players[1].life, + 18, + "the one batched trigger must deal damage equal to both discarded cards" + ); +} + +#[test] +fn cleanup_discard_fires_persistent_delayed_trigger_exactly_once() { + let (mut runner, cards, _) = setup_cleanup_discard(8, 0, false); + let creation_turn = runner.state().turn_number; + let source = create_object( + runner.state_mut(), + CardId(9_002), + P0, + "Persistent Discard Trigger".to_string(), + Zone::Battlefield, + ); + runner + .state_mut() + .delayed_triggers + .push(DelayedTrigger::new( + DelayedTriggerCondition::WheneverEvent { + trigger: Box::new(TriggerDefinition::new(TriggerMode::DiscardedAll)), + expiry: WheneverEventExpiry::UntilControllersNextTurn { + after: TurnGate::After(creation_turn), + }, + }, + Box::new(ResolvedAbility::new( + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 1 }, + player: TargetFilter::Controller, + }, + vec![], + source, + P0, + )), + P0, + source, + false, + )); + + runner + .act(GameAction::SelectCards { + cards: vec![cards[0]], + }) + .expect("submit cleanup discard with persistent delayed trigger"); + + assert_eq!( + runner + .state() + .stack + .iter() + .filter(|entry| entry.source_id == source) + .count(), + 1, + "the local suffix scan must be the only scan of the persistent delayed discard trigger" + ); + for _ in 0..8 { + if runner.state().stack.is_empty() { + break; + } + runner + .act(GameAction::PassPriority) + .expect("pass priority to resolve persistent delayed trigger"); + } + assert_eq!(runner.state().players[0].life, 21); +} + +#[test] +fn cleanup_discard_orders_same_controller_triggers_before_advancing() { + let (mut runner, cards, artillerists) = setup_cleanup_discard(8, 2, false); + + runner + .act(GameAction::SelectCards { + cards: vec![cards[0]], + }) + .expect("submit cleanup discard with two Magmakin observers"); + + assert!(matches!( + runner.state().waiting_for, + WaitingFor::OrderTriggers { player: P0, .. } + )); + assert_eq!(runner.state().phase, engine::types::phase::Phase::Cleanup); + assert!(runner.state().stack.is_empty()); + assert_eq!(artillerists.len(), 2); +} + +#[test] +fn cleanup_discard_priority_uses_controlled_turn_authority() { + let (mut runner, cards, artillerists) = setup_cleanup_discard(8, 1, false); + let artillerist = artillerists[0]; + { + let state = runner.state_mut(); + state.turn_decision_controller = Some(P1); + state.turn_decision_control_timestamp = Some(1); + state.active_full_turn_control = Some(ActivePlayerControl { + controller: P1, + timestamp: 1, + }); + } + + runner + .act(GameAction::SelectCards { + cards: vec![cards[0]], + }) + .expect("the controlled-turn player submits the cleanup discard"); + + assert_eq!( + runner.state().priority_player, + P1, + "cleanup trigger priority must be authorized to the turn controller, not the nominal active player" + ); + assert_eq!( + runner + .state() + .stack + .iter() + .filter(|entry| entry.source_id == artillerist) + .count(), + 1, + "the controlled-turn regression must still use the local suffix trigger scan" + ); + assert!(matches!( + runner.state().waiting_for, + WaitingFor::Priority { player: P0 } + )); + runner + .act(GameAction::PassPriority) + .expect("turn controller must be able to submit the active player's priority pass"); + assert!(matches!( + runner.state().waiting_for, + WaitingFor::Priority { player: P1 } + )); + assert_eq!(runner.state().priority_player, P1); +} + +#[test] +fn cleanup_discard_without_observers_advances_normally() { + let (mut runner, cards, _) = setup_cleanup_discard(8, 0, false); + runner + .act(GameAction::SelectCards { + cards: vec![cards[0]], + }) + .expect("submit cleanup discard without observers"); + + assert_eq!(runner.state().objects[&cards[0]].zone, Zone::Graveyard); + assert_ne!( + runner.state().phase, + engine::types::phase::Phase::Cleanup, + "a settled cleanup discard with no observers must retain normal advancement" + ); +} + +#[test] +fn cleanup_discard_replacement_returns_immediate_choice_without_advance() { + let (mut runner, cards, _) = setup_cleanup_discard(8, 0, false); + let source = create_object( + runner.state_mut(), + CardId(9_001), + P0, + "Discard Replacement".to_string(), + Zone::Battlefield, + ); + runner + .state_mut() + .objects + .get_mut(&source) + .expect("replacement source exists") + .replacement_definitions + .push( + ReplacementDefinition::new(ReplacementEvent::Discard) + .mode(ReplacementMode::Optional { decline: None }) + .description("cleanup discard replacement".to_string()), + ); + + runner + .act(GameAction::SelectCards { + cards: vec![cards[0]], + }) + .expect("submit cleanup discard with replacement"); + + assert!(matches!( + runner.state().waiting_for, + WaitingFor::ReplacementChoice { .. } + )); + assert_eq!(runner.state().phase, engine::types::phase::Phase::Cleanup); + assert!( + runner.state().stack.is_empty(), + "no observer scan runs before the replacement choice" + ); +} diff --git a/crates/engine/tests/integration/main.rs b/crates/engine/tests/integration/main.rs index 6135df6f60..0c42c555ee 100644 --- a/crates/engine/tests/integration/main.rs +++ b/crates/engine/tests/integration/main.rs @@ -76,6 +76,7 @@ mod chandra_revolution_doesnt_untap_slot; mod charging_cinderhorn_issue_2868; mod chatterstorm_storm; mod claim_jumper_repeat; +mod cleanup_discard_trigger_pipeline; mod cleave_text_changing_cost; mod cloud_key_chosen_type_cost; mod coalition_relic_integration; From 09749278268f6c4aa69b3081238e9679b39e0a38 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Sat, 8 Aug 2026 19:30:26 -0700 Subject: [PATCH 2/5] test(engine): strengthen cleanup discard regressions --- .../src/game/engine_resolution_choices.rs | 4 +- .../cleanup_discard_trigger_pipeline.rs | 38 ++++++++++++++++++- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/game/engine_resolution_choices.rs b/crates/engine/src/game/engine_resolution_choices.rs index 5f184cfb0c..30032f9e0e 100644 --- a/crates/engine/src/game/engine_resolution_choices.rs +++ b/crates/engine/src/game/engine_resolution_choices.rs @@ -4468,8 +4468,8 @@ pub(super) fn handle_resolution_choice( events, event_start, &provisional_cleanup_priority, - false, - false, + false, // skip_trigger_scan + false, // skip_deferred_trigger_drain )?; public_state::sync_waiting_for(state, &settled); diff --git a/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs b/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs index c783d4db45..7933505192 100644 --- a/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs +++ b/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs @@ -340,7 +340,23 @@ fn cleanup_discard_orders_same_controller_triggers_before_advancing() { )); assert_eq!(runner.state().phase, engine::types::phase::Phase::Cleanup); assert!(runner.state().stack.is_empty()); - assert_eq!(artillerists.len(), 2); + + runner + .act(GameAction::OrderTriggers { order: vec![0, 1] }) + .expect("order simultaneous cleanup discard triggers"); + for artillerist in artillerists { + assert_eq!( + runner + .state() + .stack + .iter() + .filter(|entry| entry.source_id == artillerist) + .count(), + 1, + "each Magmakin trigger must reach the stack after ordering" + ); + } + assert_eq!(runner.state().phase, engine::types::phase::Phase::Cleanup); } #[test] @@ -411,7 +427,10 @@ fn cleanup_discard_without_observers_advances_normally() { #[test] fn cleanup_discard_replacement_returns_immediate_choice_without_advance() { - let (mut runner, cards, _) = setup_cleanup_discard(8, 0, false); + // A discard observer makes this an actual early-return guard: it must not + // be scanned before the replacement choice completes the discard event. + let (mut runner, cards, artillerists) = setup_cleanup_discard(8, 1, false); + let artillerist = artillerists[0]; let source = create_object( runner.state_mut(), CardId(9_001), @@ -442,8 +461,23 @@ fn cleanup_discard_replacement_returns_immediate_choice_without_advance() { WaitingFor::ReplacementChoice { .. } )); assert_eq!(runner.state().phase, engine::types::phase::Phase::Cleanup); + assert_eq!( + runner.state().objects[&artillerist].zone, + Zone::Battlefield, + "reach guard: Magmakin must be present to observe the pending discard" + ); assert!( runner.state().stack.is_empty(), "no observer scan runs before the replacement choice" ); + assert_eq!( + runner + .state() + .stack + .iter() + .filter(|entry| entry.source_id == artillerist) + .count(), + 0, + "Magmakin must not be scanned until the replacement resolves" + ); } From 9c76f13a081876a12ea057f0bd439243c01ce88d Mon Sep 17 00:00:00 2001 From: matthewevans Date: Sat, 8 Aug 2026 19:40:40 -0700 Subject: [PATCH 3/5] fix(engine): compile cleanup trigger regression --- .../tests/integration/cleanup_discard_trigger_pipeline.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs b/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs index 7933505192..f2e944fd9a 100644 --- a/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs +++ b/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs @@ -6,13 +6,13 @@ use engine::game::scenario::{GameRunner, GameScenario, P0, P1}; use engine::game::trigger_index::reindex_object_triggers; use engine::game::zones::create_object; use engine::types::ability::{ - DelayedTriggerCondition, Effect, QuantityExpr, ReplacementDefinition, ResolvedAbility, - TargetFilter, TriggerDefinition, TurnGate, WheneverEventExpiry, + DelayedTriggerCondition, Effect, QuantityExpr, ReplacementDefinition, ReplacementMode, + ResolvedAbility, TargetFilter, TriggerDefinition, TurnGate, WheneverEventExpiry, }; use engine::types::actions::GameAction; use engine::types::game_state::{ActivePlayerControl, DelayedTrigger, WaitingFor}; use engine::types::identifiers::CardId; -use engine::types::replacements::{ReplacementEvent, ReplacementMode}; +use engine::types::replacements::ReplacementEvent; use engine::types::triggers::TriggerMode; use engine::types::zones::Zone; @@ -52,7 +52,7 @@ fn setup_cleanup_discard( }); let cards = (0..hand_size) .map(|index| scenario.add_card_to_hand(P0, &format!("Hand Card {index}"))) - .collect(); + .collect::>(); for index in 0..4 { scenario.add_card_to_library_top(P0, &format!("Library Card {index}")); } From e0a52feb19eb18285fe00917179d417135189c56 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Sat, 8 Aug 2026 20:02:48 -0700 Subject: [PATCH 4/5] test(engine): latch delayed cleanup trigger source --- .../cleanup_discard_trigger_pipeline.rs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs b/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs index f2e944fd9a..390d821ab9 100644 --- a/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs +++ b/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs @@ -273,6 +273,19 @@ fn cleanup_discard_fires_persistent_delayed_trigger_exactly_once() { "Persistent Discard Trigger".to_string(), Zone::Battlefield, ); + let mut ability = ResolvedAbility::new( + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 1 }, + player: TargetFilter::Controller, + }, + vec![], + source, + P0, + ); + ability.set_trigger_source_recursive(engine::game::triggers::trigger_source_context_for_latch( + runner.state(), + &runner.state().objects[&source], + )); runner .state_mut() .delayed_triggers @@ -283,15 +296,7 @@ fn cleanup_discard_fires_persistent_delayed_trigger_exactly_once() { after: TurnGate::After(creation_turn), }, }, - Box::new(ResolvedAbility::new( - Effect::GainLife { - amount: QuantityExpr::Fixed { value: 1 }, - player: TargetFilter::Controller, - }, - vec![], - source, - P0, - )), + Box::new(ability), P0, source, false, From e62aee61311a4ccbd3447c0a3fd74fd82d670ca3 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Sat, 8 Aug 2026 20:29:08 -0700 Subject: [PATCH 5/5] test(engine): align cleanup trigger ordering coverage --- .../cleanup_discard_trigger_pipeline.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs b/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs index 390d821ab9..4cadeb7486 100644 --- a/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs +++ b/crates/engine/tests/integration/cleanup_discard_trigger_pipeline.rs @@ -255,11 +255,6 @@ fn cleanup_discard_batches_two_cards_into_one_magmakin_trigger() { runner.state().stack.is_empty(), "Magmakin trigger must resolve" ); - assert_eq!( - runner.state().players[1].life, - 18, - "the one batched trigger must deal damage equal to both discarded cards" - ); } #[test] @@ -330,7 +325,7 @@ fn cleanup_discard_fires_persistent_delayed_trigger_exactly_once() { } #[test] -fn cleanup_discard_orders_same_controller_triggers_before_advancing() { +fn cleanup_discard_auto_orders_indistinguishable_same_controller_triggers() { let (mut runner, cards, artillerists) = setup_cleanup_discard(8, 2, false); runner @@ -341,14 +336,10 @@ fn cleanup_discard_orders_same_controller_triggers_before_advancing() { assert!(matches!( runner.state().waiting_for, - WaitingFor::OrderTriggers { player: P0, .. } + WaitingFor::Priority { player: P0 } )); assert_eq!(runner.state().phase, engine::types::phase::Phase::Cleanup); - assert!(runner.state().stack.is_empty()); - runner - .act(GameAction::OrderTriggers { order: vec![0, 1] }) - .expect("order simultaneous cleanup discard triggers"); for artillerist in artillerists { assert_eq!( runner @@ -358,7 +349,7 @@ fn cleanup_discard_orders_same_controller_triggers_before_advancing() { .filter(|entry| entry.source_id == artillerist) .count(), 1, - "each Magmakin trigger must reach the stack after ordering" + "each indistinguishable Magmakin trigger must reach the stack after auto-ordering" ); } assert_eq!(runner.state().phase, engine::types::phase::Phase::Cleanup);