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
4 changes: 4 additions & 0 deletions crates/engine/src/game/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 { .. }
// CR 702.122a / 702.171a / 702.184a: CrewContribution carries the
// modifier kind + action list (Giant Ox, Hotshot Mechanic). Runtime
// enforcement is in static_abilities.rs::object_crew_power_contribution.
Expand Down
36 changes: 35 additions & 1 deletion crates/engine/src/parser/oracle_effect/subject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(".")),
Expand Down Expand Up @@ -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(".")),
Expand Down Expand Up @@ -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
/// <filter>" clause (Fast // Furious's second sentence) must lower to a real
/// `CantBeBlockedExceptBy` evasion static on the anaphoric "It" subject —
Expand Down
1 change: 1 addition & 0 deletions crates/engine/tests/integration/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,4 +415,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 yare_extra_blockers;
mod yuriko_combat_damage;
110 changes: 110 additions & 0 deletions crates/engine/tests/integration/yare_extra_blockers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//! 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"
);
}
Loading