Skip to main content

repame_sim/
rng.rs

1//! Simulation randomness a replay can trust, with no dependency behind it.
2//!
3//! [`SimRng`] is xoshiro256\*\* (Blackman & Vigna, 2018) seeded through
4//! SplitMix64, and both algorithms are defined *in this file* as wrapping
5//! `u64` arithmetic: no `rand`, no `SmallRng`, no libm, no target-dependent
6//! rounding. Upgrading a dependency can therefore never move a draw, which
7//! matters because `SmallRng` is documented as not stable across `rand`
8//! releases while a replay is exactly the claim the crate makes.
9//!
10//! The 4-word state is `serde`-serialisable, so a save carries the stream
11//! position; a save plus an intent tape (see [`crate::tape`]) re-runs the game.
12//!
13//! Two ways to get a stream:
14//!
15//! - [`SimRng::seeded`] for the one stream a game owns outright, inserted as a
16//!   resource.
17//! - [`WorldSeed`] to derive a stream per system or per entity. Deriving is a
18//!   pure function of `(seed, tag)`, so adding a system does not shift the
19//!   streams of the others, and the same tag always yields the same stream.
20//!
21//! Sampling method names follow the `rand` API the games already call
22//! (`random_range`, `random`, `random_bool`), so porting a draw site is a
23//! change of `use` line rather than a rewrite. The floating-point and integer
24//! construction is pinned here, once, in the trait's provided methods: there is
25//! no second implementation of `unit_f32` for a different target to disagree
26//! with.
27
28use core::ops::{Range, RangeInclusive};
29
30use bevy_ecs::prelude::*;
31use serde::{Deserialize, Serialize};
32
33/// SplitMix64 increment: 2^64 divided by the golden ratio.
34const SPLITMIX_GAMMA: u64 = 0x9E37_79B9_7F4A_7C15;
35/// SplitMix64 finalizer multiplier 1.
36const SPLITMIX_MUL1: u64 = 0xBF58_476D_1CE4_E5B9;
37/// SplitMix64 finalizer multiplier 2.
38const SPLITMIX_MUL2: u64 = 0x94D0_49BB_1331_11EB;
39/// Domain separator for [`WorldSeed`] tags, so tag 0 is not the bare stream.
40const TAG_SALT: u64 = 0xD1B5_4A32_D192_ED03;
41
42/// SplitMix64's finalizer: a bijective avalanche mix of one word.
43const fn finalize(mut z: u64) -> u64 {
44    z = (z ^ (z >> 30)).wrapping_mul(SPLITMIX_MUL1);
45    z = (z ^ (z >> 27)).wrapping_mul(SPLITMIX_MUL2);
46    z ^ (z >> 31)
47}
48
49/// One seed expanded into the xoshiro256\*\* state: four SplitMix64 outputs.
50///
51/// The four counters `seed + k * GAMMA`, `k` in `1..=4`, are distinct, so at
52/// most one of them is zero, and `finalize` reaches zero only at zero. The four
53/// words are therefore never all zero: the xoshiro fixed point is out of reach
54/// from any seed, and no guard is needed.
55const fn expand(seed: u64) -> [u64; 4] {
56    let mut counter = seed;
57    let mut state = [0; 4];
58    let mut word = 0;
59    while word < 4 {
60        counter = counter.wrapping_add(SPLITMIX_GAMMA);
61        state[word] = finalize(counter);
62        word += 1;
63    }
64    state
65}
66
67/// The sim's random stream: xoshiro256\*\* holding four words of state.
68///
69/// Insert one as a resource and every draw the sim makes is reproducible from
70/// the state it holds. `Copy`, so a system can take a snapshot of the stream
71/// (record its [`state_words`](Self::state_words)) as cheaply as a value read.
72///
73/// Do not build a *series* of seeds out of arithmetic: `seeded(s)` and
74/// `seeded(s + GAMMA)`, or any small multiple of `GAMMA`, expand to states that
75/// share three of four words, and correlated streams follow. Derive a stream
76/// per system or entity from [`WorldSeed`] instead; it mixes the tag before
77/// expanding.
78#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
79pub struct SimRng {
80    state: [u64; 4],
81}
82
83impl SimRng {
84    /// A stream seeded by `seed` through SplitMix64.
85    ///
86    /// Any seed is valid, including `0`. Two runs of the same seed produce the
87    /// same stream on every target.
88    pub const fn seeded(seed: u64) -> Self {
89        Self {
90            state: expand(seed),
91        }
92    }
93
94    /// A stream from four raw state words, for a save that stores the words
95    /// itself instead of the serialised resource.
96    ///
97    /// All-zero words would make xoshiro emit zeros forever, so that one input
98    /// is remapped to `0`'s seeded stream rather than left as a trap.
99    pub const fn from_words(mut words: [u64; 4]) -> Self {
100        if words[0] == 0 && words[1] == 0 && words[2] == 0 && words[3] == 0 {
101            words = expand(0);
102        }
103        Self { state: words }
104    }
105
106    /// The four state words: the whole stream position, for a save.
107    ///
108    /// `from_words(state_words())` resumes exactly where the stream stood.
109    pub const fn state_words(&self) -> [u64; 4] {
110        self.state
111    }
112}
113
114impl Rng for SimRng {
115    /// The next 64 bits: multiply, rotate, multiply, then the state update.
116    fn next_u64(&mut self) -> u64 {
117        let result = self.state[1].wrapping_mul(5).rotate_left(7).wrapping_mul(9);
118        let t = self.state[1] << 17;
119        self.state[2] ^= self.state[0];
120        self.state[3] ^= self.state[1];
121        self.state[1] ^= self.state[2];
122        self.state[0] ^= self.state[3];
123        self.state[2] ^= t;
124        self.state[3] = self.state[3].rotate_left(45);
125        result
126    }
127}
128
129/// The world's seed, kept in the world so any system can derive its own stream.
130///
131/// ```ignore
132/// world.init_resource::<WorldSeed>();
133/// world.resource_mut::<WorldSeed>().0 = WorldSeed::new(menu_seed);
134///
135/// // in a system:
136/// let mut loot = world_seed.stream(LOOT_TAG);
137/// let mut spread = world_seed.stream(SPREAD_TAG);
138/// ```
139///
140/// Prefer [`named`](Self::named) over hand-numbered tags for per-system streams,
141/// and carry a stored tag value over an enum's discriminant: a name survives an
142/// enum being reordered, a tag derived from `as u64` does not.
143#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
144pub struct WorldSeed(u64);
145
146impl WorldSeed {
147    /// A world seed. Every `u64` is a different world, `0` included.
148    pub const fn new(seed: u64) -> Self {
149        Self(seed)
150    }
151
152    /// The seed as it was given, for menus and for the save file.
153    pub const fn get(self) -> u64 {
154        self.0
155    }
156
157    /// A stream derived from the seed and a tag.
158    ///
159    /// The tag is mixed before the words are expanded, so a run of tags
160    /// (`0, 1, 2, ...`) does not produce the `GAMMA`-shifted seed series that
161    /// [`SimRng::seeded`] warns about, and `stream(0)` is not the world's bare
162    /// seeded stream.
163    pub fn stream(&self, tag: u64) -> SimRng {
164        SimRng::seeded(finalize(self.0 ^ finalize(tag ^ TAG_SALT)))
165    }
166
167    /// A stream derived from the seed and a name.
168    ///
169    /// The name is folded with [`fnv1a64`](crate::hash::fnv1a64) first, so the
170    /// stream is stable across runs and machines.
171    pub fn named(&self, name: &str) -> SimRng {
172        self.stream(crate::hash::fnv1a64(name.as_bytes()))
173    }
174
175    /// A stream for one entity, the same for that entity's whole life.
176    ///
177    /// Derived from the seed and the entity's id bits, so it does not depend on
178    /// when the entity was spawned or on how many draws happened before. The id
179    /// bits include the generation, so a recycled slot gives a fresh stream.
180    pub fn stream_for_entity(&self, entity: Entity) -> SimRng {
181        self.stream(entity.to_bits())
182    }
183}
184
185/// Draws from a [`SimRng`]: one required method, everything else pinned here.
186///
187/// A system writes `rng: ResMut<SimRng>` and calls these methods, or takes
188/// `rng: &mut impl Rng` to stay generic over the stream. `&mut R` is a `Rng`
189/// when `R` is, so a mutable borrow passes straight through.
190///
191/// This is not `rand::Rng`: that trait's generators cannot be passed here
192/// without depending on `rand` (a foreign trait over a foreign type cannot be
193/// bridged from this crate), and `rand` is the dependency this module exists to
194/// retire. A game that still needs a `rand` generator somewhere can wrap it in
195/// its own newtype and implement this trait's one method, `next_u64`.
196pub trait Rng {
197    /// The next 64 bits of the stream. Everything else is built from these.
198    fn next_u64(&mut self) -> u64;
199
200    /// The next 32 bits, taken from the top half of a draw: the low bits of an
201    /// xoshiro word carry more linear structure than the high ones.
202    fn next_u32(&mut self) -> u32 {
203        (self.next_u64() >> 32) as u32
204    }
205
206    /// A value of `T`, uniform over its whole domain.
207    ///
208    /// `random::<f32>()` and `random::<f64>()` are uniform in `0..1`.
209    /// `random::<u64>()` is one raw draw. Narrower integers take the top bits of
210    /// a draw. `usize`/`isize` are deliberately not sampleable in full: a
211    /// 64-bit desktop draw and a 32-bit wasm draw would not agree, so use a
212    /// fixed-width type and cast.
213    fn random<T: Sample>(&mut self) -> T {
214        T::sample(self)
215    }
216
217    /// A value uniform in `range`, e.g. `random_range(0..enemies.len())`,
218    /// `random_range(0.0..TAU)`, `random_range(-8..=8)`.
219    ///
220    /// Integer ranges are half-open (or inclusive as written) and unbiased.
221    /// Float ranges are half-open: `0.0..=1.0` behaves as `0.0..1.0`, as in
222    /// `rand`. Panics on an empty or reversed range, and on a range with a NaN
223    /// bound, because a silent fallback would hide a sim bug.
224    fn random_range<R: SampleRange>(&mut self, range: R) -> R::Output {
225        range.sample_from(self)
226    }
227
228    /// One bit, exactly half the time, from the top bit of a draw.
229    fn bool(&mut self) -> bool {
230        (self.next_u64() >> 63) == 1
231    }
232
233    /// True with probability `p`, clamped to `0..=1`: `p <= 0` never, `p >= 1`
234    /// always. Panics on NaN.
235    ///
236    /// A clamped probability consumes no draw, so a degenerate call cannot shift
237    /// the stream.
238    fn random_bool(&mut self, p: f64) -> bool {
239        assert!(!p.is_nan(), "random_bool probability must not be NaN");
240        if p <= 0.0 {
241            return false;
242        }
243        if p >= 1.0 {
244            return true;
245        }
246        self.unit_f64() < p
247    }
248
249    /// Uniform in `0..1`, built from the top 24 bits of one draw.
250    ///
251    /// The result is one of the `2^24` values `k / 2^24`, exactly representable
252    /// and identical on every target; it never returns `1.0`.
253    fn unit_f32(&mut self) -> f32 {
254        (self.next_u64() >> 40) as f32 / (1u64 << 24) as f32
255    }
256
257    /// Uniform in `0..1`, built from the top 53 bits of one draw.
258    fn unit_f64(&mut self) -> f64 {
259        (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
260    }
261
262    /// Uniform in `0..bound`, `0` for `bound == 0` (which consumes no draw).
263    ///
264    /// Wide-multiply rejection (Lemire): each draw maps onto `bound * 2^64`
265    /// outcomes, and the underflow region is redrawn, so no modulo bias
266    /// survives. The number of draws varies with the stream, deterministically
267    /// for a given tape.
268    fn below(&mut self, bound: u32) -> u32 {
269        self.below_u64(bound as u64) as u32
270    }
271
272    /// [`below`](Self::below) for a 64-bit bound, e.g. an index into a huge
273    /// table or a hit roll over a wide damage range.
274    fn below_u64(&mut self, bound: u64) -> u64 {
275        if bound == 0 {
276            return 0;
277        }
278        let mut draw = self.next_u64();
279        let mut product = (draw as u128) * (bound as u128);
280        let mut low = product as u64;
281        if low < bound {
282            let threshold = bound.wrapping_neg() % bound;
283            while low < threshold {
284                draw = self.next_u64();
285                product = (draw as u128) * (bound as u128);
286                low = product as u64;
287            }
288        }
289        (product >> 64) as u64
290    }
291
292    /// A uniform index in `0..len`, or `None` when `len == 0`.
293    ///
294    /// This is the pick that does not shuffle: it consumes one draw and depends
295    /// on nothing but `len`, so it is stable whether the caller's slice is the
296    /// game's own `Vec` or a temporary built in a different order. A shuffle is
297    /// not a substitute for a pick: a permutation depends on the starting order
298    /// of the whole slice and on every draw it consumes, so picking by shuffling
299    /// changes the moment a sibling system's slice order does, and it is why
300    /// [`shuffle`](Self::shuffle) is pinned here rather than open-coded.
301    fn pick_index(&mut self, len: usize) -> Option<usize> {
302        if len == 0 {
303            return None;
304        }
305        Some(self.below_u64(len as u64) as usize)
306    }
307
308    /// A uniform element of `items`, or `None` when it is empty.
309    fn pick<'a, T>(&mut self, items: &'a [T]) -> Option<&'a T> {
310        self.pick_index(items.len()).map(|index| &items[index])
311    }
312
313    /// An index in `0..weights.len()` with probability proportional to its
314    /// weight, or `None` when no weight is positive.
315    ///
316    /// Negative, NaN and infinite weights count as zero. One draw: the weights
317    /// are summed in index order, and the running sum is `f32` in that same
318    /// order, so equal weights taste the same in every replay. Zero-weight
319    /// entries are skipped when the target lands exactly on zero.
320    fn weighted_index(&mut self, weights: &[f32]) -> Option<usize> {
321        let total: f32 = weights.iter().copied().map(effective_weight).sum();
322        if !(total > 0.0) {
323            return None;
324        }
325        let target = self.unit_f32() * total;
326        let mut acc = 0.0f32;
327        let mut last = None;
328        for (index, weight) in weights.iter().enumerate() {
329            if effective_weight(*weight) == 0.0 {
330                continue;
331            }
332            last = Some(index);
333            acc += *weight;
334            if acc > target {
335                return Some(index);
336            }
337        }
338        last
339    }
340
341    /// Fisher-Yates, backwards: for `i` from `len - 1` down to `1`, swap `i`
342    /// with `below(i + 1)` (so an element may stay put).
343    ///
344    /// A permutation is the one draw whose result depends on the *whole*
345    /// sequence consumed and on the starting order, so it is pinned here rather
346    /// than open-coded: a forward scan, a coin-flip swap, or an inclusive bound
347    /// all produce a different permutation *and* a different number of draws,
348    /// which silently moves every later draw in the run. `partial_shuffle` for a
349    /// weighted draw without replacement is deliberately absent: sampling a
350    /// weighted index and removing the winner biases the survivors, and a
351    /// correct one needs a documented algorithm of its own.
352    fn shuffle<T>(&mut self, items: &mut [T]) {
353        for i in (1..items.len()).rev() {
354            let j = self.below_u64(i as u64 + 1) as usize;
355            items.swap(i, j);
356        }
357    }
358}
359
360impl<T: Rng + ?Sized> Rng for &mut T {
361    fn next_u64(&mut self) -> u64 {
362        (**self).next_u64()
363    }
364}
365
366/// A weight that counts: finite and positive. Everything else is zero.
367fn effective_weight(weight: f32) -> f32 {
368    if weight.is_finite() && weight > 0.0 {
369        weight
370    } else {
371        0.0
372    }
373}
374
375/// One uniform offset in `0..span`.
376///
377/// A span of `2^64` means the range has no `u64`-sized length (the full domain
378/// of `u64`/`i64`), so it takes a raw draw instead of a bounded one.
379fn offset_in<R: Rng + ?Sized>(rng: &mut R, span: u128) -> u128 {
380    if span == 1u128 << 64 {
381        rng.next_u64() as u128
382    } else {
383        rng.below_u64(span as u64) as u128
384    }
385}
386
387/// Zero-width or unit-width float, for the range impls.
388trait Unit: Sized {
389    fn unit<R: Rng + ?Sized>(rng: &mut R) -> Self;
390}
391
392impl Unit for f32 {
393    fn unit<R: Rng + ?Sized>(rng: &mut R) -> f32 {
394        rng.unit_f32()
395    }
396}
397
398impl Unit for f64 {
399    fn unit<R: Rng + ?Sized>(rng: &mut R) -> f64 {
400        rng.unit_f64()
401    }
402}
403
404/// A range [`Rng::random_range`] can sample.
405///
406/// Implemented for `Range` and `RangeInclusive` of `f32`, `f64`, and the
407/// integer types `u8`..`u64`, `usize`, `i8`..`i64`, `isize`. Sealed on
408/// purpose: which primitive a range draws from, and how many draws it takes,
409/// is part of the replay contract, so it is not something a game may redefine.
410pub trait SampleRange: sealed::Sealed {
411    /// The sampled value.
412    type Output;
413
414    /// Draw one value from the range.
415    #[doc(hidden)]
416    fn sample_from<R: Rng + ?Sized>(self, rng: &mut R) -> Self::Output;
417}
418
419/// A type [`Rng::random`] can draw.
420///
421/// Implemented for `bool`, `f32`, `f64`, and the fixed-width integers
422/// `u8`..`u64` and `i8`..`i64`. Sealed for the same reason as
423/// [`SampleRange`]: the width of the draw is part of the replay contract.
424pub trait Sample: sealed::Sealed {
425    /// Draw one value.
426    #[doc(hidden)]
427    fn sample<R: Rng + ?Sized>(rng: &mut R) -> Self;
428}
429
430mod sealed {
431    /// Implemented for the types this module already samples, so a game cannot
432    /// add a `Sample`/`SampleRange` impl with a different draw layout.
433    pub trait Sealed {}
434}
435
436macro_rules! impl_uint_range {
437    ($($ty:ty),* $(,)?) => {$(
438        impl sealed::Sealed for Range<$ty> {}
439        impl SampleRange for Range<$ty> {
440            type Output = $ty;
441
442            fn sample_from<R: Rng + ?Sized>(self, rng: &mut R) -> $ty {
443                assert!(
444                    self.start < self.end,
445                    "random_range needs a non-empty range, got {}..{}",
446                    self.start,
447                    self.end
448                );
449                let span = self.end as u128 - self.start as u128;
450                self.start.wrapping_add(offset_in(rng, span) as $ty)
451            }
452        }
453
454        impl sealed::Sealed for RangeInclusive<$ty> {}
455        impl SampleRange for RangeInclusive<$ty> {
456            type Output = $ty;
457
458            fn sample_from<R: Rng + ?Sized>(self, rng: &mut R) -> $ty {
459                let start = *self.start();
460                let end = *self.end();
461                assert!(
462                    start <= end,
463                    "random_range needs a non-empty range, got {start}..={end}"
464                );
465                let span = end as u128 - start as u128 + 1;
466                start.wrapping_add(offset_in(rng, span) as $ty)
467            }
468        }
469    )*};
470}
471
472macro_rules! impl_int_range {
473    ($($ty:ty),* $(,)?) => {$(
474        impl sealed::Sealed for Range<$ty> {}
475        impl SampleRange for Range<$ty> {
476            type Output = $ty;
477
478            fn sample_from<R: Rng + ?Sized>(self, rng: &mut R) -> $ty {
479                assert!(
480                    self.start < self.end,
481                    "random_range needs a non-empty range, got {}..{}",
482                    self.start,
483                    self.end
484                );
485                let span = (self.end as i128 - self.start as i128) as u128;
486                self.start.wrapping_add(offset_in(rng, span) as $ty)
487            }
488        }
489
490        impl sealed::Sealed for RangeInclusive<$ty> {}
491        impl SampleRange for RangeInclusive<$ty> {
492            type Output = $ty;
493
494            fn sample_from<R: Rng + ?Sized>(self, rng: &mut R) -> $ty {
495                let start = *self.start();
496                let end = *self.end();
497                assert!(
498                    start <= end,
499                    "random_range needs a non-empty range, got {start}..={end}"
500                );
501                let span = (end as i128 - start as i128 + 1) as u128;
502                start.wrapping_add(offset_in(rng, span) as $ty)
503            }
504        }
505    )*};
506}
507
508macro_rules! impl_float_range {
509    ($($ty:ty),* $(,)?) => {$(
510        impl sealed::Sealed for Range<$ty> {}
511        impl SampleRange for Range<$ty> {
512            type Output = $ty;
513
514            fn sample_from<R: Rng + ?Sized>(self, rng: &mut R) -> $ty {
515                assert!(
516                    self.start < self.end,
517                    "random_range needs an ordered, non-empty range, got {}..{}",
518                    self.start,
519                    self.end
520                );
521                self.start + (self.end - self.start) * <$ty as Unit>::unit(rng)
522            }
523        }
524
525        impl sealed::Sealed for RangeInclusive<$ty> {}
526        impl SampleRange for RangeInclusive<$ty> {
527            type Output = $ty;
528
529            fn sample_from<R: Rng + ?Sized>(self, rng: &mut R) -> $ty {
530                let start = *self.start();
531                let end = *self.end();
532                assert!(
533                    start < end,
534                    "random_range needs an ordered, non-empty range, got {start}..={end}"
535                );
536                start + (end - start) * <$ty as Unit>::unit(rng)
537            }
538        }
539    )*};
540}
541
542impl_uint_range!(u8, u16, u32, u64, usize);
543impl_int_range!(i8, i16, i32, i64, isize);
544impl_float_range!(f32, f64);
545
546/// The full domain of a fixed-width integer, a uniform draw.
547///
548/// `u64` and `i64` take one raw draw; narrower types take the top bits of a
549/// draw. Signed types reinterpret those bits rather than drawing a magnitude.
550macro_rules! impl_sample_small {
551    ($(($ty:ident, $unsigned:ty)),* $(,)?) => {$(
552        impl sealed::Sealed for $ty {}
553        impl Sample for $ty {
554            fn sample<R: Rng + ?Sized>(rng: &mut R) -> $ty {
555                let bits = rng.next_u64() >> (64 - core::mem::size_of::<$unsigned>() * 8);
556                (bits as $unsigned) as $ty
557            }
558        }
559    )*};
560}
561
562impl sealed::Sealed for u64 {}
563impl Sample for u64 {
564    fn sample<R: Rng + ?Sized>(rng: &mut R) -> u64 {
565        rng.next_u64()
566    }
567}
568
569impl sealed::Sealed for i64 {}
570impl Sample for i64 {
571    fn sample<R: Rng + ?Sized>(rng: &mut R) -> i64 {
572        rng.next_u64() as i64
573    }
574}
575
576impl_sample_small!(
577    (u32, u32),
578    (u16, u16),
579    (u8, u8),
580    (i32, u32),
581    (i16, u16),
582    (i8, u8)
583);
584
585impl sealed::Sealed for bool {}
586impl Sample for bool {
587    fn sample<R: Rng + ?Sized>(rng: &mut R) -> bool {
588        rng.bool()
589    }
590}
591
592impl sealed::Sealed for f32 {}
593impl Sample for f32 {
594    fn sample<R: Rng + ?Sized>(rng: &mut R) -> f32 {
595        rng.unit_f32()
596    }
597}
598
599impl sealed::Sealed for f64 {}
600impl Sample for f64 {
601    fn sample<R: Rng + ?Sized>(rng: &mut R) -> f64 {
602        rng.unit_f64()
603    }
604}
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609
610    const SEED: u64 = 0x0123_4567_89AB_CDEF;
611
612    #[test]
613    fn core_matches_the_published_xoshiro256starstar_vectors() {
614        // rand_xoshiro 0.7.0's `reference` test: state words 1, 2, 3, 4, as the
615        // reference implementation produces for that seed.
616        let mut rng = SimRng::from_words([1, 2, 3, 4]);
617        let want = [
618            11520,
619            0,
620            1509978240,
621            1215971899390074240,
622            1216172134540287360,
623            607988272756665600,
624            16172922978634559625,
625            8476171486693032832,
626            10595114339597558777,
627            2904607092377533576,
628        ];
629        for value in want {
630            assert_eq!(rng.next_u64(), value);
631        }
632    }
633
634    #[test]
635    fn seeded_stream_is_pinned_word_for_word() {
636        let mut rng = SimRng::seeded(SEED);
637        let want = [
638            11728116837925579837,
639            431261241542867727,
640            7088239201150201886,
641            1991960781942118182,
642            16071698363884655823,
643            4123588241367215080,
644            13086776817198750337,
645            9243408305086729258,
646        ];
647        for value in want {
648            assert_eq!(rng.next_u64(), value);
649        }
650        assert_eq!(SimRng::seeded(0).state_words()[0], 0xE220_A839_7B1D_CDAF);
651    }
652
653    #[test]
654    fn seeded_zero_is_a_stream_not_a_fixed_point() {
655        let mut rng = SimRng::seeded(0);
656        for _ in 0..8 {
657            assert_ne!(rng.next_u64(), 0);
658        }
659        // A single zero word is legal: only the all-zero state traps xoshiro.
660        let mut mostly_zero = SimRng::from_words([0, 1, 2, 3]);
661        assert_ne!(mostly_zero.next_u64(), 0);
662        // And the trap itself is remapped rather than left to emit zeros.
663        assert_eq!(
664            SimRng::from_words([0; 4]).state_words(),
665            SimRng::seeded(0).state_words()
666        );
667    }
668
669    #[test]
670    fn sampling_is_pinned_value_for_value() {
671        let mut rng = SimRng::seeded(SEED);
672        assert_eq!(rng.next_u32(), 2730664992);
673        assert_eq!(rng.next_u32(), 100410832);
674
675        let mut rng = SimRng::seeded(SEED);
676        assert_eq!(rng.unit_f32(), 0.63578248_f32);
677
678        let mut rng = SimRng::seeded(SEED);
679        assert_eq!(rng.unit_f64(), 0.6357824877421369_f64);
680        assert!(rng.unit_f64() < 1.0);
681
682        let mut rng = SimRng::seeded(SEED);
683        assert_eq!(
684            [rng.below(6), rng.below(6), rng.below(6), rng.below(6)],
685            [3, 0, 2, 0]
686        );
687        assert_eq!(SimRng::seeded(SEED).below(0), 0);
688        assert_eq!(SimRng::seeded(SEED).below_u64(1 << 40), 699050238008);
689
690        let mut rng = SimRng::seeded(SEED);
691        assert_eq!(
692            [rng.bool(), rng.bool(), rng.bool(), rng.bool()],
693            [true, false, false, false]
694        );
695        assert_eq!(SimRng::seeded(SEED).random::<u8>(), 162);
696        assert_eq!(SimRng::seeded(SEED).random::<i32>(), -1564302304);
697        assert_eq!(SimRng::seeded(SEED).random::<u64>(), 11728116837925579837);
698    }
699
700    #[test]
701    fn ranges_are_pinned_and_agree_with_their_primitives() {
702        let mut rng = SimRng::seeded(SEED);
703        assert_eq!(
704            [
705                rng.random_range(0..6),
706                rng.random_range(0..6),
707                rng.random_range(0..6),
708                rng.random_range(0..6),
709                rng.random_range(0..6),
710                rng.random_range(0..6),
711                rng.random_range(0..6),
712                rng.random_range(0..6),
713            ],
714            [3, 0, 2, 0, 5, 1, 4, 3]
715        );
716        let mut rng = SimRng::seeded(SEED);
717        assert_eq!(
718            [
719                rng.random_range(2..=7),
720                rng.random_range(2..=7),
721                rng.random_range(2..=7),
722                rng.random_range(2..=7),
723            ],
724            [5, 2, 4, 2]
725        );
726        let mut rng = SimRng::seeded(SEED);
727        assert_eq!(
728            rng.random_range(0.0..1.0_f32),
729            SimRng::seeded(SEED).unit_f32()
730        );
731        let mut rng = SimRng::seeded(SEED);
732        assert_eq!(
733            [rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0)],
734            [0.27156496047973633, -0.9532426595687866]
735        );
736
737        // The widest ranges are the ones where a naive span computation
738        // overflows: the full domains of `u64` and `i64` both still sample, one
739        // raw draw each.
740        assert_eq!(
741            SimRng::seeded(SEED).random_range(u64::MIN..=u64::MAX),
742            11728116837925579837
743        );
744        assert_eq!(
745            SimRng::seeded(SEED).random_range(i64::MIN..=i64::MAX),
746            2504744801070804029
747        );
748        // A one-wide range consumes its draw and always lands on it.
749        assert_eq!(SimRng::seeded(SEED).random_range(0..1_u32), 0);
750    }
751
752    #[test]
753    fn a_pick_is_one_unbiased_draw_and_a_shuffle_is_pinned() {
754        let mut rng = SimRng::seeded(SEED);
755        let want = [3, 0, 1, 0, 4, 1, 3, 2];
756        for index in want {
757            assert_eq!(rng.pick_index(5), Some(index));
758        }
759        assert_eq!(rng.pick_index(0), None);
760        assert_eq!(rng.pick(&[] as &[u8]), None);
761        assert_eq!(rng.pick(&[10, 11, 12, 13, 14]), Some(&14));
762
763        let mut deck = [0, 1, 2, 3, 4, 5, 6, 7];
764        SimRng::seeded(SEED).shuffle(&mut deck);
765        assert_eq!(deck, [7, 1, 4, 3, 6, 2, 0, 5]);
766
767        let mut empty: [u8; 0] = [];
768        SimRng::seeded(SEED).shuffle(&mut empty);
769        let mut single = [9];
770        SimRng::seeded(SEED).shuffle(&mut single);
771        assert_eq!(single, [9]);
772    }
773
774    #[test]
775    fn below_uses_its_whole_range_without_modulo_bias() {
776        let mut rng = SimRng::seeded(7);
777        let mut faces = [0usize; 6];
778        for _ in 0..60_000 {
779            faces[rng.below(6) as usize] += 1;
780        }
781        for (face, count) in faces.iter().enumerate() {
782            assert!(
783                (9_200..=10_800).contains(count),
784                "face {face} came up {count} times in 60_000 draws"
785            );
786        }
787    }
788
789    #[test]
790    fn weighted_index_follows_its_weights() {
791        let mut rng = SimRng::seeded(2);
792        assert_eq!(rng.weighted_index(&[1.0, 2.0, 5.0]), Some(0));
793        let mut counts = [0usize; 3];
794        let mut rng = SimRng::seeded(11);
795        for _ in 0..30_000 {
796            counts[rng.weighted_index(&[1.0, 2.0, 5.0]).unwrap()] += 1;
797        }
798        for (index, want) in [(0, 3_750), (1, 7_500), (2, 18_750)] {
799            let got = counts[index];
800            assert!(
801                (want * 92 / 100..want * 108 / 100).contains(&got),
802                "index {index} came up {got} times, expected about {want}"
803            );
804        }
805        let mut rng = SimRng::seeded(SEED);
806        let before = rng.state_words();
807        assert_eq!(rng.weighted_index(&[0.0, 0.0]), None);
808        assert_eq!(rng.weighted_index(&[]), None);
809        assert_eq!(
810            rng.state_words(),
811            before,
812            "a refused weight consumes no draw"
813        );
814        assert_eq!(
815            SimRng::seeded(SEED).weighted_index(&[f32::NAN, -1.0, 2.0]),
816            Some(2)
817        );
818    }
819
820    #[test]
821    fn a_save_resumes_the_identical_stream() {
822        let mut original = SimRng::seeded(SEED);
823        for _ in 0..13 {
824            original.next_u64();
825        }
826        let mut restored = SimRng::from_words(original.state_words());
827        assert_eq!(restored.state_words(), original.state_words());
828        assert_eq!(
829            restored.state_words(),
830            [
831                0x22F8_BA83_280F_7AD2,
832                0x5DD0_0A53_80BC_904C,
833                0x54FF_F2A6_2C07_7DDF,
834                0x1C14_3DD2_89E7_2E17
835            ]
836        );
837        for _ in 0..8 {
838            assert_eq!(restored.next_u64(), original.next_u64());
839        }
840    }
841
842    #[test]
843    fn random_bool_clamps_and_consumes_nothing_for_a_certainty() {
844        let mut rng = SimRng::seeded(SEED);
845        assert!(!rng.random_bool(0.0));
846        assert!(rng.random_bool(1.0));
847        assert!(!rng.random_bool(-1.0));
848        assert!(rng.random_bool(2.0));
849        assert_eq!(rng.state_words(), SimRng::seeded(SEED).state_words());
850        assert!(rng.random_bool(0.75), "0.63578.. is below 0.75");
851    }
852
853    #[test]
854    #[should_panic(expected = "must not be NaN")]
855    fn random_bool_rejects_nan() {
856        SimRng::seeded(SEED).random_bool(f64::NAN);
857    }
858
859    #[test]
860    #[should_panic(expected = "non-empty range")]
861    fn an_empty_range_is_a_bug_not_a_default() {
862        SimRng::seeded(SEED).random_range(4..4);
863    }
864
865    #[test]
866    #[should_panic(expected = "non-empty range")]
867    fn a_reversed_float_range_is_a_bug() {
868        SimRng::seeded(SEED).random_range(1.0..=0.0);
869    }
870
871    #[test]
872    fn a_seed_series_is_the_documented_hazard_derivation_avoids() {
873        // Seeded streams a GAMMA apart share three words: that is why a game
874        // must not build seeds by adding a constant.
875        let a = SimRng::seeded(SEED).state_words();
876        let b = SimRng::seeded(SEED.wrapping_add(SPLITMIX_GAMMA)).state_words();
877        let shared = a.iter().filter(|word| b.contains(word)).count();
878        assert_eq!(shared, 3, "the hazard this module documents is real");
879
880        // Derived tags are mixed first, so no tag pair reproduces it.
881        let seed = WorldSeed::new(0xDEAD_BEEF);
882        for tag in 0..64u64 {
883            let near = seed.stream(tag.wrapping_add(1)).state_words();
884            let far = seed.stream(tag).state_words();
885            assert!(far.iter().all(|word| !near.contains(word)));
886        }
887    }
888
889    #[test]
890    fn world_seed_streams_are_distinct_and_additive_changes_do_not_shift_them() {
891        let seed = WorldSeed::new(0xDEAD_BEEF);
892        let mut born = false;
893        let mut seen = std::collections::HashSet::new();
894        for tag in 0..64 {
895            if tag == 32 {
896                born = true;
897            }
898            let mut rng = seed.stream(tag);
899            for _ in 0..64 {
900                assert!(seen.insert(rng.next_u64()), "two streams collided");
901            }
902        }
903        assert!(born);
904        // Deriving is pure: the same tag reads the same stream whatever else ran.
905        assert_eq!(
906            seed.stream(7).next_u64(),
907            WorldSeed::new(0xDEAD_BEEF).stream(7).next_u64()
908        );
909        assert_eq!(seed.stream(0).next_u64(), 0xC400_5AD9_14EA_B622);
910        assert_eq!(seed.stream(7).next_u64(), 0x8BBB_8B00_BC67_7185);
911        let base = SimRng::seeded(seed.get()).next_u64();
912        assert_ne!(
913            seed.stream(0).next_u64(),
914            base,
915            "tag 0 must not be the bare seeded stream"
916        );
917    }
918
919    #[test]
920    fn named_and_entity_streams_are_stable_and_apart_from_the_bare_stream() {
921        let seed = WorldSeed::new(0xDEAD_BEEF);
922        assert_eq!(seed.named("loot").next_u64(), 0x68F0_61C5_2FF8_2575);
923        assert_eq!(seed.named("loot").next_u64(), seed.named("loot").next_u64());
924        assert_ne!(seed.named("loot").next_u64(), seed.named("Loot").next_u64());
925
926        let mut world = World::new();
927        let entity = world.spawn_empty().id();
928        assert_eq!(
929            seed.stream_for_entity(entity).next_u64(),
930            seed.stream(entity.to_bits()).next_u64()
931        );
932        assert_eq!(
933            seed.stream_for_entity(entity).next_u64(),
934            seed.stream_for_entity(entity).next_u64()
935        );
936        assert_ne!(
937            seed.stream_for_entity(entity).next_u64(),
938            seed.stream(0).next_u64()
939        );
940    }
941
942    #[test]
943    fn a_resource_can_be_seeded_and_serialised() {
944        fn assert_serde<T: Serialize + serde::de::DeserializeOwned>() {}
945        assert_serde::<SimRng>();
946        assert_serde::<WorldSeed>();
947
948        let mut world = World::new();
949        world.insert_resource(SimRng::seeded(SEED));
950        world.insert_resource(WorldSeed::new(SEED));
951        assert_eq!(
952            world.resource_mut::<SimRng>().random_range(0..6),
953            SimRng::seeded(SEED).random_range(0..6)
954        );
955        assert_eq!(world.resource::<WorldSeed>().get(), SEED);
956    }
957
958    #[test]
959    fn a_mutable_borrow_is_a_stream() {
960        fn draw(rng: &mut impl Rng) -> u64 {
961            rng.next_u64()
962        }
963        let mut rng = SimRng::seeded(SEED);
964        let mut borrow = &mut rng;
965        let through_borrow = draw(&mut borrow);
966        assert_eq!(through_borrow, SimRng::seeded(SEED).next_u64());
967    }
968}