Skip to main content

fforge_core/
rng.rs

1//! Seeded randomness for the deterministic core.
2//!
3//! Hand-rolled xoshiro256** seeded via splitmix64 rather than pulling the
4//! `rand` ecosystem: the RNG sits *inside* the deterministic fold's blast
5//! radius, so its byte-for-byte behavior must be owned by this crate, not by
6//! a dependency's semver policy. ~60 lines buys immunity to upstream drift.
7//!
8//! Streams are *derived*, not shared: `derive_stream(seed, tag)` gives every
9//! consumer (each fixture, worldgen, ...) its own independent generator, so
10//! simulation order can never perturb another consumer's randomness.
11
12#[inline]
13fn splitmix64(state: &mut u64) -> u64 {
14    *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
15    let mut z = *state;
16    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
17    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
18    z ^ (z >> 31)
19}
20
21#[derive(Debug, Clone)]
22pub struct Rng {
23    s: [u64; 4],
24}
25
26impl Rng {
27    pub fn seed_from(seed: u64) -> Self {
28        let mut sm = seed;
29        Rng {
30            s: [
31                splitmix64(&mut sm),
32                splitmix64(&mut sm),
33                splitmix64(&mut sm),
34                splitmix64(&mut sm),
35            ],
36        }
37    }
38
39    /// xoshiro256** core step.
40    pub fn next_u64(&mut self) -> u64 {
41        let result = self.s[1].wrapping_mul(5).rotate_left(7).wrapping_mul(9);
42        let t = self.s[1] << 17;
43        self.s[2] ^= self.s[0];
44        self.s[3] ^= self.s[1];
45        self.s[1] ^= self.s[2];
46        self.s[0] ^= self.s[3];
47        self.s[2] ^= t;
48        self.s[3] = self.s[3].rotate_left(45);
49        result
50    }
51
52    /// Uniform in [0, 1) with 53 bits of precision.
53    pub fn f64(&mut self) -> f64 {
54        (self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
55    }
56
57    /// Uniform in 0..n. Modulo bias is negligible for the small n used here.
58    pub fn below(&mut self, n: u32) -> u32 {
59        debug_assert!(n > 0);
60        (self.next_u64() % n as u64) as u32
61    }
62
63    /// Uniform integer in lo..=hi.
64    pub fn range_i32(&mut self, lo: i32, hi: i32) -> i32 {
65        debug_assert!(lo <= hi);
66        lo + self.below((hi - lo + 1) as u32) as i32
67    }
68
69    /// Gaussian via Box–Muller. Same-build reproducible (the determinism bar
70    /// the architecture commits to); not bit-portable across compilers.
71    pub fn normal(&mut self, mu: f64, sigma: f64) -> f64 {
72        let u1 = self.f64().max(f64::MIN_POSITIVE);
73        let u2 = self.f64();
74        let z = (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos();
75        mu + sigma * z
76    }
77
78    /// Poisson via Knuth's method — fine for the small λ of goal counts.
79    pub fn poisson(&mut self, lambda: f64) -> u32 {
80        let l = (-lambda).exp();
81        let mut k = 0u32;
82        let mut p = 1.0f64;
83        loop {
84            p *= self.f64();
85            if p <= l {
86                return k;
87            }
88            k += 1;
89            if k > 30 {
90                return 30; // pathological λ guard; unreachable at game values
91            }
92        }
93    }
94
95    /// Fisher–Yates.
96    pub fn shuffle<T>(&mut self, slice: &mut [T]) {
97        for i in (1..slice.len()).rev() {
98            let j = self.below((i + 1) as u32) as usize;
99            slice.swap(i, j);
100        }
101    }
102}
103
104/// An independent stream for `(world seed, tag)`. Tags are stable constants
105/// per consumer (e.g. a fixture id) so replays and forks agree.
106pub fn derive_stream(seed: u64, tag: u64) -> Rng {
107    let mut sm = seed ^ tag.wrapping_mul(0xA24B_AED4_963E_E407);
108    let mixed = splitmix64(&mut sm);
109    Rng::seed_from(mixed)
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn same_seed_same_sequence() {
118        let mut a = Rng::seed_from(42);
119        let mut b = Rng::seed_from(42);
120        for _ in 0..1000 {
121            assert_eq!(a.next_u64(), b.next_u64());
122        }
123    }
124
125    #[test]
126    fn derived_streams_are_independent_of_consumption_order() {
127        let mut s1 = derive_stream(7, 100);
128        let _burn: u64 = (0..500)
129            .map(|_| derive_stream(7, 99).next_u64())
130            .fold(0u64, |acc, x| acc.wrapping_add(x));
131        let mut s2 = derive_stream(7, 100);
132        for _ in 0..100 {
133            assert_eq!(s1.next_u64(), s2.next_u64());
134        }
135    }
136}