From 4944497a543f230c10db6f4ed95cdbc39c5a560d Mon Sep 17 00:00:00 2001 From: jaso0n0818 Date: Thu, 18 Jun 2026 21:02:05 +0000 Subject: [PATCH 1/2] Add Yare Parse "can block up to N additional creatures" spell grants as ExtraBlockers and mark parameterized ExtraBlockers statics supported in coverage. Includes parser and discriminating combat validation tests. Co-authored-by: Cursor --- crates/engine/src/game/coverage.rs | 4 + .../src/parser/oracle_effect/subject.rs | 36 ++++++- crates/engine/tests/integration/main.rs | 1 + .../tests/integration/yare_extra_blockers.rs | 101 ++++++++++++++++++ 4 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 crates/engine/tests/integration/yare_extra_blockers.rs diff --git a/crates/engine/src/game/coverage.rs b/crates/engine/src/game/coverage.rs index 331470c8e9..799a2c5dce 100644 --- a/crates/engine/src/game/coverage.rs +++ b/crates/engine/src/game/coverage.rs @@ -160,6 +160,10 @@ fn is_data_carrying_static(mode: &StaticMode) -> bool { // with turns.rs::execute_untap_with_choices keeping a cap clamp as a // safety net. Parameterized — no registry entry; coverage support here. | StaticMode::MaxUntapPerType { .. } + // CR 509.1a + CR 509.1b: ExtraBlockers carries the additional-blocker + // count (Yare, Brave the Sands). Runtime enforcement is in + // combat.rs::extra_block_limit; the registry only keys Some(1)/None. + | StaticMode::ExtraBlockers { .. } ) } diff --git a/crates/engine/src/parser/oracle_effect/subject.rs b/crates/engine/src/parser/oracle_effect/subject.rs index 02f7d66588..e154594dac 100644 --- a/crates/engine/src/parser/oracle_effect/subject.rs +++ b/crates/engine/src/parser/oracle_effect/subject.rs @@ -706,11 +706,12 @@ fn try_parse_can_block_additional( parse_subject_application(subject_text.trim(), ctx)? }; - let (_rest, (_, _, _, _, count, duration, _)) = all_consuming(( + let (_rest, (_, _, _, _, _, count, duration, _)) = all_consuming(( tag("can"), tag(" "), tag("block"), tag(" "), + opt(tag("up to ")), parse_extra_blockers_count, parse_block_grant_duration, opt(tag(".")), @@ -748,6 +749,7 @@ pub(super) fn is_can_block_extra_predicate(lower: &str) -> bool { tag(" "), tag("block"), tag(" "), + opt(tag("up to ")), parse_extra_blockers_count, parse_block_grant_duration, opt(tag(".")), @@ -5217,6 +5219,38 @@ mod tests { } } + /// Yare — "That creature can block up to two additional creatures this turn." + /// The optional "up to" prefix must not swallow the extra-block grant. + #[test] + fn yare_spell_extra_blockers_up_to_two_this_turn() { + let def = crate::parser::oracle_effect::parse_effect_chain( + "Target creature defending player controls gets +3/+0 until end of turn. That creature can block up to two additional creatures this turn.", + AbilityKind::Spell, + ); + let sub = def + .sub_ability + .as_ref() + .expect("Yare extra-block clause must be a sub-ability"); + assert!( + !matches!(*sub.effect, Effect::Unimplemented { .. }), + "Yare extra-block clause must parse, got {:?}", + sub.effect + ); + let Effect::GenericEffect { + static_abilities, + duration, + .. + } = &*sub.effect + else { + panic!("expected GenericEffect, got {:?}", sub.effect); + }; + assert_eq!(duration, &Some(Duration::UntilEndOfTurn)); + assert_eq!( + static_abilities[0].mode, + StaticMode::ExtraBlockers { count: Some(2) } + ); + } + /// CR 509.1b + CR 611.2: A granted "can't be blocked [this turn] except by /// " clause (Fast // Furious's second sentence) must lower to a real /// `CantBeBlockedExceptBy` evasion static on the anaphoric "It" subject — diff --git a/crates/engine/tests/integration/main.rs b/crates/engine/tests/integration/main.rs index 06bbf8ae4c..fbe4c9630b 100644 --- a/crates/engine/tests/integration/main.rs +++ b/crates/engine/tests/integration/main.rs @@ -413,3 +413,4 @@ mod wise_mothman_milled_trigger; mod wise_mothman_target_distinctness; mod wrenn_and_six_up_to_one_optout; mod yuriko_combat_damage; +mod yare_extra_blockers; diff --git a/crates/engine/tests/integration/yare_extra_blockers.rs b/crates/engine/tests/integration/yare_extra_blockers.rs new file mode 100644 index 0000000000..0daa84b8fc --- /dev/null +++ b/crates/engine/tests/integration/yare_extra_blockers.rs @@ -0,0 +1,101 @@ +//! Yare — "That creature can block up to two additional creatures this turn." +//! +//! Parser regression: the optional "up to" prefix must lower to +//! `ExtraBlockers { count: Some(2) }`, not `Effect::Unimplemented`. +//! +//! Runtime regression: a blocker granted that static can legally block three +//! attackers (default 1 + 2 additional). + +use engine::game::combat::{validate_blockers, AttackerInfo, CombatState}; +use engine::game::zones::create_object; +use engine::parser::oracle_effect::parse_effect_chain; +use engine::types::ability::{AbilityKind, Effect, StaticDefinition}; +use engine::types::card_type::CoreType; +use engine::types::format::FormatConfig; +use engine::types::game_state::GameState; +use engine::types::identifiers::CardId; +use engine::types::player::PlayerId; +use engine::types::statics::StaticMode; +use engine::types::zones::Zone; + +const YARE_ORACLE: &str = "Target creature defending player controls gets +3/+0 until end of turn. That creature can block up to two additional creatures this turn."; + +fn create_creature(state: &mut GameState, controller: PlayerId, name: &str, power: i32, toughness: i32) -> engine::types::identifiers::ObjectId { + let id = create_object( + state, + CardId(state.next_object_id), + controller, + name.to_string(), + Zone::Battlefield, + ); + let obj = state.objects.get_mut(&id).unwrap(); + obj.card_types.core_types = vec![CoreType::Creature]; + obj.base_card_types = obj.card_types.clone(); + obj.power = Some(power); + obj.toughness = Some(toughness); + obj.base_power = Some(power); + obj.base_toughness = Some(toughness); + obj.summoning_sick = false; + id +} + +#[test] +fn yare_parses_up_to_two_additional_blockers_sub_ability() { + let def = parse_effect_chain(YARE_ORACLE, AbilityKind::Spell); + let sub = def + .sub_ability + .expect("Yare must parse a supported extra-block sub-ability"); + let Effect::GenericEffect { + static_abilities, + duration, + .. + } = &*sub.effect + else { + panic!("expected GenericEffect, got {:?}", sub.effect); + }; + assert_eq!(duration, &Some(engine::types::ability::Duration::UntilEndOfTurn)); + assert_eq!( + static_abilities[0].mode, + StaticMode::ExtraBlockers { count: Some(2) } + ); +} + +#[test] +fn yare_extra_blockers_allow_three_attackers_blocked() { + let mut state = GameState::new(FormatConfig::standard(), 2, 42); + let attacker1 = create_creature(&mut state, PlayerId(0), "Attacker A", 2, 2); + let attacker2 = create_creature(&mut state, PlayerId(0), "Attacker B", 2, 2); + let attacker3 = create_creature(&mut state, PlayerId(0), "Attacker C", 2, 2); + let blocker = create_creature(&mut state, PlayerId(1), "Defender", 2, 2); + + state + .objects + .get_mut(&blocker) + .unwrap() + .static_definitions + .push(StaticDefinition::new(StaticMode::ExtraBlockers { + count: Some(2), + })); + + state.combat = Some(CombatState { + attackers: vec![ + AttackerInfo::attacking_player(attacker1, PlayerId(1)), + AttackerInfo::attacking_player(attacker2, PlayerId(1)), + AttackerInfo::attacking_player(attacker3, PlayerId(1)), + ], + ..Default::default() + }); + + assert!( + validate_blockers( + &state, + &[ + (blocker, attacker1), + (blocker, attacker2), + (blocker, attacker3), + ], + ) + .is_ok(), + "ExtraBlockers(2) must allow blocking three attackers" + ); +} From 61daf32a9e9b9858d5cafe177041e3498f7ae309 Mon Sep 17 00:00:00 2001 From: Matt Evans Date: Thu, 18 Jun 2026 14:32:41 -0700 Subject: [PATCH 2/2] test(engine): format Yare extra blockers regression --- crates/engine/tests/integration/main.rs | 2 +- .../engine/tests/integration/yare_extra_blockers.rs | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/engine/tests/integration/main.rs b/crates/engine/tests/integration/main.rs index 8ec529b0de..c895f8753d 100644 --- a/crates/engine/tests/integration/main.rs +++ b/crates/engine/tests/integration/main.rs @@ -413,5 +413,5 @@ mod who_villainous_choice_scoped_player; mod wise_mothman_milled_trigger; mod wise_mothman_target_distinctness; mod wrenn_and_six_up_to_one_optout; -mod yuriko_combat_damage; mod yare_extra_blockers; +mod yuriko_combat_damage; diff --git a/crates/engine/tests/integration/yare_extra_blockers.rs b/crates/engine/tests/integration/yare_extra_blockers.rs index 0daa84b8fc..8245865aa6 100644 --- a/crates/engine/tests/integration/yare_extra_blockers.rs +++ b/crates/engine/tests/integration/yare_extra_blockers.rs @@ -20,7 +20,13 @@ use engine::types::zones::Zone; const YARE_ORACLE: &str = "Target creature defending player controls gets +3/+0 until end of turn. That creature can block up to two additional creatures this turn."; -fn create_creature(state: &mut GameState, controller: PlayerId, name: &str, power: i32, toughness: i32) -> engine::types::identifiers::ObjectId { +fn create_creature( + state: &mut GameState, + controller: PlayerId, + name: &str, + power: i32, + toughness: i32, +) -> engine::types::identifiers::ObjectId { let id = create_object( state, CardId(state.next_object_id), @@ -53,7 +59,10 @@ fn yare_parses_up_to_two_additional_blockers_sub_ability() { else { panic!("expected GenericEffect, got {:?}", sub.effect); }; - assert_eq!(duration, &Some(engine::types::ability::Duration::UntilEndOfTurn)); + assert_eq!( + duration, + &Some(engine::types::ability::Duration::UntilEndOfTurn) + ); assert_eq!( static_abilities[0].mode, StaticMode::ExtraBlockers { count: Some(2) }