Skip to main content

turnbase/
rng.rs

1//! Deterministic, snapshot-able pseudo-random generator.
2
3/// A small, deterministic pseudo-random generator that lives inside game state.
4///
5/// The whole reproducible position is a single `u64` ([`Prng::position`]), so
6/// the generator is `Copy`, serializes with the state it lives in (O(1)
7/// snapshot and resume), and a `Reversible` game's undo record can restore its
8/// position exactly. Everything is integer-only with a fixed algorithm, so a
9/// seed reproduces the same stream on every platform. Do not rely on the
10/// concrete algorithm (currently PCG XSH-RR 64/32, single stream): it is an
11/// implementation detail of this newtype and may change.
12///
13/// This is not cryptographically secure and must not be used for security.
14#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct Prng {
17    state: u64,
18}
19
20const MULT: u64 = 6_364_136_223_846_793_005;
21const INC: u64 = 1_442_695_040_888_963_407;
22
23impl Prng {
24    /// Creates a generator seeded from `seed`.
25    ///
26    /// Equal seeds produce identical streams; different seeds are extremely
27    /// likely to diverge immediately.
28    #[must_use]
29    pub const fn new(seed: u64) -> Self {
30        let mut rng = Self {
31            state: INC.wrapping_add(seed),
32        };
33        rng.step();
34        rng
35    }
36
37    /// Returns the generator's current position in its stream.
38    ///
39    /// Capture this before a move and pass it to [`Prng::set_position`] to
40    /// rewind, which is how a make/unmake (`Reversible`) undo record restores
41    /// the random stream.
42    #[must_use]
43    pub const fn position(&self) -> u64 {
44        self.state
45    }
46
47    /// Restores a position previously read from [`Prng::position`].
48    pub const fn set_position(&mut self, position: u64) {
49        self.state = position;
50    }
51
52    const fn step(&mut self) {
53        self.state = self.state.wrapping_mul(MULT).wrapping_add(INC);
54    }
55
56    /// Returns the next 32-bit value and advances the stream by one step.
57    // The two casts are the defining truncations of PCG XSH-RR: the output word
58    // is the low 32 bits of the xorshift, and the rotation is the top 5 bits.
59    #[allow(clippy::cast_possible_truncation)]
60    pub const fn next_u32(&mut self) -> u32 {
61        let old = self.state;
62        self.step();
63        let xorshifted = (((old >> 18) ^ old) >> 27) as u32;
64        let rot = (old >> 59) as u32;
65        xorshifted.rotate_right(rot)
66    }
67
68    /// Returns the next 64-bit value (two steps of the stream).
69    pub const fn next_u64(&mut self) -> u64 {
70        let hi = self.next_u32() as u64;
71        let lo = self.next_u32() as u64;
72        (hi << 32) | lo
73    }
74
75    /// Returns a uniformly distributed value in `0..bound`.
76    ///
77    /// Uses rejection sampling to avoid modulo bias, so a single call consumes a
78    /// variable number of steps. That is why a pre-move [`Prng::position`] must
79    /// be snapshotted rather than reconstructed by counting draws.
80    ///
81    /// # Panics
82    /// Panics if `bound` is zero.
83    pub const fn below(&mut self, bound: u64) -> u64 {
84        assert!(bound != 0, "below(0) has no valid output");
85        // Reject the low `2^64 % bound` outputs so the rest divide evenly.
86        let threshold = bound.wrapping_neg() % bound;
87        loop {
88            let value = self.next_u64();
89            if value >= threshold {
90                return value % bound;
91            }
92        }
93    }
94
95    /// Returns a uniformly distributed value in `low..high`.
96    ///
97    /// # Panics
98    /// Panics if `low >= high`.
99    pub const fn range(&mut self, low: u64, high: u64) -> u64 {
100        assert!(low < high, "range requires low < high");
101        low + self.below(high - low)
102    }
103
104    /// Returns a reference to a uniformly chosen element, or `None` if empty.
105    // `below` returns a value strictly less than `len`, which is itself a
106    // `usize`, so the cast back to `usize` cannot truncate.
107    #[allow(clippy::cast_possible_truncation)]
108    pub fn choose<'a, T>(&mut self, items: &'a [T]) -> Option<&'a T> {
109        if items.is_empty() {
110            None
111        } else {
112            let index = self.below(items.len() as u64) as usize;
113            Some(&items[index])
114        }
115    }
116
117    /// Shuffles `items` in place with an unbiased Fisher-Yates shuffle.
118    // `j` is strictly less than `i + 1 <= len`, a `usize`, so the cast cannot
119    // truncate.
120    #[allow(clippy::cast_possible_truncation)]
121    pub const fn shuffle<T>(&mut self, items: &mut [T]) {
122        let mut i = items.len();
123        while i > 1 {
124            i -= 1;
125            let j = self.below(i as u64 + 1) as usize;
126            items.swap(i, j);
127        }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::Prng;
134
135    #[test]
136    fn same_seed_same_stream() {
137        let mut a = Prng::new(42);
138        let mut b = Prng::new(42);
139        for _ in 0..1000 {
140            assert_eq!(a.next_u64(), b.next_u64());
141        }
142    }
143
144    #[test]
145    fn different_seeds_diverge() {
146        let mut a = Prng::new(1);
147        let mut b = Prng::new(2);
148        assert_ne!(a.next_u64(), b.next_u64());
149    }
150
151    #[test]
152    fn position_round_trips_through_variable_draws() {
153        let mut rng = Prng::new(7);
154        let mark = rng.position();
155        let first: Vec<u64> = (0..20).map(|_| rng.below(100)).collect();
156
157        rng.set_position(mark);
158        let second: Vec<u64> = (0..20).map(|_| rng.below(100)).collect();
159
160        assert_eq!(first, second, "restoring position replays the same draws");
161    }
162
163    #[test]
164    fn below_is_in_bounds() {
165        let mut rng = Prng::new(9);
166        for _ in 0..10_000 {
167            assert!(rng.below(6) < 6);
168        }
169    }
170
171    #[test]
172    fn range_is_in_bounds() {
173        let mut rng = Prng::new(9);
174        for _ in 0..10_000 {
175            let v = rng.range(10, 20);
176            assert!((10..20).contains(&v));
177        }
178    }
179
180    #[test]
181    fn shuffle_is_a_permutation() {
182        let mut rng = Prng::new(3);
183        let mut data: Vec<u32> = (0..50).collect();
184        rng.shuffle(&mut data);
185        let mut sorted = data.clone();
186        sorted.sort_unstable();
187        assert_eq!(sorted, (0..50).collect::<Vec<_>>());
188    }
189
190    #[test]
191    fn choose_empty_is_none() {
192        let mut rng = Prng::new(1);
193        assert!(rng.choose::<u8>(&[]).is_none());
194    }
195}