Multiplayer Turns in Commander
This document explains how multiplayer turns are implemented in the Rummage game engine for the Commander format.
Multiplayer Turn Structure
Commander is designed as a multiplayer format, typically played with 3-6 players. The multiplayer turn structure follows these rules:
- Players take turns in clockwise order, starting from a randomly determined first player
- Turn order remains fixed throughout the game unless modified by card effects
- All standard turn phases occur during each player's turn
- Players can act during other players' turns when they have priority
- Some effects specifically reference "each player" or "each opponent" which have multiplayer implications
Turn Order Implementation
#![allow(unused)] fn main() { /// Resource that tracks turn order #[derive(Resource, Debug, Clone)] pub struct TurnOrder { /// List of players in turn order pub players: Vec<Entity>, /// Current player index in the players list pub current_player_index: usize, /// Direction of turn progression (1 for clockwise, -1 for counter-clockwise) pub direction: i32, } impl TurnOrder { /// Creates a new turn order with the specified players pub fn new(players: Vec<Entity>) -> Self { Self { players, current_player_index: 0, direction: 1, } } /// Gets the current player pub fn current_player(&self) -> Entity { self.players[self.current_player_index] } /// Advances to the next player's turn pub fn advance(&mut self) { let len = self.players.len() as i32; self.current_player_index = ( (self.current_player_index as i32 + self.direction).rem_euclid(len) ) as usize; } /// Reverses the turn order direction pub fn reverse(&mut self) { self.direction = -self.direction; } /// Shuffles the player order pub fn shuffle(&mut self, rng: &mut impl Rng) { self.players.shuffle(rng); // Maintain current player at index 0 after shuffle let current = self.current_player(); let current_pos = self.players.iter().position(|&p| p == current).unwrap(); self.players.swap(0, current_pos); self.current_player_index = 0; } /// Skips the next player's turn pub fn skip_next(&mut self) { self.advance(); } /// Returns an iterator over all players starting from the current player pub fn iter_from_current(&self) -> impl Iterator<Item = Entity> + '_ { let len = self.players.len(); (0..len).map(move |offset| { let index = (self.current_player_index + offset) % len; self.players[index] }) } /// Returns an iterator over all opponents of the current player pub fn iter_opponents(&self) -> impl Iterator<Item = Entity> + '_ { let current = self.current_player(); self.players.iter().copied().filter(move |&p| p != current) } } }
Turn Advancement System
#![allow(unused)] fn main() { /// System that advances to the next player's turn pub fn advance_turn( mut turn_order: ResMut<TurnOrder>, mut turn_phase: ResMut<TurnPhase>, mut turn_events: EventWriter<TurnEvent>, mut game_state: ResMut<GameState>, time: Res<Time>, ) { if game_state.current_state != GameStateType::EndOfTurn { return; } // Get current player before advancing let previous_player = turn_order.current_player(); // Advance to next player turn_order.advance(); let next_player = turn_order.current_player(); // Reset turn phase to beginning of turn *turn_phase = TurnPhase::Beginning(BeginningPhase::Untap); // Update game state game_state.current_state = GameStateType::ActiveTurn; game_state.active_player = next_player; // Send events turn_events.send(TurnEvent::TurnEnded { player: previous_player }); turn_events.send(TurnEvent::TurnBegan { player: next_player }); } }
Multiplayer-Specific Mechanics
Turn-Based Effects
Some effects need to track when players take turns:
#![allow(unused)] fn main() { /// Component for effects that trigger at the beginning of a player's turn #[derive(Component, Debug, Clone)] pub struct BeginningOfTurnTrigger { /// Whether this triggers on the controller's turn only pub controller_only: bool, /// Whether this triggers on opponents' turns only pub opponents_only: bool, /// Function to execute when triggered pub effect: fn(Commands, Entity) -> (), } /// System that handles beginning of turn triggers pub fn handle_beginning_of_turn_triggers( mut commands: Commands, turn_order: Res<TurnOrder>, turn_phase: Res<TurnPhase>, triggers: Query<(Entity, &BeginningOfTurnTrigger, &Owner)>, ) { // Only execute during the beginning of turn phase if !matches!(turn_phase.as_ref(), TurnPhase::Beginning(BeginningPhase::Upkeep)) { return; } let current_player = turn_order.current_player(); for (entity, trigger, owner) in triggers.iter() { let is_owner_turn = owner.0 == current_player; // Check if trigger condition is met if (trigger.controller_only && is_owner_turn) || (trigger.opponents_only && !is_owner_turn) || (!trigger.controller_only && !trigger.opponents_only) { (trigger.effect)(commands, entity); } } } }
Simultaneous Effects
In multiplayer games, effects that affect all players need special handling:
#![allow(unused)] fn main() { /// Handles effects that apply to all players simultaneously pub fn handle_global_effects( mut commands: Commands, players: Query<Entity, With<Player>>, global_effects: Query<(Entity, &GlobalEffect)>, ) { for (effect_entity, global_effect) in global_effects.iter() { match global_effect.target_type { GlobalTargetType::AllPlayers => { for player in players.iter() { (global_effect.apply_effect)(commands, effect_entity, player); } }, GlobalTargetType::AllOpponents => { let turn_order = commands.world().resource::<TurnOrder>(); let current_player = turn_order.current_player(); for player in players.iter() { if player != current_player { (global_effect.apply_effect)(commands, effect_entity, player); } } }, // Other global target types... } } } }
Special Turn Interactions
Extra Turns
Commander allows for extra turn effects:
#![allow(unused)] fn main() { #[derive(Resource, Debug)] pub struct ExtraTurns { /// Queue of players taking extra turns pub queue: VecDeque<Entity>, } /// System that handles extra turn insertion pub fn handle_extra_turns( mut turn_order: ResMut<TurnOrder>, mut extra_turns: ResMut<ExtraTurns>, mut turn_events: EventWriter<TurnEvent>, ) { if let Some(player) = extra_turns.queue.pop_front() { // Current player takes an extra turn let current = turn_order.current_player(); if player == current { turn_events.send(TurnEvent::ExtraTurn { player }); } else { // Another player takes the next turn // Temporarily modify turn order let current_index = turn_order.current_player_index; let player_index = turn_order.players.iter().position(|&p| p == player).unwrap(); turn_order.current_player_index = player_index; turn_events.send(TurnEvent::ExtraTurn { player }); // Store original order to restore after the extra turn commands.insert_resource(PendingTurnRestoration { restore_index: current_index, }); } } } }
Turn Modifications
Some cards can modify turn order or skip turns:
#![allow(unused)] fn main() { /// Event for turn order modifications #[derive(Event, Debug, Clone)] pub enum TurnOrderEvent { /// Reverses turn order direction Reverse, /// Skips the next player's turn SkipNext, /// Takes an extra turn after the current one ExtraTurn { player: Entity }, /// Exchanges turns between players ExchangeTurn { player_a: Entity, player_b: Entity }, } /// System that handles turn order modifications pub fn handle_turn_modifications( mut turn_order: ResMut<TurnOrder>, mut extra_turns: ResMut<ExtraTurns>, mut turn_events: EventReader<TurnOrderEvent>, mut game_events: EventWriter<GameEvent>, ) { for event in turn_events.read() { match event { TurnOrderEvent::Reverse => { turn_order.reverse(); game_events.send(GameEvent::TurnOrderReversed); }, TurnOrderEvent::SkipNext => { turn_order.skip_next(); game_events.send(GameEvent::TurnSkipped); }, TurnOrderEvent::ExtraTurn { player } => { extra_turns.queue.push_back(*player); game_events.send(GameEvent::ExtraTurnAdded { player: *player }); }, TurnOrderEvent::ExchangeTurn { player_a, player_b } => { let pos_a = turn_order.players.iter().position(|&p| p == *player_a).unwrap(); let pos_b = turn_order.players.iter().position(|&p| p == *player_b).unwrap(); turn_order.players.swap(pos_a, pos_b); game_events.send(GameEvent::TurnOrderModified); }, } } } }
UI Representation
Multiplayer turns are visually represented in the UI:
- Turn Order Display: Shows all players in order with the current player highlighted
- Active Player Indicator: Clearly highlights whose turn it is
- Turn Direction Indicator: Shows the current direction of play (clockwise/counter-clockwise)
- Extra Turn Queue: Displays any pending extra turns
- Phase Tracker: Shows the current phase of the active player's turn
Testing
Example Test
#![allow(unused)] fn main() { #[test] fn test_multiplayer_turn_order() { // Create test app with required systems let mut app = App::new(); app.add_systems(Update, advance_turn) .add_event::<TurnEvent>() .init_resource::<GameState>(); // Create four players for a typical 4-player game let player1 = app.world.spawn_empty().id(); let player2 = app.world.spawn_empty().id(); let player3 = app.world.spawn_empty().id(); let player4 = app.world.spawn_empty().id(); let players = vec![player1, player2, player3, player4]; // Initialize turn order app.insert_resource(TurnOrder::new(players.clone())); app.insert_resource(TurnPhase::Ending(EndingPhase::End)); // Set game state to end of turn to trigger advancement let mut game_state = GameState::default(); game_state.current_state = GameStateType::EndOfTurn; app.insert_resource(game_state); // First turn should be player1 let turn_order = app.world.resource::<TurnOrder>(); assert_eq!(turn_order.current_player(), player1); // Advance turn app.update(); // Should now be player2's turn let turn_order = app.world.resource::<TurnOrder>(); assert_eq!(turn_order.current_player(), player2); // Test turn events let events = app.world.resource::<Events<TurnEvent>>(); let mut reader = events.get_reader(); let mut saw_end = false; let mut saw_begin = false; for event in reader.read(&events) { match event { TurnEvent::TurnEnded { player } => { assert_eq!(*player, player1); saw_end = true; }, TurnEvent::TurnBegan { player } => { assert_eq!(*player, player2); saw_begin = true; }, _ => {} } } assert!(saw_end, "Should have seen turn ended event"); assert!(saw_begin, "Should have seen turn began event"); } #[test] fn test_reversed_turn_order() { // Similar test setup let mut app = App::new(); // ... // Insert reversed turn order let mut turn_order = TurnOrder::new(players.clone()); turn_order.reverse(); // Direction becomes -1 app.insert_resource(turn_order); // Advance from player1 app.update(); // Should now be player4's turn (counter-clockwise) let turn_order = app.world.resource::<TurnOrder>(); assert_eq!(turn_order.current_player(), player4); } }
Summary
Multiplayer turns in Commander are implemented with a flexible system that:
- Maintains proper turn order in multiplayer games
- Supports direction changes (clockwise/counter-clockwise)
- Handles extra turns and turn skipping
- Processes effects that affect multiple players
- Provides clear UI indicators for turn progression
- Is thoroughly tested for all turn modification scenarios