Skip to main content

sgl_core/
random.rs

1//! Seedable RNG — a thin wrapper over `fastrand::Rng` (stack.md).
2//!
3//! The wrapper is the seam: sim code takes `&mut Rng` so tests can seed it for
4//! determinism (AR-7 INV-5), and the backend stays swappable in one place.
5//! Never use thread-local/global randomness in deterministic simulation code.
6
7/// A deterministic, seedable random number generator.
8#[derive(Debug, Clone)]
9pub struct Rng {
10    inner: fastrand::Rng,
11}
12
13impl Rng {
14    /// A generator with an explicit seed: the same seed always yields the same
15    /// sequence.
16    #[must_use]
17    pub fn from_seed(seed: u64) -> Self {
18        Self {
19            inner: fastrand::Rng::with_seed(seed),
20        }
21    }
22
23    /// A `f32` uniformly distributed in `[0, 1)`.
24    pub fn f32(&mut self) -> f32 {
25        self.inner.f32()
26    }
27
28    /// A `u32` uniformly distributed in `range`.
29    pub fn u32(&mut self, range: std::ops::Range<u32>) -> u32 {
30        self.inner.u32(range)
31    }
32
33    /// An `i32` uniformly distributed in `range`.
34    pub fn i32(&mut self, range: std::ops::Range<i32>) -> i32 {
35        self.inner.i32(range)
36    }
37
38    /// A `usize` uniformly distributed in `range` (e.g. an index).
39    pub fn usize(&mut self, range: std::ops::Range<usize>) -> usize {
40        self.inner.usize(range)
41    }
42
43    /// A fair coin flip.
44    pub fn bool(&mut self) -> bool {
45        self.inner.bool()
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use wasm_bindgen_test::wasm_bindgen_test;
53
54    /// Same seed, same sequence — the determinism contract sim tests rely on.
55    #[wasm_bindgen_test(unsupported = test)]
56    fn same_seed_same_sequence() {
57        let mut a = Rng::from_seed(42);
58        let mut b = Rng::from_seed(42);
59        for _ in 0..100 {
60            assert_eq!(a.u32(0..1000), b.u32(0..1000));
61            assert_eq!(a.f32().to_bits(), b.f32().to_bits());
62            assert_eq!(a.bool(), b.bool());
63        }
64    }
65
66    /// Different seeds diverge (not a proof, just a sanity check).
67    #[wasm_bindgen_test(unsupported = test)]
68    fn different_seeds_diverge() {
69        let mut a = Rng::from_seed(1);
70        let mut b = Rng::from_seed(2);
71        let sa: Vec<u32> = (0..16).map(|_| a.u32(0..u32::MAX)).collect();
72        let sb: Vec<u32> = (0..16).map(|_| b.u32(0..u32::MAX)).collect();
73        assert_ne!(sa, sb);
74    }
75
76    /// Ranges are respected.
77    #[wasm_bindgen_test(unsupported = test)]
78    fn values_stay_in_range() {
79        let mut rng = Rng::from_seed(7);
80        for _ in 0..1000 {
81            let v = rng.usize(3..9);
82            assert!((3..9).contains(&v));
83            let f = rng.f32();
84            assert!((0.0..1.0).contains(&f));
85        }
86    }
87}