Testing RNG Synchronization in Networked Gameplay
This document outlines testing strategies for ensuring deterministic and synchronized random number generation across network boundaries in our MTG Commander game engine.
Table of Contents
- Testing Goals
- Test Categories
- Test Fixtures
- Automated Tests
- Manual Testing
- Performance Considerations
Testing Goals
Testing RNG synchronization focuses on these key goals:
- Determinism: Verify that identical RNG seeds produce identical random sequences on all clients
- State Preservation: Ensure RNG state is properly serialized, transmitted, and restored
- Resilience: Test recovery from network disruptions or client reconnections
- Sequence Integrity: Confirm that game actions using randomness always produce the same results
Test Categories
Basic Determinism Tests
These tests verify that the underlying RNG components work deterministically:
#![allow(unused)] fn main() { #[test] fn test_rng_determinism() { // Create two RNGs with the same seed let seed = 12345u64; let mut rng1 = WyRand::seed_from_u64(seed); let mut rng2 = WyRand::seed_from_u64(seed); // Generate sequences from both RNGs let sequence1: Vec<u32> = (0..100).map(|_| rng1.next_u32()).collect(); let sequence2: Vec<u32> = (0..100).map(|_| rng2.next_u32()).collect(); // Verify sequences are identical assert_eq!(sequence1, sequence2); } }
Serialization Tests
These tests verify that RNG state can be properly serialized and deserialized:
#![allow(unused)] fn main() { #[test] fn test_rng_serialization() { // Create an RNG and use it to generate some values let mut original_rng = GlobalEntropy::<WyRand>::from_entropy(); let original_values: Vec<u32> = (0..10).map(|_| original_rng.next_u32()).collect(); // Serialize the RNG state let serialized_state = original_rng.try_serialize_state().expect("Failed to serialize RNG state"); // Create a new RNG and deserialize the state into it let mut new_rng = GlobalEntropy::<WyRand>::from_entropy(); new_rng.deserialize_state(&serialized_state).expect("Failed to deserialize RNG state"); // Generate the same number of values from the new RNG let new_values: Vec<u32> = (0..10).map(|_| new_rng.next_u32()).collect(); // The values should be the same, since the states were synchronized assert_eq!(original_values, new_values); } }
Network Transmission Tests
These tests verify that RNG state can be properly transmitted across the network:
#![allow(unused)] fn main() { #[test] fn test_rng_network_transmission() { // Setup server and client apps let mut server_app = App::new(); let mut client_app = App::new(); // Configure apps for network testing setup_network_test(&mut server_app, true, false); setup_network_test(&mut client_app, false, true); // Run updates to establish connection for _ in 0..5 { server_app.update(); client_app.update(); } // Generate some random values on the server let server_values = { let mut rng = server_app.world.resource_mut::<GlobalEntropy<WyRand>>(); (0..10).map(|_| rng.next_u32()).collect::<Vec<_>>() }; // Serialize and send RNG state from server to client let serialized_state = { let rng = server_app.world.resource::<GlobalEntropy<WyRand>>(); rng.try_serialize_state().expect("Failed to serialize RNG state") }; // Simulate network transmission let rng_message = RngStateMessage { state: serialized_state, timestamp: server_app.world.resource::<Time>().elapsed_seconds(), }; // Apply the RNG state to the client { let mut client_rng = client_app.world.resource_mut::<GlobalEntropy<WyRand>>(); client_rng.deserialize_state(&rng_message.state).expect("Failed to deserialize RNG state"); } // Generate the same number of values on the client let client_values = { let mut rng = client_app.world.resource_mut::<GlobalEntropy<WyRand>>(); (0..10).map(|_| rng.next_u32()).collect::<Vec<_>>() }; // Verify the values match assert_eq!(server_values, client_values); } }
Player-Specific RNG Tests
These tests verify that player-specific RNGs maintain determinism:
#![allow(unused)] fn main() { #[test] fn test_player_rng_forking() { let mut app = App::new(); app.add_plugins(MinimalPlugins) .add_plugins(EntropyPlugin::<WyRand>::default()) .init_resource::<PlayerRegistry>(); // Add test systems app.add_systems(Startup, setup_test_players) .add_systems(Update, fork_player_rngs); // Run the app to set up players and fork RNGs app.update(); // Get player entities let player_registry = app.world.resource::<PlayerRegistry>(); let player1 = player_registry.get_player(1); let player2 = player_registry.get_player(2); // Generate random sequences for each player let player1_values = generate_random_sequence(&mut app.world, player1); let player2_values = generate_random_sequence(&mut app.world, player2); // Values should be different because they were forked from the global RNG assert_ne!(player1_values, player2_values); // Save player RNG states save_player_rng_states(&mut app.world); // Create a new app and restore the states let mut new_app = App::new(); // Configure new app // ... // Restore the player RNG states restore_player_rng_states(&mut new_app.world); // Generate new sequences let new_player1_values = generate_random_sequence(&mut new_app.world, player1); let new_player2_values = generate_random_sequence(&mut new_app.world, player2); // New sequences should match the original ones assert_eq!(player1_values, new_player1_values); assert_eq!(player2_values, new_player2_values); } }
Game Action Tests
These tests verify that game actions involving randomness produce consistent results:
#![allow(unused)] fn main() { #[test] fn test_shuffle_library_determinism() { // Setup test environment let mut app = setup_multiplayer_test_app(); // Create a deck with known order let cards = (1..53).collect::<Vec<_>>(); // Setup player and library let player_entity = spawn_test_player(&mut app.world); let library_entity = spawn_test_library(&mut app.world, player_entity, cards.clone()); // Seed the player's RNG seed_player_rng(&mut app.world, player_entity, 12345u64); // First shuffle app.world.send_event(ShuffleLibraryEvent { library_entity }); app.update(); // Get shuffled order let first_shuffle = get_library_order(&app.world, library_entity); // Reset library and RNG to original state reset_library(&mut app.world, library_entity, cards.clone()); seed_player_rng(&mut app.world, player_entity, 12345u64); // Second shuffle app.world.send_event(ShuffleLibraryEvent { library_entity }); app.update(); // Get shuffled order let second_shuffle = get_library_order(&app.world, library_entity); // Both shuffles should result in the same order assert_eq!(first_shuffle, second_shuffle); } }
Test Fixtures
Common test fixtures for RNG testing:
#![allow(unused)] fn main() { /// Sets up a test environment with multiple clients pub fn setup_multiplayer_rng_test() -> TestHarness { let mut harness = TestHarness::new(); // Setup server harness.create_server_app(); // Setup multiple clients for i in 0..4 { harness.create_client_app(i); } // Initialize RNG with a fixed seed harness.seed_global_rng(12345u64); // Connect clients to server harness.connect_all_clients(); harness } /// Executes a randomized game action on all clients and verifies consistency pub fn verify_random_action_consistency(harness: &mut TestHarness, action: RandomizedAction) { // Execute action on server harness.execute_on_server(action.clone()); // Synchronize RNG state to clients harness.sync_rng_state(); // Execute same action on all clients let results = harness.execute_on_all_clients(action); // All results should be identical let first_result = &results[0]; for result in &results[1..] { assert_eq!(first_result, result); } } }
Automated Tests
Integration with CI Pipeline
Include these RNG synchronization tests in the CI pipeline:
# .github/workflows/rng-tests.yml
name: RNG Synchronization Tests
on:
push:
branches: [ main ]
paths:
- 'src/networking/rng/**'
- 'src/game_engine/actions/**'
pull_request:
branches: [ main ]
paths:
- 'src/networking/rng/**'
- 'src/game_engine/actions/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- name: Run RNG tests
run: cargo test --package rummage --lib networking::rng
Test Coverage Requirements
Aim for these coverage targets for RNG synchronization code:
- Line Coverage: At least 90%
- Branch Coverage: At least 85%
- Function Coverage: 100%
Manual Testing
Some aspects of RNG synchronization should be manually tested:
- Disconnection Recovery: Test that clients reconnecting receive correct RNG state
- High Latency Scenarios: Test with artificially high network latency
- Packet Loss: Test with simulated packet loss to verify recovery
- Cross-Platform Consistency: Verify RNG consistency between different operating systems
Performance Considerations
When testing RNG synchronization, monitor these performance metrics:
- Serialization Size: RNG state should be compact
- Synchronization Frequency: Balance consistency vs. network overhead
- CPU Overhead: Monitor CPU usage during RNG-heavy operations
- Memory Usage: Track memory usage when many player-specific RNGs are active
Documentation and Logging
Implement thorough logging for RNG synchronization to aid in debugging:
#![allow(unused)] fn main() { pub fn log_rng_sync( rng_state: Res<RngStateTracker>, client_id: Option<Res<ClientId>>, ) { if let Some(client_id) = client_id { info!( "RNG sync: Client {} received state of size {} bytes (timestamp: {})", client_id.0, rng_state.global_state.len(), rng_state.last_sync ); } else { info!( "RNG sync: Server updated state of size {} bytes (timestamp: {})", rng_state.global_state.len(), rng_state.last_sync ); } } }