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
16 changes: 9 additions & 7 deletions crates/engine/src/game/combat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1643,7 +1643,7 @@ pub fn validate_blockers_for_player(
}
}

// CR 509.1a + CR 509.1b: Enforce per-blocker limit on how many attackers it can block.
// CR 509.1a: Enforce per-blocker limit on how many attackers it can block.
// Default is 1; ExtraBlockers { count: Some(n) } allows 1 + n; count: None = unlimited.
{
for (&blocker_id, &num_blocked) in &attackers_per_blocker {
Expand All @@ -1654,7 +1654,7 @@ pub fn validate_blockers_for_player(
.objects
.get(&blocker_id)
.ok_or_else(|| format!("Blocker {:?} not found during limit check", blocker_id))?;
// Find the best ExtraBlockers grant on this creature
// Sum every ExtraBlockers grant on this creature.
let max_allowed = extra_block_limit(state, blocker);
if num_blocked > max_allowed {
return Err(format!(
Expand Down Expand Up @@ -4937,21 +4937,23 @@ fn ring_bearer_unblockable_by_greater_power(
&& blocker.power.unwrap_or(0) > attacker.power.unwrap_or(0)
}

/// CR 509.1a + CR 509.1b: Compute the maximum number of attackers a creature can block.
/// CR 509.1a: Compute the maximum number of attackers a creature can block.
/// Default is 1. ExtraBlockers { count: Some(n) } adds n (so 1+n). count: None = unlimited (u32::MAX).
/// Multiple ExtraBlockers stack: the best (highest) limit wins.
/// Multiple finite ExtraBlockers grants stack additively.
fn extra_block_limit(state: &GameState, blocker: &GameObject) -> u32 {
let mut max: u32 = 1;
// Each grant raises the limit independently, so the additional counts sum.
// A single `None` grant ("any number of creatures") makes the limit unbounded.
let mut extra: u32 = 0;
// CR 702.26b + CR 604.1: `active_static_definitions` owns the gating.
for sd in super::functioning_abilities::active_static_definitions(state, blocker) {
if let StaticMode::ExtraBlockers { count } = &sd.mode {
match count {
None => return u32::MAX, // unlimited
Some(n) => max = max.max(1 + n),
Some(n) => extra = extra.saturating_add(*n),
}
}
}
max
extra.saturating_add(1)
}

/// For each valid blocker, compute which attackers it can legally block.
Expand Down
62 changes: 62 additions & 0 deletions crates/engine/tests/integration/yare_extra_blockers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,65 @@ fn yare_extra_blockers_allow_three_attackers_blocked() {
"ExtraBlockers(2) must allow blocking three attackers"
);
}

#[test]
fn two_extra_blocker_grants_are_cumulative_not_max() {
// Two separate "can block an additional creature" effects on the same
// creature stack additively — e.g. two copies of Brave the Sands grant
// a limit of 1 (default) + 1 + 1 = 3, not max(1 + 1, 1 + 1) = 2.
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 attacker4 = create_creature(&mut state, PlayerId(0), "Attacker D", 2, 2);
let blocker = create_creature(&mut state, PlayerId(1), "Defender", 2, 2);

// Two independent grants, each "an additional creature" (Some(1)).
let defs = &mut state.objects.get_mut(&blocker).unwrap().static_definitions;
defs.push(StaticDefinition::new(StaticMode::ExtraBlockers {
count: Some(1),
}));
defs.push(StaticDefinition::new(StaticMode::ExtraBlockers {
count: Some(1),
}));

state.combat = Some(CombatState {
attackers: vec![
AttackerInfo::attacking_player(attacker1, PlayerId(1)),
AttackerInfo::attacking_player(attacker2, PlayerId(1)),
AttackerInfo::attacking_player(attacker3, PlayerId(1)),
AttackerInfo::attacking_player(attacker4, PlayerId(1)),
],
..Default::default()
});

// Fails before the fix: the limit is computed as max(2, 2) = 2, so this
// three-attacker block is rejected.
assert!(
validate_blockers(
&state,
&[
(blocker, attacker1),
(blocker, attacker2),
(blocker, attacker3),
],
)
.is_ok(),
"two ExtraBlockers(1) grants must sum to a limit of 3 (1 default + 1 + 1)"
);

// Upper bound is exactly 3: a fourth simultaneous block is still illegal.
assert!(
validate_blockers(
&state,
&[
(blocker, attacker1),
(blocker, attacker2),
(blocker, attacker3),
(blocker, attacker4),
],
)
.is_err(),
"two ExtraBlockers(1) grants cap the limit at 3, so a fourth block is illegal"
);
}
Loading