Declare Blockers Step
Overview
The Declare Blockers step is the phase of combat where defending players assign their creatures to block attacking creatures. In Commander, this step can be particularly complex due to the multiplayer nature of the format, where multiple players may be defending against attacks simultaneously. This document outlines the implementation of the Declare Blockers step in our game engine.
Core Implementation
Phase Structure
The Declare Blockers step follows the Declare Attackers step in the combat phase sequence:
#![allow(unused)] fn main() { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CombatStep { BeginningOfCombat, DeclareAttackers, DeclareBlockers, CombatDamage, EndOfCombat, } }
Declare Blockers System
The core system that handles the Declare Blockers step:
#![allow(unused)] fn main() { pub fn declare_blockers_system( mut commands: Commands, turn_manager: Res<TurnManager>, mut game_events: EventWriter<GameEvent>, mut next_phase: ResMut<NextState<Phase>>, mut priority_system: ResMut<PrioritySystem>, combat_system: Res<CombatSystem>, mut block_declarations: EventReader<BlockDeclarationEvent>, ) { // Only run during Declare Blockers step if turn_manager.current_phase != Phase::Combat(CombatStep::DeclareBlockers) { return; } // If this is the first time entering the step if !priority_system.priority_given { // Emit Declare Blockers event game_events.send(GameEvent::DeclareBlockersStep); // Process block requirements and restrictions commands.run_system(process_block_requirements); // Create turn order for block declarations priority_system.set_player_order_for_blockers(combat_system.attacked_players()); // Grant priority to first defending player priority_system.grant_initial_priority(); } // Process any block declarations for event in block_declarations.iter() { process_block_declaration(&mut commands, event, &combat_system, &mut game_events); } // If all defending players have passed priority with block declarations finalized if priority_system.all_defenders_passed && combat_system.block_declarations_finalized { // Process post-declaration triggers commands.run_system(process_block_triggers); // Reset priority for all players to respond to blocks priority_system.reset_with_active_player_priority(); } // If all players have passed priority and the stack is empty if priority_system.all_players_passed() && priority_system.stack.is_empty() { // Proceed to Combat Damage step next_phase.set(Phase::Combat(CombatStep::CombatDamage)); priority_system.priority_given = false; } } // Helper function to process a block declaration fn process_block_declaration( commands: &mut Commands, event: &BlockDeclarationEvent, combat_system: &CombatSystem, game_events: &mut EventWriter<GameEvent>, ) { let BlockDeclarationEvent { blocker, attackers } = event; // Validate block declaration if let Some(reason) = validate_block(*blocker, attackers, combat_system) { game_events.send(GameEvent::InvalidBlockDeclaration { blocker: *blocker, attackers: attackers.clone(), reason, }); return; } // Record the block in the combat system commands.entity(*blocker).insert(Blocking { blocked_attackers: attackers.clone(), }); // Emit block declaration event for attacker in attackers { game_events.send(GameEvent::BlockDeclared { blocker: *blocker, attacker: *attacker, }); } } }
Block Validation
Blocks must be validated according to various rules and restrictions:
#![allow(unused)] fn main() { fn validate_block( blocker: Entity, attackers: &[Entity], combat_system: &CombatSystem, ) -> Option<String> { // Check if creature can block at all if let Some(restrictions) = combat_system.block_restrictions.get(&blocker) { for restriction in restrictions { match restriction { BlockRestriction::CantBlock => { return Some("Creature cannot block".to_string()); }, // Other general block restrictions... } } } // Check if creature can block multiple attackers if attackers.len() > 1 { let can_block_multiple = check_can_block_multiple(blocker, combat_system); if !can_block_multiple { return Some("Creature cannot block multiple attackers".to_string()); } } // Check attacker-specific restrictions for attacker in attackers { // Check if this attacker can be blocked by this blocker if let Some(restrictions) = combat_system.attacker_restrictions.get(attacker) { for restriction in restrictions { match restriction { AttackerRestriction::CantBeBlocked => { return Some(format!("Attacker {} cannot be blocked", attacker.index())); }, AttackerRestriction::CantBeBlockedBy(condition) => { if condition.matches(blocker) { return Some(format!("Attacker {} cannot be blocked by this creature", attacker.index())); } }, // Other attacker-specific restrictions... } } } } // Check special blocking requirements if let Some(requirements) = combat_system.attacker_block_requirements.get(&attackers[0]) { for requirement in requirements { match requirement { BlockRequirement::MustBeBlockedByAtLeast(count) => { if combat_system.get_blockers_for_attacker(attackers[0]).len() + 1 < *count { // This single blocker is not enough to satisfy the requirement // Note: In a real implementation, this would need to check if the requirement // could be satisfied with other declared blocks return Some(format!("Attacker requires at least {} blockers", count)); } }, // Other block requirements... } } } // All checks passed None } // Helper function to check if a creature can block multiple attackers fn check_can_block_multiple( blocker: Entity, combat_system: &CombatSystem, ) -> bool { // Check if creature has a special ability that allows blocking multiple attackers if let Some(special_abilities) = combat_system.creature_special_abilities.get(&blocker) { if special_abilities.contains(&SpecialAbility::CanBlockAdditionalCreature(1)) { return true; } if special_abilities.contains(&SpecialAbility::CanBlockAnyNumber) { return true; } } // By default, creatures can only block one attacker false } }
Block Requirements
Some effects in the game can force creatures to block:
#![allow(unused)] fn main() { pub fn process_block_requirements( combat_system: Res<CombatSystem>, mut game_events: EventWriter<GameEvent>, creature_query: Query<(Entity, &Creature, &Controllable)>, player_query: Query<(Entity, &Player)>, ) { // Identify all players who are being attacked let attacked_players: HashSet<Entity> = combat_system.attackers .iter() .filter_map(|(_, attack_data)| { if player_query.contains(attack_data.defender) { Some(attack_data.defender) } else { None } }) .collect(); // Process creatures with block requirements for (entity, creature, controllable) in creature_query.iter() { // Only check creatures controlled by players being attacked if !attacked_players.contains(&controllable.controller) { continue; } // Check if creature has block requirements if let Some(requirements) = combat_system.block_requirements.get(&entity) { for requirement in requirements { match requirement { BlockRequirement::MustBlock => { // Creature must block if able game_events.send(GameEvent::BlockRequirement { creature: entity, requirement: "Must block if able".to_string(), }); }, BlockRequirement::MustBlockAttacker(attacker) => { // Creature must block a specific attacker if combat_system.attackers.contains_key(attacker) { game_events.send(GameEvent::BlockRequirement { creature: entity, requirement: format!("Must block attacker {:?} if able", attacker), }); } }, // Other requirements... } } } } } }
Evasion Abilities
Evasion abilities like Flying, Menace, etc. are essential to the blocking rules:
#![allow(unused)] fn main() { pub fn apply_evasion_restrictions( mut combat_system: ResMut<CombatSystem>, creature_query: Query<(Entity, &Creature)>, ) { // Process flying for (entity, creature) in creature_query.iter() { if creature.has_ability(CreatureAbility::Flying) { // Flying creatures can only be blocked by creatures with flying or reach combat_system.add_attacker_restriction( entity, AttackerRestriction::CantBeBlockedBy(BlockerCondition::NotFlyingOrReach), ); } if creature.has_ability(CreatureAbility::Menace) { // Menace creatures must be blocked by at least two creatures combat_system.add_attacker_block_requirement( entity, BlockRequirement::MustBeBlockedByAtLeast(2), ); } if creature.has_ability(CreatureAbility::Fear) { // Fear creatures can only be blocked by black or artifact creatures combat_system.add_attacker_restriction( entity, AttackerRestriction::CantBeBlockedBy(BlockerCondition::NotBlackOrArtifact), ); } // Implement other evasion abilities } } }
Multiplayer Considerations
In multiplayer Commander games, multiple players might need to declare blockers:
#![allow(unused)] fn main() { pub fn handle_multiplayer_blocks( combat_system: Res<CombatSystem>, turn_manager: Res<TurnManager>, player_query: Query<(Entity, &Player)>, mut priority_system: ResMut<PrioritySystem>, ) { // Group attackers by defending player let mut attackers_by_defender: HashMap<Entity, Vec<Entity>> = HashMap::new(); for (attacker, attack_data) in &combat_system.attackers { if player_query.contains(attack_data.defender) { attackers_by_defender.entry(attack_data.defender) .or_insert_with(Vec::new) .push(*attacker); } } // Set up priority for each defending player in turn order let mut defender_order = Vec::new(); for player_idx in 0..turn_manager.player_order.len() { let player_entity = turn_manager.player_order[player_idx]; if attackers_by_defender.contains_key(&player_entity) { defender_order.push(player_entity); } } // Update priority system with defender order priority_system.defender_order = defender_order; } }
Special Blocking Rules
Multiple Blockers
When a single attacker is blocked by multiple creatures:
#![allow(unused)] fn main() { pub fn handle_multiple_blockers( mut combat_system: ResMut<CombatSystem>, blocking_query: Query<(Entity, &Blocking)>, mut game_events: EventWriter<GameEvent>, ) { // Build a map of attackers to their blockers let mut blockers_by_attacker: HashMap<Entity, Vec<Entity>> = HashMap::new(); for (blocker, blocking) in blocking_query.iter() { for attacker in &blocking.blocked_attackers { blockers_by_attacker.entry(*attacker) .or_insert_with(Vec::new) .push(blocker); } } // Process attackers with multiple blockers for (attacker, blockers) in blockers_by_attacker.iter() { if blockers.len() > 1 { // Attacker's controller declares damage assignment order game_events.send(GameEvent::DamageAssignmentOrderNeeded { attacker: *attacker, blockers: blockers.clone(), }); } } } }
Triggered Abilities
When blockers are declared, various triggered abilities might occur:
#![allow(unused)] fn main() { pub fn process_block_triggers( combat_system: Res<CombatSystem>, blocking_query: Query<(Entity, &Blocking)>, mut ability_triggers: ResMut<AbilityTriggerQueue>, trigger_sources: Query<(Entity, &AbilityTrigger)>, ) { // Create attacker to blocker map let mut blockers_by_attacker: HashMap<Entity, Vec<Entity>> = HashMap::new(); for (blocker, blocking) in blocking_query.iter() { for attacker in &blocking.blocked_attackers { blockers_by_attacker.entry(*attacker) .or_insert_with(Vec::new) .push(blocker); } } // Process "when this creature blocks" triggers for (blocker, blocking) in blocking_query.iter() { if let Ok((entity, trigger)) = trigger_sources.get(blocker) { if let TriggerCondition::WhenBlocks = trigger.condition { for attacker in &blocking.blocked_attackers { ability_triggers.queue.push_back(AbilityTriggerEvent { source: entity, trigger: trigger.clone(), targets: vec![*attacker], // The attacker is the target }); } } } } // Process "when this creature becomes blocked" triggers for (attacker, blockers) in blockers_by_attacker.iter() { if let Ok((entity, trigger)) = trigger_sources.get(*attacker) { if let TriggerCondition::WhenBecomesBlocked = trigger.condition { ability_triggers.queue.push_back(AbilityTriggerEvent { source: entity, trigger: trigger.clone(), targets: blockers.clone(), // All blockers are targets }); } } } // Process "whenever a creature blocks" triggers for (entity, trigger) in trigger_sources.iter() { if let TriggerCondition::WheneverCreatureBlocks { controller_only } = trigger.condition { let should_trigger = if controller_only { // Only trigger for creatures controlled by the same player // Implementation details omitted for brevity true } else { // Trigger for any blocking creature !blocking_query.is_empty() }; if should_trigger { ability_triggers.queue.push_back(AbilityTriggerEvent { source: entity, trigger: trigger.clone(), targets: Vec::new(), // No specific targets }); } } } } }
State Tracking
Once all blockers are declared, we need to update the game state:
#![allow(unused)] fn main() { pub fn update_creature_state_on_block( mut commands: Commands, blocking_query: Query<(Entity, &Blocking, &mut Creature)>, ) { // Update all blocking creatures for (entity, blocking, mut creature) in blocking_query.iter_mut() { // Mark creature as blocking creature.blocking = blocking.blocked_attackers.clone(); // Tap the creature if it has vigilance for blocking // Note: This is not a standard MTG rule but could be a house rule or special card effect if creature.has_ability(CreatureAbility::VigilanceForBlocking) { commands.entity(entity).insert(Tapped(true)); } } } }
Edge Cases and Special Interactions
Banding
Banding is a complex ability that affects blocks:
#![allow(unused)] fn main() { pub fn handle_banding( mut combat_system: ResMut<CombatSystem>, creature_query: Query<(Entity, &Creature)>, blocking_query: Query<(Entity, &Blocking)>, ) { // Find all creatures with banding let banding_creatures: HashSet<Entity> = creature_query .iter() .filter_map(|(entity, creature)| { if creature.has_ability(CreatureAbility::Banding) { Some(entity) } else { None } }) .collect(); // Process attackers blocked by banding creatures let mut defenders_control_damage_assignment: HashSet<Entity> = HashSet::new(); for (blocker, blocking) in blocking_query.iter() { if banding_creatures.contains(&blocker) { // If a banding creature is blocking, the defender controls damage assignment for attacker in &blocking.blocked_attackers { defenders_control_damage_assignment.insert(*attacker); } } } // Update combat system for attacker in defenders_control_damage_assignment { combat_system.add_attacker_flag(attacker, AttackerFlag::DefenderControlsDamageAssignment); } } }
Protection
Protection affects blocking in various ways:
#![allow(unused)] fn main() { pub fn apply_protection_block_restrictions( mut combat_system: ResMut<CombatSystem>, creature_query: Query<(Entity, &Creature, &Protection)>, ) { for (entity, _, protection) in creature_query.iter() { match protection.from { ProtectionFrom::Color(color) => { // Creature with protection from a color can't be blocked by creatures of that color combat_system.add_attacker_restriction( entity, AttackerRestriction::CantBeBlockedBy(BlockerCondition::HasColor(color)), ); // Creature with protection can't block creatures of that color combat_system.add_block_restriction( entity, BlockRestriction::CantBlockCreature(AttackerCondition::HasColor(color)), ); }, ProtectionFrom::Type(card_type) => { // Similar restrictions for protection from a type // Implementation details omitted for brevity }, // Other protection types... } } } }
Testing Strategy
Unit Tests
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; #[test] fn test_block_validation() { // Test with a creature that can block let result = validate_block( /* mock entities and combat system */ Entity::from_raw(1), &[Entity::from_raw(2)], &CombatSystem::default(), ); assert_eq!(result, None, "Valid block should return None"); // Test with a creature that can't block let mut combat_system = CombatSystem::default(); combat_system.add_block_restriction( Entity::from_raw(1), BlockRestriction::CantBlock, ); let result = validate_block( Entity::from_raw(1), &[Entity::from_raw(2)], &combat_system, ); assert!(result.is_some(), "Invalid block should return an error message"); } #[test] fn test_flying_restriction() { let mut app = App::new(); app.add_systems(Update, apply_evasion_restrictions); // Create a flying creature let flying_creature = app.world.spawn(( Creature { abilities: vec![CreatureAbility::Flying], ..Default::default() }, )).id(); // Set up combat system let combat_system = CombatSystem::default(); app.insert_resource(combat_system); // Run the system app.update(); // Check if flying restriction was applied let combat_system = app.world.resource::<CombatSystem>(); let has_flying_restriction = combat_system.attacker_restrictions.get(&flying_creature) .map_or(false, |reqs| reqs.iter().any(|req| matches!(req, AttackerRestriction::CantBeBlockedBy(BlockerCondition::NotFlyingOrReach)))); assert!(has_flying_restriction, "Flying creature should have CantBeBlockedBy restriction"); } #[test] fn test_multiple_blockers() { let mut app = App::new(); app.add_systems(Update, handle_multiple_blockers); app.add_event::<GameEvent>(); // Set up an attacker and multiple blockers let attacker = app.world.spawn(Creature::default()).id(); let blocker1 = app.world.spawn(( Creature::default(), Blocking { blocked_attackers: vec![attacker] }, )).id(); let blocker2 = app.world.spawn(( Creature::default(), Blocking { blocked_attackers: vec![attacker] }, )).id(); // Set up combat system let combat_system = CombatSystem::default(); app.insert_resource(combat_system); // Run the system app.update(); // Check for damage assignment order event let mut event_reader = app.world.resource_mut::<Events<GameEvent>>(); let mut found_event = false; for event in event_reader.iter() { if let GameEvent::DamageAssignmentOrderNeeded { attacker: a, blockers } = event { if *a == attacker && blockers.contains(&blocker1) && blockers.contains(&blocker2) { found_event = true; break; } } } assert!(found_event, "Damage assignment order event should be emitted for multiple blockers"); } // Additional unit tests... } }
Integration Tests
#![allow(unused)] fn main() { #[cfg(test)] mod integration_tests { use super::*; #[test] fn test_declare_blockers_workflow() { let mut app = App::new(); // Add all relevant systems app.add_systems(Update, ( declare_blockers_system, process_block_requirements, apply_evasion_restrictions, handle_multiple_blockers, process_block_triggers, update_creature_state_on_block, )); // Set up game state with attackers and defenders // Implementation details omitted for brevity // Simulate player declaring blockers app.world.resource_mut::<Events<BlockDeclarationEvent>>().send( BlockDeclarationEvent { blocker: Entity::from_raw(1), attackers: vec![Entity::from_raw(2)], } ); // Run update to process declarations app.update(); // Verify blockers are properly recorded and state is updated // Implementation details omitted for brevity } #[test] fn test_evasion_abilities_interaction() { let mut app = App::new(); // Set up attacker with flying let flying_attacker = app.world.spawn(( Creature { abilities: vec![CreatureAbility::Flying], ..Default::default() }, )).id(); // Set up potential blockers: one with flying, one with reach, one with neither let flying_blocker = app.world.spawn(( Creature { abilities: vec![CreatureAbility::Flying], ..Default::default() }, )).id(); let reach_blocker = app.world.spawn(( Creature { abilities: vec![CreatureAbility::Reach], ..Default::default() }, )).id(); let normal_blocker = app.world.spawn(Creature::default()).id(); // Set up combat system with the attacker let mut combat_system = CombatSystem::default(); combat_system.attackers.insert(flying_attacker, AttackData { attacker: flying_attacker, defender: Entity::from_raw(10), // Some player is_commander: false, requirements: Vec::new(), restrictions: Vec::new(), }); app.insert_resource(combat_system); // Add relevant systems app.add_systems(Update, apply_evasion_restrictions); // Run the system app.update(); // Try to declare blocks with each blocker let can_flying_block = validate_block(flying_blocker, &[flying_attacker], &app.world.resource::<CombatSystem>()); let can_reach_block = validate_block(reach_blocker, &[flying_attacker], &app.world.resource::<CombatSystem>()); let can_normal_block = validate_block(normal_blocker, &[flying_attacker], &app.world.resource::<CombatSystem>()); // Verify results assert_eq!(can_flying_block, None, "Flying creature should be able to block flying attacker"); assert_eq!(can_reach_block, None, "Reach creature should be able to block flying attacker"); assert!(can_normal_block.is_some(), "Normal creature should not be able to block flying attacker"); } // Additional integration tests... } }
UI Considerations
The UI during the Declare Blockers step needs to clearly communicate various states:
#![allow(unused)] fn main() { pub fn update_declare_blockers_ui( turn_manager: Res<TurnManager>, combat_system: Res<CombatSystem>, creature_query: Query<(Entity, &Creature, &Controllable)>, attacking_query: Query<Entity, With<Attacking>>, blocking_query: Query<(Entity, &Blocking)>, mut ui_state: ResMut<UiState>, ) { // Only run during Declare Blockers step if turn_manager.current_phase != Phase::Combat(CombatStep::DeclareBlockers) { return; } // Update phase display ui_state.current_phase_text = "Declare Blockers".to_string(); // Get active player and current player with priority let active_player = turn_manager.get_active_player(); let current_player = turn_manager.get_current_player(); // Highlight attackers ui_state.attackers.clear(); for attacker in attacking_query.iter() { ui_state.attackers.insert(attacker); } // Highlight potential blockers for current player ui_state.potential_blockers.clear(); for (entity, creature, controllable) in creature_query.iter() { if controllable.controller == current_player && creature.can_block() { ui_state.potential_blockers.insert(entity); // Mark creatures that must block if let Some(requirements) = combat_system.block_requirements.get(&entity) { if requirements.iter().any(|req| matches!(req, BlockRequirement::MustBlock)) { ui_state.creatures_with_requirements.insert(entity, "Must block if able".to_string()); } } } } // Show current block declarations ui_state.current_blocks.clear(); for (blocker, blocking) in blocking_query.iter() { ui_state.current_blocks.insert(blocker, blocking.blocked_attackers.clone()); } // Highlight legal blocks ui_state.legal_blocks.clear(); for attacker in attacking_query.iter() { let legal_blockers = creature_query .iter() .filter_map(|(entity, creature, controllable)| { if controllable.controller == current_player && validate_block(entity, &[attacker], &combat_system).is_none() { Some(entity) } else { None } }) .collect::<Vec<_>>(); ui_state.legal_blocks.insert(attacker, legal_blockers); } } }
Performance Considerations
-
Efficient Block Validation: The validation of blocks should be optimized to avoid redundant checks.
-
Caching Block Results: Once block declarations are finalized, the results can be cached for use in subsequent steps.
-
Minimize Entity Queries: Group related queries to minimize entity access operations.
-
Parallel Processing: For games with many blockers, processing block triggers could be done in parallel.
Conclusion
The Declare Blockers step is a critical part of the combat phase in Commander, particularly in multiplayer games where multiple players may be defending simultaneously. A robust implementation ensures that all game rules are properly enforced, including evasion abilities, protection effects, and multi-blocker scenarios. By handling block declarations, restrictions, requirements, and triggers correctly, we provide the foundation for accurate combat resolution in the following steps.