Skip to content
111 changes: 75 additions & 36 deletions crates/engine/src/parser/oracle_nom/quantity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3052,13 +3052,6 @@
// "basic land type" is not mis-consumed as a creature/permanent type.
// Anaphoric "they control" binds to the iterating/scoped player here.
|i| parse_basic_land_types_among_lands_controlled_by_ref(i, they_controller.clone()),
// CR 122.1 + CR 109.4: "[other] <type> you control with a <kind> counter on
// it" — a controller-scoped count gated on a counter predicate. Must
// precede `parse_for_each_controlled_type`, whose bare " you control"
// match would otherwise strand the trailing counter clause as an
// unconsumed remainder (Armorcraft Judge, High Sentinels of Arashin,
// Inspiring Call).
parse_for_each_controlled_type_with_counter,
parse_for_each_controlled_type,
// CR 201.2: "for each [other] <type> named <CardName> you control"
// (Seven Dwarves). The `named X` qualifier sits between the type word
Expand Down Expand Up @@ -4086,47 +4079,20 @@
))
}

/// CR 122.1 + CR 109.4: Parse "[other] <type> you [already] control with a
/// <kind> counter on it" in a "for each" clause -> a controller-scoped
/// (`ControllerRef::You`) population count of permanents of the given type that
/// carry the named counter, with an optional "other"/"another" exclusion of the
/// source object.
///
/// The trailing counter predicate is delegated to the shared
/// `oracle_target::parse_counter_suffix` building block (the same authority that
/// backs "target creature with a +1/+1 counter on it"), so the whole
/// with/without/with-no and typed/any counter grammar is covered — this arm only
/// adds the controller scoping. Must precede `parse_for_each_controlled_type`,
/// whose bare " you control" arm would otherwise match first and strand the
/// " with a … counter on it" clause as an unconsumed remainder, dropping the
/// quantity. Backs dynamic P/T anthems and per-count effects such as High
/// Sentinels of Arashin ("~ gets +1/+1 for each other creature you control with
/// a +1/+1 counter on it"), Armorcraft Judge, Inspiring Call, and Hamza.
fn parse_for_each_controlled_type_with_counter(input: &str) -> OracleResult<'_, QuantityRef> {

let (rest, has_other) =
opt(alt((value((), tag("other ")), value((), tag("another "))))).parse(input)?;
let (rest, tf) = parse_type_filter_word(rest)?;
// Mirror the bare `parse_for_each_controlled_type` controller phrase,
// tolerating the "already" adverb ("<type> you already control with …").
let (rest, _) = tag(" you").parse(rest)?;
let (rest, _) = opt(tag(" already")).parse(rest)?;
let (rest, _) = tag(" control").parse(rest)?;
// Delegate " with a <kind> counter on it" to the shared counter-suffix
// combinator, which returns the typed `FilterProp::Counters` and the number
// of bytes it consumed from `rest`.
let Some((counter_prop, consumed)) = parse_counter_suffix(rest) else {
return Err(nom::Err::Error(nom::error::Error::new(
rest,
nom::error::ErrorKind::Fail,
)));
};
let rest = &rest[consumed..];


let mut properties = Vec::new();
if has_other.is_some() {
properties.push(FilterProp::Another);
}
properties.push(counter_prop);

Ok((
rest,
Expand All @@ -4138,7 +4104,7 @@
}),
},
))
}

Check failure on line 4107 in crates/engine/src/parser/oracle_nom/quantity.rs

View workflow job for this annotation

GitHub Actions / Rust tests (shard 2/2)

unexpected closing delimiter: `}`

Check failure on line 4107 in crates/engine/src/parser/oracle_nom/quantity.rs

View workflow job for this annotation

GitHub Actions / WASM compile check

unexpected closing delimiter: `}`

Check failure on line 4107 in crates/engine/src/parser/oracle_nom/quantity.rs

View workflow job for this annotation

GitHub Actions / Rust tests (shard 1/2)

unexpected closing delimiter: `}`

Check failure on line 4107 in crates/engine/src/parser/oracle_nom/quantity.rs

View workflow job for this annotation

GitHub Actions / Card data (generate, validate, coverage)

unexpected closing delimiter: `}`

Check failure on line 4107 in crates/engine/src/parser/oracle_nom/quantity.rs

View workflow job for this annotation

GitHub Actions / Rust lint (fmt, clippy, parser gate)

unexpected closing delimiter: `}`

fn parse_for_each_controlled_type(input: &str) -> OracleResult<'_, QuantityRef> {
// CR 109.4: Only objects on the stack or on the battlefield have a
Expand Down Expand Up @@ -4687,6 +4653,79 @@
}
}

