Skip to content
Merged
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
27 changes: 27 additions & 0 deletions crates/engine/src/parser/oracle_static/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3028,6 +3028,33 @@ pub(crate) fn parse_continuous_subject_filter(subject: &str) -> Option<TargetFil
return parse_continuous_subject_filter(rest_tp.original.trim());
}

// CR 605.1 / CR 113.1: strip a trailing "with a mana ability" / "with no
// abilities" object qualifier, parse the base subject recursively, and
// attach the runtime-evaluated `FilterProp`. Covers Raggadragga, Goregutter
// ("Each creature you control with a mana ability gets +2/+2"), Muraganda
// Petroglyphs ("Creatures with no abilities get +2/+2"), and Ruxa, Patient
// Professor ("Creatures you control with no abilities get +1/+1"). Both
// props are matched authoritatively by `game::filter`
// (`HasManaAbility` via the mana-ability classifier, `HasNoAbilities` via
// `object_has_no_abilities`), so this is a grammar-only seam. The qualifier
// must sit at the very end of the subject phrase (`after` empty) so a
// mid-phrase "with ..." clause is not misclaimed.
for (needle, prop) in [
(" with a mana ability", FilterProp::HasManaAbility),
(" with no abilities", FilterProp::HasNoAbilities),
] {
let mut parse_trailing_qualifier = all_consuming(terminated(
take_until::<_, _, OracleError<'_>>(needle),
tag::<_, _, OracleError<'_>>(needle),
));
if let Ok((_, base_lower)) = parse_trailing_qualifier.parse(tp.lower) {
if !base_lower.trim().is_empty() {
let base = lower_subslice_to_original(&tp, base_lower)?.trim();
return parse_continuous_subject_filter(base).map(|f| add_property(f, prop));
}
}
}

if let Some(filter) = parse_shared_controller_compound_subject_filter(&tp) {
return Some(filter);
}
Expand Down
64 changes: 64 additions & 0 deletions crates/engine/src/parser/oracle_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4677,6 +4677,70 @@ fn tempest_hawk_oracle_text_produces_no_unimplemented_static() {
);
}

/// Helper: assert a subject qualifier produces a real continuous static (no
/// `static_structure` Unimplemented) whose affected filter carries `prop_name`.
fn assert_anthem_carries_prop(text: &str, name: &str, types: &[&str], prop_name: &str) {
let r = parse(text, name, &[], types, &[]);
let has_static_unimpl = r.abilities.iter().any(
|a| matches!(&*a.effect, Effect::Unimplemented { name, .. } if name == "static_structure"),
);
assert!(
!has_static_unimpl,
"{name}: qualifier must be structured, but produced static_structure Unimplemented: {:#?}",
r.abilities
);
assert!(
!r.statics.is_empty(),
"{name}: expected a continuous static, got none"
);
let dump = format!("{:?}", r.statics);
assert!(
dump.contains(prop_name),
"{name}: affected filter must carry {prop_name}, got {dump}"
);
}

#[test]
fn anthem_subject_with_mana_ability_qualifier_carries_has_mana_ability() {
// CR 605.1: Raggadragga, Goregutter — "Each creature you control with a
// mana ability gets +2/+2." The "with a mana ability" object qualifier is
// stripped in `parse_continuous_subject_filter` and lowered to the
// runtime-evaluated `FilterProp::HasManaAbility`. Before the fix the line
// fell through to `Effect::Unimplemented { name: "static_structure", .. }`.
assert_anthem_carries_prop(
"Each creature you control with a mana ability gets +2/+2.",
"Raggadragga, Goregutter",
&["Creature"],
"HasManaAbility",
);
}

#[test]
fn anthem_subject_with_no_abilities_qualifier_carries_has_no_abilities() {
// CR 113.1: Ruxa, Patient Professor — "Creatures you control with no
// abilities get +1/+1." The controller-scoped plural form lowers to
// `FilterProp::HasNoAbilities`.
assert_anthem_carries_prop(
"Creatures you control with no abilities get +1/+1.",
"Ruxa, Patient Professor",
&["Creature"],
"HasNoAbilities",
);
}

#[test]
fn anthem_global_with_no_abilities_qualifier_carries_has_no_abilities() {
// CR 113.1: Muraganda Petroglyphs — "Creatures with no abilities get
// +2/+2." The global (no "you control") form lowers to the same
// `FilterProp::HasNoAbilities` on an uncontrolled creature subject.
assert_anthem_carries_prop(
"Creatures with no abilities get +2/+2.",
"Muraganda Petroglyphs",
&["Enchantment"],
"HasNoAbilities",
);
}

#[test]
fn lost_in_thought_ignore_effect_rider_fails_closed() {
let r = parse(
Expand Down
Loading