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    ///
30    /// # Panics
31    /// If `range` is empty (`range.start >= range.end`), an authoring error.
32    pub fn u32(&mut self, range: std::ops::Range<u32>) -> u32 {
33        self.inner.u32(range)
34    }
35
36    /// An `i32` uniformly distributed in `range`.
37    ///
38    /// # Panics
39    /// If `range` is empty (`range.start >= range.end`), an authoring error.
40    pub fn i32(&mut self, range: std::ops::Range<i32>) -> i32 {
41        self.inner.i32(range)
42    }
43
44    /// A `usize` uniformly distributed in `range` (e.g. an index).
45    ///
46    /// # Panics
47    /// If `range` is empty (`range.start >= range.end`), an authoring error.
48    #[allow(clippy::cast_possible_truncation)] // The draw is below `range.end`, a `usize`.
49    pub fn usize(&mut self, range: std::ops::Range<usize>) -> usize {
50        // One 64-bit draw on every target: `fastrand::Rng::usize` reduces only
51        // 32 bits on wasm32, giving different values than native.
52        self.inner.u64(range.start as u64..range.end as u64) as usize
53    }
54
55    /// A fair coin flip.
56    pub fn bool(&mut self) -> bool {
57        self.inner.bool()
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use wasm_bindgen_test::wasm_bindgen_test;
65
66    /// Same seed, same sequence — the determinism contract sim tests rely on.
67    #[wasm_bindgen_test(unsupported = test)]
68    fn same_seed_same_sequence() {
69        let mut a = Rng::from_seed(42);
70        let mut b = Rng::from_seed(42);
71        for _ in 0..100 {
72            assert_eq!(a.u32(0..1000), b.u32(0..1000));
73            assert_eq!(a.f32().to_bits(), b.f32().to_bits());
74            assert_eq!(a.bool(), b.bool());
75        }
76    }
77
78    /// Different seeds diverge (not a proof, just a sanity check).
79    #[wasm_bindgen_test(unsupported = test)]
80    fn different_seeds_diverge() {
81        let mut a = Rng::from_seed(1);
82        let mut b = Rng::from_seed(2);
83        let sa: Vec<u32> = (0..16).map(|_| a.u32(0..u32::MAX)).collect();
84        let sb: Vec<u32> = (0..16).map(|_| b.u32(0..u32::MAX)).collect();
85        assert_ne!(sa, sb);
86    }
87
88    /// Ranges are respected.
89    #[wasm_bindgen_test(unsupported = test)]
90    fn values_stay_in_range() {
91        let mut rng = Rng::from_seed(7);
92        for _ in 0..1000 {
93            let v = rng.usize(3..9);
94            assert!((3..9).contains(&v));
95            let f = rng.f32();
96            assert!((0.0..1.0).contains(&f));
97        }
98    }
99}