// Simulation RNG system // Injectable ChaCha RNG resource for deterministic replay (D-030) // Ensures same seed produces same outcomes use bevy_ecs::prelude::*; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; /// Simulation RNG resource — shared world-level randomness. /// /// Used for non-entity randomness: storyteller dice, world events, ticker rotation. /// For entity-level randomness, use [`EntityRng`] instead — it enables parallelism /// because each entity has its own independent, deterministic RNG stream. #[derive(Resource)] pub struct SimRng { pub rng: ChaCha20Rng, seed: u64, } impl SimRng { /// Create a new SimRng with the given seed pub fn new(seed: u64) -> Self { Self { rng: ChaCha20Rng::seed_from_u64(seed), seed, } } /// Get the seed used to initialize this RNG pub fn seed(&self) -> u64 { self.seed } } /// Per-entity RNG component — deterministic, parallel-safe (#843). /// /// Each entity gets its own ChaCha20 stream seeded from `world_seed + StableId`. /// Systems that need randomness for a specific entity query `&mut EntityRng` /// instead of `ResMut`. This unlocks parallelism: Bevy can run systems /// on disjoint entity sets concurrently. /// /// Determinism: same `world_seed` + same `StableId` = same RNG sequence, /// regardless of thread execution order. #[derive(Component)] pub struct EntityRng { pub rng: ChaCha20Rng, } impl EntityRng { /// Create a new EntityRng from a world seed and entity stable ID. /// /// Uses splitmix64 mixing to combine the two seeds with good avalanche /// properties — avoids correlated sequences for entities with similar IDs. pub fn from_seed_and_id(world_seed: u64, stable_id: u64) -> Self { // Mix each input independently then XOR — avoids the collision class // where (seed=0, id=N) == (seed=1, id=N-1) that wrapping_add creates. let mixed = splitmix64(world_seed) ^ splitmix64(stable_id); Self { rng: ChaCha20Rng::seed_from_u64(mixed), } } } /// Splitmix64 mixing function — good avalanche properties for seed derivation. /// Ensures that similar inputs (e.g., consecutive StableIds) produce /// uncorrelated output seeds. fn splitmix64(mut x: u64) -> u64 { x = x.wrapping_add(0x9e3779b97f4a7c15); x = (x ^ (x >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); x = (x ^ (x >> 27)).wrapping_mul(0x94d049bb133111eb); x ^ (x >> 31) } #[cfg(test)] mod tests { use super::*; use rand::Rng; #[test] fn same_seed_same_sequence() { let mut rng1 = SimRng::new(42); let mut rng2 = SimRng::new(42); let vals1: Vec = (0..100).map(|_| rng1.rng.random()).collect(); let vals2: Vec = (0..100).map(|_| rng2.rng.random()).collect(); assert_eq!(vals1, vals2); } #[test] fn different_seed_different_sequence() { let mut rng1 = SimRng::new(42); let mut rng2 = SimRng::new(43); let val1: u32 = rng1.rng.random(); let val2: u32 = rng2.rng.random(); assert_ne!(val1, val2); } #[test] fn entity_rng_same_seed_same_id_same_sequence() { let mut a = EntityRng::from_seed_and_id(42, 7); let mut b = EntityRng::from_seed_and_id(42, 7); let va: Vec = (0..100).map(|_| a.rng.random()).collect(); let vb: Vec = (0..100).map(|_| b.rng.random()).collect(); assert_eq!(va, vb); } #[test] fn entity_rng_same_seed_different_id_different_sequence() { let mut a = EntityRng::from_seed_and_id(42, 0); let mut b = EntityRng::from_seed_and_id(42, 1); let va: u32 = a.rng.random(); let vb: u32 = b.rng.random(); assert_ne!(va, vb); } #[test] fn entity_rng_adjacent_seeds_no_collision() { // Verify (seed=0, id=N) != (seed=1, id=N-1) — the collision class // that wrapping_add would create. let mut a = EntityRng::from_seed_and_id(0, 5); let mut b = EntityRng::from_seed_and_id(1, 4); let va: u32 = a.rng.random(); let vb: u32 = b.rng.random(); assert_ne!(va, vb); } }