The original RandSeed is program-global and is initialized once at startup. Rust stored the exact Borland generator inside Game but discarded its final state at game over and seeded every following game from Macroquad again, breaking stream continuity across high-score/attract flow. Expose the current seed, retain it when the App consumes a finished game, and construct the next game from that value. Attract, high-score, and portable UI continue to consume no gameplay draws. Test Plan: - `cargo test --workspace --all-targets --all-features` -- 125 passed - `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- passed - `rumdl check --flavor commonmark RECONSTRUCTION.md CHANGELOG.md` -- passed - `git diff --cached --check` -- passed
104 lines
2.8 KiB
Rust
104 lines
2.8 KiB
Rust
//! Borland Win16 random-number stream used by the original executable.
|
|
|
|
use crate::real48::Real48;
|
|
|
|
const MULTIPLIER: u32 = 0x0808_8405;
|
|
#[cfg(test)]
|
|
const TWO_TO_32: f64 = 4_294_967_296.0;
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct BorlandRandom {
|
|
seed: u32,
|
|
}
|
|
|
|
impl BorlandRandom {
|
|
pub const fn new(seed: u32) -> Self {
|
|
Self { seed }
|
|
}
|
|
|
|
pub fn next_u32(&mut self) -> u32 {
|
|
self.seed = self.seed.wrapping_mul(MULTIPLIER).wrapping_add(1);
|
|
self.seed
|
|
}
|
|
|
|
#[allow(clippy::cast_possible_truncation)]
|
|
pub fn below(&mut self, upper_bound: u16) -> u16 {
|
|
let product = u64::from(self.next_u32()) * u64::from(upper_bound);
|
|
(product >> 32) as u16
|
|
}
|
|
|
|
#[allow(clippy::cast_possible_truncation)]
|
|
pub fn real48(&mut self) -> Real48 {
|
|
let mut random = self.next_u32();
|
|
if random == 0 {
|
|
return Real48::ZERO;
|
|
}
|
|
let mut exponent = 0x80_u8;
|
|
while random & 0x8000_0000 == 0 {
|
|
random <<= 1;
|
|
exponent = exponent.wrapping_sub(1);
|
|
}
|
|
random &= 0x7fff_ffff;
|
|
Real48::from_bytes([
|
|
exponent,
|
|
0,
|
|
random as u8,
|
|
(random >> 8) as u8,
|
|
(random >> 16) as u8,
|
|
(random >> 24) as u8,
|
|
])
|
|
}
|
|
|
|
/// Exact host representation of the x87 `Random` result in `[0, 1)`.
|
|
#[cfg(test)]
|
|
pub fn unit_interval(&mut self) -> f64 {
|
|
f64::from(self.next_u32()) / TWO_TO_32
|
|
}
|
|
|
|
pub const fn seed(self) -> u32 {
|
|
self.seed
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn stream_matches_the_reconstructed_borland_runtime() {
|
|
let mut random = BorlandRandom::new(0x1234_5678);
|
|
let first = 0x1234_5678_u32.wrapping_mul(MULTIPLIER).wrapping_add(1);
|
|
assert_eq!(random.next_u32(), first);
|
|
|
|
assert_eq!(random.below(3_800), 1_810);
|
|
}
|
|
|
|
#[test]
|
|
fn zero_bounds_still_advance_the_seed() {
|
|
let mut random = BorlandRandom::new(7);
|
|
assert_eq!(random.below(0), 0);
|
|
assert_eq!(random.seed(), 7_u32.wrapping_mul(MULTIPLIER).wrapping_add(1));
|
|
}
|
|
|
|
#[test]
|
|
fn unit_interval_is_the_exact_unsigned_seed_fraction() {
|
|
let mut random = BorlandRandom::new(0xfedc_ba98);
|
|
let expected_seed = 0xfedc_ba98_u32
|
|
.wrapping_mul(MULTIPLIER)
|
|
.wrapping_add(1);
|
|
assert_eq!(
|
|
random.unit_interval().to_bits(),
|
|
(f64::from(expected_seed) / TWO_TO_32).to_bits()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn real48_register_result_matches_the_reconstructed_normalization() {
|
|
let mut random = BorlandRandom::new(7);
|
|
assert_eq!(
|
|
random.real48(),
|
|
Real48::from_bytes([0x7e, 0, 0x90, 0x70, 0xee, 0x60])
|
|
);
|
|
}
|
|
}
|