Skip to main content

dotzuki_engine/battle/
rng.rs

1//! Injected randomness for the battle turn-execution driver (P0b).
2//!
3//! The engine is **100% game-agnostic** and must never link the `rand` crate
4//! (architecture rule C2). All randomness used by [`crate::battle::driver`]
5//! flows through the [`BattleRng`] trait, so the *game* owns the generator and
6//! therefore the exact draw sequence.
7//!
8//! Controlling the draw order game-side is essential to reproduce Gen-1 quirks
9//! such as the 1/256 "miss", critical-hit rolls, the speed-tie coin flip, and
10//! partial-trap duration rolls: the original game consumes its RNG stream in a
11//! specific order, and only the game knows that order.
12//!
13//! ## Determinism
14//!
15//! Implementations may wrap any generator (an LCG matching the original ROM, a
16//! seedable PRNG for tests, or a fixed script of bytes). The driver only calls
17//! the methods on this trait; it makes no assumptions about the distribution
18//! beyond the documented contracts below.
19
20/// A source of randomness for the battle driver.
21///
22/// The single required method is [`BattleRng::next_u8`]; the others have
23/// default implementations layered on top of it so most games only implement
24/// one method. Implementations are free to override any of them to match an
25/// exact original-game draw sequence.
26pub trait BattleRng {
27    /// Return the next raw byte in the stream (`0..=255`).
28    ///
29    /// This is the lowest-level primitive and the one most faithful to the
30    /// original 8-bit hardware RNG. Higher-level helpers derive from it.
31    fn next_u8(&mut self) -> u8;
32
33    /// Uniform integer in `[0, bound)`.
34    ///
35    /// Returns `0` when `bound == 0`. The default implementation folds
36    /// successive bytes; games that need the *exact* original modulo behaviour
37    /// (including its bias) should override this.
38    fn range(&mut self, bound: u32) -> u32 {
39        if bound == 0 {
40            return 0;
41        }
42        if bound <= 256 {
43            // Single byte covers the range; plain modulo mirrors the 8-bit
44            // hardware path (and its bias) used by the original game.
45            return (self.next_u8() as u32) % bound;
46        }
47        // Wider ranges: assemble enough bytes, then reduce.
48        let mut acc: u32 = 0;
49        let mut produced: u64 = 1;
50        while produced < bound as u64 {
51            acc = acc.wrapping_shl(8) | self.next_u8() as u32;
52            produced = produced.saturating_mul(256);
53        }
54        acc % bound
55    }
56
57    /// Coin flip succeeding with probability `num / den`.
58    ///
59    /// Returns `false` when `den == 0`. Used for crit checks, the speed-tie
60    /// flip, the 255/256-style hit checks, status-proc rolls, etc.
61    fn chance(&mut self, num: u32, den: u32) -> bool {
62        if den == 0 {
63            return false;
64        }
65        self.range(den) < num
66    }
67}
68
69/// A [`BattleRng`] that replays a fixed script of bytes, then repeats the last
70/// byte (or `0` if empty) forever.
71///
72/// This makes driver behaviour fully deterministic in tests: a test can pin the
73/// exact bytes the driver will observe and assert on the resulting event
74/// stream, proving turn-order ties, gates, and residuals resolve as expected.
75#[derive(Debug, Clone)]
76pub struct ScriptedRng {
77    bytes: Vec<u8>,
78    pos: usize,
79}
80
81impl ScriptedRng {
82    /// Create a scripted RNG that yields `bytes` in order.
83    pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
84        Self {
85            bytes: bytes.into(),
86            pos: 0,
87        }
88    }
89
90    /// Number of bytes consumed so far. Useful for asserting draw-order parity.
91    pub fn consumed(&self) -> usize {
92        self.pos
93    }
94}
95
96impl BattleRng for ScriptedRng {
97    fn next_u8(&mut self) -> u8 {
98        let b = if self.bytes.is_empty() {
99            0
100        } else if self.pos < self.bytes.len() {
101            self.bytes[self.pos]
102        } else {
103            *self.bytes.last().unwrap()
104        };
105        self.pos += 1;
106        b
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn scripted_yields_bytes_in_order_then_repeats_last() {
116        let mut rng = ScriptedRng::new(vec![1, 2, 3]);
117        assert_eq!(rng.next_u8(), 1);
118        assert_eq!(rng.next_u8(), 2);
119        assert_eq!(rng.next_u8(), 3);
120        // Past the end → repeats the last byte.
121        assert_eq!(rng.next_u8(), 3);
122        assert_eq!(rng.consumed(), 4);
123    }
124
125    #[test]
126    fn empty_script_yields_zero() {
127        let mut rng = ScriptedRng::new(Vec::new());
128        assert_eq!(rng.next_u8(), 0);
129        assert_eq!(rng.next_u8(), 0);
130    }
131
132    #[test]
133    fn range_zero_bound_is_zero() {
134        let mut rng = ScriptedRng::new(vec![200]);
135        assert_eq!(rng.range(0), 0);
136    }
137
138    #[test]
139    fn range_small_bound_is_modulo_of_byte() {
140        let mut rng = ScriptedRng::new(vec![10]);
141        // 10 % 4 == 2
142        assert_eq!(rng.range(4), 2);
143    }
144
145    #[test]
146    fn range_wide_bound_assembles_bytes() {
147        let mut rng = ScriptedRng::new(vec![0x00, 0x01]);
148        // bound > 256 → two bytes assembled: (0x00 << 8 | 0x01) = 1; 1 % 1000 = 1
149        assert_eq!(rng.range(1000), 1);
150    }
151
152    #[test]
153    fn chance_uses_range() {
154        // byte 0 → range(2) == 0 < 1 → true
155        let mut rng = ScriptedRng::new(vec![0]);
156        assert!(rng.chance(1, 2));
157        // byte 1 → range(2) == 1, not < 1 → false
158        let mut rng = ScriptedRng::new(vec![1]);
159        assert!(!rng.chance(1, 2));
160    }
161
162    #[test]
163    fn chance_zero_den_is_false() {
164        let mut rng = ScriptedRng::new(vec![0]);
165        assert!(!rng.chance(1, 0));
166    }
167}