Skip to main content

banished_core/
random.rs

1//! The game's random number generator: a Mersenne Twister (MT19937).
2//!
3//! The whole simulation draws from one [`Random`], seeded with the map seed. A port must draw the
4//! same numbers in the same order as the original, so every call here matches one call of the
5//! engine's `System::Random`.
6//!
7//! The state is the same as the engine object's: a `u32` index and 624 state words. A save stores
8//! the index, then the words ([`Random::from_parts`] restores them).
9//!
10//! ```
11//! use banished_core::random::Random;
12//!
13//! let mut rng = Random::new(1234);
14//! let die = rng.rand_range(1, 6);
15//! assert!((1..=6).contains(&die));
16//! ```
17//!
18//! Original: `System::Random`, Runtime `0x1800ca560` to `0x1800ca8b0`.
19
20/// An optional log of draws, to compare the port's draws with the original's.
21///
22/// The original's draws are recorded in the emulator (`tools/emu/rng_trace.py`), and
23/// `tools/triage/rngdiff.py` finds the first draw that differs. Only the outermost draw of a call
24/// is recorded (`rand_float` calling `rand` logs once), with the location of the Rust caller.
25pub mod trace {
26    use std::cell::{Cell, RefCell};
27    use std::panic::Location;
28
29    #[derive(Debug, Clone, PartialEq, Eq)]
30    /// One recorded draw.
31    pub struct Draw {
32        /// `Rand`, `Rand(n)`, `Rand(lo,hi)`, `RandTol(t)`, `RandFloat`, `RandFloatTol`.
33        pub kind: String,
34        /// The Rust code that made the draw.
35        pub at: &'static Location<'static>,
36    }
37
38    thread_local! {
39        static LOG: RefCell<Option<Vec<Draw>>> = const { RefCell::new(None) };
40        static DEPTH: Cell<u32> = const { Cell::new(0) };
41    }
42
43    /// Start recording on this thread (clears any previous log).
44    pub fn start() {
45        LOG.with(|l| *l.borrow_mut() = Some(Vec::new()));
46    }
47
48    /// Stop recording and return the draws.
49    pub fn take() -> Vec<Draw> {
50        LOG.with(|l| l.borrow_mut().take()).unwrap_or_default()
51    }
52
53    /// Write draws as `kind<TAB>caller` lines (the format `rngdiff.py` reads).
54    pub fn write(path: &std::path::Path, draws: &[Draw]) -> std::io::Result<()> {
55        let text: String = draws
56            .iter()
57            .map(|d| format!("{}\t{}:{}\n", d.kind, d.at.file(), d.at.line()))
58            .collect();
59        std::fs::write(path, text)
60    }
61
62    pub(super) struct Guard;
63
64    impl Drop for Guard {
65        fn drop(&mut self) {
66            DEPTH.with(|d| d.set(d.get() - 1));
67        }
68    }
69
70    /// Record one draw unless already inside another draw.
71    #[track_caller]
72    pub(super) fn enter(kind: impl FnOnce() -> String) -> Guard {
73        let at = Location::caller();
74        let outer = DEPTH.with(|d| {
75            let v = d.get();
76            d.set(v + 1);
77            v == 0
78        });
79        if outer {
80            LOG.with(|l| {
81                if let Some(log) = l.borrow_mut().as_mut() {
82                    log.push(Draw { kind: kind(), at });
83                }
84            });
85        }
86        Guard
87    }
88}
89
90const N: usize = 624;
91const M: usize = 397;
92
93/// The game's random number generator. See the [module documentation](self).
94#[derive(Clone, PartialEq, Eq)]
95pub struct Random {
96    index: u32,
97    state: [u32; N],
98}
99
100impl std::fmt::Debug for Random {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        f.debug_struct("Random")
103            .field("index", &self.index)
104            .finish_non_exhaustive()
105    }
106}
107
108impl Random {
109    /// A generator seeded with `seed`, as the engine seeds one.
110    ///
111    /// Original: `Random::Random(unsigned int)`, Runtime `0x1800ca740`.
112    pub fn new(seed: u32) -> Random {
113        let mut state = [0u32; N];
114        state[0] = seed;
115        for i in 1..N {
116            let prev = state[i - 1];
117            state[i] = (prev ^ (prev >> 30))
118                .wrapping_mul(0x6c07_8965)
119                .wrapping_add(i as u32);
120        }
121        Random { index: 0, state }
122    }
123
124    /// A generator restored from a save: its index, then its 624 state words.
125    pub fn from_parts(index: u32, state: [u32; N]) -> Random {
126        Random { index, state }
127    }
128
129    /// The position of the next word in the state.
130    pub fn index(&self) -> u32 {
131        self.index
132    }
133
134    /// The 624 state words.
135    pub fn state(&self) -> &[u32; N] {
136        &self.state
137    }
138
139    /// `NextGroup` (0x1800ca640).
140    fn next_group(&mut self) {
141        for i in 0..N {
142            let y = (self.state[i] & 0x8000_0000) | (self.state[(i + 1) % N] & 0x7fff_ffff);
143            let mut v = self.state[(i + M) % N] ^ (y >> 1);
144            if y & 1 != 0 {
145                v ^= 0x9908_b0df;
146            }
147            self.state[i] = v;
148        }
149    }
150
151    /// The next number, any `u32`.
152    ///
153    /// Original: `Rand()`, Runtime `0x1800ca800`.
154    #[track_caller]
155    pub fn rand(&mut self) -> u32 {
156        let _t = trace::enter(|| "Rand".into());
157        if self.index == 0 {
158            self.next_group();
159        }
160        let mut y = self.state[self.index as usize];
161        y ^= y >> 11;
162        y ^= (y & 0xff3a_58ad) << 7;
163        y ^= (y & 0xffff_df8c) << 15;
164        self.index = if self.index + 1 == N as u32 {
165            0
166        } else {
167            self.index + 1
168        };
169        y ^ (y >> 18)
170    }
171
172    /// A number from 0 to `n - 1`. When `n` is 0, it returns 0 and draws nothing.
173    ///
174    /// Original: `Rand(unsigned int)`, Runtime `0x1800ca8b0`.
175    #[track_caller]
176    pub fn rand_below(&mut self, n: u32) -> u32 {
177        let _t = trace::enter(|| format!("Rand({n})"));
178        if n == 0 {
179            return 0;
180        }
181        self.rand() % n
182    }
183
184    /// A number from `lo` to `hi`, both included.
185    ///
186    /// Original: `Rand(int, int)`, Runtime `0x1800ca5e0`.
187    #[track_caller]
188    pub fn rand_range(&mut self, lo: i32, hi: i32) -> i32 {
189        let _t = trace::enter(|| format!("Rand({lo},{hi})"));
190        let r = self.rand();
191        lo.wrapping_add((r % ((hi.wrapping_sub(lo) as u32).wrapping_add(1))) as i32)
192    }
193
194    /// A number from `-t` to `t`, both included.
195    ///
196    /// Original: `RandTol(int)`, Runtime `0x1800ca610`.
197    #[track_caller]
198    pub fn rand_tol(&mut self, t: i32) -> i32 {
199        let _t = trace::enter(|| format!("RandTol({t})"));
200        let r = self.rand();
201        (r % ((t as u32).wrapping_mul(2).wrapping_add(1))) as i32 - t
202    }
203
204    /// A float from 0 to 1, 1 not included: `(float)u × 2⁻³²`.
205    ///
206    /// Original: `RandFloat()`, Runtime `0x1800ca5b0`.
207    #[track_caller]
208    pub fn rand_float(&mut self) -> f32 {
209        let _t = trace::enter(|| "RandFloat".into());
210        self.rand() as f32 * 2.328_306_4e-10
211    }
212
213    /// A float from -1 to 1, -1 not included: `1 − (float)u × 2⁻³² × 2`.
214    ///
215    /// Original: `RandFloatTol()`, Runtime `0x1800ca870`.
216    #[track_caller]
217    pub fn rand_float_tol(&mut self) -> f32 {
218        let _t = trace::enter(|| "RandFloatTol".into());
219        1.0 - self.rand() as f32 * 2.328_306_4e-10 * 2.0
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn trace_records_outermost_draws_only() {
229        let mut r = Random::new(1);
230        trace::start();
231        r.rand_float();
232        r.rand_below(7);
233        let log = trace::take();
234        assert_eq!(
235            log.iter().map(|d| d.kind.as_str()).collect::<Vec<_>>(),
236            ["RandFloat", "Rand(7)"]
237        );
238        assert!(log[0].at.file().ends_with("random.rs"));
239        r.rand();
240        assert!(trace::take().is_empty());
241    }
242
243    #[test]
244    fn matches_reference_mt19937() {
245        // Reference MT19937 outputs for the canonical default seed 5489.
246        let mut r = Random::new(5489);
247        assert_eq!(r.rand(), 3_499_211_612);
248        assert_eq!(r.rand(), 581_869_302);
249        assert_eq!(r.rand(), 3_890_346_734);
250        for _ in 3..9999 {
251            r.rand();
252        }
253        assert_eq!(r.rand(), 4_123_659_995); // 10000th output
254    }
255}
256
257#[cfg(test)]
258mod golden {
259    //! Values produced by the original Runtime DLL under emulation (tools/emu/check_random.py).
260    use super::*;
261
262    #[test]
263    fn matches_original_engine() {
264        let mut r = Random::new(290);
265        let v: Vec<u32> = (0..5).map(|_| r.rand()).collect();
266        assert_eq!(
267            v,
268            [1174298092, 3342169725, 1127261350, 2332665128, 3191663389]
269        );
270        assert_eq!(
271            [r.rand_tol(0xffffff), r.rand_tol(0xffffff)],
272            [11962091, -9300278]
273        );
274        assert_eq!(
275            r.rand_float_tol().to_bits(),
276            0.20040464401245117f32.to_bits()
277        );
278        assert_eq!(
279            r.rand_float_tol().to_bits(),
280            0.34909939765930176f32.to_bits()
281        );
282    }
283}