fix(game): use the original Borland random stream

Replace the Xorshift state with the reconstructed Borland Win16 generator,
including its wrapping linear update and high-word Random(n) mapping. Route
launcher clamp variation, effect selection, and claw terminal choices through
one shared stream so seeded event order follows the executable.

Use the same stream for the recovered magnetic-field response. Fields now scale
horizontal speed by 0.9 and choose the upward impulse from
-3800 * (1 - Random * 0.3) instead of assigning a fixed -3000 velocity. Adjust
deterministic scenario expectations where the original stream permits a second
natural claw capture or does not force one during a particular autoplay seed.

Test Plan:
- `cargo test --all-targets` -- passed, 56 tests
- `cargo clippy --all-targets -- -D warnings` -- passed
- `rumdl check CHANGELOG.md RECONSTRUCTION.md` -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-23 16:52:17 +02:00
parent 9de3dcc755
commit c25287186a
6 changed files with 92 additions and 29 deletions
+13 -23
View File
@@ -1,4 +1,5 @@
use crate::{
borland_random::BorlandRandom,
geometry::{Segment, closest_point},
original_physics::{
CollisionResponse, GRAVITY_MILLI_PER_STEP, MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec,
@@ -235,7 +236,7 @@ pub struct Game {
launcher_next_repeat: f32,
launcher_velocity_milli: i32,
player_entry: PlayerEntry,
claw_rng_state: u32,
random: BorlandRandom,
pending_flipper_edges: [i8; 2],
trigger_contacts: [bool; 176],
object_active: [bool; 176],
@@ -276,7 +277,7 @@ impl Game {
launcher_next_repeat: LAUNCHER_REPEAT_DELAY_SECONDS,
launcher_velocity_milli: 0,
player_entry: PlayerEntry::Open,
claw_rng_state: seed.max(1),
random: BorlandRandom::new(seed),
pending_flipper_edges: [0; 2],
trigger_contacts: [false; 176],
object_active: initial_object_activity(),
@@ -315,11 +316,8 @@ impl Game {
self.launcher_velocity_milli -= LAUNCHER_IMPULSE_MILLI;
let mut launch_velocity = self.launcher_velocity_milli;
if launch_velocity < -MAXIMUM_SPEED_MILLI_PER_STEP {
let variation = (self.next_random_value()
% u32::try_from(MAXIMUM_SPEED_MILLI_PER_STEP).unwrap_or(3_800))
/ 40;
launch_velocity =
-MAXIMUM_SPEED_MILLI_PER_STEP + i32::try_from(variation).unwrap_or_default();
let variation = self.random.below(3_800) / 40;
launch_velocity = -MAXIMUM_SPEED_MILLI_PER_STEP + i32::from(variation);
}
self.ball.in_launcher = false;
self.ball.velocity = MilliVec {
@@ -714,8 +712,7 @@ impl Game {
} else {
let previous = self.target_effect;
loop {
self.target_effect =
u8::try_from(self.next_random_value() % 6 + 1).unwrap_or(1);
self.target_effect = u8::try_from(self.random.below(6) + 1).unwrap_or(1);
if self.target_effect != previous {
break;
}
@@ -927,6 +924,7 @@ impl Game {
self.launcher_velocity_milli = 0;
}
#[allow(clippy::cast_possible_truncation)]
fn apply_magnetic_fields(&mut self, old_position: MilliVec, velocity: &mut MilliVec) {
for (object_id, min_x, min_y, max_x, max_y) in [
(6, 143_000, 421_000, 169_000, 452_000),
@@ -948,7 +946,9 @@ impl Game {
{
continue;
}
velocity.y = velocity.y.min(-3_000);
velocity.x = (f64::from(velocity.x) * 0.9).round() as i32;
let vertical_factor = 1.0 - self.random.unit_interval() * 0.3;
velocity.y = -(f64::from(MAXIMUM_SPEED_MILLI_PER_STEP) * vertical_factor).round() as i32;
if old_position.add(*velocity).y < min_y {
self.object_active[object_id] = false;
}
@@ -965,17 +965,7 @@ impl Game {
}
fn next_claw_terminal_frame(&mut self) -> u8 {
let value = self.next_random_value();
CLAW_TERMINAL_FRAMES[value as usize % CLAW_TERMINAL_FRAMES.len()]
}
fn next_random_value(&mut self) -> u32 {
let mut value = self.claw_rng_state;
value ^= value << 13;
value ^= value >> 17;
value ^= value << 5;
self.claw_rng_state = value;
value
CLAW_TERMINAL_FRAMES[usize::from(self.random.below(4))]
}
fn apply_flipper_kicks(&mut self) {
@@ -1870,7 +1860,7 @@ mod tests {
#[test]
fn top_sensor_uses_the_original_score_and_contact_latch() {
let mut game = Game::new(1);
let mut game = Game::new_with_seed(1, 7);
game.ball.in_launcher = false;
game.ball.position = vec2(205.0, 55.0);
let mut events = Vec::new();
@@ -1894,7 +1884,7 @@ mod tests {
},
&mut field_velocity,
);
assert_eq!(field_velocity, MilliVec { x: 0, y: -3_000 });
assert_eq!(field_velocity, MilliVec { x: 0, y: -3_550 });
game.apply_magnetic_fields(
MilliVec {
x: 15_000,