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}