Turn Structure
Overview
The Turn Structure module manages the flow of a Commander game, handling the sequence of phases and steps within each player's turn. It coordinates player transitions, priority passing, and phase-specific actions while accounting for the multiplayer nature of Commander.
Core Turn Sequence
A turn in Commander follows the standard Magic: The Gathering sequence:
-
Beginning Phase
- Untap Step
- Upkeep Step
- Draw Step
-
Precombat Main Phase
-
Combat Phase
- Beginning of Combat Step
- Declare Attackers Step
- Declare Blockers Step
- Combat Damage Step
- End of Combat Step
-
Postcombat Main Phase
-
Ending Phase
- End Step
- Cleanup Step
Multiplayer Considerations
In Commander, turn order proceeds clockwise from the starting player. The format introduces special considerations:
- Turn Order Determination: Typically random at the start of the game
- Player Elimination: When a player loses, turns continue with the remaining players
- Extra Turns: Cards that grant extra turns work the same as in standard Magic
- "Skip your next turn" effects: These follow standard Magic rules but can have significant political impact
Data Structures
The turn structure is managed through the following robust data structures:
#![allow(unused)] fn main() { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Phase { Beginning(BeginningStep), Precombat(PrecombatStep), Combat(CombatStep), Postcombat(PostcombatStep), Ending(EndingStep), } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum BeginningStep { Untap, Upkeep, Draw, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum PrecombatStep { Main, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CombatStep { Beginning, DeclareAttackers, DeclareBlockers, FirstStrike, CombatDamage, End, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum PostcombatStep { Main, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum EndingStep { End, Cleanup, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ExtraTurnSource { Card(Entity), Ability(Entity, Entity), // (Source, Ability) Rule, } #[derive(Resource)] pub struct TurnManager { // Current turn information pub current_phase: Phase, pub active_player_index: usize, pub turn_number: u32, // Priority system pub priority_player_index: usize, pub all_players_passed: bool, pub stack_is_empty: bool, // Multiplayer tracking pub player_order: Vec<Entity>, pub extra_turns: VecDeque<(Entity, ExtraTurnSource)>, pub skipped_turns: HashSet<Entity>, // Time tracking pub time_limits: HashMap<Entity, Duration>, pub turn_started_at: Option<Instant>, // Phase history for reverting game states and debugging pub phase_history: VecDeque<(Phase, Entity)>, // Special phase tracking pub additional_phases: VecDeque<(Phase, Entity)>, pub modified_phases: HashMap<Phase, PhaseModification>, } #[derive(Debug, Clone)] pub struct PhaseModification { pub source: Entity, pub skip_priority: bool, pub skip_actions: bool, pub additional_actions: Vec<GameAction>, } }
Detailed Implementation
Phase Progression System
#![allow(unused)] fn main() { impl TurnManager { // Advances to the next phase in the turn sequence pub fn advance_phase(&mut self) -> Result<Phase, TurnError> { let previous_phase = self.current_phase; self.current_phase = match self.current_phase { Phase::Beginning(BeginningStep::Untap) => Phase::Beginning(BeginningStep::Upkeep), Phase::Beginning(BeginningStep::Upkeep) => Phase::Beginning(BeginningStep::Draw), Phase::Beginning(BeginningStep::Draw) => Phase::Precombat(PrecombatStep::Main), Phase::Precombat(PrecombatStep::Main) => Phase::Combat(CombatStep::Beginning), Phase::Combat(CombatStep::Beginning) => Phase::Combat(CombatStep::DeclareAttackers), Phase::Combat(CombatStep::DeclareAttackers) => Phase::Combat(CombatStep::DeclareBlockers), // Check if first strike damage should happen Phase::Combat(CombatStep::DeclareBlockers) => { if self.has_first_strike_creatures() { Phase::Combat(CombatStep::FirstStrike) } else { Phase::Combat(CombatStep::CombatDamage) } }, Phase::Combat(CombatStep::FirstStrike) => Phase::Combat(CombatStep::CombatDamage), Phase::Combat(CombatStep::CombatDamage) => Phase::Combat(CombatStep::End), Phase::Combat(CombatStep::End) => Phase::Postcombat(PostcombatStep::Main), Phase::Postcombat(PostcombatStep::Main) => Phase::Ending(EndingStep::End), Phase::Ending(EndingStep::End) => Phase::Ending(EndingStep::Cleanup), Phase::Ending(EndingStep::Cleanup) => { // Check for additional or extra turns self.process_turn_end()?; // The next turn's first phase Phase::Beginning(BeginningStep::Untap) } }; // Record phase change in history self.phase_history.push_back((previous_phase, self.get_active_player())); // Check if this phase should be modified or skipped if let Some(modification) = self.modified_phases.get(&self.current_phase) { if modification.skip_actions { // Skip this phase and move to the next one return self.advance_phase(); } } // Handle priority assignment for the new phase self.reset_priority_for_new_phase(); Ok(self.current_phase) } // Process the end of a turn fn process_turn_end(&mut self) -> Result<(), TurnError> { // Check if there are additional phases for this turn if let Some((phase, _)) = self.additional_phases.pop_front() { self.current_phase = phase; return Ok(()); } // Move to the next player let next_player = self.determine_next_player()?; self.active_player_index = self.player_order.iter() .position(|&p| p == next_player) .ok_or(TurnError::InvalidPlayerIndex)?; self.turn_number += 1; // Reset turn timer self.turn_started_at = Some(Instant::now()); Ok(()) } // Determine who takes the next turn fn determine_next_player(&mut self) -> Result<Entity, TurnError> { // Check for extra turns first if let Some((player, source)) = self.extra_turns.pop_front() { // Log extra turn for game record info!("Player {:?} is taking an extra turn from source {:?}", player, source); return Ok(player); } // Get the next player in turn order let next_index = (self.active_player_index + 1) % self.player_order.len(); let next_player = self.player_order[next_index]; // Check if the next player should skip their turn if self.skipped_turns.remove(&next_player) { // Record the skipped turn info!("Player {:?} is skipping their turn", next_player); // Move to the player after that self.active_player_index = next_index; return self.determine_next_player(); } Ok(next_player) } } }
Priority System
#![allow(unused)] fn main() { impl TurnManager { // Reset priority for a new phase fn reset_priority_for_new_phase(&mut self) { self.all_players_passed = false; // In most phases, active player gets priority first self.priority_player_index = self.active_player_index; // For Beginning::Untap and Ending::Cleanup, no player gets priority by default match self.current_phase { Phase::Beginning(BeginningStep::Untap) | Phase::Ending(EndingStep::Cleanup) => { self.all_players_passed = true; }, _ => {} } } // Pass priority to the next player pub fn pass_priority(&mut self) -> Result<(), TurnError> { // Calculate next player with priority let next_index = (self.priority_player_index + 1) % self.player_order.len(); // If we're back to the active player, everyone has passed if next_index == self.active_player_index { self.all_players_passed = true; // If the stack is empty and everyone has passed, move to the next phase if self.stack_is_empty { self.advance_phase()?; } else { // Resolve the top item on the stack // This would trigger a different system self.stack_is_empty = false; // This would be set by the stack system } } else { // Pass to the next player self.priority_player_index = next_index; } Ok(()) } } }
Edge Cases and Their Handling
Player Elimination During a Turn
#![allow(unused)] fn main() { impl TurnManager { // Handle a player being eliminated pub fn handle_player_elimination(&mut self, eliminated_player: Entity) -> Result<(), TurnError> { // Remove player from the turn order let index = self.player_order.iter() .position(|&p| p == eliminated_player) .ok_or(TurnError::PlayerNotFound)?; self.player_order.remove(index); // Adjust active player index if necessary if index <= self.active_player_index && self.active_player_index > 0 { self.active_player_index -= 1; } // Adjust priority player index if necessary if index <= self.priority_player_index && self.priority_player_index > 0 { self.priority_player_index -= 1; } // Remove any extra or skipped turns for this player self.extra_turns.retain(|(player, _)| *player != eliminated_player); self.skipped_turns.remove(&eliminated_player); // If the active player was eliminated if index == self.active_player_index { // Move to the next player's turn let next_player = self.determine_next_player()?; self.active_player_index = self.player_order.iter() .position(|&p| p == next_player) .ok_or(TurnError::InvalidPlayerIndex)?; // Reset to the beginning of the turn self.current_phase = Phase::Beginning(BeginningStep::Untap); self.reset_priority_for_new_phase(); } Ok(()) } } }
Handling Multiple Extra Turns
When multiple extra turns are queued, they're processed in the order they were created:
#![allow(unused)] fn main() { impl TurnManager { // Add an extra turn to the queue pub fn add_extra_turn(&mut self, player: Entity, source: ExtraTurnSource) { self.extra_turns.push_back((player, source)); } } }
Time Limits and Slow Play
#![allow(unused)] fn main() { impl TurnManager { // Check if a player has exceeded their time limit pub fn check_time_limit(&self, player: Entity) -> Result<bool, TurnError> { if let Some(limit) = self.time_limits.get(&player) { if let Some(start_time) = self.turn_started_at { return Ok(start_time.elapsed() > *limit); } } Ok(false) } } }
Verification and Testing Strategy
Unit Tests
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; // Test normal phase progression #[test] fn test_normal_phase_progression() { let mut turn_manager = setup_test_turn_manager(); // Start at Beginning::Untap assert_eq!(turn_manager.current_phase, Phase::Beginning(BeginningStep::Untap)); // Advance through all phases for _ in 0..12 { // Total number of phases/steps in a turn turn_manager.advance_phase().unwrap(); } // Should be back at Beginning::Untap for the next player assert_eq!(turn_manager.current_phase, Phase::Beginning(BeginningStep::Untap)); assert_eq!(turn_manager.active_player_index, 1); // Next player } // Test extra turn handling #[test] fn test_extra_turn() { let mut turn_manager = setup_test_turn_manager(); let player1 = turn_manager.player_order[0]; // Add an extra turn for the current player turn_manager.add_extra_turn(player1, ExtraTurnSource::Rule); // Advance through a full turn for _ in 0..12 { turn_manager.advance_phase().unwrap(); } // Should still be the same player's turn assert_eq!(turn_manager.active_player_index, 0); } // Test skipped turn #[test] fn test_skipped_turn() { let mut turn_manager = setup_test_turn_manager(); let player2 = turn_manager.player_order[1]; // Mark player 2's turn to be skipped turn_manager.skipped_turns.insert(player2); // Advance through a full turn for _ in 0..12 { turn_manager.advance_phase().unwrap(); } // Should skip to player 3 assert_eq!(turn_manager.active_player_index, 2); } // Test player elimination #[test] fn test_player_elimination() { let mut turn_manager = setup_test_turn_manager(); let player2 = turn_manager.player_order[1]; // Eliminate player 2 turn_manager.handle_player_elimination(player2).unwrap(); // Check player order is updated assert_eq!(turn_manager.player_order.len(), 3); assert!(!turn_manager.player_order.contains(&player2)); // Complete current turn for _ in 0..12 { turn_manager.advance_phase().unwrap(); } // Should skip from player 1 to player 3 (now at index 1) assert_eq!(turn_manager.active_player_index, 1); } // Test priority passing #[test] fn test_priority_passing() { let mut turn_manager = setup_test_turn_manager(); // Move to a phase where priority is assigned turn_manager.advance_phase().unwrap(); // To Beginning::Upkeep // Active player should have priority assert_eq!(turn_manager.priority_player_index, 0); // Pass priority to each player for i in 1..4 { turn_manager.pass_priority().unwrap(); assert_eq!(turn_manager.priority_player_index, i); } // After all players pass, should move to next phase turn_manager.pass_priority().unwrap(); assert_eq!(turn_manager.current_phase, Phase::Beginning(BeginningStep::Draw)); } // Setup helper fn setup_test_turn_manager() -> TurnManager { let player1 = Entity::from_raw(1); let player2 = Entity::from_raw(2); let player3 = Entity::from_raw(3); let player4 = Entity::from_raw(4); TurnManager { current_phase: Phase::Beginning(BeginningStep::Untap), active_player_index: 0, turn_number: 1, priority_player_index: 0, all_players_passed: true, stack_is_empty: true, player_order: vec![player1, player2, player3, player4], extra_turns: VecDeque::new(), skipped_turns: HashSet::new(), time_limits: HashMap::new(), turn_started_at: Some(Instant::now()), phase_history: VecDeque::new(), additional_phases: VecDeque::new(), modified_phases: HashMap::new(), } } } }
Integration Tests
#![allow(unused)] fn main() { #[cfg(test)] mod integration_tests { use super::*; use crate::game_engine::stack::Stack; use crate::game_engine::state::GameState; // Test turns and stack interaction #[test] fn test_turns_with_stack() { let mut app = App::new(); // Set up game resources app.insert_resource(setup_test_turn_manager()); app.insert_resource(Stack::default()); app.insert_resource(GameState::default()); // Add relevant systems app.add_systems(Update, ( phase_transition_system, stack_resolution_system, priority_system, )); // Simulate a turn with stack interactions app.update(); // Add a spell to the stack let mut stack = app.world.resource_mut::<Stack>(); stack.push(create_test_spell()); // Verify stack is not empty let turn_manager = app.world.resource::<TurnManager>(); assert!(!turn_manager.stack_is_empty); // Simulate priority passing and stack resolution for _ in 0..5 { app.update(); } // Stack should be empty and phase should have advanced let turn_manager = app.world.resource::<TurnManager>(); assert!(turn_manager.stack_is_empty); assert_eq!(turn_manager.current_phase, Phase::Beginning(BeginningStep::Draw)); } // Test full turn cycle with multiple players #[test] fn test_full_turn_cycle() { let mut app = App::new(); // Set up game resources app.insert_resource(setup_test_turn_manager()); // Add systems app.add_systems(Update, phase_transition_system); // Track starting player let start_player = app.world.resource::<TurnManager>().active_player_index; // Simulate a full turn cycle (all players take one turn) for _ in 0..48 { // 4 players × 12 phases app.update(); } // Should be back to starting player let turn_manager = app.world.resource::<TurnManager>(); assert_eq!(turn_manager.active_player_index, start_player); assert_eq!(turn_manager.turn_number, 5); // Starting turn + 4 players } } }
End-to-End Testing
#![allow(unused)] fn main() { #[cfg(test)] mod e2e_tests { use super::*; use crate::test_utils::setup_full_game; // Test a full game scenario #[test] fn test_game_until_winner() { let mut app = setup_full_game(4); // 4 players // Run the game until only one player remains let max_turns = 100; // Prevent infinite loop for _ in 0..max_turns { app.update(); // Check if the game has ended let turn_manager = app.world.resource::<TurnManager>(); if turn_manager.player_order.len() == 1 { break; } // Simulate random player actions based on the current phase simulate_player_actions(&mut app); } // Verify we have a winner let turn_manager = app.world.resource::<TurnManager>(); assert_eq!(turn_manager.player_order.len(), 1, "Game should end with one player remaining"); } // Test complex turn interaction with time limits #[test] fn test_time_limits() { let mut app = setup_full_game(4); // Set time limits for all players { let mut turn_manager = app.world.resource_mut::<TurnManager>(); for &player in &turn_manager.player_order { turn_manager.time_limits.insert(player, Duration::from_secs(30)); } } // Fast-forward time for current player let current_player = { let turn_manager = app.world.resource::<TurnManager>(); turn_manager.player_order[turn_manager.active_player_index] }; // Mock time advancing past the limit for testing { let mut turn_manager = app.world.resource_mut::<TurnManager>(); turn_manager.turn_started_at = Some(Instant::now() - Duration::from_secs(31)); } // Add system to check time limits app.add_systems(Update, check_time_limits_system); // Run one update app.update(); // Verify player's turn was ended due to time limit let turn_manager = app.world.resource::<TurnManager>(); assert_ne!(turn_manager.active_player_index, turn_manager.player_order.iter().position(|&p| p == current_player).unwrap()); } } }
Performance Considerations
-
The
TurnManager
uses efficient data structures to minimize overhead:HashMap
for O(1) lookup of player time limitsVecDeque
for O(1) queue operations for extra turns and phase historyHashSet
for O(1) lookup of skipped turns
-
Phase transitions are optimized to only trigger necessary game events
-
Time complexity analysis:
- Advancing phase: O(1)
- Determining next player: O(1) in most cases, O(n) worst case (all players skip)
- Priority passing: O(1)
- Player elimination: O(n) where n is the number of players
Extensibility
The design allows for easy extension with new phases or rules:
- Additional phases can be added to the
Phase
enum - Custom phase progression can be implemented by overriding the
advance_phase
method - Game variants can be supported by modifying the
TurnManager
behavior
By following this implementation approach, the turn structure will robustly handle all standard Magic: The Gathering rules as well as the specific requirements of the Commander format.