pure_rng/lib.rs
1//! PureRng is a [`rand`](https://crates.io/crates/rand)-compatible RNG library
2//! for generating repeatable, controlled random values, designed primarily for
3//! use in games. It uses a hash function to generate exactly one random value
4//! per seed. A convenient API is provided to distribute seed values throughout
5//! your program in a hierarchical fashion.
6//!
7//! # Usage
8//!
9//! ```rust
10//! use pure_rng::prelude::*;
11//!
12//! // Create the root rng using an initial seed, typically from an external source.
13//! let rng = PureRng::new(1234);
14//!
15//! // Seed two more RNGs with arbitrary labels.
16//! // These effectively have their seed appended to that of the parent.
17//! let rng_a = rng.seed("a convenient label");
18//! let rng_b = rng.seed("a different label");
19//!
20//! // Generate a value from the first RNG, consuming it.
21//! let value_a: u32 = rng_a.random();
22//!
23//! // Create two more forks of the second RNG, and generate values from them inline.
24//! let value_b1: i64 = rng_b.seed(1).random();
25//! let value_b2: f64 = rng_b.seed(2).random();
26//!
27//! // Use your custom types
28//! #[derive(Hash)]
29//! struct Point { x: i32, y: i32 }
30//!
31//! let value_from_point: u64 = rng
32//! .seed(Point { x: 10, y: 12 })
33//! .random();
34//!
35//! // Use the all the usual `rand` API features
36//! let character = rng
37//! .seed("character")
38//! .sample(rand::distr::Alphanumeric) as char;
39//! ```
40//!
41//! For more information see the README, the
42//! [accompanying blog post](https://jcd.pub/2025/03/14/pure-rng/), and the examples
43pub mod prelude;
44mod rng_core;
45pub mod seq;
46
47use rand::distr::uniform::SampleRange;
48use rand::distr::uniform::SampleUniform;
49use rand::distr::Distribution;
50use rand::distr::StandardUniform;
51use rand::Fill;
52use rand::Rng;
53use std::hash::{Hash, Hasher};
54
55/// Default generator using Rapidhash
56#[cfg(feature = "rapidhash")]
57pub type PureRng = PureRandomGenerator<rapidhash::RapidHasher>;
58
59/// Generic RNG type over any Hasher
60///
61/// Typically you would use [`PureRng`] for the default Rapidhash algorithm, or
62/// redefine that alias with your choice of hasher, rather than using this type
63/// directly.
64#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub struct PureRandomGenerator<H>
67where
68 H: Hasher + Default + Clone,
69{
70 hasher: H,
71}
72
73impl<H> PureRandomGenerator<H>
74where
75 H: Hasher + Default + Clone,
76{
77 /// Creates a new generator with the given hashable value as the seed. In a
78 /// game this might be the world seed, or just the system time etc.
79 ///
80 /// # Examples
81 ///
82 /// ```
83 /// use pure_rng::PureRng;
84 ///
85 /// let rng = PureRng::new("initial seed");
86 /// ```
87 pub fn new(hashable: impl Hash) -> Self {
88 Self::default().seed(hashable)
89 }
90
91 /// Forks the generator, and advances the fork's state by hashing the given
92 /// value.
93 ///
94 /// This is the core of the API - sometimes called "splitting" or "forking"
95 /// an RNG. The difference is that with PureRng you split every time you
96 /// generate a new value.
97 ///
98 /// # Examples
99 ///
100 /// ```
101 /// use pure_rng::PureRng;
102 ///
103 /// let rng = PureRng::new("initial seed");
104 /// let sub_rng = rng.seed("a convenient label to differentiate");
105 /// let ten_values: Vec<u64> = (0..10).map(|i| sub_rng.seed(i).random()).collect();
106 ///
107 /// #[derive(Hash)]
108 /// struct Point { x: i32, y: i32 }
109 ///
110 /// let value_from_point: u64 = rng
111 /// .seed(Point { x: 10, y: 12 })
112 /// .random();
113 /// ```
114 ///
115 /// More usage example in the readme, and in `examples/complex.rs`
116 #[inline]
117 pub fn seed(&self, hashable: impl Hash) -> Self {
118 let mut fork = self.clone();
119 hashable.hash(&mut fork.hasher);
120
121 fork
122 }
123}
124/// Wrappers for the [`Rng`] trait functions.
125impl<H> PureRandomGenerator<H>
126where
127 H: Hasher + Default + Clone,
128{
129 /// Return a random value via the [`StandardUniform`] distribution.
130 ///
131 /// See [`Rng::random`].
132 ///
133 /// [`StandardUniform`]: rand::distr::StandardUniform
134 #[inline]
135 pub fn random<T>(mut self) -> T
136 where
137 StandardUniform: Distribution<T>,
138 {
139 Rng::random(&mut self)
140 }
141
142 /// Generate a random value in the given range.
143 ///
144 /// See [`Rng::random_range`].
145 #[track_caller]
146 pub fn random_range<T, Q>(mut self, range: Q) -> T
147 where
148 T: SampleUniform,
149 Q: SampleRange<T>,
150 {
151 Rng::random_range(&mut self, range)
152 }
153
154 /// Sample a new value, using the given distribution.
155 ///
156 /// See [`Rng::sample`].
157 pub fn sample<T, D: Distribution<T>>(mut self, distr: D) -> T {
158 Rng::sample(&mut self, distr)
159 }
160
161 /// Create an iterator that generates values using the given distribution.
162 ///
163 /// See [`Rng::sample_iter`].
164 pub fn sample_iter<T, D>(self, distr: D) -> rand::distr::Iter<D, Self, T>
165 where
166 D: Distribution<T>,
167 Self: Sized,
168 {
169 Rng::sample_iter(self, distr)
170 }
171
172 /// Fill any type implementing [`Fill`] with random data.
173 ///
174 /// See [`Rng::fill`].
175 #[track_caller]
176 pub fn fill<T: Fill + ?Sized>(mut self, dest: &mut T) {
177 Rng::fill(&mut self, dest)
178 }
179
180 /// Return a bool with a probability `p` of being true.
181 ///
182 /// See [`Rng::random_bool`].
183 #[inline]
184 #[track_caller]
185 pub fn random_bool(mut self, p: f64) -> bool {
186 Rng::random_bool(&mut self, p)
187 }
188
189 /// Return a bool with a probability of `numerator/denominator` of being
190 /// true.
191 ///
192 /// See [`Rng::random_ratio`].
193 #[inline]
194 #[track_caller]
195 pub fn random_ratio(mut self, numerator: u32, denominator: u32) -> bool {
196 Rng::random_ratio(&mut self, numerator, denominator)
197 }
198}
199
200#[cfg(test)]
201pub(crate) mod tests {
202 use crate::PureRng;
203
204 #[test]
205 fn test_repeatable() {
206 let rng = PureRng::default();
207
208 let val_1: u64 = rng.seed("lol").random();
209 let val_2: u64 = rng.seed("lol").random();
210 assert_eq!(val_1, val_2);
211
212 let rng_2 = rng.seed("foo");
213
214 let val_3: u64 = rng_2.seed("lol").random();
215 let val_4: u64 = rng_2.seed("lol").random();
216 assert_eq!(val_3, val_4);
217
218 assert_ne!(val_1, val_3);
219
220 let rng_3 = rng_2.seed("bar");
221
222 let val_5: u64 = rng_3.seed("lol").random();
223 let val_6: u64 = rng_3.seed("lol").random();
224 assert_eq!(val_5, val_6);
225
226 assert_ne!(val_3, val_5);
227 assert_ne!(val_2, val_5);
228 }
229}