From d5e7dc14cedfbf4fc59f08c6b6664d648e3b8d2a Mon Sep 17 00:00:00 2001 From: Andriy Polanski Date: Thu, 23 Jul 2026 12:43:32 +0000 Subject: [PATCH 01/13] fix(engine): keep Portent exile picks out of the graveyard (#6498) Model put-the-rest after ForEachCategory as LastRevealed still in the library, and preserve dynamic X on reveal Digs instead of collapsing to RevealTop{1}. Co-authored-by: Cursor --- PR_6498.md | 24 +++ .../src/game/effects/choose_from_zone.rs | 6 + crates/engine/src/game/effects/dig.rs | 45 +++-- crates/engine/src/game/effects/mod.rs | 13 +- .../src/parser/oracle_effect/assembly.rs | 34 +++- .../src/parser/oracle_effect/sequence.rs | 74 +++++++- .../issue_6498_portent_of_calamity.rs | 177 ++++++++++++++++++ crates/engine/tests/integration/main.rs | 1 + 8 files changed, 346 insertions(+), 28 deletions(-) create mode 100644 PR_6498.md create mode 100644 crates/engine/tests/integration/issue_6498_portent_of_calamity.rs diff --git a/PR_6498.md b/PR_6498.md new file mode 100644 index 0000000000..9ddd9d3209 --- /dev/null +++ b/PR_6498.md @@ -0,0 +1,24 @@ +Closes #6498 + +## Summary + +Discord report: **Portent of Calamity** did not let you keep selected revealed cards — they went to the graveyard. + +Oracle: `Reveal the top X cards … For each card type, you may exile a card of that type from among them. Put the rest into your graveyard. …` + +## Root cause + +1. **"Put the rest into your graveyard"** was parsed as `ChangeZoneAll { Exile → Graveyard, TrackedSet }`. After `ForEachCategory` exile, the tracked set is the *exiled* picks — so selecting a card immediately dumped it to the graveyard. +2. Dig→`RevealTop` demotion collapsed non-fixed counts (X) to `1`. + +## Changes + +- Keep dynamic-count reveal Digs as reveal-only peeks (`keep_count: 0`) instead of collapsing X to `RevealTop { count: 1 }`. +- After `ForEachCategory` exile, "put the rest" emits `ChangeZoneAll { Library → Graveyard, LastRevealed }` so only non-exiled revealed cards go to the graveyard. +- Publish `CardsRevealed` from reveal-only Digs into the chain tracked-set path; `ForEachCategory` pool falls back to `last_revealed_ids`. + +## Test plan + +- [x] `cargo test -p engine --lib -- portent` +- [x] `cargo test -p engine --test integration -- portent` +- [ ] CI green diff --git a/crates/engine/src/game/effects/choose_from_zone.rs b/crates/engine/src/game/effects/choose_from_zone.rs index 263ede166f..dbbb903f01 100644 --- a/crates/engine/src/game/effects/choose_from_zone.rs +++ b/crates/engine/src/game/effects/choose_from_zone.rs @@ -556,6 +556,12 @@ fn resolve_category_pool(state: &GameState, ability: &ResolvedAbility) -> Vec>(); let raw_keep_count = raw_keep_num.min(cards.len()); - // CR 701.20e: Pure-peek pattern (keep_count = 0): "look at the top card" with no - // player selection — the sub_ability condition decides whether to take it. Set - // last_revealed_ids so RevealedHasCardType can evaluate, then return without - // creating a DigChoice interaction. - if raw_keep_count == 0 && !is_reveal { + // CR 701.20e / CR 701.20a: Pure-peek pattern (keep_count = 0): "look at" / + // "reveal the top N" with no player selection on this step — a following + // ForEachCategory / LastRevealed move decides disposition (Portent of + // Calamity). Set last_revealed_ids (and emit CardsRevealed for public + // reveals) then return without creating a DigChoice interaction. + if raw_keep_count == 0 { state.last_revealed_ids = cards.clone(); - // CR 701.20e: "look at" privately reveals the cards to the looking - // player. The looker is the ability controller (e.g. Delver of Secrets' - // "look at the top card of your library"). Record the looker-scoped peek - // window so `filter_state_for_viewer` keeps these cards visible to the - // looker — and only the looker — through any subsequent "you may reveal - // that card" optional decision, instead of leaving the looking player to - // choose blind. - state.private_look_ids = cards.clone(); - state.private_look_player = Some(ability.controller); + if is_reveal { + // CR 701.20a: public reveal — show to all players. + for &card_id in &cards { + state.revealed_cards.insert(card_id); + } + let card_names: Vec = cards + .iter() + .filter_map(|id| state.objects.get(id).map(|o| o.name.clone())) + .collect(); + events.push(GameEvent::CardsRevealed { + player: ability.controller, + card_ids: cards.clone(), + card_names, + }); + } else { + // CR 701.20e: "look at" privately reveals the cards to the looking + // player. The looker is the ability controller (e.g. Delver of Secrets' + // "look at the top card of your library"). Record the looker-scoped peek + // window so `filter_state_for_viewer` keeps these cards visible to the + // looker — and only the looker — through any subsequent "you may reveal + // that card" optional decision, instead of leaving the looking player to + // choose blind. + state.private_look_ids = cards.clone(); + state.private_look_player = Some(ability.controller); + } events.push(GameEvent::EffectResolved { kind: EffectKind::from(&ability.effect), source_id: ability.source_id, diff --git a/crates/engine/src/game/effects/mod.rs b/crates/engine/src/game/effects/mod.rs index 414d1df19a..1eb98cb8e1 100644 --- a/crates/engine/src/game/effects/mod.rs +++ b/crates/engine/src/game/effects/mod.rs @@ -4742,8 +4742,17 @@ fn affected_objects_from_events( // CR 701.20b + CR 608.2c: Reveal instructions do not move cards, so they // emit `CardsRevealed` rather than `ZoneChanged`. Publish the revealed // card ids for downstream "from among the revealed cards" - // `ChooseFromZone` continuations (Atraxa, Grand Unifier class). - Effect::RevealTop { .. } | Effect::RevealHand { .. } | Effect::Clash => events + // `ChooseFromZone` / `ForEachCategory` continuations (Atraxa, Portent of + // Calamity). Reveal-only Digs with `keep_count: 0` take the same path — + // they never emit ZoneChanged either. + Effect::RevealTop { .. } + | Effect::RevealHand { .. } + | Effect::Clash + | Effect::Dig { + reveal: true, + keep_count: Some(0), + .. + } => events .iter() .filter_map(|event| match event { GameEvent::CardsRevealed { card_ids, .. } => Some(card_ids.as_slice()), diff --git a/crates/engine/src/parser/oracle_effect/assembly.rs b/crates/engine/src/parser/oracle_effect/assembly.rs index 904f2a96ea..d91a0b3816 100644 --- a/crates/engine/src/parser/oracle_effect/assembly.rs +++ b/crates/engine/src/parser/oracle_effect/assembly.rs @@ -2302,6 +2302,11 @@ pub(crate) fn assemble_effect_chain(ir: &EffectChainIr) -> AbilityDefinition { // continuation patched it. An unpatched Dig { reveal: true, keep_count: None, filter: Any } // is a simple "reveal the top N" with no player selection — it must resolve synchronously // (via RevealTop) so that sub_ability chains like RevealedHasCardType evaluate inline. + // + // CR 107.3 + CR 701.20a: Dynamic counts (Portent of Calamity's "top X cards") cannot + // round-trip through `RevealTop { count: u32 }` without collapsing to 1. Keep those + // Digs as reveal-only peeks (`keep_count: 0`) so X resolves at runtime; a later + // ForEachCategory / LastRevealed rest-move consumes the revealed pool. for def in &mut defs { if let Effect::Dig { count, @@ -2317,14 +2322,27 @@ pub(crate) fn assemble_effect_chain(ir: &EffectChainIr) -> AbilityDefinition { if destination == &Some(Zone::Library) && rest_destination == &Some(Zone::Library) { continue; } - let count_val = match count { - QuantityExpr::Fixed { value } => *value as u32, - _ => 1, - }; - *def.effect = Effect::RevealTop { - player: player.clone(), - count: count_val, - }; + match count { + QuantityExpr::Fixed { value } => { + *def.effect = Effect::RevealTop { + player: player.clone(), + count: *value as u32, + }; + } + _ => { + if let Effect::Dig { + keep_count, + destination, + rest_destination, + .. + } = &mut *def.effect + { + *keep_count = Some(0); + *destination = None; + *rest_destination = None; + } + } + } } } diff --git a/crates/engine/src/parser/oracle_effect/sequence.rs b/crates/engine/src/parser/oracle_effect/sequence.rs index 465c44a762..61afe130c2 100644 --- a/crates/engine/src/parser/oracle_effect/sequence.rs +++ b/crates/engine/src/parser/oracle_effect/sequence.rs @@ -3975,6 +3975,40 @@ pub(super) fn apply_clause_continuation( destination, reorder_all, } => { + // CR 608.2c + CR 701.20b (Portent of Calamity): After a per-category + // exile from among revealed cards, "put the rest into " moves + // the revealed cards still in the library — `LastRevealed` ∩ origin + // Library — NOT Dig.rest_destination (a keep_count-0 reveal Dig + // returns before applying rest) and NOT the chain tracked set of + // cards just exiled (which would dump the player's picks into the + // graveyard). Prefer this over Dig patching when both antecedents + // exist in the clause list. + let for_each_bound = defs.iter().rposition(|def| { + matches!( + &*def.effect, + Effect::ForEachCategory { + action: ForEachCategoryAction::ExileFromPool { .. }, + .. + } + ) + }); + if for_each_bound.is_some() { + defs.push(AbilityDefinition::new( + kind, + Effect::ChangeZoneAll { + origin: Some(Zone::Library), + destination, + target: TargetFilter::LastRevealed, + enters_under: None, + enter_tapped: crate::types::zones::EtbTapState::Unspecified, + enter_with_counters: vec![], + face_down_profile: None, + library_position: None, + random_order: false, + }, + )); + return; + } // Absorbed into preceding Dig or RevealUntil — sets rest_destination // for unchosen/non-matching cards. CR 608.2c: When the preceding def is // a conditional "instead" alternative (new def with `else_ability = @@ -3995,10 +4029,13 @@ pub(super) fn apply_clause_continuation( None, super::assembly::OnMiss::Ignore, ); - let Some(bound_index) = bound else { - return; - }; - patch_rest_destination_recursively(&mut defs[bound_index], destination, reorder_all); + if let Some(bound_index) = bound { + patch_rest_destination_recursively( + &mut defs[bound_index], + destination, + reorder_all, + ); + } } ContinuationAst::DigFromAmong { quantity, @@ -6615,6 +6652,35 @@ pub(super) fn parse_followup_continuation_ast( reorder_all: false, }) } + // CR 608.2c + CR 701.20b (Portent of Calamity / Sanar class): "Put the + // rest into your graveyard" after a per-category exile from among the + // revealed cards. The rest are the revealed cards still in the library + // (not the cards just exiled into the chain tracked set). + Effect::ForEachCategory { + action: ForEachCategoryAction::ExileFromPool { .. }, + .. + } if nom_primitives::scan_contains(&lower, "put the rest") => + { + let destination = if nom_primitives::scan_contains(&lower, "into your graveyard") + || nom_primitives::scan_contains(&lower, "into their graveyard") + { + Zone::Graveyard + } else if nom_primitives::scan_contains(&lower, "into your hand") + || nom_primitives::scan_contains(&lower, "into their hand") + { + Zone::Hand + } else if nom_primitives::scan_contains(&lower, "on the bottom") + || nom_primitives::scan_contains(&lower, "on top of") + { + Zone::Library + } else { + Zone::Graveyard + }; + Some(ContinuationAst::PutRest { + destination, + reorder_all: false, + }) + } // CR 701.20a + CR 608.2c: A reveal-until rest-pile clause may be // separated from the RevealUntil by a transparent intervening effect // ("~ deals damage equal to that card's mana value. Put that card into diff --git a/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs b/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs new file mode 100644 index 0000000000..0b17d4bf53 --- /dev/null +++ b/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs @@ -0,0 +1,177 @@ +//! Issue #6498 — Portent of Calamity: revealed cards cannot be selected; +//! they just go to the graveyard. +//! +//! Oracle: `Reveal the top X cards of your library. For each card type, you +//! may exile a card of that type from among them. Put the rest into your +//! graveyard. You may cast a spell from among the exiled cards without paying +//! its mana cost if you exiled four or more cards this way. Then put the rest +//! of the exiled cards into your hand.` +//! +//! Discord report: after revealing, the player could not keep selected cards — +//! picks dumped to the graveyard. Root cause: "Put the rest into your +//! graveyard" was modeled as `ChangeZoneAll { Exile → Graveyard, TrackedSet }`, +//! so the per-type exile picks (the tracked set) were immediately moved to the +//! graveyard. Also, Dig→RevealTop demotion collapsed X to 1. +//! +//! DISCRIMINATING: with X=4 and four distinct types, exile one of each type; +//! those four stay in Exile (and later may go to hand); the unrevealed library +//! cards are untouched; non-exiled revealed cards go to the graveyard. + +use engine::game::scenario::{GameRunner, GameScenario}; +use engine::parser::oracle_effect::parse_effect_chain; +use engine::types::ability::{ + AbilityKind, Effect, ForEachCategoryAction, QuantityExpr, QuantityRef, TargetFilter, +}; +use engine::types::actions::GameAction; +use engine::types::card_type::CoreType; +use engine::types::game_state::WaitingFor; +use engine::types::identifiers::ObjectId; +use engine::types::mana::{ManaCost, ManaCostShard, ManaType, ManaUnit}; +use engine::types::phase::Phase; +use engine::types::player::PlayerId; +use engine::types::zones::Zone; + +const P0: PlayerId = PlayerId(0); + +const PORTENT: &str = "Reveal the top X cards of your library. For each card type, you may exile a card of that type from among them. Put the rest into your graveyard. You may cast a spell from among the exiled cards without paying its mana cost if you exiled four or more cards this way. Then put the rest of the exiled cards into your hand."; + +fn add_mana(runner: &mut GameRunner, amount_blue: u32, amount_colorless: u32) { + let dummy = ObjectId(0); + let pool = &mut runner.state_mut().players[0].mana_pool; + for _ in 0..amount_blue { + pool.add(ManaUnit::new(ManaType::Blue, dummy, false, vec![])); + } + for _ in 0..amount_colorless { + pool.add(ManaUnit::new(ManaType::Colorless, dummy, false, vec![])); + } +} + +#[test] +fn portent_parses_dynamic_reveal_and_last_revealed_rest_to_graveyard() { + let def = parse_effect_chain(PORTENT, AbilityKind::Spell); + // Head: Dig reveal with Variable X (not RevealTop count 1). + match &*def.effect { + Effect::Dig { + reveal: true, + keep_count: Some(0), + count: + QuantityExpr::Ref { + qty: QuantityRef::Variable { name }, + }, + .. + } => assert_eq!(name, "X"), + Effect::RevealTop { count: 1, .. } => { + panic!("X must not collapse to RevealTop {{ count: 1 }}") + } + other => panic!("expected reveal Dig with X, got {other:?}"), + } + + let mut node = &def; + let mut saw_for_each = false; + let mut saw_rest_to_gy = false; + loop { + match &*node.effect { + Effect::ForEachCategory { + action: ForEachCategoryAction::ExileFromPool { .. }, + .. + } => saw_for_each = true, + Effect::ChangeZoneAll { + origin: Some(Zone::Library), + destination: Zone::Graveyard, + target: TargetFilter::LastRevealed, + .. + } => saw_rest_to_gy = true, + Effect::ChangeZoneAll { + origin: Some(Zone::Exile), + destination: Zone::Graveyard, + target: TargetFilter::TrackedSet { .. }, + .. + } => panic!( + "put-the-rest must NOT dump the exile tracked set into the graveyard (Discord #6498)" + ), + _ => {} + } + match node.sub_ability.as_deref() { + Some(next) => node = next, + None => break, + } + } + assert!(saw_for_each, "must parse ForEachCategory exile"); + assert!( + saw_rest_to_gy, + "must emit ChangeZoneAll Library+LastRevealed→Graveyard for put-the-rest" + ); +} + +#[test] +fn portent_exiled_picks_stay_exiled_not_dumped_to_graveyard() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let spell = { + let mut b = + scenario.add_spell_to_hand_from_oracle(P0, "Portent of Calamity", false, PORTENT); + b.with_mana_cost(ManaCost::Cost { + shards: vec![ManaCostShard::X, ManaCostShard::Blue, ManaCostShard::Blue], + generic: 0, + }); + b.id() + }; + let types = [ + CoreType::Creature, + CoreType::Artifact, + CoreType::Enchantment, + CoreType::Sorcery, + ]; + let mut lib_ids = Vec::new(); + for (i, ty) in types.iter().enumerate() { + let id = scenario + .add_spell_to_library_top(P0, &format!("Lib {i}"), true) + .id(); + lib_ids.push((*ty, id)); + } + let mut runner = scenario.build(); + for (ty, id) in &lib_ids { + runner + .state_mut() + .objects + .get_mut(id) + .unwrap() + .card_types + .core_types = vec![*ty]; + } + // X=4 + {U}{U} + add_mana(&mut runner, 2, 4); + + let _outcome = runner.cast(spell).x(4).resolve(); + + // Exile one card at each per-type prompt (four distinct types). + let mut exiled = Vec::new(); + while let WaitingFor::ChooseFromZoneChoice { cards, .. } = &runner.state().waiting_for { + let pick = cards[0]; + exiled.push(pick); + runner + .act(GameAction::SelectCards { cards: vec![pick] }) + .expect("per-type exile selection"); + } + + assert_eq!( + exiled.len(), + 4, + "must prompt once per distinct revealed type" + ); + for id in &exiled { + assert_eq!( + runner.state().objects[id].zone, + Zone::Exile, + "exiled picks must remain in Exile — not dumped to GY by put-the-rest" + ); + } + // Free-cast gate opens (4 exiled); decline or let the runner stop at the cast prompt. + // Either way, the four must not be in the graveyard. + for id in &exiled { + assert!( + !runner.state().players[0].graveyard.contains(id), + "exiled pick must not be in the graveyard" + ); + } +} diff --git a/crates/engine/tests/integration/main.rs b/crates/engine/tests/integration/main.rs index cc9b9a781f..ac55943b6f 100644 --- a/crates/engine/tests/integration/main.rs +++ b/crates/engine/tests/integration/main.rs @@ -576,6 +576,7 @@ mod issue_6006_aminatou_veil_piercer_own_turn_miracle; mod issue_6092_ability_block_reason; mod issue_6102_ragavan_exile_cast; mod issue_629_fractured_sanity_cycling; +mod issue_6498_portent_of_calamity; mod issue_6500_loreseekers_stone_hand_cost; mod issue_654_stridehangar_automaton; mod issue_680_shalai_and_hallar_forgotten_ancient; From f0235059fa0685dae374a84c8e1ccccaf2df3e54 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Thu, 23 Jul 2026 07:06:48 -0700 Subject: [PATCH 02/13] fix(PR-6545): remove duplicate PR narrative --- PR_6498.md | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 PR_6498.md diff --git a/PR_6498.md b/PR_6498.md deleted file mode 100644 index 9ddd9d3209..0000000000 --- a/PR_6498.md +++ /dev/null @@ -1,24 +0,0 @@ -Closes #6498 - -## Summary - -Discord report: **Portent of Calamity** did not let you keep selected revealed cards — they went to the graveyard. - -Oracle: `Reveal the top X cards … For each card type, you may exile a card of that type from among them. Put the rest into your graveyard. …` - -## Root cause - -1. **"Put the rest into your graveyard"** was parsed as `ChangeZoneAll { Exile → Graveyard, TrackedSet }`. After `ForEachCategory` exile, the tracked set is the *exiled* picks — so selecting a card immediately dumped it to the graveyard. -2. Dig→`RevealTop` demotion collapsed non-fixed counts (X) to `1`. - -## Changes - -- Keep dynamic-count reveal Digs as reveal-only peeks (`keep_count: 0`) instead of collapsing X to `RevealTop { count: 1 }`. -- After `ForEachCategory` exile, "put the rest" emits `ChangeZoneAll { Library → Graveyard, LastRevealed }` so only non-exiled revealed cards go to the graveyard. -- Publish `CardsRevealed` from reveal-only Digs into the chain tracked-set path; `ForEachCategory` pool falls back to `last_revealed_ids`. - -## Test plan - -- [x] `cargo test -p engine --lib -- portent` -- [x] `cargo test -p engine --test integration -- portent` -- [ ] CI green From dfb940e0264087f09a9bd8fdd3c30a851cc6f771 Mon Sep 17 00:00:00 2001 From: Andriy Polanski Date: Thu, 23 Jul 2026 15:55:53 +0000 Subject: [PATCH 03/13] fix(engine): resolve Portent's two remainder sets and hand tail (#6498) Separate revealed-library rest from exiled-card cleanup via LastRevealed and TrackedSetFiltered(Exiled), emit explicit LastRevealed siblings for dynamic reveal Digs, and lower the hand tail as ChangeZoneAll. Co-authored-by: Cursor --- .../src/game/effects/choose_from_zone.rs | 8 +- .../src/parser/oracle_effect/imperative.rs | 20 +- .../src/parser/oracle_effect/sequence.rs | 51 ++++- crates/engine/src/parser/oracle_target.rs | 20 +- .../issue_6498_portent_of_calamity.rs | 195 ++++++++++++++---- 5 files changed, 244 insertions(+), 50 deletions(-) diff --git a/crates/engine/src/game/effects/choose_from_zone.rs b/crates/engine/src/game/effects/choose_from_zone.rs index dbbb903f01..fd3e662459 100644 --- a/crates/engine/src/game/effects/choose_from_zone.rs +++ b/crates/engine/src/game/effects/choose_from_zone.rs @@ -513,7 +513,13 @@ pub(crate) fn complete_per_category_exile( events: &mut Vec, ) { if !chosen.is_empty() { - super::publish_tracked_set(state, chosen); + super::publish_tracked_set_with_causes( + state, + chosen + .iter() + .map(|&id| (id, Some(crate::types::ability::ThisWayCause::Exiled))) + .collect(), + ); } let _ = prompt_next_category_member(state, &ability, &pool, remaining_member_filters, events); } diff --git a/crates/engine/src/parser/oracle_effect/imperative.rs b/crates/engine/src/parser/oracle_effect/imperative.rs index ab8ea00b1b..7e94bfc4ca 100644 --- a/crates/engine/src/parser/oracle_effect/imperative.rs +++ b/crates/engine/src/parser/oracle_effect/imperative.rs @@ -2359,10 +2359,13 @@ pub(super) fn lower_targeted_action_ast(ast: TargetedImperativeAst) -> Effect { enter_tapped, enter_with_counters, } => { - let origin = if matches!(target, TargetFilter::ExiledBySource) { - Some(Zone::Exile) - } else { - origin + let origin = match &target { + TargetFilter::ExiledBySource => Some(Zone::Exile), + TargetFilter::TrackedSetFiltered { + caused_by: Some(crate::types::ability::ThisWayCause::Exiled), + .. + } => Some(Zone::Exile), + _ => origin, }; Effect::ChangeZoneAll { origin, @@ -6524,15 +6527,18 @@ pub(super) fn lower_put_ast(ast: PutImperativeAst) -> Effect { choice_count: _, enter_with_counters, } => { - // CR 610.3: Mass filters (ExiledBySource, TrackedSet) act on all matching - // objects without individual targeting — use ChangeZoneAll. + // CR 610.3: Mass filters (ExiledBySource, TrackedSet, + // TrackedSetFiltered) act on all matching objects without individual + // targeting — use ChangeZoneAll. // ExiledBySource always originates from Exile regardless of inferred zone. // CR 122.1: ChangeZoneAll has no counter-stamping channel — those // patterns are single-target only in current Oracle text, so the // mass-filter branch deliberately drops `enter_with_counters`. if matches!( target, - TargetFilter::ExiledBySource | TargetFilter::TrackedSet { .. } + TargetFilter::ExiledBySource + | TargetFilter::TrackedSet { .. } + | TargetFilter::TrackedSetFiltered { .. } ) && enter_with_counters.is_empty() { Effect::ChangeZoneAll { diff --git a/crates/engine/src/parser/oracle_effect/sequence.rs b/crates/engine/src/parser/oracle_effect/sequence.rs index 61afe130c2..25b05bdbd4 100644 --- a/crates/engine/src/parser/oracle_effect/sequence.rs +++ b/crates/engine/src/parser/oracle_effect/sequence.rs @@ -3982,7 +3982,9 @@ pub(super) fn apply_clause_continuation( // returns before applying rest) and NOT the chain tracked set of // cards just exiled (which would dump the player's picks into the // graveyard). Prefer this over Dig patching when both antecedents - // exist in the clause list. + // exist in the clause list. The exiled-card tail ("put the rest of + // the exiled cards …") is a distinct remainder set and must stay on + // the imperative `ExiledBySource` path. let for_each_bound = defs.iter().rposition(|def| { matches!( &*def.effect, @@ -3992,7 +3994,7 @@ pub(super) fn apply_clause_continuation( } ) }); - if for_each_bound.is_some() { + if for_each_bound.is_some() && destination != Zone::Hand { defs.push(AbilityDefinition::new( kind, Effect::ChangeZoneAll { @@ -4030,6 +4032,37 @@ pub(super) fn apply_clause_continuation( super::assembly::OnMiss::Ignore, ); if let Some(bound_index) = bound { + // CR 701.20a + CR 608.2c: Dynamic-count reveal-only Digs + // (`keep_count: 0`) return before `Dig.rest_destination` is + // applied at runtime. Emit an explicit `LastRevealed` sibling + // for the revealed-library remainder instead of patching an + // unused field (Sunbird's Invocation / Enshrined Memories class). + if !reorder_all + && matches!( + &*defs[bound_index].effect, + Effect::Dig { + keep_count: Some(0), + reveal: true, + .. + } + ) + { + defs.push(AbilityDefinition::new( + kind, + Effect::ChangeZoneAll { + origin: Some(Zone::Library), + destination, + target: TargetFilter::LastRevealed, + enters_under: None, + enter_tapped: crate::types::zones::EtbTapState::Unspecified, + enter_with_counters: vec![], + face_down_profile: None, + library_position: None, + random_order: false, + }, + )); + return; + } patch_rest_destination_recursively( &mut defs[bound_index], destination, @@ -4962,6 +4995,16 @@ fn apply_search_destination_to_ability_chain( } } +/// CR 608.2c + CR 701.20b: True for "put the rest …" clauses that move the +/// revealed-library remainder after a per-category exile. False for the distinct +/// exiled-card tail ("put the rest of the exiled cards …"), which must bind to +/// `ExiledBySource` instead of `LastRevealed` / chain `TrackedSet`. +fn put_rest_targets_revealed_remainder(lower: &str) -> bool { + nom_primitives::scan_contains(lower, "put the rest") + && !nom_primitives::scan_contains(lower, "of the exiled cards") + && !nom_primitives::scan_contains(lower, "of those exiled cards") +} + /// Recursively patch `rest_destination` on Dig/RevealUntil effects reachable from /// `def` via `else_ability`. CR 608.2c: When a preceding def is a conditional /// "instead" wrapper (new_def with `else_ability = base_def`), a trailing @@ -6659,7 +6702,7 @@ pub(super) fn parse_followup_continuation_ast( Effect::ForEachCategory { action: ForEachCategoryAction::ExileFromPool { .. }, .. - } if nom_primitives::scan_contains(&lower, "put the rest") => + } if put_rest_targets_revealed_remainder(&lower) => { let destination = if nom_primitives::scan_contains(&lower, "into your graveyard") || nom_primitives::scan_contains(&lower, "into their graveyard") @@ -6674,7 +6717,7 @@ pub(super) fn parse_followup_continuation_ast( { Zone::Library } else { - Zone::Graveyard + Zone::Library }; Some(ContinuationAst::PutRest { destination, diff --git a/crates/engine/src/parser/oracle_target.rs b/crates/engine/src/parser/oracle_target.rs index ae5af8ddcc..68d08aa176 100644 --- a/crates/engine/src/parser/oracle_target.rs +++ b/crates/engine/src/parser/oracle_target.rs @@ -13,7 +13,7 @@ use crate::types::ability::{ Comparator, ControllerRef, CountScope, DamageKindFilter, FilterProp, ObjectProperty, ObjectScope, ParitySource, PlayerFilter, PtStat, PtValueScope, QuantityExpr, QuantityRef, SeatDirection, SharedQuality, SharedQualityRelation, TargetFilter, TargetSelectionMode, - TypeFilter, TypedFilter, + ThisWayCause, TypeFilter, TypedFilter, }; use crate::types::card_type::Supertype; use crate::types::counter::{CounterMatch, CounterType}; @@ -1026,6 +1026,24 @@ pub fn parse_target_with_syntax<'a>( return (filter, rest, syntax); } + // CR 608.2c + CR 607.2a (Portent of Calamity): "the rest of the exiled cards" + // names the cards still linked to this resolution's exile step — not the bare + // "the rest" tracked-set anaphor, which can absorb unrelated chain members + // after an intervening revealed-library cleanup publishes to the chain set. + if let Ok((rest, _)) = + tag::<_, _, OracleError<'_>>("the rest of the exiled cards").parse(lower.as_str()) + { + return ( + TargetFilter::TrackedSetFiltered { + id: TrackedSetId(0), + filter: Box::new(TargetFilter::Any), + caused_by: Some(ThisWayCause::Exiled), + }, + &text[lower.len() - rest.len()..], + syntax, + ); + } + // CR 603.7: Anaphoric tracked-set pronouns static TRACKED_SET_PHRASES: &[&str] = &[ "the chosen cards", diff --git a/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs b/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs index 0b17d4bf53..d369506d72 100644 --- a/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs +++ b/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs @@ -13,9 +13,10 @@ //! so the per-type exile picks (the tracked set) were immediately moved to the //! graveyard. Also, Dig→RevealTop demotion collapsed X to 1. //! -//! DISCRIMINATING: with X=4 and four distinct types, exile one of each type; -//! those four stay in Exile (and later may go to hand); the unrevealed library -//! cards are untouched; non-exiled revealed cards go to the graveyard. +//! DISCRIMINATING: with X=5, exile one of each of four types plus a duplicate +//! creature; the unselected revealed creature goes to the graveyard; after +//! declining the free cast, the four exiled picks reach hand via +//! `ExiledBySource`, not chain `TrackedSet`. use engine::game::scenario::{GameRunner, GameScenario}; use engine::parser::oracle_effect::parse_effect_chain; @@ -69,6 +70,7 @@ fn portent_parses_dynamic_reveal_and_last_revealed_rest_to_graveyard() { let mut node = &def; let mut saw_for_each = false; let mut saw_rest_to_gy = false; + let mut saw_final_hand_cleanup = false; loop { match &*node.effect { Effect::ForEachCategory { @@ -81,6 +83,15 @@ fn portent_parses_dynamic_reveal_and_last_revealed_rest_to_graveyard() { target: TargetFilter::LastRevealed, .. } => saw_rest_to_gy = true, + Effect::ChangeZoneAll { + origin: Some(Zone::Exile), + destination: Zone::Hand, + target: TargetFilter::TrackedSetFiltered { + caused_by: Some(engine::types::ability::ThisWayCause::Exiled), + .. + }, + .. + } => saw_final_hand_cleanup = true, Effect::ChangeZoneAll { origin: Some(Zone::Exile), destination: Zone::Graveyard, @@ -101,53 +112,131 @@ fn portent_parses_dynamic_reveal_and_last_revealed_rest_to_graveyard() { saw_rest_to_gy, "must emit ChangeZoneAll Library+LastRevealed→Graveyard for put-the-rest" ); + assert!( + saw_final_hand_cleanup, + "final tail must bind to action-stamped TrackedSetFiltered(Exiled), not chain TrackedSet" + ); + + let mut node = &def; + let cast = loop { + if matches!(&*node.effect, Effect::CastFromZone { .. }) { + break node; + } + node = node + .sub_ability + .as_ref() + .expect("Portent chain must reach CastFromZone"); + }; + let hand_cleanup = |node: &engine::types::ability::AbilityDefinition| { + matches!( + &*node.effect, + Effect::ChangeZoneAll { + origin: Some(Zone::Exile), + destination: Zone::Hand, + target: TargetFilter::TrackedSetFiltered { + caused_by: Some(engine::types::ability::ThisWayCause::Exiled), + .. + }, + .. + } + ) + }; + assert!( + cast.sub_ability.as_deref().is_some_and(hand_cleanup) + || cast.else_ability.as_deref().is_some_and(hand_cleanup), + "CastFromZone must chain into the exiled-card hand cleanup; sub={:?}, else={:?}", + cast.sub_ability.as_ref().map(|s| &*s.effect), + cast.else_ability.as_ref().map(|s| &*s.effect), + ); + assert!( + cast.sub_ability + .as_deref() + .is_some_and(hand_cleanup), + "hand cleanup must be the accept/decline sub_ability (SequentialSibling), not only else_ability" + ); + assert_eq!( + cast.sub_ability.as_ref().unwrap().sub_link, + engine::types::ability::SubAbilityLink::SequentialSibling, + "Portent hand cleanup must resolve on optional cast decline" + ); } #[test] -fn portent_exiled_picks_stay_exiled_not_dumped_to_graveyard() { +fn portent_full_resolution_exiles_picks_to_hand_and_unselected_reveal_to_graveyard() { let mut scenario = GameScenario::new(); scenario.at_phase(Phase::PreCombatMain); let spell = { let mut b = scenario.add_spell_to_hand_from_oracle(P0, "Portent of Calamity", false, PORTENT); b.with_mana_cost(ManaCost::Cost { - shards: vec![ManaCostShard::X, ManaCostShard::Blue, ManaCostShard::Blue], + shards: vec![ManaCostShard::X, ManaCostShard::Blue], generic: 0, }); b.id() }; - let types = [ - CoreType::Creature, - CoreType::Artifact, - CoreType::Enchantment, - CoreType::Sorcery, - ]; - let mut lib_ids = Vec::new(); - for (i, ty) in types.iter().enumerate() { - let id = scenario - .add_spell_to_library_top(P0, &format!("Lib {i}"), true) - .id(); - lib_ids.push((*ty, id)); - } + + // Five revealed cards: four distinct types plus a duplicate creature. + let creature_a = scenario + .add_spell_to_library_top(P0, "Creature A", true) + .id(); + let creature_b = scenario + .add_spell_to_library_top(P0, "Creature B", true) + .id(); + let artifact = scenario.add_spell_to_library_top(P0, "Artifact", true).id(); + let enchantment = scenario + .add_spell_to_library_top(P0, "Enchantment", true) + .id(); + let sorcery = scenario.add_spell_to_library_top(P0, "Sorcery", true).id(); + let mut runner = scenario.build(); - for (ty, id) in &lib_ids { - runner - .state_mut() - .objects - .get_mut(id) - .unwrap() - .card_types - .core_types = vec![*ty]; - } - // X=4 + {U}{U} - add_mana(&mut runner, 2, 4); + runner + .state_mut() + .objects + .get_mut(&creature_a) + .unwrap() + .card_types + .core_types = vec![CoreType::Creature]; + runner + .state_mut() + .objects + .get_mut(&creature_b) + .unwrap() + .card_types + .core_types = vec![CoreType::Creature]; + runner + .state_mut() + .objects + .get_mut(&artifact) + .unwrap() + .card_types + .core_types = vec![CoreType::Artifact]; + runner + .state_mut() + .objects + .get_mut(&enchantment) + .unwrap() + .card_types + .core_types = vec![CoreType::Enchantment]; + runner + .state_mut() + .objects + .get_mut(&sorcery) + .unwrap() + .card_types + .core_types = vec![CoreType::Sorcery]; + + // X=5 + {U} + add_mana(&mut runner, 1, 5); - let _outcome = runner.cast(spell).x(4).resolve(); + let _outcome = runner.cast(spell).x(5).resolve(); - // Exile one card at each per-type prompt (four distinct types). let mut exiled = Vec::new(); while let WaitingFor::ChooseFromZoneChoice { cards, .. } = &runner.state().waiting_for { - let pick = cards[0]; + let pick = if cards.contains(&creature_a) && cards.contains(&creature_b) { + creature_a + } else { + cards[0] + }; exiled.push(pick); runner .act(GameAction::SelectCards { cards: vec![pick] }) @@ -157,21 +246,53 @@ fn portent_exiled_picks_stay_exiled_not_dumped_to_graveyard() { assert_eq!( exiled.len(), 4, - "must prompt once per distinct revealed type" + "must exile exactly one card per distinct revealed type" ); for id in &exiled { assert_eq!( runner.state().objects[id].zone, Zone::Exile, - "exiled picks must remain in Exile — not dumped to GY by put-the-rest" + "exiled picks must remain in Exile through the revealed rest cleanup" ); } - // Free-cast gate opens (4 exiled); decline or let the runner stop at the cast prompt. - // Either way, the four must not be in the graveyard. + + let unselected_creature = if exiled.contains(&creature_a) { + creature_b + } else { + creature_a + }; + assert_eq!( + runner.state().objects[&unselected_creature].zone, + Zone::Graveyard, + "the revealed-but-unselected duplicate creature must go to the graveyard" + ); + + // Decline the optional free cast, then drive the final hand cleanup. + while !matches!(runner.state().waiting_for, WaitingFor::Priority { .. }) { + match &runner.state().waiting_for { + WaitingFor::OptionalEffectChoice { .. } => { + runner + .act(GameAction::DecideOptionalEffect { accept: false }) + .expect("decline optional free cast"); + } + WaitingFor::CastOffer { .. } => { + runner + .act(GameAction::PassPriority) + .expect("decline cast offer"); + } + other => panic!("unexpected prompt before final cleanup: {other:?}"), + } + } + for id in &exiled { + assert_eq!( + runner.state().objects[id].zone, + Zone::Hand, + "remaining exiled cards must reach hand via ExiledBySource tail" + ); assert!( !runner.state().players[0].graveyard.contains(id), - "exiled pick must not be in the graveyard" + "exiled pick must not be stranded in the graveyard" ); } } From 55fe7a3f65766ebcec4d9d3d324f72d281167d26 Mon Sep 17 00:00:00 2001 From: Andriy Polanski Date: Thu, 23 Jul 2026 16:25:47 +0000 Subject: [PATCH 04/13] fix(engine): unblock PR 6545 clippy and dynamic-reveal rest path (#6498) Fix clippy if-same-then-else, emit LastRevealed siblings for dynamic reveal Digs before assembly demotion, and add parse coverage for the shared put-the-rest grammar class Matthew requested. Co-authored-by: Cursor --- .../src/parser/oracle_effect/sequence.rs | 38 ++++++++------ .../issue_6498_portent_of_calamity.rs | 50 ++++++++++++++++++- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/crates/engine/src/parser/oracle_effect/sequence.rs b/crates/engine/src/parser/oracle_effect/sequence.rs index 25b05bdbd4..5fa5e649c8 100644 --- a/crates/engine/src/parser/oracle_effect/sequence.rs +++ b/crates/engine/src/parser/oracle_effect/sequence.rs @@ -4037,16 +4037,7 @@ pub(super) fn apply_clause_continuation( // applied at runtime. Emit an explicit `LastRevealed` sibling // for the revealed-library remainder instead of patching an // unused field (Sunbird's Invocation / Enshrined Memories class). - if !reorder_all - && matches!( - &*defs[bound_index].effect, - Effect::Dig { - keep_count: Some(0), - reveal: true, - .. - } - ) - { + if !reorder_all && dig_needs_last_revealed_rest_sibling(&defs[bound_index].effect) { defs.push(AbilityDefinition::new( kind, Effect::ChangeZoneAll { @@ -5005,6 +4996,28 @@ fn put_rest_targets_revealed_remainder(lower: &str) -> bool { && !nom_primitives::scan_contains(lower, "of those exiled cards") } +/// CR 701.20a + CR 608.2c: True when a trailing PutRest must become an explicit +/// `LastRevealed` sibling rather than patching `Dig.rest_destination`. Matches +/// reveal-only Digs already at `keep_count: 0` and dynamic-count reveal Digs +/// that assembly demotes to `keep_count: 0` after continuations are applied. +fn dig_needs_last_revealed_rest_sibling(effect: &Effect) -> bool { + match effect { + Effect::Dig { + keep_count: Some(0), + reveal: true, + .. + } => true, + Effect::Dig { + keep_count: None, + reveal: true, + filter: TargetFilter::Any, + count, + .. + } => !matches!(count, QuantityExpr::Fixed { .. }), + _ => false, + } +} + /// Recursively patch `rest_destination` on Dig/RevealUntil effects reachable from /// `def` via `else_ability`. CR 608.2c: When a preceding def is a conditional /// "instead" wrapper (new_def with `else_ability = base_def`), a trailing @@ -6712,11 +6725,8 @@ pub(super) fn parse_followup_continuation_ast( || nom_primitives::scan_contains(&lower, "into their hand") { Zone::Hand - } else if nom_primitives::scan_contains(&lower, "on the bottom") - || nom_primitives::scan_contains(&lower, "on top of") - { - Zone::Library } else { + // "on the bottom", "on top of", and other library rest piles. Zone::Library }; Some(ContinuationAst::PutRest { diff --git a/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs b/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs index d369506d72..d9e9508381 100644 --- a/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs +++ b/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs @@ -16,7 +16,7 @@ //! DISCRIMINATING: with X=5, exile one of each of four types plus a duplicate //! creature; the unselected revealed creature goes to the graveyard; after //! declining the free cast, the four exiled picks reach hand via -//! `ExiledBySource`, not chain `TrackedSet`. +//! `TrackedSetFiltered { caused_by: Exiled }`, not chain `TrackedSet`. use engine::game::scenario::{GameRunner, GameScenario}; use engine::parser::oracle_effect::parse_effect_chain; @@ -161,6 +161,52 @@ fn portent_parses_dynamic_reveal_and_last_revealed_rest_to_graveyard() { ); } +#[test] +fn dynamic_reveal_put_rest_emits_last_revealed_sibling() { + // Shared grammar class from the parse-diff (Sunbird's Invocation tail, + // Enshrined Memories-style separate rest clause, etc.): dynamic-count + // reveal-only Dig + trailing "put the rest …". + const DYNAMIC_REVEAL_REST: &str = "Reveal the top X cards of your library. Put the rest on the bottom of your library in any order."; + let def = parse_effect_chain(DYNAMIC_REVEAL_REST, AbilityKind::Spell); + match &*def.effect { + Effect::Dig { + reveal: true, + keep_count: Some(0), + count: + QuantityExpr::Ref { + qty: QuantityRef::Variable { name }, + }, + .. + } => assert_eq!(name, "X"), + other => panic!("expected dynamic reveal Dig with X, got {other:?}"), + } + + let mut node = &def; + let mut saw_last_revealed_rest = false; + loop { + if matches!( + &*node.effect, + Effect::ChangeZoneAll { + origin: Some(Zone::Library), + destination: Zone::Library, + target: TargetFilter::LastRevealed, + .. + } + ) { + saw_last_revealed_rest = true; + } + match node.sub_ability.as_deref() { + Some(next) => node = next, + None => break, + } + } + assert!( + saw_last_revealed_rest, + "dynamic reveal Dig with put-the-rest must emit explicit LastRevealed sibling \ + instead of relying on unused Dig.rest_destination" + ); +} + #[test] fn portent_full_resolution_exiles_picks_to_hand_and_unselected_reveal_to_graveyard() { let mut scenario = GameScenario::new(); @@ -288,7 +334,7 @@ fn portent_full_resolution_exiles_picks_to_hand_and_unselected_reveal_to_graveya assert_eq!( runner.state().objects[id].zone, Zone::Hand, - "remaining exiled cards must reach hand via ExiledBySource tail" + "remaining exiled cards must reach hand via TrackedSetFiltered(Exiled) tail" ); assert!( !runner.state().players[0].graveyard.contains(id), From 6273576c28c61359a64d3221e477c1ffcfc40f17 Mon Sep 17 00:00:00 2001 From: Andriy Polanski Date: Thu, 23 Jul 2026 16:50:45 +0000 Subject: [PATCH 05/13] fix(engine): add runtime coverage for dynamic reveal rest path (#6498) Add a discriminating cast-pipeline test for the shared dynamic-reveal + put-the-rest grammar class, stamp library-bottom placement on the explicit LastRevealed sibling, and narrow TrackedSetFiltered mass-move origin defaults. Co-authored-by: Cursor --- .../src/parser/oracle_effect/imperative.rs | 10 ++- .../src/parser/oracle_effect/sequence.rs | 4 +- .../issue_6498_portent_of_calamity.rs | 69 +++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/parser/oracle_effect/imperative.rs b/crates/engine/src/parser/oracle_effect/imperative.rs index 7e94bfc4ca..fd45f0d76e 100644 --- a/crates/engine/src/parser/oracle_effect/imperative.rs +++ b/crates/engine/src/parser/oracle_effect/imperative.rs @@ -6541,6 +6541,14 @@ pub(super) fn lower_put_ast(ast: PutImperativeAst) -> Effect { | TargetFilter::TrackedSetFiltered { .. } ) && enter_with_counters.is_empty() { + let origin = match target { + TargetFilter::TrackedSetFiltered { + caused_by: Some(crate::types::ability::ThisWayCause::Exiled), + .. + } => origin.or(Some(Zone::Exile)), + TargetFilter::TrackedSetFiltered { .. } => origin, + _ => origin.or(Some(Zone::Exile)), + }; Effect::ChangeZoneAll { // CR 608.2c + CR 400.7: A tracked-set / impulse mass move // defaults to scanning Exile (cascade, impulse-draw, and the @@ -6549,7 +6557,7 @@ pub(super) fn lower_put_ast(ast: PutImperativeAst) -> Effect { // (Breach the Multiverse's graveyard choose stamps // `origin: Some(Graveyard)` in `parse_put_ast`), honor it so // the chosen cards are read out of the right zone. - origin: origin.or(Some(Zone::Exile)), + origin, destination, target, // CR 110.2a: Preserve the parsed entering-controller override diff --git a/crates/engine/src/parser/oracle_effect/sequence.rs b/crates/engine/src/parser/oracle_effect/sequence.rs index 5fa5e649c8..9ed248baca 100644 --- a/crates/engine/src/parser/oracle_effect/sequence.rs +++ b/crates/engine/src/parser/oracle_effect/sequence.rs @@ -4038,6 +4038,8 @@ pub(super) fn apply_clause_continuation( // for the revealed-library remainder instead of patching an // unused field (Sunbird's Invocation / Enshrined Memories class). if !reorder_all && dig_needs_last_revealed_rest_sibling(&defs[bound_index].effect) { + let library_position = + (destination == Zone::Library).then_some(LibraryPosition::Bottom); defs.push(AbilityDefinition::new( kind, Effect::ChangeZoneAll { @@ -4048,7 +4050,7 @@ pub(super) fn apply_clause_continuation( enter_tapped: crate::types::zones::EtbTapState::Unspecified, enter_with_counters: vec![], face_down_profile: None, - library_position: None, + library_position, random_order: false, }, )); diff --git a/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs b/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs index d9e9508381..addc697fe9 100644 --- a/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs +++ b/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs @@ -207,6 +207,75 @@ fn dynamic_reveal_put_rest_emits_last_revealed_sibling() { ); } +#[test] +fn dynamic_reveal_put_rest_moves_revealed_cards_to_library_bottom() { + // CR 701.20a + CR 608.2c + CR 401.4: dynamic reveal-only Dig with a trailing + // put-the-rest clause must move the revealed library remainder to the bottom, + // leaving cards below the reveal window untouched (Enshrined Memories class). + const DYNAMIC_REVEAL_REST: &str = "Reveal the top X cards of your library. Put the rest on the bottom of your library in any order."; + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + + // Library top-to-bottom: [rev3, rev2, rev1, deep]. X=3 reveals the top three. + let deep = scenario + .add_spell_to_library_top(P0, "Deep Card", true) + .id(); + let rev1 = scenario + .add_spell_to_library_top(P0, "Revealed 1", true) + .id(); + let rev2 = scenario + .add_spell_to_library_top(P0, "Revealed 2", true) + .id(); + let rev3 = scenario + .add_spell_to_library_top(P0, "Revealed 3", true) + .id(); + + let spell = { + let mut b = scenario.add_spell_to_hand_from_oracle( + P0, + "Dynamic Reveal Probe", + false, + DYNAMIC_REVEAL_REST, + ); + b.with_mana_cost(ManaCost::Cost { + shards: vec![ManaCostShard::X], + generic: 0, + }); + b.id() + }; + + let mut runner = scenario.build(); + add_mana(&mut runner, 0, 3); + + let _outcome = runner.cast(spell).x(3).resolve(); + + let library = &runner.state().players[0].library; + assert_eq!( + library.len(), + 4, + "spell must not remove the unrevealed fourth card from the library" + ); + assert_eq!( + library[0], deep, + "the card below the X-card reveal window must remain on top" + ); + for id in [rev1, rev2, rev3] { + assert_eq!( + runner.state().objects[&id].zone, + Zone::Library, + "revealed cards must stay in the library, not be stranded elsewhere" + ); + let position = library + .iter() + .position(|&candidate| candidate == id) + .expect("revealed card must remain in the library"); + assert!( + position >= 1, + "revealed remainder must be placed below the untouched top card" + ); + } +} + #[test] fn portent_full_resolution_exiles_picks_to_hand_and_unselected_reveal_to_graveyard() { let mut scenario = GameScenario::new(); From 02ecfff31d2b8cd864c741653c31d60e6b48c031 Mon Sep 17 00:00:00 2001 From: Andriy Polanski Date: Thu, 23 Jul 2026 17:05:30 +0000 Subject: [PATCH 06/13] fix(test): assert exact library-bottom order in dynamic reveal regression The runtime sibling test for reveal-only Dig + put-the-rest was failing CI because it assumed reveal order rather than the engine's deterministic batch placement order. Co-authored-by: Cursor --- .../issue_6498_portent_of_calamity.rs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs b/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs index addc697fe9..25e17f2535 100644 --- a/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs +++ b/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs @@ -247,7 +247,11 @@ fn dynamic_reveal_put_rest_moves_revealed_cards_to_library_bottom() { let mut runner = scenario.build(); add_mana(&mut runner, 0, 3); - let _outcome = runner.cast(spell).x(3).resolve(); + let outcome = runner.cast(spell).x(3).resolve(); + assert!( + matches!(outcome.final_waiting_for(), WaitingFor::Priority { .. }), + "dynamic reveal rest cleanup must finish without an unhandled prompt" + ); let library = &runner.state().players[0].library; assert_eq!( @@ -259,20 +263,21 @@ fn dynamic_reveal_put_rest_moves_revealed_cards_to_library_bottom() { library[0], deep, "the card below the X-card reveal window must remain on top" ); + // CR 401.4: the three revealed cards must land on the bottom as a contiguous + // tail in the engine's deterministic batch order (no EffectZoneChoice when + // reorder_all is false and the clause only restates the default). + let bottom_tail: Vec = library.iter().skip(1).copied().collect(); + assert_eq!( + bottom_tail, + vec![rev2, rev3, rev1], + "revealed remainder must be placed on the library bottom as a contiguous tail" + ); for id in [rev1, rev2, rev3] { assert_eq!( runner.state().objects[&id].zone, Zone::Library, "revealed cards must stay in the library, not be stranded elsewhere" ); - let position = library - .iter() - .position(|&candidate| candidate == id) - .expect("revealed card must remain in the library"); - assert!( - position >= 1, - "revealed remainder must be placed below the untouched top card" - ); } } From 09bed92c51f7835279382793e7e34c434302f4a9 Mon Sep 17 00:00:00 2001 From: Andriy Polanski Date: Thu, 23 Jul 2026 17:39:45 +0000 Subject: [PATCH 07/13] fix(engine): prompt library order for ChangeZoneAll bottom rest (#6498) CR 401.4: when multiple cards are placed at the same library position with random_order false, route through EffectZoneChoice instead of a silent batch order. Update the dynamic-reveal sibling regression to submit a non-default bottom tail and verify it sticks. Co-authored-by: Cursor --- crates/engine/src/game/effects/change_zone.rs | 39 +++++++++++++++++++ .../issue_6498_portent_of_calamity.rs | 17 ++++---- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/game/effects/change_zone.rs b/crates/engine/src/game/effects/change_zone.rs index 7b464bdf65..ab6961c42f 100644 --- a/crates/engine/src/game/effects/change_zone.rs +++ b/crates/engine/src/game/effects/change_zone.rs @@ -1666,6 +1666,45 @@ pub fn resolve_all( state.push_devour_change_zone_snapshot(state.battlefield.iter().copied().collect()); } + // CR 401.4: When multiple objects are placed at the same library position + // simultaneously and `random_order` is false, the owner arranges their + // relative order ("in any order" restates this default). Route through the + // shared `EffectZoneChoice` + `PutAtLibraryPosition` production path instead + // of silently picking an engine-default batch order. + if dest_zone == Zone::Library + && effect_library_position.is_some() + && !random_order + && matching.len() > 1 + { + let choice_count = matching.len(); + state.waiting_for = WaitingFor::EffectZoneChoice { + player: filter_controller, + cards: matching, + count: choice_count, + min_count: choice_count, + up_to: false, + source_id: ability.source_id, + effect_kind: EffectKind::PutAtLibraryPosition, + zone: Zone::Library, + destination: None, + enter_tapped: EtbTapState::Unspecified, + enter_transformed: false, + enters_under_player: None, + enters_attacking: false, + owner_library: false, + track_exiled_by_source: false, + face_down_profile: None, + enter_with_counters: vec![], + conditional_enter_with_counters: vec![], + count_param: 0, + library_position: effect_library_position.clone(), + is_cost_payment: false, + enters_modified_if: None, + duration: ability.duration.clone(), + }; + return Ok(()); + } + // CR 401.4: When placing objects on the bottom of a library "in a random // order", randomize the processing order so the final bottom-to-top sequence // is non-deterministic without shuffling the rest of the library. Top diff --git a/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs b/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs index 25e17f2535..fc1d1b1716 100644 --- a/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs +++ b/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs @@ -247,10 +247,16 @@ fn dynamic_reveal_put_rest_moves_revealed_cards_to_library_bottom() { let mut runner = scenario.build(); add_mana(&mut runner, 0, 3); - let outcome = runner.cast(spell).x(3).resolve(); + // CR 401.4: submit a non-default bottom order through the production + // `EffectZoneChoice` path — not an engine-default batch order. + let outcome = runner + .cast(spell) + .x(3) + .effect_zone(&[rev3, rev1, rev2]) + .resolve(); assert!( matches!(outcome.final_waiting_for(), WaitingFor::Priority { .. }), - "dynamic reveal rest cleanup must finish without an unhandled prompt" + "dynamic reveal rest cleanup must finish after the library-order choice" ); let library = &runner.state().players[0].library; @@ -263,14 +269,11 @@ fn dynamic_reveal_put_rest_moves_revealed_cards_to_library_bottom() { library[0], deep, "the card below the X-card reveal window must remain on top" ); - // CR 401.4: the three revealed cards must land on the bottom as a contiguous - // tail in the engine's deterministic batch order (no EffectZoneChoice when - // reorder_all is false and the clause only restates the default). let bottom_tail: Vec = library.iter().skip(1).copied().collect(); assert_eq!( bottom_tail, - vec![rev2, rev3, rev1], - "revealed remainder must be placed on the library bottom as a contiguous tail" + vec![rev3, rev1, rev2], + "revealed remainder must land on the bottom in the player's submitted order" ); for id in [rev1, rev2, rev3] { assert_eq!( From 04c01b1aa16a8d1d07aa4e12250245a3b29831c3 Mon Sep 17 00:00:00 2001 From: Andriy Polanski Date: Thu, 23 Jul 2026 17:44:35 +0000 Subject: [PATCH 08/13] fix(parser): keep up-to tracked-set puts on ChangeZone (#6498) The TrackedSetFiltered mass-move promotion must not absorb bounded "up to N" picks like "put up to one land discarded this way onto the battlefield". Co-authored-by: Cursor --- crates/engine/src/parser/oracle_effect/imperative.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/parser/oracle_effect/imperative.rs b/crates/engine/src/parser/oracle_effect/imperative.rs index fd45f0d76e..abcaaaf21c 100644 --- a/crates/engine/src/parser/oracle_effect/imperative.rs +++ b/crates/engine/src/parser/oracle_effect/imperative.rs @@ -6529,7 +6529,9 @@ pub(super) fn lower_put_ast(ast: PutImperativeAst) -> Effect { } => { // CR 610.3: Mass filters (ExiledBySource, TrackedSet, // TrackedSetFiltered) act on all matching objects without individual - // targeting — use ChangeZoneAll. + // targeting — use ChangeZoneAll. Bounded "up to N" picks from the + // tracked set ("put up to one land discarded this way") remain + // `ChangeZone` so the player selects a subset at resolution. // ExiledBySource always originates from Exile regardless of inferred zone. // CR 122.1: ChangeZoneAll has no counter-stamping channel — those // patterns are single-target only in current Oracle text, so the @@ -6540,6 +6542,7 @@ pub(super) fn lower_put_ast(ast: PutImperativeAst) -> Effect { | TargetFilter::TrackedSet { .. } | TargetFilter::TrackedSetFiltered { .. } ) && enter_with_counters.is_empty() + && !up_to { let origin = match target { TargetFilter::TrackedSetFiltered { From afc561cc41d8c28c5183f26ab79fc9d0cbaa80b5 Mon Sep 17 00:00:00 2001 From: Andriy Polanski Date: Thu, 23 Jul 2026 18:22:46 +0000 Subject: [PATCH 09/13] fix --- crates/engine/src/game/effects/change_zone.rs | 278 ++++++++++++++++-- .../src/game/engine_resolution_choices.rs | 12 + crates/engine/src/types/game_state.rs | 24 ++ 3 files changed, 288 insertions(+), 26 deletions(-) diff --git a/crates/engine/src/game/effects/change_zone.rs b/crates/engine/src/game/effects/change_zone.rs index ab6961c42f..d30326b47c 100644 --- a/crates/engine/src/game/effects/change_zone.rs +++ b/crates/engine/src/game/effects/change_zone.rs @@ -1390,6 +1390,79 @@ pub(crate) fn process_one_zone_move_with_terminal( result } +/// CR 401.4: Group object ids by owner in APNAP order for per-owner library +/// arrangement prompts. +fn group_object_ids_by_owner_apnap( + state: &GameState, + object_ids: &[ObjectId], +) -> Vec<(PlayerId, Vec)> { + use std::collections::BTreeMap; + let mut by_owner: BTreeMap> = BTreeMap::new(); + for &id in object_ids { + let owner = state.objects[&id].owner; + by_owner.entry(owner).or_default().push(id); + } + by_owner.into_iter().collect() +} + +fn mass_library_order_effect_zone_choice( + owner: PlayerId, + cards: Vec, + source_id: ObjectId, + library_position: crate::types::ability::LibraryPosition, + track_exiled_by_source: bool, + duration: Option, +) -> WaitingFor { + let choice_count = cards.len(); + WaitingFor::EffectZoneChoice { + player: owner, + cards, + count: choice_count, + min_count: choice_count, + up_to: false, + source_id, + effect_kind: EffectKind::PutAtLibraryPosition, + zone: Zone::Library, + destination: None, + enter_tapped: EtbTapState::Unspecified, + enter_transformed: false, + enters_under_player: None, + enters_attacking: false, + owner_library: false, + track_exiled_by_source, + face_down_profile: None, + enter_with_counters: vec![], + conditional_enter_with_counters: vec![], + count_param: 0, + library_position: Some(library_position), + is_cost_payment: false, + enters_modified_if: None, + duration, + } +} + +/// CR 401.4: After one owner batch of a mass library-order prompt completes, +/// surface the next owner's `EffectZoneChoice` if any remain. +pub(crate) fn resume_next_mass_library_order_choice(state: &mut GameState) -> Option { + let mut pending = state.pending_mass_library_order_choice.take()?; + let (owner, cards) = pending.remaining_batches.first()?.clone(); + pending.remaining_batches.remove(0); + if pending.remaining_batches.is_empty() { + state.pending_mass_library_order_choice = None; + } else { + state.pending_mass_library_order_choice = Some(pending.clone()); + } + state.waiting_for = mass_library_order_effect_zone_choice( + owner, + cards, + pending.source_id, + pending.library_position, + pending.track_exiled_by_source, + pending.duration, + ); + Some(owner) +} + /// Move all objects matching the filter from `Origin` zone to `Destination` zone. pub fn resolve_all( state: &mut GameState, @@ -1676,32 +1749,34 @@ pub fn resolve_all( && !random_order && matching.len() > 1 { - let choice_count = matching.len(); - state.waiting_for = WaitingFor::EffectZoneChoice { - player: filter_controller, - cards: matching, - count: choice_count, - min_count: choice_count, - up_to: false, - source_id: ability.source_id, - effect_kind: EffectKind::PutAtLibraryPosition, - zone: Zone::Library, - destination: None, - enter_tapped: EtbTapState::Unspecified, - enter_transformed: false, - enters_under_player: None, - enters_attacking: false, - owner_library: false, - track_exiled_by_source: false, - face_down_profile: None, - enter_with_counters: vec![], - conditional_enter_with_counters: vec![], - count_param: 0, - library_position: effect_library_position.clone(), - is_cost_payment: false, - enters_modified_if: None, - duration: ability.duration.clone(), - }; + let owner_batches = group_object_ids_by_owner_apnap(state, &matching); + let (first_owner, first_cards) = owner_batches + .first() + .expect("matching.len() > 1 guarantees at least one owner batch") + .clone(); + let remaining_batches: Vec<_> = owner_batches.into_iter().skip(1).collect(); + if !remaining_batches.is_empty() { + state.pending_mass_library_order_choice = + Some(crate::types::game_state::PendingMassLibraryOrderChoice { + source_id: ability.source_id, + library_position: effect_library_position + .clone() + .expect("library-order branch requires an explicit library position"), + track_exiled_by_source, + duration: ability.duration.clone(), + remaining_batches, + }); + } + state.waiting_for = mass_library_order_effect_zone_choice( + first_owner, + first_cards, + ability.source_id, + effect_library_position + .clone() + .expect("library-order branch requires an explicit library position"), + track_exiled_by_source, + ability.duration.clone(), + ); return Ok(()); } @@ -5594,6 +5669,157 @@ mod tests { ); } + /// CR 401.4: Mass library-bottom placement must prompt each card's owner to + /// arrange order, not the spell's `filter_controller` when those differ. + #[test] + fn change_zone_all_library_bottom_order_prompts_card_owner_not_controller() { + let mut state = GameState::new_two_player(42); + let card_a = create_object( + &mut state, + CardId(701), + PlayerId(1), + "Opponent Revealed A".to_string(), + Zone::Library, + ); + let card_b = create_object( + &mut state, + CardId(702), + PlayerId(1), + "Opponent Revealed B".to_string(), + Zone::Library, + ); + let card_c = create_object( + &mut state, + CardId(703), + PlayerId(1), + "Opponent Revealed C".to_string(), + Zone::Library, + ); + state.players[1].library = im::vector![card_a, card_b, card_c]; + state.last_revealed_ids = vec![card_a, card_b, card_c]; + + let ability = ResolvedAbility::new( + Effect::ChangeZoneAll { + origin: Some(Zone::Library), + destination: Zone::Library, + target: TargetFilter::LastRevealed, + enters_under: None, + enter_tapped: crate::types::zones::EtbTapState::Unspecified, + enter_with_counters: vec![], + face_down_profile: None, + library_position: Some(LibraryPosition::Bottom), + random_order: false, + }, + vec![], + ObjectId(900), + PlayerId(0), + ); + + let mut events = Vec::new(); + resolve_all(&mut state, &ability, &mut events).unwrap(); + + match &state.waiting_for { + WaitingFor::EffectZoneChoice { + player, + cards, + effect_kind, + .. + } => { + assert_eq!( + *player, + PlayerId(1), + "opponent-owned cards must be ordered by their owner, not the caster" + ); + assert_eq!(cards.len(), 3); + assert_eq!(*effect_kind, EffectKind::PutAtLibraryPosition); + } + other => panic!("expected EffectZoneChoice, got {other:?}"), + } + } + + /// CR 401.4: When one mass move covers multiple owners' cards, each owner + /// receives an independent library-order prompt in APNAP order. + #[test] + fn change_zone_all_library_bottom_order_prompts_each_owner_sequentially() { + let mut state = GameState::new_two_player(42); + let p0_card = create_object( + &mut state, + CardId(711), + PlayerId(0), + "Self Revealed".to_string(), + Zone::Library, + ); + let p1_a = create_object( + &mut state, + CardId(712), + PlayerId(1), + "Opponent Revealed A".to_string(), + Zone::Library, + ); + let p1_b = create_object( + &mut state, + CardId(713), + PlayerId(1), + "Opponent Revealed B".to_string(), + Zone::Library, + ); + state.players[0].library = im::vector![p0_card]; + state.players[1].library = im::vector![p1_a, p1_b]; + state.last_revealed_ids = vec![p0_card, p1_a, p1_b]; + + let ability = ResolvedAbility::new( + Effect::ChangeZoneAll { + origin: Some(Zone::Library), + destination: Zone::Library, + target: TargetFilter::LastRevealed, + enters_under: None, + enter_tapped: crate::types::zones::EtbTapState::Unspecified, + enter_with_counters: vec![], + face_down_profile: None, + library_position: Some(LibraryPosition::Bottom), + random_order: false, + }, + vec![], + ObjectId(901), + PlayerId(0), + ); + + let mut events = Vec::new(); + resolve_all(&mut state, &ability, &mut events).unwrap(); + + match &state.waiting_for { + WaitingFor::EffectZoneChoice { player, cards, .. } => { + assert_eq!(*player, PlayerId(0), "APNAP: active player's batch first"); + assert_eq!(cards, &vec![p0_card]); + } + other => panic!("expected first-owner EffectZoneChoice, got {other:?}"), + } + assert!( + state.pending_mass_library_order_choice.is_some(), + "opponent batch must remain queued" + ); + + apply_as_current( + &mut state, + GameAction::SelectCards { + cards: vec![p0_card], + }, + ) + .unwrap(); + + match &state.waiting_for { + WaitingFor::EffectZoneChoice { player, cards, .. } => { + assert_eq!(*player, PlayerId(1), "second owner receives their batch"); + let mut sorted = cards.clone(); + sorted.sort_by_key(|id| id.0); + let mut expect = vec![p1_a, p1_b]; + expect.sort_by_key(|id| id.0); + assert_eq!(sorted, expect); + } + other => panic!("expected second-owner EffectZoneChoice, got {other:?}"), + } + } + /// Build an Exhume-shaped ability: `Effect::ChangeZone` Graveyard → /// Battlefield with a `Typed{Creature}` target carrying the post-fix /// owner constraint `Owned{ScopedPlayer}` + `InZone Graveyard`, and diff --git a/crates/engine/src/game/engine_resolution_choices.rs b/crates/engine/src/game/engine_resolution_choices.rs index 8bdf2c66df..6dde5e8c74 100644 --- a/crates/engine/src/game/engine_resolution_choices.rs +++ b/crates/engine/src/game/engine_resolution_choices.rs @@ -4970,6 +4970,14 @@ pub(super) fn handle_resolution_choice( &library_position, events, ); + if let Some(next_owner) = + effects::change_zone::resume_next_mass_library_order_choice(state) + { + state.priority_player = next_owner; + return Ok(ResolutionChoiceOutcome::WaitingFor( + state.waiting_for.clone(), + )); + } } else { // The selected EffectZoneChoice is now consumed. Clear it // before the pipeline may park a CR 616.1 prompt; otherwise @@ -6824,6 +6832,10 @@ fn finish_effect_zone_put_at_library_position( &library_position, events, ); + if let Some(next_owner) = effects::change_zone::resume_next_mass_library_order_choice(state) { + state.priority_player = next_owner; + return; + } if state.active_ability_continuation().is_some() { let tracked = if matches!(library_position, LibraryPosition::Bottom) { state diff --git a/crates/engine/src/types/game_state.rs b/crates/engine/src/types/game_state.rs index b0483203a3..3f53126b8c 100644 --- a/crates/engine/src/types/game_state.rs +++ b/crates/engine/src/types/game_state.rs @@ -3582,6 +3582,22 @@ pub struct PendingPerPlayerZoneChoice { pub accumulated: bool, } +/// CR 401.4 + CR 608.2c: Per-owner library-order prompts for one +/// `ChangeZoneAll` instruction that places multiple owners' cards at the same +/// library position with `random_order: false`. The first owner's batch is +/// surfaced immediately as `WaitingFor::EffectZoneChoice`; remaining owner +/// batches drain after each batch completes. +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct PendingMassLibraryOrderChoice { + pub source_id: ObjectId, + pub library_position: crate::types::ability::LibraryPosition, + pub track_exiled_by_source: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub duration: Option, + /// Remaining (owner, cards) batches in APNAP order after the current prompt. + pub remaining_batches: Vec<(PlayerId, Vec)>, +} + /// CR 101.4: If players make choices for one instruction, they choose in /// APNAP order before the simultaneous action happens. /// CR 701.21a: To sacrifice a permanent, its controller moves it from the @@ -12242,6 +12258,10 @@ pub struct GameState { /// `EffectZoneChoice`. #[serde(default, skip_serializing_if = "Option::is_none")] pub pending_player_scope_sacrifice_choice: Option, + /// CR 401.4: Remaining per-owner library-order batches for a mass + /// `ChangeZoneAll` instruction paused on `EffectZoneChoice`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pending_mass_library_order_choice: Option, /// CR 101.4 + CR 701.23i: Pending private selections for a simultaneous /// scoped self-library search. Kept separate from the generic continuation /// so the action phase cannot begin before every player has chosen. @@ -16207,6 +16227,7 @@ impl GameState { merged_card_component_route: None, resolution_coin_flip: None, pending_player_scope_sacrifice_choice: None, + pending_mass_library_order_choice: None, pending_scoped_library_search: None, pending_library_search_delivery: None, pending_search_found_batch: None, @@ -17540,6 +17561,7 @@ fn _gamestate_partition_is_total(s: &GameState) { // Priority`, effects/mod.rs:759) or a constant direct-assigned count across a real // copy-token loop, so COMPARING never suppresses a legitimate loop's detection. pending_player_scope_sacrifice_choice: _, + pending_mass_library_order_choice: _, pending_scoped_library_search: _, pending_library_search_delivery: _, pending_search_found_batch: _, @@ -17739,6 +17761,8 @@ impl PartialEq for GameState { && self.resolution_coin_flip == other.resolution_coin_flip && self.pending_player_scope_sacrifice_choice == other.pending_player_scope_sacrifice_choice + && self.pending_mass_library_order_choice + == other.pending_mass_library_order_choice && self.pending_scoped_library_search == other.pending_scoped_library_search && self.pending_library_search_delivery == other.pending_library_search_delivery && self.pending_search_found_batch == other.pending_search_found_batch From 15de355e10e90b6557ad0d0b8f50f8f4d3a578e3 Mon Sep 17 00:00:00 2001 From: Andriy Polanski Date: Thu, 23 Jul 2026 18:34:36 +0000 Subject: [PATCH 10/13] fix --- .../issue_6498_portent_of_calamity.rs | 108 +++++++++++++++++- 1 file changed, 103 insertions(+), 5 deletions(-) diff --git a/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs b/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs index fc1d1b1716..beb308c11e 100644 --- a/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs +++ b/crates/engine/tests/integration/issue_6498_portent_of_calamity.rs @@ -18,19 +18,23 @@ //! declining the free cast, the four exiled picks reach hand via //! `TrackedSetFiltered { caused_by: Exiled }`, not chain `TrackedSet`. -use engine::game::scenario::{GameRunner, GameScenario}; +use engine::game::effects::change_zone::resolve_all; +use engine::game::engine::apply_as_current; +use engine::game::scenario::{GameRunner, GameScenario, P1}; +use engine::game::zones::create_object; use engine::parser::oracle_effect::parse_effect_chain; use engine::types::ability::{ - AbilityKind, Effect, ForEachCategoryAction, QuantityExpr, QuantityRef, TargetFilter, + AbilityKind, Effect, EffectKind, ForEachCategoryAction, LibraryPosition, QuantityExpr, + QuantityRef, ResolvedAbility, TargetFilter, }; use engine::types::actions::GameAction; use engine::types::card_type::CoreType; -use engine::types::game_state::WaitingFor; -use engine::types::identifiers::ObjectId; +use engine::types::game_state::{GameState, WaitingFor}; +use engine::types::identifiers::{CardId, ObjectId}; use engine::types::mana::{ManaCost, ManaCostShard, ManaType, ManaUnit}; use engine::types::phase::Phase; use engine::types::player::PlayerId; -use engine::types::zones::Zone; +use engine::types::zones::{EtbTapState, Zone}; const P0: PlayerId = PlayerId(0); @@ -284,6 +288,100 @@ fn dynamic_reveal_put_rest_moves_revealed_cards_to_library_bottom() { } } +#[test] +fn opponent_library_bottom_order_prompts_owner_and_applies_submitted_order() { + // CR 401.4: mass library-bottom placement on opponent-owned revealed cards + // must prompt the opponent (not the spell controller) and honor their order. + let mut state = GameState::new_two_player(42); + let deep = create_object( + &mut state, + CardId(801), + P1, + "Deep Card".to_string(), + Zone::Library, + ); + let rev3 = create_object( + &mut state, + CardId(802), + P1, + "Revealed 3".to_string(), + Zone::Library, + ); + let rev1 = create_object( + &mut state, + CardId(803), + P1, + "Revealed 1".to_string(), + Zone::Library, + ); + let rev2 = create_object( + &mut state, + CardId(804), + P1, + "Revealed 2".to_string(), + Zone::Library, + ); + state.players[P1.0 as usize].library = im::vector![rev3, rev2, rev1, deep]; + state.last_revealed_ids = vec![rev3, rev2, rev1]; + + let ability = ResolvedAbility::new( + Effect::ChangeZoneAll { + origin: Some(Zone::Library), + destination: Zone::Library, + target: TargetFilter::LastRevealed, + enters_under: None, + enter_tapped: EtbTapState::Unspecified, + enter_with_counters: vec![], + face_down_profile: None, + library_position: Some(LibraryPosition::Bottom), + random_order: false, + }, + vec![], + ObjectId(900), + P0, + ); + + let mut events = Vec::new(); + resolve_all(&mut state, &ability, &mut events).unwrap(); + + match &state.waiting_for { + WaitingFor::EffectZoneChoice { + player, + cards, + effect_kind, + .. + } => { + assert_eq!( + *player, P1, + "opponent-owned cards must be ordered by their owner, not the caster" + ); + assert_eq!(cards.len(), 3); + assert_eq!(*effect_kind, EffectKind::PutAtLibraryPosition); + } + other => panic!("expected library-order prompt for opponent-owned cards, got {other:?}"), + } + + apply_as_current( + &mut state, + GameAction::SelectCards { + cards: vec![rev3, rev1, rev2], + }, + ) + .unwrap(); + + assert!( + matches!(state.waiting_for, WaitingFor::Priority { .. }), + "library-order cleanup must finish after the owner submits order" + ); + let library = &state.players[P1.0 as usize].library; + assert_eq!(library[0], deep, "unrevealed card stays on top"); + assert_eq!( + library.iter().skip(1).copied().collect::>(), + vec![rev3, rev1, rev2], + "bottom tail must match the opponent's submitted order" + ); +} + #[test] fn portent_full_resolution_exiles_picks_to_hand_and_unselected_reveal_to_graveyard() { let mut scenario = GameScenario::new(); From 855ee9897cf5ac7ab34643e498c39e20aec442f3 Mon Sep 17 00:00:00 2001 From: Andriy Polanski Date: Thu, 23 Jul 2026 20:25:56 +0000 Subject: [PATCH 11/13] fix --- crates/engine/src/game/scenario.rs | 26 ++++++++++++++++++ .../fixtures/cr733/authority_matrix.json.gz | Bin 39308 -> 40275 bytes 2 files changed, 26 insertions(+) diff --git a/crates/engine/src/game/scenario.rs b/crates/engine/src/game/scenario.rs index 0cd300b29a..09a8280af9 100644 --- a/crates/engine/src/game/scenario.rs +++ b/crates/engine/src/game/scenario.rs @@ -1497,6 +1497,32 @@ impl GameRunner { super::triggers::drain_order_triggers_with_identity(&mut self.state); continue; } + // CR 401.4: mass library-bottom placement parks `EffectZoneChoice` even + // when the stack is empty (Teferi's Puzzle Box draw-step trigger). Tests + // that drive phase advancement without an explicit `.effect_zone()` policy + // submit the engine-listed card order so resolution can finish. + if let WaitingFor::EffectZoneChoice { + cards, + count, + min_count, + up_to, + .. + } = &self.state.waiting_for + { + if *up_to || cards.len() < *min_count as usize { + break; + } + let chosen: Vec<_> = cards.iter().take(*count as usize).copied().collect(); + if chosen.len() != *count as usize { + break; + } + if apply_as_current(&mut self.state, GameAction::SelectCards { cards: chosen }) + .is_err() + { + break; + } + continue; + } if self.state.stack.is_empty() { break; } diff --git a/crates/engine/tests/fixtures/cr733/authority_matrix.json.gz b/crates/engine/tests/fixtures/cr733/authority_matrix.json.gz index 6ca1045bfb9a5a80430093726de41ef48acba75d..fa28dbd56ac22fec137341793ffc1a24c84d4ec2 100644 GIT binary patch literal 40275 zcmV)WK(4Xm4_9ba`KGVRUk7crI#lZ*BnWy<3l@N|G-6zJCR( zmou|f*~$G9>uEG2ZMF7{W@)<5%N&gm5s;a55!?n`O|jjk4Se`5a`hmvq9lpE_+{hR zp002FB#s=*iJi!cV$ZhpI5xh0XFq?5wrTbS1a^7TQH3v}EGsrqQ&jMw;Thk>=gCKs zH(|0(>pCrR_|h}<_RBPXDXMMMuy1Uy{i4cW@lmJ!;Im4b1U@pIz^2ds!3XpjZIil* znk0NllPs?38JB-qCDG`q@w~u@<9NM}W6yUJd+oW2YuT0=xvp<)9A|CE=8Iw4o@s41hP}4dFSc&@iRVPV zm$Ve&RoDyyRm%y)VBV4_mX^j^TbAovj+rF(#&nEL{NnoF%i2lSp7r9ny5s7$VQ;?u zFFqUJ+8Ox&h+w-m^f|5eI?dANE3A^b$aXXzVe^*4|Lv;$TRYjCEP~Z~NjG9;zLix` z7IhMb(XM$bU@g9+r)k2%-dow-O14YTf9>zx;PNPw=+auidahQuw5wH?#IH%UT191< z@sWSPK-kA&5~q#!PrsVqwLJO!=Wb9fy%0AC2|)J}r5V;qwC$cQKJzqx4Yx(S%aV2k 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 literal 39308 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 C|OsUrIyr6}%# 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 Date: Thu, 23 Jul 2026 22:06:41 +0000 Subject: [PATCH 12/13] fix(engine): use APNAP order for per-owner library prompts (#6545) CR 101.4: group mass library-order batches via apnap_order instead of seat id, make the multi-owner unit test active on P1's turn, and scope scenario auto-answers to PutAtLibraryPosition only. Co-authored-by: Cursor --- crates/engine/src/game/effects/change_zone.rs | 34 ++++++++++++------- crates/engine/src/game/scenario.rs | 6 +++- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/crates/engine/src/game/effects/change_zone.rs b/crates/engine/src/game/effects/change_zone.rs index d30326b47c..ee2f55903c 100644 --- a/crates/engine/src/game/effects/change_zone.rs +++ b/crates/engine/src/game/effects/change_zone.rs @@ -1396,13 +1396,16 @@ fn group_object_ids_by_owner_apnap( state: &GameState, object_ids: &[ObjectId], ) -> Vec<(PlayerId, Vec)> { - use std::collections::BTreeMap; - let mut by_owner: BTreeMap> = BTreeMap::new(); + use std::collections::HashMap; + let mut by_owner: HashMap> = HashMap::new(); for &id in object_ids { let owner = state.objects[&id].owner; by_owner.entry(owner).or_default().push(id); } - by_owner.into_iter().collect() + crate::game::players::apnap_order(state) + .into_iter() + .filter_map(|pid| by_owner.remove(&pid).map(|cards| (pid, cards))) + .collect() } fn mass_library_order_effect_zone_choice( @@ -5766,6 +5769,7 @@ mod tests { state.players[0].library = im::vector![p0_card]; state.players[1].library = im::vector![p1_a, p1_b]; state.last_revealed_ids = vec![p0_card, p1_a, p1_b]; + state.active_player = PlayerId(1); let ability = ResolvedAbility::new( Effect::ChangeZoneAll { @@ -5789,32 +5793,36 @@ mod tests { match &state.waiting_for { WaitingFor::EffectZoneChoice { player, cards, .. } => { - assert_eq!(*player, PlayerId(0), "APNAP: active player's batch first"); - assert_eq!(cards, &vec![p0_card]); + assert_eq!( + *player, + PlayerId(1), + "APNAP: active player's batch first even when active player is not seat 0" + ); + let mut sorted = cards.clone(); + sorted.sort_by_key(|id| id.0); + let mut expect = vec![p1_a, p1_b]; + expect.sort_by_key(|id| id.0); + assert_eq!(sorted, expect); } other => panic!("expected first-owner EffectZoneChoice, got {other:?}"), } assert!( state.pending_mass_library_order_choice.is_some(), - "opponent batch must remain queued" + "non-active owner batch must remain queued" ); apply_as_current( &mut state, GameAction::SelectCards { - cards: vec![p0_card], + cards: vec![p1_a, p1_b], }, ) .unwrap(); match &state.waiting_for { WaitingFor::EffectZoneChoice { player, cards, .. } => { - assert_eq!(*player, PlayerId(1), "second owner receives their batch"); - let mut sorted = cards.clone(); - sorted.sort_by_key(|id| id.0); - let mut expect = vec![p1_a, p1_b]; - expect.sort_by_key(|id| id.0); - assert_eq!(sorted, expect); + assert_eq!(*player, PlayerId(0), "second owner receives their batch"); + assert_eq!(cards, &vec![p0_card]); } other => panic!("expected second-owner EffectZoneChoice, got {other:?}"), } diff --git a/crates/engine/src/game/scenario.rs b/crates/engine/src/game/scenario.rs index 09a8280af9..901d3a1c39 100644 --- a/crates/engine/src/game/scenario.rs +++ b/crates/engine/src/game/scenario.rs @@ -15,7 +15,7 @@ use crate::game::game_object::GameObject; use crate::game::printed_cards::apply_card_face_to_object; use crate::game::zones::create_object; use crate::types::ability::{ - AbilityDefinition, AbilityKind, AdditionalCost, Effect, PtValue, QuantityExpr, + AbilityDefinition, AbilityKind, AdditionalCost, Effect, EffectKind, PtValue, QuantityExpr, ReplacementDefinition, ResolvedAbility, StaticDefinition, TargetFilter, TargetRef, TriggerDefinition, }; @@ -1506,9 +1506,13 @@ impl GameRunner { count, min_count, up_to, + effect_kind, .. } = &self.state.waiting_for { + if *effect_kind != EffectKind::PutAtLibraryPosition { + break; + } if *up_to || cards.len() < *min_count as usize { break; } From 451908c496107e9bf8a253da6b2754855f09329c Mon Sep 17 00:00:00 2001 From: Andriy Polanski Date: Thu, 23 Jul 2026 22:56:00 +0000 Subject: [PATCH 13/13] fix(ci): unblock PR 6545 clippy and Tauri lock sync Remove unnecessary usize casts in scenario auto-answer path and bump phase-tauri to 0.35.2 in client/src-tauri/Cargo.lock after the main merge. Co-authored-by: Cursor --- client/src-tauri/Cargo.lock | 2 +- crates/engine/src/game/scenario.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src-tauri/Cargo.lock b/client/src-tauri/Cargo.lock index 0e9d0e5f13..ce777b5dac 100644 --- a/client/src-tauri/Cargo.lock +++ b/client/src-tauri/Cargo.lock @@ -2616,7 +2616,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "phase-tauri" -version = "0.35.1" +version = "0.35.2" dependencies = [ "futures-util", "minisign-verify", diff --git a/crates/engine/src/game/scenario.rs b/crates/engine/src/game/scenario.rs index 901d3a1c39..97f5ccfae3 100644 --- a/crates/engine/src/game/scenario.rs +++ b/crates/engine/src/game/scenario.rs @@ -1513,11 +1513,11 @@ impl GameRunner { if *effect_kind != EffectKind::PutAtLibraryPosition { break; } - if *up_to || cards.len() < *min_count as usize { + if *up_to || cards.len() < *min_count { break; } - let chosen: Vec<_> = cards.iter().take(*count as usize).copied().collect(); - if chosen.len() != *count as usize { + let chosen: Vec<_> = cards.iter().take(*count).copied().collect(); + if chosen.len() != *count { break; } if apply_as_current(&mut self.state, GameAction::SelectCards { cards: chosen })