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.
26
27pub trait BattleRng {
28    /// Return the next raw byte in the stream (`0..=255`).
29    ///
30    /// This is the lowest-level primitive and the one most faithful to the
31    /// original 8-bit hardware RNG. Higher-level helpers derive from it.
32    fn next_u8(&mut self) -> u8;
33
34    /// Uniform integer in `[0, bound)`.
35    ///
36    /// Returns `0` when `bound == 0`. The default implementation folds
37    /// successive bytes; games that need the *exact* original modulo behaviour
38    /// (including its bias) should override this.
39    fn range(&mut self, bound: u32) -> u32 {
40        if bound == 0 {
41            return 0;
42        }
43        if bound <= 256 {
44            // Single byte covers the range; plain modulo mirrors the 8-bit
45            // hardware path (and its bias) used by the original game.
46            return (self.next_u8() as u32) % bound;
47        }
48        // Wider ranges: assemble enough bytes, then reduce.
49        let mut acc: u32 = 0;
50        let mut produced: u64 = 1;
51        while produced < bound as u64 {
52            acc = acc.wrapping_shl(8) | self.next_u8() as u32;
53            produced = produced.saturating_mul(256);
54        }
55        acc % bound
56    }
57
58    /// Coin flip succeeding with probability `num / den`.
59    ///
60    /// Returns `false` when `den == 0`. Used for crit checks, the speed-tie
61    /// flip, the 255/256-style hit checks, status-proc rolls, etc.
62    fn chance(&mut self, num: u32, den: u32) -> bool {
63        if den == 0 {
64            return false;
65        }
66        self.range(den) < num
67    }
68}
69
70/// A [`BattleRng`] that replays a fixed script of bytes, then repeats the last
71/// byte (or `0` if empty) forever.
72///
73/// This makes driver behaviour fully deterministic in tests: a test can pin the
74/// exact bytes the driver will observe and assert on the resulting event
75/// stream, proving turn-order ties, gates, and residuals resolve as expected.
76#[derive(Debug, Clone)]
77pub struct ScriptedRng {
78    bytes: Vec<u8>,
79    pos: usize,
80}
81
82impl ScriptedRng {
83    /// Create a scripted RNG that yields `bytes` in order.
84    pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
85        Self {
86            bytes: bytes.into(),
87            pos: 0,
88        }
89    }
90
91    /// Number of bytes consumed so far. Useful for asserting draw-order parity.
92    pub fn consumed(&self) -> usize {
93        self.pos
94    }
95}
96
97impl BattleRng for ScriptedRng {
98    fn next_u8(&mut self) -> u8 {
99        let b = if self.bytes.is_empty() {
100            0
101        } else if self.pos < self.bytes.len() {
102            self.bytes[self.pos]
103        } else {
104            *self.bytes.last().unwrap()
105        };
106        self.pos += 1;
107        b
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn scripted_yields_bytes_in_order_then_repeats_last() {
117        let mut rng = ScriptedRng::new(vec![1, 2, 3]);
118        assert_eq!(rng.next_u8(), 1);
119        assert_eq!(rng.next_u8(), 2);
120        assert_eq!(rng.next_u8(), 3);
121        // Past the end → repeats the last byte.
122        assert_eq!(rng.next_u8(), 3);
123        assert_eq!(rng.consumed(), 4);
124    }
125
126    #[test]
127    fn empty_script_yields_zero() {
128        let mut rng = ScriptedRng::new(Vec::new());
129        assert_eq!(rng.next_u8(), 0);
130        assert_eq!(rng.next_u8(), 0);
131    }
132
133    #[test]
134    fn range_zero_bound_is_zero() {
135        let mut rng = ScriptedRng::new(vec![200]);
136        assert_eq!(rng.range(0), 0);
137    }
138
139    #[test]
140    fn range_small_bound_is_modulo_of_byte() {
141        let mut rng = ScriptedRng::new(vec![10]);
142        // 10 % 4 == 2
143        assert_eq!(rng.range(4), 2);
144    }
145
146    #[test]
147    fn range_wide_bound_assembles_bytes() {
148        let mut rng = ScriptedRng::new(vec![0x00, 0x01]);
149        // bound > 256 → two bytes assembled: (0x00 << 8 | 0x01) = 1; 1 % 1000 = 1
150        assert_eq!(rng.range(1000), 1);
151    }
152
153    #[test]
154    fn chance_uses_range() {
155        // byte 0 → range(2) == 0 < 1 → true
156        let mut rng = ScriptedRng::new(vec![0]);
157        assert!(rng.chance(1, 2));
158        // byte 1 → range(2) == 1, not < 1 → false
159        let mut rng = ScriptedRng::new(vec![1]);
160        assert!(!rng.chance(1, 2));
161    }
162
163    #[test]
164    fn chance_zero_den_is_false() {
165        let mut rng = ScriptedRng::new(vec![0]);
166        assert!(!rng.chance(1, 0));
167    }
168}