Skip to main content

motion_core/
random.rs

1//! Key-derived deterministic random streams without global or frame-local state.
2
3use std::hash::{Hash, Hasher};
4use std::ops::Range;
5
6use crate::Vector2;
7
8/// A reproducible local random stream derived entirely from a hashed key.
9///
10/// Key bytes use FNV-1a, followed by SplitMix64 for each sample. Integer hashing
11/// uses little-endian bytes and widens usize/isize to 64 bits. Custom Hash
12/// implementations must themselves be deterministic. Rust's Hash encodings are
13/// not a serialization format; cross-version key encoding stability is not
14/// promised. This generator is not cryptographic and has no shared mutable state.
15///
16/// ```
17/// use motion_core::Random;
18/// let mut first = Random::from_key(("spark", 42_u64));
19/// let mut second = Random::from_key(("spark", 42_u64));
20/// assert_eq!(first.next_float(), second.next_float());
21/// assert!((10.0..20.0).contains(&first.range(10.0..20.0)));
22/// assert_eq!(first.choose(&[7]), Some(&7));
23/// ```
24#[derive(Clone, Debug)]
25pub struct Random {
26    state: u64,
27}
28
29impl Random {
30    /// Starts a fresh stream; equal keys reproduce the same sequence.
31    pub fn from_key(key: impl Hash) -> Self {
32        let mut hasher = KeyHasher(0xcbf29ce484222325);
33        key.hash(&mut hasher);
34        Self {
35            state: hasher.finish(),
36        }
37    }
38
39    /// Starts a counter-keyed particle stream without Hash encoding or shared state.
40    /// Equal seed/index/step values are independent of traversal and worker count.
41    pub fn for_particle(seed: u64, index: u64, step: u64) -> Self {
42        fn mix(mut value: u64) -> u64 {
43            value = (value ^ (value >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
44            value = (value ^ (value >> 27)).wrapping_mul(0x94d049bb133111eb);
45            value ^ (value >> 31)
46        }
47        Self {
48            state: mix(seed
49                ^ mix(index.wrapping_add(0x9e3779b97f4a7c15))
50                ^ mix(step.wrapping_add(0xd1b54a32d192ed03))),
51        }
52    }
53
54    fn next_bits(&mut self) -> u64 {
55        self.state = self.state.wrapping_add(0x9e3779b97f4a7c15);
56        let mut value = self.state;
57        value = (value ^ (value >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
58        value = (value ^ (value >> 27)).wrapping_mul(0x94d049bb133111eb);
59        value ^ (value >> 31)
60    }
61
62    /// Returns one of 2^53 equally spaced floating-point values in [0, 1).
63    pub fn next_float(&mut self) -> f64 {
64        (self.next_bits() >> 11) as f64 / (1_u64 << 53) as f64
65    }
66
67    /// Samples a finite half-open interval, including adjacent float endpoints.
68    ///
69    /// Panics for empty, reversed or nonfinite ranges. Rounding is clamped below
70    /// the upper endpoint; no overflowing endpoint subtraction is performed.
71    pub fn range(&mut self, range: Range<f64>) -> f64 {
72        assert!(
73            range.start.is_finite() && range.end.is_finite() && range.start < range.end,
74            "random range must have finite, strictly increasing endpoints"
75        );
76        let progress = self.next_float();
77        let value = range.start * (1.0 - progress) + range.end * progress;
78        value.max(range.start).min(range.end.next_down())
79    }
80
81    /// Samples a uniform angle using deterministic libm trigonometry.
82    pub fn unit_vector(&mut self) -> Vector2 {
83        let angle = self.next_float() * std::f64::consts::TAU;
84        Vector2::new(libm::cos(angle), libm::sin(angle))
85    }
86
87    /// Selects an element without modulo bias; an empty slice returns None.
88    pub fn choose<'a, Value>(&mut self, values: &'a [Value]) -> Option<&'a Value> {
89        if values.is_empty() {
90            return None;
91        }
92        let count = u64::try_from(values.len()).expect("slice length fits u64");
93        let threshold = count.wrapping_neg() % count;
94        loop {
95            let bits = self.next_bits();
96            if bits >= threshold {
97                return Some(&values[(bits % count) as usize]);
98            }
99        }
100    }
101}
102
103struct KeyHasher(u64);
104
105impl Hasher for KeyHasher {
106    fn finish(&self) -> u64 {
107        self.0
108    }
109
110    fn write(&mut self, bytes: &[u8]) {
111        for byte in bytes {
112            self.0 = (self.0 ^ u64::from(*byte)).wrapping_mul(0x100000001b3);
113        }
114    }
115
116    fn write_u8(&mut self, value: u8) {
117        self.write(&value.to_le_bytes());
118    }
119
120    fn write_u16(&mut self, value: u16) {
121        self.write(&value.to_le_bytes());
122    }
123
124    fn write_u32(&mut self, value: u32) {
125        self.write(&value.to_le_bytes());
126    }
127
128    fn write_u64(&mut self, value: u64) {
129        self.write(&value.to_le_bytes());
130    }
131
132    fn write_u128(&mut self, value: u128) {
133        self.write(&value.to_le_bytes());
134    }
135
136    fn write_usize(&mut self, value: usize) {
137        self.write_u64(value as u64);
138    }
139
140    fn write_i8(&mut self, value: i8) {
141        self.write(&value.to_le_bytes());
142    }
143
144    fn write_i16(&mut self, value: i16) {
145        self.write(&value.to_le_bytes());
146    }
147
148    fn write_i32(&mut self, value: i32) {
149        self.write(&value.to_le_bytes());
150    }
151
152    fn write_i64(&mut self, value: i64) {
153        self.write(&value.to_le_bytes());
154    }
155
156    fn write_i128(&mut self, value: i128) {
157        self.write(&value.to_le_bytes());
158    }
159
160    fn write_isize(&mut self, value: isize) {
161        self.write_i64(value as i64);
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn fixed_sequence_matches_independent_integer_reference() {
171        let mut random = Random::from_key(42_u64);
172        assert_eq!(random.state, 0xff3add6b3789daef);
173        for expected in [
174            0xa4e6579fd9ba8f6d,
175            0x14e9a1cc21726518,
176            0x29700e43bfeb9b12,
177            0xebc4f50c4cfbc8c4,
178        ] {
179            assert_eq!(random.next_bits(), expected);
180        }
181        let mut random = Random::from_key(42_u64);
182        assert_eq!(random.next_float(), 0.6441397443065658);
183        assert_eq!(
184            Random::from_key(42_usize).state,
185            Random::from_key(42_u64).state
186        );
187        assert_eq!(
188            Random::from_key(-42_isize).state,
189            Random::from_key(-42_i64).state
190        );
191    }
192
193    #[test]
194    fn equal_keys_and_cloned_streams_are_repeatable_without_global_state() {
195        let mut first = Random::from_key(("spark", 27_u32));
196        let mut same = first.clone();
197        let mut different = Random::from_key(("spark", 28_u32));
198        for _ in 0..1024 {
199            let value = first.next_float();
200            assert!((0.0..1.0).contains(&value));
201            assert_eq!(value, same.next_float());
202        }
203        assert_ne!(same.next_float(), different.next_float());
204        let expected = Random::from_key("parallel").next_float();
205        std::thread::scope(|scope| {
206            let handles: Vec<_> = (0..8)
207                .map(|_| scope.spawn(|| Random::from_key("parallel").next_float()))
208                .collect();
209            for handle in handles {
210                assert_eq!(handle.join().unwrap(), expected);
211            }
212        });
213    }
214
215    #[test]
216    fn ranges_vectors_and_choices_obey_their_domains() {
217        let mut random = Random::from_key("domains");
218        let ranges = [
219            -f64::MAX..f64::MAX,
220            f64::MAX.next_down()..f64::MAX,
221            -f64::MAX..(-f64::MAX).next_up(),
222            0.0..f64::from_bits(1),
223            -f64::from_bits(1)..0.0,
224            1.0..1.0_f64.next_up(),
225            -3.0..7.0,
226        ];
227        let choices = [2, 5, 9];
228        let mut seen = [false; 3];
229        for _ in 0..2048 {
230            for range in &ranges {
231                let value = random.range(range.clone());
232                assert!(range.contains(&value), "{range:?}: {value:?}");
233            }
234            let vector = random.unit_vector();
235            assert!((vector.x * vector.x + vector.y * vector.y - 1.0).abs() <= 4.0 * f64::EPSILON);
236            let choice = random.choose(&choices).unwrap();
237            seen[choices.iter().position(|value| value == choice).unwrap()] = true;
238        }
239        assert_eq!(seen, [true; 3]);
240        assert_eq!(random.choose::<u32>(&[]), None);
241        assert_eq!(random.choose(&[7]), Some(&7));
242    }
243
244    #[test]
245    fn invalid_ranges_fail_explicitly() {
246        for range in [
247            0.0..0.0,
248            2.0..1.0,
249            f64::NAN..1.0,
250            0.0..f64::INFINITY,
251            f64::NEG_INFINITY..0.0,
252        ] {
253            assert!(std::panic::catch_unwind(|| Random::from_key(0_u32).range(range)).is_err());
254        }
255    }
256}