RNG Synchronization and Rollback Integration
This document explains how the random number generator (RNG) synchronization system integrates with the state rollback mechanism to maintain consistent game state across network disruptions.
Table of Contents
- Overview
- Challenges
- Integration Architecture
- Implementation Details
- Example Scenarios
- Performance Considerations
Overview
Card games like Magic: The Gathering require randomization for shuffling libraries, coin flips, dice rolls, and "random target" selection. In a networked environment, these random operations must produce identical results across all clients despite network disruptions. Our solution combines the RNG synchronization system with the state rollback mechanism to ensure consistent gameplay.
Challenges
Several challenges must be addressed:
- Deterministic Recovery: After a network disruption, random operations must produce the same results as before
- Hidden Information: RNG state must be preserved without revealing hidden information (like library order)
- Partial Rollbacks: Some clients may need to roll back while others do not
- Performance: RNG state serialization and transmission must be efficient
- Cheating Prevention: The system must prevent manipulation of random outcomes
Integration Architecture
The integration of RNG synchronization with the rollback system follows this architecture:
┌────────────────────┐ ┌───────────────────┐ ┌────────────────────┐
│ │ │ │ │ │
│ RNG STATE SYSTEM │◄────┤ ROLLBACK SYSTEM │────►│ GAME STATE SYSTEM │
│ │ │ │ │ │
└───────┬────────────┘ └─────────┬─────────┘ └─────────┬──────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌────────────────────┐ ┌───────────────────┐ ┌────────────────────┐
│ │ │ │ │ │
│ HISTORY TRACKER │◄────┤ SEQUENCE TRACKER │────►│ ACTION PROCESSOR │
│ │ │ │ │ │
└────────────────────┘ └───────────────────┘ └────────────────────┘
Key Components
- RNG State System: Manages the global and player-specific RNG states
- Rollback System: Handles state rollbacks due to network disruptions
- Game State System: Maintains the authoritative game state
- History Tracker: Records RNG states at key sequence points
- Sequence Tracker: Assigns sequence IDs to game actions
- Action Processor: Applies game actions deterministically
Implementation Details
RNG State Snapshots
For every game state snapshot, we also capture the corresponding RNG state:
#![allow(unused)] fn main() { /// System to capture RNG state with game state snapshots pub fn capture_rng_with_state_snapshot( mut state_history: ResMut<StateHistory>, global_rng: Res<GlobalEntropy<WyRand>>, players: Query<(Entity, &PlayerRng)>, ) { if let Some(current_snapshot) = state_history.snapshots.last_mut() { // Save global RNG state if let Some(global_state) = global_rng.try_serialize_state() { current_snapshot.rng_state = global_state; } // Save player-specific RNG states let mut player_rng_states = HashMap::new(); for (entity, player_rng) in players.iter() { if let Some(state) = player_rng.rng.try_serialize_state() { player_rng_states.insert(entity, state); } } current_snapshot.player_rng_states = player_rng_states; } } }
Randomized Action Handling
Randomized actions require special attention during rollbacks:
#![allow(unused)] fn main() { /// Apply a randomized action with RNG state consistency pub fn apply_randomized_action( action: &GameAction, rng_state: Option<&[u8]>, global_rng: &mut GlobalEntropy<WyRand>, player_rngs: &mut Query<&mut PlayerRng>, ) -> ActionResult { // If we have a saved RNG state for this action, restore it first if let Some(state) = rng_state { global_rng.deserialize_state(state).expect("Failed to restore RNG state"); } match action { GameAction::ShuffleLibrary { player, library } => { // Get the player's RNG component if let Ok(mut player_rng) = player_rngs.get_mut(*player) { // Perform the shuffle using the player's RNG // This will produce the same result if the RNG state is the same // ... ActionResult::Success } else { ActionResult::PlayerNotFound } }, // Handle other randomized actions... _ => ActionResult::NotRandomized, } } }
Rollback With RNG Recovery
During a rollback, both game state and RNG state are restored:
#![allow(unused)] fn main() { /// System to perform a rollback with RNG state recovery pub fn perform_rollback_with_rng( mut game_state: ResMut<GameState>, mut global_rng: ResMut<GlobalEntropy<WyRand>>, mut player_rngs: Query<(Entity, &mut PlayerRng)>, rollback_event: Res<RollbackEvent>, ) { // Restore game state deserialize_game_state(&mut game_state, &rollback_event.snapshot.game_state); // Restore global RNG state global_rng.deserialize_state(&rollback_event.snapshot.rng_state) .expect("Failed to restore global RNG state"); // Restore player-specific RNG states for (entity, mut player_rng) in player_rngs.iter_mut() { if let Some(state) = rollback_event.snapshot.player_rng_states.get(&entity) { player_rng.rng.deserialize_state(state) .expect("Failed to restore player RNG state"); } } // Log the rollback info!("Performed rollback to sequence {} with RNG state recovery", rollback_event.snapshot.sequence_id); } }
Example Scenarios
Scenario 1: Library Shuffle During Network Disruption
- Player A initiates a library shuffle
- Network disruption occurs during processing
- System detects the disruption and initiates rollback
- RNG state from before the shuffle is restored
- Shuffle action is replayed with identical RNG state
- All clients observe the same shuffle outcome despite the disruption
#![allow(unused)] fn main() { // Handling a shuffle during network disruption pub fn handle_shuffle_during_disruption( mut commands: Commands, mut game_state: ResMut<GameState>, mut global_rng: ResMut<GlobalEntropy<WyRand>>, mut player_rngs: Query<&mut PlayerRng>, mut rollback_events: EventReader<RollbackEvent>, action_history: Res<ActionHistory>, ) { for event in rollback_events.read() { // Restore game and RNG state perform_rollback_with_rng(&mut game_state, &mut global_rng, &mut player_rngs, event); // Get actions to replay let actions = action_history.get_actions_after(event.snapshot.sequence_id); // Replay actions deterministically for action in actions { // If this is a shuffle action, it will produce the same result // because the RNG state has been restored apply_action(&mut commands, &mut game_state, &mut global_rng, &mut player_rngs, &action); } } } }
Scenario 2: Client Reconnection After Multiple Random Events
- Client disconnects during a game with several random actions
- Random actions continue to occur (coin flips, shuffles, etc.)
- Client reconnects after several actions
- System identifies the last confirmed sequence point for the client
- Rollback state is sent with corresponding RNG state
- Client replays all actions, producing identical random results
- Client's game state is now synchronized with the server
Performance Considerations
Integrating RNG with rollbacks introduces performance considerations:
-
RNG State Size: RNG state serialization should be compact
- WyRand RNG state is typically only 8 bytes
- Avoid large RNG algorithms for frequent serialization
-
Selective Snapshots: Not every action needs RNG state preservation
- Only save RNG state before randomized actions
- Use sequence IDs to correlate RNG states with actions
-
Batched Updates: Group randomized actions to minimize state snapshots
- Example: When shuffling multiple permanents, capture RNG once
-
Compressed History: Use a sliding window approach for history
- Discard old RNG states when they're no longer needed
- Keep only enough history for realistic rollback scenarios
-
Optimized Serialization: Use efficient binary serialization
- Consider custom serialization for RNG state if needed
- Avoid JSON or other verbose formats for RNG state
Implementation Notes
- The RNG synchronization system must be initialized before the rollback system
- All random operations must use the synchronized RNG, never
thread_rng()
or other sources - Player-specific operations should use player-specific RNG components
- RNG state should be included in regular network synchronization
- Client reconnection should always include RNG state restoration