From 1e57da186f33d92ac5575f30f4a8746e718fdb51 Mon Sep 17 00:00:00 2001 From: Clayton Date: Wed, 29 Jul 2026 04:53:07 -0500 Subject: [PATCH 1/2] fix(engine): stamp PlayFromExile for play-mode CastFromZone A play-mode CastFromZone (e.g. hideaway) must grant face-down exile look/play authority via CastingPermission::PlayFromExile. This fixes Mosswort Bridge and adds integration coverage. Co-authored-by: Cursor --- .../engine/src/game/effects/cast_from_zone.rs | 143 +++++- ...brisk_heights_hideaway_exiled_by_source.rs | 29 ++ ...ssue_6435_mosswort_bridge_hideaway_play.rs | 448 ++++++++++++++++++ crates/engine/tests/integration/main.rs | 1 + 4 files changed, 620 insertions(+), 1 deletion(-) create mode 100644 crates/engine/tests/integration/issue_6435_mosswort_bridge_hideaway_play.rs diff --git a/crates/engine/src/game/effects/cast_from_zone.rs b/crates/engine/src/game/effects/cast_from_zone.rs index 218aafe81a..800a6ab934 100644 --- a/crates/engine/src/game/effects/cast_from_zone.rs +++ b/crates/engine/src/game/effects/cast_from_zone.rs @@ -8,7 +8,8 @@ use crate::types::events::GameEvent; use crate::types::game_state::{BatchCompletion, CastingVariant, GameState, WaitingFor}; use crate::types::identifiers::ObjectId; use crate::types::mana::ManaCost; -use crate::types::zones::Zone; +use crate::types::statics::CastFrequency; +use crate::types::zones::{EtbTapState, Zone}; /// CR 400.1/400.2: Recursively extract a filter's own `controller` axis, /// looking through the composed forms (`Not`/`And`/`Or`) a real card's target @@ -1135,6 +1136,7 @@ fn record_lingering_permissions( target_ids: &[ObjectId], ) -> Result<(), EffectError> { let ( + mode, without_paying, cast_transformed, alt_ability_cost, @@ -1143,6 +1145,7 @@ fn record_lingering_permissions( mana_spend_permission, ) = match &ability.effect { Effect::CastFromZone { + mode, without_paying_mana_cost, cast_transformed, alt_ability_cost, @@ -1151,6 +1154,7 @@ fn record_lingering_permissions( mana_spend_permission, .. } => ( + *mode, *without_paying_mana_cost, *cast_transformed, alt_ability_cost.clone(), @@ -1253,6 +1257,41 @@ fn record_lingering_permissions( if !obj.casting_permissions.contains(&permission) { obj.casting_permissions.push(permission); } + + // CR 305.1: A `CastFromZone` in `mode: Play` must also authorize + // playing the card when it is a land. The look-to-play contract + // for face-down exile is keyed on `CastingPermission::PlayFromExile` + // (CR 406.3a + CR 406.3b), not on `ExileWithAltCost` alone. + if matches!(mode, crate::types::ability::CardPlayMode::Play) + && alt_ability_cost.is_none() + { + // CR 305.1: lands are played (not cast) but still require + // face-down exile look/play authority. + let play_duration = duration.clone().unwrap_or_else(|| { + matches!(current_zone, Some(Zone::Graveyard | Zone::Hand)) + .then_some(Duration::UntilEndOfTurn) + .unwrap_or(Duration::Permanent) + }); + + let play_permission = CastingPermission::PlayFromExile { + duration: play_duration, + granted_to: ability.controller, + frequency: CastFrequency::Unlimited, + source_id: Some(ability.source_id), + invalidation: None, + exiled_by_ability_controller: Some(ability.controller), + mana_spend_permission, + card_filter: None, + single_use_group: None, + single_use: false, + cast_cost_raise: None, + land_enter_tapped: EtbTapState::Unspecified, + }; + + if !obj.casting_permissions.contains(&play_permission) { + obj.casting_permissions.push(play_permission); + } + } } } Ok(()) @@ -1330,6 +1369,108 @@ mod tests { obj_id } + #[test] + fn play_mode_without_paying_stamps_zero_cost_cast_and_play_from_exile() { + let mut state = make_test_state(); + let obj_id = add_card_to_exile(&mut state, PlayerId(1), CardId(100)); + + let ability = ResolvedAbility::new( + Effect::CastFromZone { + target: TargetFilter::Any, + without_paying_mana_cost: true, + mode: CardPlayMode::Play, + cast_transformed: false, + alt_ability_cost: None, + constraint: None, + duration: None, + driver: crate::types::ability::CastFromZoneDriver::LingeringPermission, + mana_spend_permission: None, + }, + vec![TargetRef::Object(obj_id)], + ObjectId(999), + PlayerId(0), + ); + + let mut events = vec![]; + resolve(&mut state, &ability, &mut events).unwrap(); + + let obj = state.objects.get(&obj_id).unwrap(); + + assert!( + obj.casting_permissions.iter().any(|p| matches!( + p, + CastingPermission::ExileWithAltCost { + cost, + granted_to: Some(PlayerId(0)), + duration: None, + .. + } if *cost == ManaCost::zero() + )), + "play-mode without-paying must grant a zero-cost exile alt-cost cast permission" + ); + + assert!( + obj.casting_permissions.iter().any(|p| matches!( + p, + CastingPermission::PlayFromExile { + duration: Duration::Permanent, + granted_to, + source_id: Some(ObjectId(999)), + exiled_by_ability_controller: Some(PlayerId(0)), + mana_spend_permission: None, + card_filter: None, + .. + } if *granted_to == PlayerId(0) + )), + "play-mode without-paying must also stamp PlayFromExile for face-down exile look/play" + ); + } + + #[test] + fn play_mode_with_alt_ability_cost_does_not_stamp_play_from_exile() { + let mut state = make_test_state(); + let obj_id = add_card_to_exile(&mut state, PlayerId(1), CardId(101)); + + let ability = ResolvedAbility::new( + Effect::CastFromZone { + target: TargetFilter::Any, + without_paying_mana_cost: false, + mode: CardPlayMode::Play, + cast_transformed: false, + alt_ability_cost: Some( + crate::types::ability::AbilityCost::KeywordCostOfCastSpell { + keyword: crate::types::keywords::KeywordKind::Suspend, + }, + ), + constraint: None, + duration: None, + driver: crate::types::ability::CastFromZoneDriver::LingeringPermission, + mana_spend_permission: None, + }, + vec![TargetRef::Object(obj_id)], + ObjectId(999), + PlayerId(0), + ); + + let mut events = vec![]; + resolve(&mut state, &ability, &mut events).unwrap(); + + let obj = state.objects.get(&obj_id).unwrap(); + assert!( + !obj.casting_permissions.iter().any(|p| matches!( + p, + CastingPermission::PlayFromExile { .. } + )), + "play-mode with alt-ability cost is a spell-cost override; it must not accidentally grant PlayFromExile" + ); + assert!( + obj.casting_permissions + .iter() + .any(|p| matches!(p, CastingPermission::ExileWithAltAbilityCost { .. })), + "play-mode with alt-ability cost must still grant ExileWithAltAbilityCost" + ); + } + fn electrodominance_hand_ability(max_value: i32) -> ResolvedAbility { ResolvedAbility::new( Effect::CastFromZone { diff --git a/crates/engine/tests/integration/issue_3246_windbrisk_heights_hideaway_exiled_by_source.rs b/crates/engine/tests/integration/issue_3246_windbrisk_heights_hideaway_exiled_by_source.rs index 813cbe08ad..0fc02f1883 100644 --- a/crates/engine/tests/integration/issue_3246_windbrisk_heights_hideaway_exiled_by_source.rs +++ b/crates/engine/tests/integration/issue_3246_windbrisk_heights_hideaway_exiled_by_source.rs @@ -233,6 +233,28 @@ fn windbrisk_heights_plays_correct_hidden_card_across_unrelated_tracked_set_acti via ExiledBySource, got {:?}", runner.state().objects[&shock].casting_permissions ); + let shock_has_play_from_exile_permission = runner.state().objects[&shock] + .casting_permissions + .iter() + .any(|p| { + matches!( + p, + CastingPermission::PlayFromExile { + granted_to, + exiled_by_ability_controller, + card_filter, + .. + } if *granted_to == P0 + && *exiled_by_ability_controller == Some(P0) + && *card_filter == None + ) + }); + assert!( + shock_has_play_from_exile_permission, + "Windbrisk must also grant a PlayFromExile look/play permission on the hidden Shock \ + (face-down exile authority), got {:?}", + runner.state().objects[&shock].casting_permissions + ); assert!( runner.state().objects[&bolt_decoy] .casting_permissions @@ -240,6 +262,13 @@ fn windbrisk_heights_plays_correct_hidden_card_across_unrelated_tracked_set_acti "the unrelated TrackedSet(0) decoy must NOT receive a play permission, got {:?}", runner.state().objects[&bolt_decoy].casting_permissions ); + assert!( + !runner.state().objects[&bolt_decoy] + .casting_permissions + .iter() + .any(|p| matches!(p, CastingPermission::PlayFromExile { .. })), + "the unrelated TrackedSet(0) decoy must not receive PlayFromExile" + ); assert_eq!( runner.state().objects.get(&bolt_decoy).map(|o| o.zone), Some(Zone::Exile), diff --git a/crates/engine/tests/integration/issue_6435_mosswort_bridge_hideaway_play.rs b/crates/engine/tests/integration/issue_6435_mosswort_bridge_hideaway_play.rs new file mode 100644 index 0000000000..2edeb2046a --- /dev/null +++ b/crates/engine/tests/integration/issue_6435_mosswort_bridge_hideaway_play.rs @@ -0,0 +1,448 @@ +//! Issue #6435: Mosswort Bridge hideaway "play the exiled card" must +//! actually grant face-down exile look/play authority (CR 406.6 / 607.1 / +//! 607.2a / 702.75a) and allow both: +//! - casting the hidden spell without paying mana cost +//! - playing the hidden land as a land drop +//! +//! Root cause: `Effect::CastFromZone { mode: Play, ... }` currently granted +//! only `CastingPermission::ExileWithAltCost` and missed the `PlayFromExile` +//! authority needed for face-down exile actions. + +use engine::game::scenario::{GameRunner, GameScenario, P0, P1}; +use engine::game::scenario_db::GameScenarioDbExt; +use engine::types::ability::CastingPermission; +use engine::types::actions::GameAction; +use engine::types::game_state::{ExileLinkKind, WaitingFor}; +use engine::types::identifiers::ObjectId; +use engine::types::mana::{ManaCost, ManaType, ManaUnit}; +use engine::types::phase::Phase; +use engine::types::zones::Zone; + +use crate::support::shared_card_db as load_db; + +fn fund_green(runner: &mut GameRunner) { + let dummy = ObjectId(0); + let pool = &mut runner + .state_mut() + .players + .iter_mut() + .find(|p| p.id == P0) + .unwrap() + .mana_pool; + pool.add(ManaUnit::new(ManaType::Green, dummy, false, vec![])); +} + +fn play_mosswort_and_hide(runner: &mut GameRunner, mosswort: ObjectId, hidden: ObjectId) { + let mosswort_card_id = runner.state().objects[&mosswort].card_id; + runner + .act(GameAction::PlayLand { + object_id: mosswort, + card_id: mosswort_card_id, + }) + .expect("playing Mosswort Bridge as land must be legal"); + + let mut saw_dig_choice = false; + for _ in 0..128 { + match runner.state().waiting_for.clone() { + WaitingFor::DigChoice { cards, .. } => { + saw_dig_choice = true; + assert!( + cards.contains(&hidden), + "reach-guard: the hidden card must be offered by the Hideaway pick" + ); + runner + .act(GameAction::SelectCards { + cards: vec![hidden], + }) + .expect("selecting the Hideaway card must succeed"); + } + WaitingFor::Priority { .. } => { + if runner.state().stack.is_empty() && saw_dig_choice { + break; + } + runner.act(GameAction::PassPriority).expect("pass priority"); + } + other => panic!("unexpected waiting state while driving Hideaway ETB: {other:?}"), + } + } + assert!(saw_dig_choice, "Hideaway must surface a DigChoice prompt"); +} + +fn activate_mosswort_second_ability( + runner: &mut GameRunner, + mosswort: ObjectId, + accept: bool, +) -> bool { + let activate = runner.act(GameAction::ActivateAbility { + source_id: mosswort, + ability_index: 1, + }); + if let Err(e) = &activate { + panic!( + "Mosswort Bridge second ability must be activatable: {:?}. phase={:?} waiting_for={:?} active={:?} priority_player={:?}", + e, + runner.state().phase, + runner.state().waiting_for, + runner.state().active_player, + runner.state().priority_player + ); + } + activate.expect("Mosswort Bridge second ability must be activatable"); + + let mut saw_optional = false; + for _ in 0..128 { + match runner.state().waiting_for.clone() { + WaitingFor::OptionalEffectChoice { .. } => { + saw_optional = true; + runner + .act(GameAction::DecideOptionalEffect { accept }) + .expect("deciding the optional offer must succeed"); + } + WaitingFor::Priority { .. } => { + if runner.state().stack.is_empty() { + break; + } + runner.act(GameAction::PassPriority).expect("pass priority"); + } + other => { + panic!("unexpected waiting state while driving Mosswort activation: {other:?}") + } + } + } + saw_optional +} + +fn hidden_is_face_down_exiled_linked_to_source( + runner: &GameRunner, + hidden: ObjectId, + mosswort: ObjectId, +) { + let obj = runner.state().objects.get(&hidden).unwrap(); + assert_eq!(obj.zone, Zone::Exile, "hidden card must be in exile"); + assert!(obj.face_down, "hidden card must be face down (Hideaway)"); + assert!( + runner.state().exile_links.iter().any(|link| { + link.exiled_id == hidden + && link.source_id == mosswort + && link.kind == ExileLinkKind::HideawayLookable + }), + "hidden card must be linked to Mosswort Bridge via HideawayLookable" + ); +} + +#[test] +fn mosswort_bridge_hideaway_free_casts_hidden_spell_when_total_power_ge_10() { + let Some(db) = load_db() else { + return; + }; + + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + + let mosswort = scenario.add_real_card(P0, "Mosswort Bridge", Zone::Hand, db); + let hidden_spell = scenario.add_real_card(P0, "Shock", Zone::Library, db); + // Hideaway 4 looks at the top 4 cards; seed enough so P0 doesn't lose + // from an empty library after the hideaway exile removes the chosen card. + scenario.add_real_card(P0, "Forest", Zone::Library, db); + scenario.add_real_card(P0, "Island", Zone::Library, db); + scenario.add_real_card(P0, "Mountain", Zone::Library, db); + // Prevent the game from ending on P1's next draw step due to an empty library. + scenario.add_real_card(P1, "Island", Zone::Library, db); + + // Total power >= 10 gate. + scenario.add_creature(P0, "Power 10", 10, 1); + + let mut runner = scenario.build(); + engine::game::rehydrate_game_from_card_db(runner.state_mut(), db); + + // Turn 1: play Mosswort; resolve Hideaway ETB to exile the Shock. + play_mosswort_and_hide(&mut runner, mosswort, hidden_spell); + hidden_is_face_down_exiled_linked_to_source(&runner, hidden_spell, mosswort); + + // Advance to the next turn so Mosswort is untapped. + runner.advance_to_end_step(); + runner.advance_to_upkeep(); + runner.advance_to_phase(Phase::PreCombatMain); + if matches!( + runner.state().waiting_for, + WaitingFor::DeclareAttackers { .. } + ) { + // There are no Hideaway-related requirements for combat in this test; + // declare no attackers so we can reach the next untap/main window. + runner + .act(GameAction::DeclareAttackers { + attacks: vec![], + bands: vec![], + }) + .expect("declare no attackers"); + runner.advance_to_phase(Phase::PreCombatMain); + } + if runner.state().priority_player != P0 { + runner.advance_to_end_step(); + runner.advance_to_upkeep(); + runner.advance_to_phase(Phase::PreCombatMain); + if matches!( + runner.state().waiting_for, + WaitingFor::DeclareAttackers { .. } + ) { + runner + .act(GameAction::DeclareAttackers { + attacks: vec![], + bands: vec![], + }) + .expect("declare no attackers"); + runner.advance_to_phase(Phase::PreCombatMain); + } + } + + fund_green(&mut runner); + + // Activate, accept the optional "you may play" offer. + let saw_optional = activate_mosswort_second_ability(&mut runner, mosswort, true); + assert!(saw_optional, "power>=10 must surface the optional offer"); + + // The hidden spell must have both the free-cast permission and the + // face-down exile look/play authority. + let obj = runner.state().objects.get(&hidden_spell).unwrap(); + assert!( + obj.casting_permissions.iter().any(|p| matches!( + p, + CastingPermission::ExileWithAltCost { + cost, + granted_to: Some(P0), + duration: None, + .. + } if *cost == ManaCost::zero() + )), + "Mosswort must stamp a zero-cost ExileWithAltCost permission on the hidden spell" + ); + assert!( + obj.casting_permissions.iter().any(|p| matches!( + p, + CastingPermission::PlayFromExile { + granted_to, + exiled_by_ability_controller, + card_filter, + source_id: Some(src), + .. + } if *granted_to == P0 + && *exiled_by_ability_controller == Some(P0) + && *card_filter == None + && *src == mosswort + )), + "Mosswort must also stamp PlayFromExile so the player can look/cast the face-down exiled spell" + ); + + let life_before = runner.state().players[1].life; + let mana_before = runner.state().players[0].mana_pool.total(); + + let hidden_card_id = obj.card_id; + runner + .act(GameAction::CastSpell { + object_id: hidden_spell, + card_id: hidden_card_id, + targets: vec![], + payment_mode: engine::types::game_state::CastPaymentMode::Auto, + }) + .expect("cast of the face-down exiled hidden spell must be accepted"); + + // Drive target selection / stack settlement. + for _ in 0..128 { + match runner.state().waiting_for.clone() { + WaitingFor::TargetSelection { .. } | WaitingFor::TriggerTargetSelection { .. } => { + runner + .act(GameAction::ChooseTarget { + target: Some(engine::types::ability::TargetRef::Player(P1)), + }) + .expect("choose shock target"); + } + WaitingFor::Priority { .. } if runner.state().stack.is_empty() => break, + WaitingFor::Priority { .. } => { + runner.act(GameAction::PassPriority).expect("pass priority"); + } + other => panic!("unexpected waiting state while settling hidden spell cast: {other:?}"), + } + } + runner.advance_until_stack_empty(); + + assert_eq!( + runner.state().players[1].life, + life_before - 2, + "Shock must resolve and deal 2 damage to the targeted player" + ); + assert_eq!( + runner.state().players[0].mana_pool.total(), + mana_before, + "casting the hidden spell without paying mana cost must not drain mana pool" + ); +} + +#[test] +fn mosswort_bridge_hideaway_free_plays_hidden_land_when_total_power_ge_10() { + let Some(db) = load_db() else { + return; + }; + + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + + let mosswort = scenario.add_real_card(P0, "Mosswort Bridge", Zone::Hand, db); + let hidden_land = scenario.add_real_card(P0, "Forest", Zone::Library, db); + scenario.add_real_card(P0, "Island", Zone::Library, db); + scenario.add_real_card(P0, "Mountain", Zone::Library, db); + scenario.add_real_card(P0, "Swamp", Zone::Library, db); + scenario.add_real_card(P1, "Island", Zone::Library, db); + scenario.add_creature(P0, "Power 10", 10, 1); + + let mut runner = scenario.build(); + engine::game::rehydrate_game_from_card_db(runner.state_mut(), db); + + play_mosswort_and_hide(&mut runner, mosswort, hidden_land); + hidden_is_face_down_exiled_linked_to_source(&runner, hidden_land, mosswort); + + runner.advance_to_end_step(); + runner.advance_to_upkeep(); + runner.advance_to_phase(Phase::PreCombatMain); + if matches!( + runner.state().waiting_for, + WaitingFor::DeclareAttackers { .. } + ) { + runner + .act(GameAction::DeclareAttackers { + attacks: vec![], + bands: vec![], + }) + .expect("declare no attackers"); + runner.advance_to_phase(Phase::PreCombatMain); + } + if runner.state().priority_player != P0 { + runner.advance_to_end_step(); + runner.advance_to_upkeep(); + runner.advance_to_phase(Phase::PreCombatMain); + if matches!( + runner.state().waiting_for, + WaitingFor::DeclareAttackers { .. } + ) { + runner + .act(GameAction::DeclareAttackers { + attacks: vec![], + bands: vec![], + }) + .expect("declare no attackers"); + runner.advance_to_phase(Phase::PreCombatMain); + } + } + + fund_green(&mut runner); + let saw_optional = activate_mosswort_second_ability(&mut runner, mosswort, true); + assert!(saw_optional, "power>=10 must surface the optional offer"); + + let hidden_card_id = runner.state().objects[&hidden_land].card_id; + assert!( + runner.state().objects[&hidden_land].face_down, + "reach-guard: the hidden land is still face down before playing it" + ); + let lands_before = runner.state().lands_played_this_turn; + + runner + .act(GameAction::PlayLand { + object_id: hidden_land, + card_id: hidden_card_id, + }) + .expect("playing the face-down exiled hidden land must be accepted"); + + assert_eq!( + runner.state().objects[&hidden_land].zone, + Zone::Battlefield, + "playing the hidden land must move it to the battlefield" + ); + assert!( + !runner.state().objects[&hidden_land].face_down, + "playing a land must turn it face up on entry" + ); + assert_eq!( + runner.state().lands_played_this_turn, + lands_before + 1, + "playing the hidden land consumes exactly one land play action" + ); +} + +#[test] +fn mosswort_bridge_hideaway_with_power_lt_10_offers_nothing() { + let Some(db) = load_db() else { + return; + }; + + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + + let mosswort = scenario.add_real_card(P0, "Mosswort Bridge", Zone::Hand, db); + let hidden_spell = scenario.add_real_card(P0, "Shock", Zone::Library, db); + scenario.add_real_card(P1, "Island", Zone::Library, db); + scenario.add_real_card(P0, "Forest", Zone::Library, db); + scenario.add_real_card(P0, "Island", Zone::Library, db); + scenario.add_real_card(P0, "Mountain", Zone::Library, db); + + // Total power < 10. + scenario.add_creature(P0, "Power 9", 9, 1); + + let mut runner = scenario.build(); + engine::game::rehydrate_game_from_card_db(runner.state_mut(), db); + + play_mosswort_and_hide(&mut runner, mosswort, hidden_spell); + hidden_is_face_down_exiled_linked_to_source(&runner, hidden_spell, mosswort); + + runner.advance_to_end_step(); + runner.advance_to_upkeep(); + runner.advance_to_phase(Phase::PreCombatMain); + if matches!( + runner.state().waiting_for, + WaitingFor::DeclareAttackers { .. } + ) { + runner + .act(GameAction::DeclareAttackers { + attacks: vec![], + bands: vec![], + }) + .expect("declare no attackers"); + runner.advance_to_phase(Phase::PreCombatMain); + } + if runner.state().priority_player != P0 { + runner.advance_to_end_step(); + runner.advance_to_upkeep(); + runner.advance_to_phase(Phase::PreCombatMain); + if matches!( + runner.state().waiting_for, + WaitingFor::DeclareAttackers { .. } + ) { + runner + .act(GameAction::DeclareAttackers { + attacks: vec![], + bands: vec![], + }) + .expect("declare no attackers"); + runner.advance_to_phase(Phase::PreCombatMain); + } + } + + fund_green(&mut runner); + let saw_optional = activate_mosswort_second_ability(&mut runner, mosswort, true); + assert!( + !saw_optional, + "power<10 must not surface the optional \"you may play\" offer" + ); + + let obj = runner.state().objects.get(&hidden_spell).unwrap(); + assert!( + obj.casting_permissions + .iter() + .all(|p| !matches!(p, CastingPermission::ExileWithAltCost { .. })), + "power<10 must not grant ExileWithAltCost to the hidden spell" + ); + assert!( + obj.casting_permissions + .iter() + .all(|p| !matches!(p, CastingPermission::PlayFromExile { .. })), + "power<10 must not grant PlayFromExile to the hidden spell" + ); +} diff --git a/crates/engine/tests/integration/main.rs b/crates/engine/tests/integration/main.rs index 46216a29fa..20bcf0fc25 100644 --- a/crates/engine/tests/integration/main.rs +++ b/crates/engine/tests/integration/main.rs @@ -617,6 +617,7 @@ mod issue_6403_moonmist_mass_transform; mod issue_6405_aang_multicolor_cost_reduction; mod issue_6416_extra_turn_resume_order; mod issue_6431_lava_dart_flashback_control_turn; +mod issue_6435_mosswort_bridge_hideaway_play; mod issue_6440_mockingbird_uncast_copy_ceiling; mod issue_6498_portent_of_calamity; mod issue_6499_flickering_ward_protection_exemption; From 53d4dcbe6357dd1cb46dbd097a05179d767bada7 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Wed, 29 Jul 2026 03:44:40 -0700 Subject: [PATCH 2/2] fix(tests): satisfy clippy option checks --- .../issue_3246_windbrisk_heights_hideaway_exiled_by_source.rs | 2 +- .../integration/issue_6435_mosswort_bridge_hideaway_play.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/engine/tests/integration/issue_3246_windbrisk_heights_hideaway_exiled_by_source.rs b/crates/engine/tests/integration/issue_3246_windbrisk_heights_hideaway_exiled_by_source.rs index 0fc02f1883..9e074885f7 100644 --- a/crates/engine/tests/integration/issue_3246_windbrisk_heights_hideaway_exiled_by_source.rs +++ b/crates/engine/tests/integration/issue_3246_windbrisk_heights_hideaway_exiled_by_source.rs @@ -246,7 +246,7 @@ fn windbrisk_heights_plays_correct_hidden_card_across_unrelated_tracked_set_acti .. } if *granted_to == P0 && *exiled_by_ability_controller == Some(P0) - && *card_filter == None + && card_filter.is_none() ) }); assert!( diff --git a/crates/engine/tests/integration/issue_6435_mosswort_bridge_hideaway_play.rs b/crates/engine/tests/integration/issue_6435_mosswort_bridge_hideaway_play.rs index 2edeb2046a..dd7ea84e1f 100644 --- a/crates/engine/tests/integration/issue_6435_mosswort_bridge_hideaway_play.rs +++ b/crates/engine/tests/integration/issue_6435_mosswort_bridge_hideaway_play.rs @@ -227,7 +227,7 @@ fn mosswort_bridge_hideaway_free_casts_hidden_spell_when_total_power_ge_10() { .. } if *granted_to == P0 && *exiled_by_ability_controller == Some(P0) - && *card_filter == None + && card_filter.is_none() && *src == mosswort )), "Mosswort must also stamp PlayFromExile so the player can look/cast the face-down exiled spell"