Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 1 addition & 36 deletions crates/engine/src/parser/oracle_static/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -451,21 +451,6 @@ pub(crate) fn parse_damage_not_removed_during_cleanup(
)
}

/// Split a trailing " as long as <condition>" rider off a static line, returning
/// the condition text when present (combinator form, no string-method dispatch).
fn split_trailing_as_long_as(lower: &str) -> Option<&str> {
opt(preceded(
(
take_until::<_, _, OracleError<'_>>(" as long as "),
tag(" as long as "),
),
rest,
))
.parse(lower)
.ok()
.and_then(|(_, condition)| condition)
}

/// CR 509.1b: "Creatures with power <comparison> <quantity> can't
/// block this creature." — a can't-be-blocked-by restriction whose blocker
/// filter gates on a power threshold that may be DYNAMIC (Kraken of the Straits:
Expand Down Expand Up @@ -526,26 +511,6 @@ fn filter_prop_has_power_comparison(prop: &FilterProp) -> bool {
}
}

/// CR 611.3a: A static restriction may carry a trailing gate introduced by
/// either `" as long as <condition>"` (continuous) or `" if <condition>"` (state
/// gate) — e.g. Rock Jockey: "You can't play lands if this creature was cast
/// this turn." Returns the condition text for `parse_static_condition`. The
/// `as long as` form is tried first so a card carrying both keywords anchors on
/// the continuous form; a bare `if` gate is the fallback. As with
/// `split_trailing_as_long_as`, an unrecognized condition downstream leaves the
/// line unsupported rather than enforcing the restriction unconditionally.
fn split_trailing_gate_condition(lower: &str) -> Option<&str> {
split_trailing_as_long_as(lower).or_else(|| {
opt(preceded(
(take_until::<_, _, OracleError<'_>>(" if "), tag(" if ")),
rest,
))
.parse(lower)
.ok()
.and_then(|(_, condition)| condition)
})
}

pub(crate) fn parse_static_line_inner(
text: &str,
inverted: InvertedAsLongAs,
Expand Down Expand Up @@ -2834,7 +2799,7 @@ pub(crate) fn parse_static_line_inner(
// gates the restriction. If the rider is present but its condition is
// NOT recognized, leave the whole line unsupported (return None) rather
// than marking it a CantPlayLand enforced unconditionally.
return match split_trailing_gate_condition(tp.lower) {
return match super::shared::split_trailing_gate_condition(tp.lower) {
Some(condition_text) => Some(def.condition(parse_static_condition(condition_text)?)),
None => Some(def),
};
Expand Down
58 changes: 58 additions & 0 deletions crates/engine/src/parser/oracle_static/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,64 @@ pub(crate) fn nom_tag_tp<'a>(tp: &TextPair<'a>, prefix: &str) -> Option<TextPair
})
}

/// CR 611.3a: Split a trailing `" as long as <condition>"` rider off a static
/// line, returning the condition text when present (combinator form, no
/// string-method dispatch). Shared trailing-gate building block alongside
/// [`split_trailing_gate_condition`].
pub(crate) fn split_trailing_as_long_as(lower: &str) -> Option<&str> {
opt(preceded(
(
take_until::<_, _, OracleError<'_>>(" as long as "),
tag(" as long as "),
),
rest,
))
.parse(lower)
.ok()
.and_then(|(_, condition)| condition)
}

