Skip to main content

cuqueclicker_lib/game/tree/
rng.rs

1//! Position-seeded deterministic RNG for tree generation.
2//!
3//! SplitMix64 — small state, fast, statistically fine for procgen. Every
4//! call site seeds a fresh instance from `(TREE_SEED, lot_x, lot_y, salt)`
5//! and consumes it locally, so generation is referentially transparent: two
6//! calls to `node_at(3, -4)` produce byte-identical results.
7
8/// Tiny deterministic PRNG. State is one u64; output passes the obvious
9/// goodness tests (BigCrush-ish) and is more than enough for upgrade-tree
10/// procgen. Not for cryptography.
11pub struct SplitMix64 {
12    state: u64,
13}
14
15impl SplitMix64 {
16    pub fn new(seed: u64) -> Self {
17        Self { state: seed }
18    }
19
20    /// Combine a base seed with three coordinate-like ints into a fresh
21    /// PRNG state. `salt` lets adjacent call sites avoid stepping on each
22    /// other (e.g. "noise lookup" vs "edge roll" at the same lot).
23    pub fn from_coords(seed: u64, x: i32, y: i32, salt: u64) -> Self {
24        // Mix the coords through SplitMix64's avalanche step before XORing
25        // into the base seed so neighboring (x, y) pairs don't produce
26        // trivially-correlated states.
27        let mix = mix_u64(
28            seed ^ salt
29                ^ ((x as i64 as u64).rotate_left(13))
30                ^ ((y as i64 as u64)
31                    .rotate_left(31)
32                    .wrapping_mul(0x9E37_79B9_7F4A_7C15)),
33        );
34        Self::new(mix)
35    }
36
37    pub fn next_u64(&mut self) -> u64 {
38        self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
39        mix_u64(self.state)
40    }
41
42    /// Returns a value in `[0.0, 1.0)`. 53-bit mantissa for double precision.
43    pub fn next_f64(&mut self) -> f64 {
44        (self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
45    }
46
47    /// Returns a value in `[lo, hi)`.
48    pub fn range_f64(&mut self, lo: f64, hi: f64) -> f64 {
49        lo + (hi - lo) * self.next_f64()
50    }
51
52    /// Returns an integer in `[0, max)`. Trivial modulo bias is acceptable
53    /// for procgen — the bias for `max < 1e9` is well below any other source
54    /// of variance in the system.
55    pub fn range_usize(&mut self, max: usize) -> usize {
56        if max == 0 {
57            return 0;
58        }
59        (self.next_u64() % (max as u64)) as usize
60    }
61
62    /// True with probability `p`.
63    pub fn bool_with_prob(&mut self, p: f64) -> bool {
64        self.next_f64() < p
65    }
66}
67
68/// SplitMix64's avalanche finalizer. Standalone so other modules can fold
69/// coords into seeds without spinning up an RNG instance.
70pub const fn mix_u64(seed: u64) -> u64 {
71    let mut z = seed;
72    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
73    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
74    z ^ (z >> 31)
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn same_seed_same_sequence() {
83        let mut a = SplitMix64::new(42);
84        let mut b = SplitMix64::new(42);
85        for _ in 0..16 {
86            assert_eq!(a.next_u64(), b.next_u64());
87        }
88    }
89
90    #[test]
91    fn different_coords_different_states() {
92        let a = SplitMix64::from_coords(7, 1, 1, 0).state;
93        let b = SplitMix64::from_coords(7, 1, 2, 0).state;
94        assert_ne!(a, b);
95    }
96
97    #[test]
98    fn salt_separates_call_sites() {
99        let a = SplitMix64::from_coords(7, 1, 1, 0).state;
100        let b = SplitMix64::from_coords(7, 1, 1, 1).state;
101        assert_ne!(a, b);
102    }
103
104    #[test]
105    fn range_f64_within_bounds() {
106        let mut r = SplitMix64::new(1234);
107        for _ in 0..1000 {
108            let v = r.range_f64(-2.0, 5.0);
109            assert!((-2.0..5.0).contains(&v), "{}", v);
110        }
111    }
112
113    #[test]
114    fn range_usize_within_bounds() {
115        let mut r = SplitMix64::new(1234);
116        for _ in 0..1000 {
117            let v = r.range_usize(10);
118            assert!(v < 10);
119        }
120    }
121}