/// CR 109.4: controller-scoped "for each [other] <type> you control with
/// <keyword>" count — the "you control" sibling of
/// `parse_for_each_battlefield_type_with_keyword`, generalized over the
/// KEYWORDS table and the optional "other"/"another" exclusion. Backs Skycat
/// Sovereign, Aven Gagglemaster, and Overgrown Battlement.
#[test]
fn parse_for_each_controlled_type_with_keyword_scoped_count() {
for (clause, other, kw) in [
(
"other creature you control with flying",
true,
Keyword::Flying,
),
(
"creature you control with vigilance",
false,
Keyword::Vigilance,
),
(
"creature you control with defender",
false,
Keyword::Defender,
),
] {
let (rest, q) = parse_for_each_clause_ref(clause).unwrap();
assert_eq!(rest, "", "{clause:?} should fully consume");
match q {
QuantityRef::ObjectCount {
filter: TargetFilter::Typed(tf),
} => {
assert_eq!(
tf.controller,
Some(ControllerRef::You),
"{clause:?}: scoped to the source's controller"
);
assert_eq!(
tf.properties.contains(&FilterProp::Another),
other,
"{clause:?}: Another presence must match the other/another prefix"
);
assert!(
tf.properties
.contains(&FilterProp::WithKeyword { value: kw }),
"{clause:?}: must gate on the named keyword"
);
}
other => panic!("{clause:?}: expected ObjectCount, got {other:?}"),
}
}
}

/// CR 109.4: the bare "for each <type> you control" arm still parses without
/// a keyword predicate — the new keyword arm must not shadow it.
#[test]
fn parse_for_each_controlled_type_bare_still_parses() {
let (rest, q) = parse_for_each_clause_ref("creature you control").unwrap();
assert_eq!(rest, "");
match q {
QuantityRef::ObjectCount {
filter: TargetFilter::Typed(tf),
} => {
assert_eq!(tf.controller, Some(ControllerRef::You));
assert!(
!tf.properties
.iter()
.any(|p| matches!(p, FilterProp::WithKeyword { .. })),
"bare arm must not gate on a keyword"
);
}
other => panic!("expected ObjectCount, got {other:?}"),
}
}

/// CR 604.3 + CR 109.4: opponent-controlled and chosen-player CDA counts.
#[test]
fn parse_number_of_controlled_type_opponent_and_chosen_player_cda() {
Expand Down
52 changes: 0 additions & 52 deletions crates/engine/src/parser/oracle_static/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24059,59 +24059,7 @@ fn enchanted_permanent_is_ongoing_grants_supertype() {
);
}

/// CR 604.1 + CR 611.3a + CR 613.4c + CR 122.1 + CR 109.4: High Sentinels of
/// Arashin — "~ gets +1/+1 for each other creature you control with a +1/+1
/// counter on it." The controller scope ("you control") and the counter
/// qualifier ("with a +1/+1 counter on it") both trail the type word; the "for
/// each" grammar previously had no combinator for a controller-scoped count
/// gated on a counter predicate, so this dynamic pump failed to parse and the
/// whole modification was dropped.
#[test]
fn static_self_dynamic_pump_for_each_other_creature_you_control_with_counter() {
let def = parse_static_line(
"~ gets +1/+1 for each other creature you control with a +1/+1 counter on it.",
)
.expect("High Sentinels of Arashin's dynamic pump must parse");
assert_eq!(def.mode, StaticMode::Continuous);
assert_eq!(def.affected, Some(TargetFilter::SelfRef));

let is_expected_count = |value: &QuantityExpr| -> bool {
let QuantityExpr::Ref {
qty:
QuantityRef::ObjectCount {
filter: TargetFilter::Typed(tf),
},
} = value
else {
return false;
};
tf.controller == Some(ControllerRef::You)
&& tf.type_filters == vec![TypeFilter::Creature]
&& tf
.properties
.iter()
.any(|p| matches!(p, FilterProp::Another))
&& tf.properties.iter().any(|p| {
matches!(
p,
FilterProp::Counters {
counters: crate::types::counter::CounterMatch::OfType(
CounterType::Plus1Plus1
),
..
}
)
})
};

assert!(def
.modifications
.iter()
.any(|m| matches!(m, ContinuousModification::AddDynamicPower { value } if is_expected_count(value))));
assert!(def
.modifications
.iter()
.any(|m| matches!(m, ContinuousModification::AddDynamicToughness { value } if is_expected_count(value))));
assert!(
!def.modifications.iter().any(|m| matches!(
m,
Expand Down
Loading