Skip to main content

game_toolkit_core/
rng.rs

1use std::ops::Range;
2
3/// Deterministic xorshift64* pseudo-random number generator.
4///
5/// Same seed always yields the same sequence across runs and platforms.
6///
7/// Not cryptographically secure: the full state is recoverable from the output, so never use it
8/// for security-sensitive values (tokens, secrets, nonces). It is meant for gameplay - procedural
9/// generation, spawn jitter, AI variation - where reproducibility matters and predictability does not.
10#[derive(Clone, Debug)]
11pub struct Rng {
12    state: u64,
13}
14
15impl Rng {
16    /// Fallback state used when a zero seed is supplied.
17    pub const DEFAULT_SEED: u64 = 0x4d59_5df4_d0f3_3173;
18    /// xorshift64* output scrambler multiplier.
19    const XORSHIFT64_STAR_MULTIPLIER: u64 = 0x2545_f491_4f6c_dd1d;
20    /// Number of random mantissa bits used for a uniform `f32` in `[0, 1)`.
21    const F32_MANTISSA_BITS: u32 = 24;
22
23    /// Creates a new deterministic generator from `seed`.
24    pub const fn new(seed: u64) -> Self {
25        Self {
26            state: if seed == 0 { Self::DEFAULT_SEED } else { seed },
27        }
28    }
29
30    /// Reseeds the generator. A zero seed falls back to [`Self::DEFAULT_SEED`].
31    pub const fn reseed(&mut self, seed: u64) {
32        self.state = if seed == 0 { Self::DEFAULT_SEED } else { seed };
33    }
34
35    /// Returns the next `u64`.
36    #[must_use]
37    pub fn next_u64(&mut self) -> u64 {
38        let mut x = self.state;
39        x ^= x >> 12;
40        x ^= x << 25;
41        x ^= x >> 27;
42        self.state = x;
43        x.wrapping_mul(Self::XORSHIFT64_STAR_MULTIPLIER)
44    }
45
46    /// Returns the next `u32`.
47    #[must_use]
48    pub fn next_u32(&mut self) -> u32 {
49        (self.next_u64() >> 32) as u32
50    }
51
52    /// Returns a uniform `u32` sampled from the half-open `range`.
53    ///
54    /// # Panics
55    ///
56    /// Panics if `range` is empty (`range.start >= range.end`).
57    #[must_use]
58    pub fn range_u32(&mut self, range: Range<u32>) -> u32 {
59        assert!(range.start < range.end, "range must be non-empty");
60        range.start + self.uniform_below_u32(range.end - range.start)
61    }
62
63    /// Returns a uniform `usize` sampled from the half-open `range`.
64    ///
65    /// # Panics
66    ///
67    /// Panics if `range` is empty (`range.start >= range.end`).
68    #[must_use]
69    pub fn range_usize(&mut self, range: Range<usize>) -> usize {
70        assert!(range.start < range.end, "range must be non-empty");
71        range.start + self.uniform_below_usize(range.end - range.start)
72    }
73
74    /// Returns a uniform `f32` in `[0, 1)`.
75    #[must_use]
76    pub fn next_f32(&mut self) -> f32 {
77        const INV_2_POW_24: f32 = 1.0 / ((1u32 << Rng::F32_MANTISSA_BITS) as f32);
78        ((self.next_u32() >> (u32::BITS - Self::F32_MANTISSA_BITS)) as f32) * INV_2_POW_24
79    }
80
81    fn uniform_below_u32(&mut self, upper_exclusive: u32) -> u32 {
82        let zone = u32::MAX - (u32::MAX % upper_exclusive);
83        loop {
84            let value = self.next_u32();
85            if value < zone {
86                return value % upper_exclusive;
87            }
88        }
89    }
90
91    fn uniform_below_usize(&mut self, upper_exclusive: usize) -> usize {
92        // Reject-sample entirely in u64 so range_usize yields the same sequence on 32- and
93        // 64-bit targets, honouring the across-platforms determinism guarantee. The accepted
94        // value is already < upper_exclusive, so the cast back to usize is lossless on both.
95        let upper = upper_exclusive as u64;
96        let zone = u64::MAX - (u64::MAX % upper);
97        loop {
98            let value = self.next_u64();
99            if value < zone {
100                return (value % upper) as usize;
101            }
102        }
103    }
104}
105
106impl Default for Rng {
107    /// Seeds from [`Rng::DEFAULT_SEED`].
108    fn default() -> Self {
109        Self::new(Self::DEFAULT_SEED)
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::Rng;
116
117    #[test]
118    fn same_seed_same_sequence() {
119        let mut a = Rng::new(1337);
120        let mut b = Rng::new(1337);
121        for _ in 0..32 {
122            assert_eq!(a.next_u64(), b.next_u64());
123        }
124    }
125
126    #[test]
127    fn range_u32_stays_within_bounds() {
128        let mut rng = Rng::new(42);
129        for _ in 0..2000 {
130            let value = rng.range_u32(10..20);
131            assert!((10..20).contains(&value));
132        }
133    }
134
135    #[test]
136    fn range_usize_stays_within_bounds() {
137        let mut rng = Rng::new(7);
138        for _ in 0..2000 {
139            let value = rng.range_usize(3..9);
140            assert!((3..9).contains(&value));
141        }
142    }
143
144    #[test]
145    fn f32_is_in_zero_to_one() {
146        let mut rng = Rng::new(1);
147        for _ in 0..2000 {
148            let value = rng.next_f32();
149            assert!((0.0..1.0).contains(&value));
150        }
151    }
152
153    #[test]
154    fn reseed_restores_the_sequence() {
155        let mut rng = Rng::new(99);
156        let first: Vec<u64> = (0..8).map(|_| rng.next_u64()).collect();
157        rng.reseed(99);
158        let second: Vec<u64> = (0..8).map(|_| rng.next_u64()).collect();
159        assert_eq!(first, second);
160    }
161
162    #[test]
163    fn zero_seed_falls_back_to_default() {
164        // A zero seed must not freeze the generator at state 0 (xorshift is a no-op there).
165        assert_eq!(
166            Rng::new(0).next_u64(),
167            Rng::new(Rng::DEFAULT_SEED).next_u64()
168        );
169        assert_eq!(
170            Rng::default().next_u64(),
171            Rng::new(Rng::DEFAULT_SEED).next_u64()
172        );
173    }
174}