Skip to main content

vortex_core/
rng.rs

1//! Deterministic random number generator.
2//!
3//! Seeded PRNG using a PCG-style LCG. Same seed produces identical sequences
4//! across runs, platforms, and Rust versions. No external entropy is ever used.
5
6/// A deterministic pseudo-random number generator.
7///
8/// Uses a 64-bit LCG (Linear Congruential Generator) with PCG constants.
9/// Same seed always produces the same sequence — this is the foundation
10/// of Vortex's reproducibility guarantee.
11pub struct DetRng {
12    state: u64,
13}
14
15impl DetRng {
16    /// Create a new RNG with the given seed.
17    pub fn new(seed: u64) -> Self {
18        Self { state: seed }
19    }
20
21    /// Create a child RNG for a subsystem by mixing the master seed with a domain tag.
22    ///
23    /// This ensures different subsystems (network, storage, clock) get independent
24    /// but deterministic random streams from the same master seed.
25    ///
26    /// ```
27    /// # use vortex_core::DetRng;
28    /// let master = 42u64;
29    /// let net_rng = DetRng::derive(master, "network");
30    /// let disk_rng = DetRng::derive(master, "storage");
31    /// // net_rng and disk_rng produce different sequences
32    /// ```
33    pub fn derive(master_seed: u64, domain: &str) -> Self {
34        let mut hash: u64 = 14695981039346656037; // FNV-1a offset basis
35        // Mix in master seed bytes
36        for byte in master_seed.to_le_bytes() {
37            hash ^= byte as u64;
38            hash = hash.wrapping_mul(1099511628211);
39        }
40        // Mix in domain string
41        for byte in domain.as_bytes() {
42            hash ^= *byte as u64;
43            hash = hash.wrapping_mul(1099511628211);
44        }
45        Self { state: hash }
46    }
47
48    /// Advance the RNG state and return the next raw u64.
49    #[inline]
50    pub fn next_u64(&mut self) -> u64 {
51        self.state = self
52            .state
53            .wrapping_mul(6364136223846793005)
54            .wrapping_add(1442695040888963407);
55        self.state
56    }
57
58    /// Return a random f64 in [0.0, 1.0).
59    #[inline]
60    pub fn next_f64(&mut self) -> f64 {
61        let val = self.next_u64();
62        (val >> 11) as f64 / (1u64 << 53) as f64
63    }
64
65    /// Return a random u64 in [0, max) (exclusive upper bound).
66    #[inline]
67    pub fn next_u64_below(&mut self, max: u64) -> u64 {
68        if max == 0 {
69            return 0;
70        }
71        self.next_u64() % max
72    }
73
74    /// Return a random u64 in [min, max] (inclusive both bounds).
75    #[inline]
76    pub fn next_u64_range(&mut self, min: u64, max: u64) -> u64 {
77        if min >= max {
78            return min;
79        }
80        let range = max - min + 1;
81        min + self.next_u64_below(range)
82    }
83
84    /// Return true with the given probability [0.0, 1.0].
85    #[inline]
86    pub fn chance(&mut self, probability: f64) -> bool {
87        self.next_f64() < probability
88    }
89
90    /// Shuffle a slice in-place (Fisher-Yates).
91    pub fn shuffle<T>(&mut self, slice: &mut [T]) {
92        let len = slice.len();
93        if len < 2 {
94            return;
95        }
96        for i in (1..len).rev() {
97            let j = self.next_u64_below(i as u64 + 1) as usize;
98            slice.swap(i, j);
99        }
100    }
101
102    /// Get the current internal state (for debugging/logging).
103    pub fn state(&self) -> u64 {
104        self.state
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_deterministic_same_seed() {
114        let mut rng1 = DetRng::new(42);
115        let mut rng2 = DetRng::new(42);
116        let seq1: Vec<u64> = (0..100).map(|_| rng1.next_u64()).collect();
117        let seq2: Vec<u64> = (0..100).map(|_| rng2.next_u64()).collect();
118        assert_eq!(seq1, seq2, "Same seed must produce identical sequences");
119    }
120
121    #[test]
122    fn test_different_seeds_differ() {
123        let mut rng1 = DetRng::new(42);
124        let mut rng2 = DetRng::new(99);
125        let seq1: Vec<u64> = (0..10).map(|_| rng1.next_u64()).collect();
126        let seq2: Vec<u64> = (0..10).map(|_| rng2.next_u64()).collect();
127        assert_ne!(seq1, seq2);
128    }
129
130    #[test]
131    fn test_derive_produces_different_streams() {
132        let mut net = DetRng::derive(42, "network");
133        let mut disk = DetRng::derive(42, "storage");
134        let a = net.next_u64();
135        let b = disk.next_u64();
136        assert_ne!(a, b);
137    }
138
139    #[test]
140    fn test_derive_is_deterministic() {
141        let mut r1 = DetRng::derive(42, "network");
142        let mut r2 = DetRng::derive(42, "network");
143        let seq1: Vec<u64> = (0..50).map(|_| r1.next_u64()).collect();
144        let seq2: Vec<u64> = (0..50).map(|_| r2.next_u64()).collect();
145        assert_eq!(seq1, seq2);
146    }
147
148    #[test]
149    fn test_f64_in_range() {
150        let mut rng = DetRng::new(42);
151        for _ in 0..10_000 {
152            let v = rng.next_f64();
153            assert!((0.0..1.0).contains(&v), "f64 out of [0,1): {v}");
154        }
155    }
156
157    #[test]
158    fn test_range_in_bounds() {
159        let mut rng = DetRng::new(42);
160        for _ in 0..10_000 {
161            let v = rng.next_u64_range(10, 50);
162            assert!((10..=50).contains(&v), "Range out of [10,50]: {v}");
163        }
164    }
165
166    #[test]
167    fn test_chance_probability() {
168        let mut rng = DetRng::new(42);
169        let total = 10_000;
170        let hits: usize = (0..total).filter(|_| rng.chance(0.5)).count();
171        let rate = hits as f64 / total as f64;
172        assert!(
173            rate > 0.45 && rate < 0.55,
174            "50% chance should be ~50%, got {:.1}%",
175            rate * 100.0
176        );
177    }
178
179    #[test]
180    fn test_shuffle_deterministic() {
181        let mut rng1 = DetRng::new(42);
182        let mut rng2 = DetRng::new(42);
183        let mut a: Vec<i32> = (0..20).collect();
184        let mut b: Vec<i32> = (0..20).collect();
185        rng1.shuffle(&mut a);
186        rng2.shuffle(&mut b);
187        assert_eq!(a, b, "Same seed shuffle must match");
188    }
189
190    #[test]
191    fn test_shuffle_actually_shuffles() {
192        let mut rng = DetRng::new(42);
193        let original: Vec<i32> = (0..20).collect();
194        let mut shuffled = original.clone();
195        rng.shuffle(&mut shuffled);
196        assert_ne!(original, shuffled, "Shuffle should reorder elements");
197    }
198}