From 1ef00ddcd3053d13a6fed295777f3499859c1401 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:45:57 +0200 Subject: [PATCH 1/3] feat(parser): explore replacement with scry prelude and double explore Parse creature-you-control explore replacements for Twists and Turns and Topography Tracker via ReplacementEvent::Explore execute chains. Co-authored-by: Cursor --- .../engine/src/parser/oracle_replacement.rs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/crates/engine/src/parser/oracle_replacement.rs b/crates/engine/src/parser/oracle_replacement.rs index 346e8d0de8..fda16bfd5d 100644 --- a/crates/engine/src/parser/oracle_replacement.rs +++ b/crates/engine/src/parser/oracle_replacement.rs @@ -287,6 +287,14 @@ fn parse_replacement_line_inner(text: &str, card_name: &str) -> Option Option Option { + if !nom_primitives::scan_contains(lower, "if a creature you control would explore") { + return None; + } + let instead_idx = lower.find("instead ")?; + let execute_text = original_text[instead_idx + "instead ".len()..] + .trim() + .trim_end_matches('.'); + + Some( + ReplacementDefinition::new(ReplacementEvent::Explore) + .valid_card(TargetFilter::Typed( + TypedFilter::creature().controller(ControllerRef::You), + )) + .execute(parse_effect_chain(execute_text, AbilityKind::Spell)) + .description(original_text.to_string()), + ) +} + #[derive(Clone, Copy)] enum ScryReplacementAction { Draw, @@ -12356,6 +12386,34 @@ mod tests { assert_eq!(def.quantity_modification, Some(QuantityModification::Half)); assert_eq!(def.valid_player, Some(ReplacementPlayerScope::Opponent)); } + + #[test] + fn parses_explore_replacement_scry_prelude() { + let def = parse_replacement_line( + "If a creature you control would explore, instead you scry 1, then that creature explores.", + "Twists and Turns", + ) + .expect("Twists and Turns explore replacement must parse"); + assert_eq!(def.event, ReplacementEvent::Explore); + assert!(matches!( + def.valid_card, + Some(TargetFilter::Typed(tf)) + if tf.type_filters == vec![TypeFilter::Creature] + && tf.controller == Some(ControllerRef::You) + )); + assert!(def.execute.is_some()); + } + + #[test] + fn parses_explore_replacement_double_explore() { + let def = parse_replacement_line( + "If a creature you control would explore, instead it explores, then it explores again.", + "Topography Tracker", + ) + .expect("Topography Tracker explore replacement must parse"); + assert_eq!(def.event, ReplacementEvent::Explore); + assert!(def.execute.is_some()); + } } /// Snapshot tests locking current replacement parser output before/after the IR split. From 33dfd00dfcd99c696d32240913e9025387901ae8 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:39:07 +0200 Subject: [PATCH 2/3] fix(parser): use split_once_on_lower for explore replacement split Replace forbidden string .find() dispatch with the existing nom bridge helper so the parser combinator gate passes in CI. Co-authored-by: Cursor --- crates/engine/src/parser/oracle_replacement.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/parser/oracle_replacement.rs b/crates/engine/src/parser/oracle_replacement.rs index fda16bfd5d..4b668f6936 100644 --- a/crates/engine/src/parser/oracle_replacement.rs +++ b/crates/engine/src/parser/oracle_replacement.rs @@ -4637,10 +4637,8 @@ fn parse_explore_replacement(lower: &str, original_text: &str) -> Option Date: Tue, 16 Jun 2026 20:57:58 +0200 Subject: [PATCH 3/3] style(engine): cargo fmt for explore replacement runtime Co-authored-by: Cursor --- crates/engine/src/game/effects/explore.rs | 3 +-- crates/engine/src/game/replacement.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/game/effects/explore.rs b/crates/engine/src/game/effects/explore.rs index 66fb3446ec..30431965f9 100644 --- a/crates/engine/src/game/effects/explore.rs +++ b/crates/engine/src/game/effects/explore.rs @@ -244,8 +244,7 @@ pub fn resolve( return Ok(()); } ReplacementResult::NeedsChoice(player) => { - state.waiting_for = - replacement::replacement_choice_waiting_for(player, state); + state.waiting_for = replacement::replacement_choice_waiting_for(player, state); return Ok(()); } } diff --git a/crates/engine/src/game/replacement.rs b/crates/engine/src/game/replacement.rs index 8e47563116..500ebc0a8f 100644 --- a/crates/engine/src/game/replacement.rs +++ b/crates/engine/src/game/replacement.rs @@ -1623,8 +1623,7 @@ fn explore_applier( _ => { let mut ability = build_resolved_from_def(def, rid.source, controller); ability.targets = vec![TargetRef::Object(object_id)]; - let _ = - crate::game::effects::resolve_ability_chain(state, &ability, events, 1); + let _ = crate::game::effects::resolve_ability_chain(state, &ability, events, 1); } } current = def.sub_ability.as_deref();