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}