Skip to main content

vimp_engine_core/
rng.rs

1use serde::{Deserialize, Serialize};
2
3/// Детерминированный PRNG (SplitMix64): замена Math.random() у ботов и
4/// разброса оружия. Целочисленное ядро — результат бит-в-бит одинаков на
5/// любой платформе, что сохраняет детерминизм WASM (Spike C).
6#[derive(Clone, Serialize, Deserialize)]
7pub struct Rng {
8    state: u64,
9}
10
11impl Rng {
12    pub fn new(seed: u64) -> Self {
13        Self { state: seed }
14    }
15
16    fn next_u64(&mut self) -> u64 {
17        self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15);
18        let mut z = self.state;
19
20        z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
21        z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
22        z ^ (z >> 31)
23    }
24
25    /// Аналог Math.random(): значение в [0, 1).
26    pub fn next_f32(&mut self) -> f32 {
27        (self.next_u64() >> 40) as f32 / (1u64 << 24) as f32
28    }
29
30    /// Аналог randomRange(min, max) из lib/math.js.
31    pub fn range(&mut self, min: f32, max: f32) -> f32 {
32        self.next_f32() * (max - min) + min
33    }
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn deterministic_for_same_seed() {
42        let mut a = Rng::new(42);
43        let mut b = Rng::new(42);
44
45        for _ in 0..100 {
46            assert_eq!(a.next_u64(), b.next_u64());
47        }
48    }
49
50    #[test]
51    fn next_f32_in_unit_range() {
52        let mut rng = Rng::new(7);
53
54        for _ in 0..1000 {
55            let v = rng.next_f32();
56            assert!((0.0..1.0).contains(&v));
57        }
58    }
59
60    #[test]
61    fn range_respects_bounds() {
62        let mut rng = Rng::new(3);
63
64        for _ in 0..1000 {
65            let v = rng.range(-0.25, 0.25);
66            assert!((-0.25..0.25).contains(&v));
67        }
68    }
69}