/// CR 611.3a: A static restriction may carry a trailing gate introduced by
/// either `" as long as <condition>"` (continuous) or `" if <condition>"` (state
/// gate) — e.g. Rock Jockey: "You can't play lands if this creature was cast
/// this turn." Returns the condition text for `parse_static_condition`.
///
/// The `as long as` form is tried first so a card carrying both keywords anchors
/// on the continuous form. For the bare `if` fallback the condition is taken
/// after the LAST trailing `" if "` that is not the tail of an `"as if"`
/// permission rider — e.g. "… cast as if it had flash if you control a Swamp"
/// gates on "you control a Swamp", not "it had flash …". Anchoring on the first
/// `" if "` (as the original inline helper did) would otherwise attach the wrong
/// condition or drop the real trailing gate.
///
/// As with [`split_trailing_as_long_as`], an unrecognized condition downstream
/// leaves the line unsupported rather than enforcing the restriction
/// unconditionally.
pub(crate) fn split_trailing_gate_condition(lower: &str) -> Option<&str> {
if let Some(condition) = split_trailing_as_long_as(lower) {
return Some(condition);
}

// Walk every `" if "` boundary via `take_until`/`tag`, keeping the last one
// whose preceding word is not "as" (so an "as if" rider is skipped). The
// scan advances one boundary per iteration and stays combinator-based.
let mut remaining = lower;
let mut consumed = 0usize;
let mut gate_start: Option<usize> = None;
while let Ok((after, before)) =
terminated(take_until::<_, _, OracleError<'_>>(" if "), tag(" if ")).parse(remaining)
{
let after_if = consumed + before.len() + " if ".len();
if before.rsplit(' ').next() != Some("as") {
gate_start = Some(after_if);
}
consumed = after_if;
remaining = after;
}

gate_start.map(|start| lower[start..].trim())
}

/// Recognizes the first token/phrase of an effect clause that follows the
/// condition-vs-effect comma in an inverted `"As long as <cond>, <effect>"` line.
///
Expand Down
58 changes: 58 additions & 0 deletions crates/engine/src/parser/oracle_static/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,64 @@ fn rock_jockey_cant_play_land_gated_on_source_cast_this_turn() {
"no clause should be swallowed; warnings = {:?}",
parsed.parse_warnings
);

// The real card references itself by name; card-name normalization ("Rock
// Jockey" → "~") must still route through the shared trailing-gate splitter
// and produce the same gated static.
let named = crate::parser::oracle::parse_oracle_text(
"You can't play lands if Rock Jockey was cast this turn.",
"Rock Jockey",
&[],
&["Creature".to_string()],
&[],
);
assert!(
named.statics.iter().any(
|d| matches!(&d.mode, StaticMode::Other(n) if n == "CantPlayLand")
&& matches!(&d.condition, Some(StaticCondition::And { conditions })
if conditions.contains(&StaticCondition::WasCast { zone: None })
&& conditions.contains(&StaticCondition::SourceEnteredThisTurn))
),
"card-name-normalized dispatch must produce the gated CantPlayLand, got {:?}",
named.statics
);
}

/// CR 611.3a: `split_trailing_gate_condition` anchors on the LAST ` if ` and
/// skips an `as if` rider. Asserted on the splitter building block directly —
/// the production `can't play lands` arm does not model a middle `as if` rider,
/// so driving it through dispatch would silently erase the rider and prove
/// nothing about the splitter's `as if` handling.
#[test]
fn split_trailing_gate_condition_skips_as_if_rider() {
use super::shared::split_trailing_gate_condition;

// The real trailing gate is the LAST ` if `, not the ` if ` inside `as if`.
assert_eq!(
split_trailing_gate_condition(
"you can't play lands as if it had flash if you control a swamp"
),
Some("you control a swamp")
);
// A plain trailing `if` (no rider) is unchanged.
assert_eq!(
split_trailing_gate_condition("you can't play lands if this creature was cast this turn"),
Some("this creature was cast this turn")
);
// `as long as` still takes precedence over a later bare `if`.
assert_eq!(
split_trailing_gate_condition(
"you can't play lands as long as ten or more lands are on the battlefield"
),
Some("ten or more lands are on the battlefield")
);
// A line whose only ` if ` sits inside `as if` has no real trailing gate.
assert_eq!(
split_trailing_gate_condition("cast this as if it had flash"),
None
);
// No trailing gate at all.
assert_eq!(split_trailing_gate_condition("you can't play lands"), None);
}

/// CR 508.1 + CR 611.3a: A trailing "if a[n] <type> is on the battlefield" gate
Expand Down
Loading