Priority System
Overview
The priority system is a fundamental mechanism in Magic: The Gathering that determines when players can cast spells and activate abilities. This document explains how priority is implemented in Rummage and how it controls the flow of gameplay.
Priority Basics
In Magic: The Gathering, priority is the right to take an action such as casting a spell, activating an ability, or making a special game action. The priority system follows these key principles:
- The active player receives priority at the beginning of each step and phase, except for the untap step and most cleanup steps
- A player with priority may take an action or pass priority
- When all players pass priority in succession without taking an action, the top object on the stack resolves or, if the stack is empty, the current step or phase ends
- Whenever an object is put on the stack, all players pass priority, and the stack resolves, priority is given to the active player
Implementation in Rummage
In Rummage, the priority system is implemented using a combination of resources and systems:
#![allow(unused)] fn main() { /// Resource tracking priority state #[derive(Resource)] pub struct PrioritySystem { /// Current player with priority pub current_priority: Option<Entity>, /// Players who have passed priority without taking an action pub passed_players: HashSet<Entity>, /// Whether the game is currently waiting for priority actions pub waiting_for_priority: bool, /// The last player to take an action pub last_action_player: Option<Entity>, } /// Component marking the active player #[derive(Component)] pub struct ActivePlayer; }
Priority Assignment
Priority is assigned according to turn order, starting with the active player:
#![allow(unused)] fn main() { pub fn assign_priority_system( mut priority_system: ResMut<PrioritySystem>, turn_system: Res<TurnSystem>, game_state: Res<GameState>, active_player_query: Query<Entity, With<ActivePlayer>>, ) { // Only assign priority if not already waiting for someone if priority_system.waiting_for_priority { return; } // When a phase or step begins, active player gets priority if game_state.pending_phase_change { let active_player = active_player_query.get_single().unwrap(); priority_system.current_priority = Some(active_player); priority_system.passed_players.clear(); priority_system.waiting_for_priority = true; } // After stack resolution, active player gets priority if game_state.stack_just_resolved { let active_player = active_player_query.get_single().unwrap(); priority_system.current_priority = Some(active_player); priority_system.passed_players.clear(); priority_system.waiting_for_priority = true; } } }
Passing Priority
When a player passes priority, it's passed to the next player in turn order:
#![allow(unused)] fn main() { pub fn handle_pass_priority( mut priority_system: ResMut<PrioritySystem>, mut pass_priority_events: EventReader<PassPriorityEvent>, players: Query<(Entity, &Player)>, turn_system: Res<TurnSystem>, mut game_events: EventWriter<GameEvent>, ) { for event in pass_priority_events.iter() { // Only process if this player currently has priority if let Some(current_priority) = priority_system.current_priority { if current_priority == event.player { // Add player to passed list priority_system.passed_players.insert(current_priority); // Find next player in turn order let next_player = get_next_player_in_turn_order( current_priority, &players, &turn_system ); // If next player has already passed, check for all players passing if priority_system.passed_players.contains(&next_player) || priority_system.passed_players.len() == players.iter().count() { // All players have passed priority game_events.send(GameEvent::AllPlayersPassed); priority_system.waiting_for_priority = false; priority_system.current_priority = None; } else { // Pass to next player priority_system.current_priority = Some(next_player); game_events.send(GameEvent::PriorityPassed { from: current_priority, to: next_player, }); } } } } } }
Priority After Actions
After a player takes an action, they receive priority again:
#![allow(unused)] fn main() { pub fn handle_player_action( mut priority_system: ResMut<PrioritySystem>, mut player_action_events: EventReader<PlayerActionEvent>, mut game_events: EventWriter<GameEvent>, ) { for event in player_action_events.iter() { // Record player taking action priority_system.last_action_player = Some(event.player); // Clear passed players list since an action was taken priority_system.passed_players.clear(); // Return priority to the player who took action priority_system.current_priority = Some(event.player); priority_system.waiting_for_priority = true; game_events.send(GameEvent::PriorityAssigned { player: event.player, reason: PriorityReason::AfterAction, }); } } }
Stack Resolution
When all players pass priority, either the top of the stack resolves or the current phase/step ends:
#![allow(unused)] fn main() { pub fn handle_all_players_passed( mut commands: Commands, mut stack: ResMut<Stack>, mut game_state: ResMut<GameState>, mut priority_system: ResMut<PrioritySystem>, active_player_query: Query<Entity, With<ActivePlayer>>, mut game_events: EventWriter<GameEvent>, ) { // Process what happens when all players pass priority if !stack.items.is_empty() { // Resolve top item of stack let top_item = stack.items.pop().unwrap(); game_events.send(GameEvent::StackItemResolved { item_id: top_item.id, }); // Mark for priority reassignment after resolution game_state.stack_just_resolved = true; } else { // Empty stack, end current phase/step game_state.proceed_to_next_phase_or_step(); game_events.send(GameEvent::PhaseStepEnded { phase: game_state.current_phase.clone(), step: game_state.current_step.clone(), }); } } }
Special Priority Rules
Special Timing Rules
Some game actions use special timing rules that modify how priority works:
#![allow(unused)] fn main() { pub fn handle_special_timing( mut commands: Commands, mut priority_system: ResMut<PrioritySystem>, mut special_action_events: EventReader<SpecialActionEvent>, mut game_events: EventWriter<GameEvent>, ) { for event in special_action_events.iter() { match event.action_type { SpecialActionType::PlayLand => { // Playing a land doesn't use the stack and doesn't pass priority // but does count as taking an action for priority purposes priority_system.last_action_player = Some(event.player); priority_system.passed_players.clear(); priority_system.current_priority = Some(event.player); game_events.send(GameEvent::PriorityRetained { player: event.player, reason: "Played a land", }); }, SpecialActionType::ManaAbility => { // Mana abilities don't use the stack and don't change priority game_events.send(GameEvent::ManaAbilityResolved { player: event.player, source: event.source, }); }, // Other special timing rules... } } } }
State-Based Actions
State-based actions are checked before a player would receive priority:
#![allow(unused)] fn main() { pub fn check_state_based_actions( mut commands: Commands, mut priority_system: ResMut<PrioritySystem>, mut game_state: ResMut<GameState>, // Other query parameters... ) { // Only check when a player would receive priority if !priority_system.waiting_for_priority && game_state.stack_just_resolved { // Process all state-based actions let sba_performed = perform_state_based_actions( &mut commands, // Other parameters... ); // If any state-based actions were performed, check again // before assigning priority if sba_performed { game_state.sba_check_needed = true; } else { game_state.sba_check_needed = false; game_state.stack_just_resolved = false; } } } }
Turn-Based Actions
Turn-based actions happen automatically at specific points regardless of priority:
#![allow(unused)] fn main() { pub fn handle_turn_based_actions( mut commands: Commands, game_state: Res<GameState>, mut turn_action_events: EventWriter<TurnBasedActionEvent>, ) { match game_state.current_phase { Phase::Beginning if game_state.current_step == Step::Draw => { // Draw step: Active player draws a card turn_action_events.send(TurnBasedActionEvent::ActivePlayerDraws); }, Phase::Combat if game_state.current_step == Step::EndOfCombat => { // End of combat: Remove all creatures from combat turn_action_events.send(TurnBasedActionEvent::RemoveFromCombat); }, // Other turn-based actions... _ => {} } } }
UI Integration
The priority system is visually represented to players:
#![allow(unused)] fn main() { pub fn update_priority_ui( priority_system: Res<PrioritySystem>, stack: Res<Stack>, players: Query<(Entity, &Player, &PlayerName)>, mut ui_state: ResMut<UiState>, ) { if let Some(current_priority) = priority_system.current_priority { // Highlight player with priority if let Ok((_, _, name)) = players.get(current_priority) { ui_state.priority_indicator = Some(PriorityIndicator { player: current_priority, name: name.0.clone(), has_passed: false, }); } } else { ui_state.priority_indicator = None; } // Update UI for players who have passed for entity in &priority_system.passed_players { if let Ok((_, _, name)) = players.get(*entity) { ui_state.passed_priority_indicators.push(PriorityIndicator { player: *entity, name: name.0.clone(), has_passed: true, }); } } // Update stack indicator ui_state.stack_size = stack.items.len(); } }
Multiplayer Priority
In multiplayer games, priority follows turn order (APNAP - Active Player, Non-Active Player):
#![allow(unused)] fn main() { pub fn get_next_player_in_turn_order( current_player: Entity, players: &Query<(Entity, &Player)>, turn_system: &Res<TurnSystem>, ) -> Entity { let turn_order = &turn_system.player_order; let current_index = turn_order.iter() .position(|&p| p == current_player) .unwrap_or(0); // Get next player, wrapping around to the beginning let next_index = (current_index + 1) % turn_order.len(); turn_order[next_index] } }
Testing Priority Flow
The priority system is tested through a variety of scenarios:
#![allow(unused)] fn main() { #[test] fn test_priority_basic_flow() { // Setup test environment let mut app = App::new(); app.add_plugins(MinimalPlugins) .add_system(assign_priority_system) .add_system(handle_pass_priority) .add_system(handle_player_action) .add_system(handle_all_players_passed) // Other test setup... // Create test players let player1 = spawn_test_player(&mut app.world, "Player 1"); let player2 = spawn_test_player(&mut app.world, "Player 2"); // Set active player app.world.entity_mut(player1).insert(ActivePlayer); // Begin test phase let mut game_state = app.world.resource_mut::<GameState>(); game_state.pending_phase_change = true; // Run systems app.update(); // Verify active player got priority let priority_system = app.world.resource::<PrioritySystem>(); assert_eq!(priority_system.current_priority, Some(player1)); // Simulate player1 passing priority app.world.resource_mut::<Events<PassPriorityEvent>>() .send(PassPriorityEvent { player: player1 }); // Run systems app.update(); // Verify priority passed to player2 let priority_system = app.world.resource::<PrioritySystem>(); assert_eq!(priority_system.current_priority, Some(player2)); // More test assertions... } }
Edge Cases
The priority system handles several edge cases:
Split Second
The "split second" keyword prevents players from casting spells or activating non-mana abilities while a spell with split second is on the stack:
#![allow(unused)] fn main() { pub fn handle_split_second( stack: Res<Stack>, mut priority_system: ResMut<PrioritySystem>, mut player_action_events: EventReader<PlayerActionEvent>, mut game_events: EventWriter<GameEvent>, ) { // Check if a split second spell is on the stack let split_second_active = stack.items.iter().any(|item| { if let StackItemType::Spell { abilities, .. } = &item.item_type { abilities.contains(&Ability::SplitSecond) } else { false } }); if split_second_active { // Only allow certain actions while split second is active for event in player_action_events.iter() { match event.action_type { ActionType::ActivateManaAbility(_) => { // Mana abilities are allowed // Process normally }, ActionType::TriggerSpecialAction(SpecialActionType::MorphFaceDown) => { // Special actions like turning a face-down creature face up are allowed // Process normally }, _ => { // All other actions are denied game_events.send(GameEvent::ActionDenied { player: event.player, action_type: event.action_type.clone(), reason: "Split second prevents this action", }); continue; } } } } } }
No Action Possible
If a player cannot take any action, they must pass priority:
#![allow(unused)] fn main() { pub fn handle_no_action_possible( mut priority_system: ResMut<PrioritySystem>, players: Query<(Entity, &Player, &Hand)>, mut pass_priority_events: EventWriter<PassPriorityEvent>, mut game_events: EventWriter<GameEvent>, ) { if let Some(current_priority) = priority_system.current_priority { if let Ok((entity, player, hand)) = players.get(current_priority) { // Check if player can take any action if hand.cards.is_empty() && !player_has_playable_permanents(entity) { // Player has no possible actions, auto-pass priority pass_priority_events.send(PassPriorityEvent { player: entity }); game_events.send(GameEvent::AutoPassPriority { player: entity, reason: "No possible actions", }); } } } } }
Conclusion
The priority system is a fundamental aspect of Magic: The Gathering that controls the flow of game actions. Rummage's implementation carefully follows the official rules, ensuring that players can take actions in the correct order and that game state advances properly.
Next: Stack