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}