From e46f553094e3817b120a5b2f1003968c574ce058 Mon Sep 17 00:00:00 2001 From: boskodev790 <233739930+boskodev790@users.noreply.github.com> Date: Wed, 22 Jul 2026 05:35:42 -0400 Subject: [PATCH 1/2] fix(engine): sum cumulative "block an additional creature" grants `extra_block_limit` took the max of ExtraBlockers grants instead of summing them, so two "can block an additional creature" effects on one creature (e.g. two copies of Brave the Sands) produced a block limit of 2 rather than 3. Per CR 509.1b these effects are cumulative: the limit is the default 1 plus the sum of every grant's additional count. A `None` grant ("any number of creatures") still short-circuits to unbounded. Adds a regression test: two ExtraBlockers(1) grants must allow exactly three simultaneous blocks and reject a fourth. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/engine/src/game/combat.rs | 12 ++-- .../tests/integration/yare_extra_blockers.rs | 62 +++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/game/combat.rs b/crates/engine/src/game/combat.rs index 4e383bb5e0..457d979d49 100644 --- a/crates/engine/src/game/combat.rs +++ b/crates/engine/src/game/combat.rs @@ -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 (CR 509.1b). let max_allowed = extra_block_limit(state, blocker); if num_blocked > max_allowed { return Err(format!( @@ -4941,17 +4941,21 @@ fn ring_bearer_unblockable_by_greater_power( /// 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. fn extra_block_limit(state: &GameState, blocker: &GameObject) -> u32 { - let mut max: u32 = 1; + // CR 509.1b: multiple "can block an additional creature" effects are + // cumulative — each grant raises the limit independently, so the additional + // counts SUM (default 1 + the sum of every grant's `n`). 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. diff --git a/crates/engine/tests/integration/yare_extra_blockers.rs b/crates/engine/tests/integration/yare_extra_blockers.rs index 8245865aa6..e214827790 100644 --- a/crates/engine/tests/integration/yare_extra_blockers.rs +++ b/crates/engine/tests/integration/yare_extra_blockers.rs @@ -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() { + // CR 509.1b: 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" + ); +} From 58c36fc33d5d27739b87a6b90432280b4f29de0b Mon Sep 17 00:00:00 2001 From: matthewevans Date: Wed, 22 Jul 2026 02:44:22 -0700 Subject: [PATCH 2/2] fix(PR-6330): correct extra-blocker rule documentation Correct stale cumulative-limit documentation while preserving the reviewed additive combat logic and regression.\n\nCo-authored-by: boskodev790 <233739930+boskodev790@users.noreply.github.com> --- crates/engine/src/game/combat.rs | 14 ++++++-------- .../tests/integration/yare_extra_blockers.rs | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/engine/src/game/combat.rs b/crates/engine/src/game/combat.rs index 457d979d49..118a72664d 100644 --- a/crates/engine/src/game/combat.rs +++ b/crates/engine/src/game/combat.rs @@ -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 { @@ -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))?; - // Sum every ExtraBlockers grant on this creature (CR 509.1b). + // Sum every ExtraBlockers grant on this creature. let max_allowed = extra_block_limit(state, blocker); if num_blocked > max_allowed { return Err(format!( @@ -4937,14 +4937,12 @@ 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 { - // CR 509.1b: multiple "can block an additional creature" effects are - // cumulative — each grant raises the limit independently, so the additional - // counts SUM (default 1 + the sum of every grant's `n`). A single `None` - // grant ("any number of creatures") makes the limit unbounded. + // 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) { diff --git a/crates/engine/tests/integration/yare_extra_blockers.rs b/crates/engine/tests/integration/yare_extra_blockers.rs index e214827790..e0d4b6f453 100644 --- a/crates/engine/tests/integration/yare_extra_blockers.rs +++ b/crates/engine/tests/integration/yare_extra_blockers.rs @@ -111,8 +111,8 @@ fn yare_extra_blockers_allow_three_attackers_blocked() { #[test] fn two_extra_blocker_grants_are_cumulative_not_max() { - // CR 509.1b: two separate "can block an additional creature" effects on the - // same creature stack additively — e.g. two copies of Brave the Sands grant + // 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);