Files
tdkpin/tdkpin-rs/src/borland_random.rs
T
ddidderr 1f0085902c fix(physics): port the Borland Real48 collision core
Add a bit-exact six-byte Real48 implementation for integer conversion,
rounding, comparison, add/subtract, multiply/divide, and Newton square root.
Use the normalized Borland random-register result and route speed clamps,
segment/circle detection, moving flippers, captures, triggers, and magnetic
fields through the recovered arithmetic instead of host floating formulas.

Preserve Real48 spin per ball and feed it through the original tangent/spin
response, separating zero-spin C fixtures from retained-spin Wine traces.
Replace path-progress selection with surface-distance ordering and implement
dynamic records 174/175, including impulse transfer to the other slot,
normal_velocity-1000 response, and the second post-collision speed clamp.

Test Plan:
- `cargo test --all-targets` -- passed, 69 tests
- `cargo clippy --all-targets -- -D warnings` -- passed
- `rumdl check tdkpin-rs/CHANGELOG.md tdkpin-rs/RECONSTRUCTION.md` -- passed
- `git diff --cached --check` -- passed
2026-08-23 17:36:03 +02:00

105 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
}
#[cfg(test)]
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])
);
}
}