fix(game): honor original mechanism timer choices

Map detail levels 1-5 to the recovered 50, 40, 30, 20, and 10 ms callback
periods for claw animation while retaining the invariant 10 ms physics step.
This removes the hard-coded default cadence from non-default configurations.

Keep repaint batching documented separately because Rust exposes intermediate
physics states instead of reproducing Win16 GDI callback batching.

Test Plan:
- `cargo test --all-targets` -- 51 passed
- `cargo clippy --all-targets -- -D warnings` -- passed
- `cargo build --profile production` -- passed
- all five claw cadence boundary tests -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-22 21:55:21 +02:00
parent 77c93ec36b
commit 25e32b2c06
3 changed files with 34 additions and 15 deletions
+8 -10
View File
@@ -50,18 +50,16 @@ renamed.
## Explicit pending evidence
- Differentially verify the recovered 10/20/30/40/50 ms timer choices against
live original captures; the Rust port intentionally uses the default 30 ms
claw cadence independently of render rate.
- Transcribe the full fixed-point flipper impulse calculation, then compare it
with the Rust floating-point collision response.
- Force each chosen claw terminal in the live original and compare its complete
frame/trajectory sequence with the deterministic Rust trace. A controlled
collision probe has covered terminal 18; terminals 1, 6, and 7 remain.
- Transcribe the full raw fixed-point flipper impulse calculation. Production
behavior currently uses live-fitted up/down transfer curves with exact record
geometry and probe tests.
- Reproduce original callback batching for non-default detail settings. Physics
substeps and claw cadence are recovered, but Rust presents intermediate 10 ms
physics states instead of batching 1-5 substeps into one Win16 repaint.
Those items are intentionally not counted as semantic parity. The readable
C file preserves their raw constants and labels the unresolved conversion so a
later implementation cannot silently turn an estimate into claimed evidence.
C files preserve their raw constants and label unresolved batching/fitted
arithmetic so later work cannot silently turn an estimate into claimed evidence.
## Runtime comparison on 2026-08-22
+3 -2
View File
@@ -64,8 +64,9 @@ validation runs.
| Sound | F12 | - |
The five original speed choices remain available in settings. Physics now uses
the original invariant 100 Hz millipixel substep; exact setting-specific timer
batching for presentation and mechanism animation remains under reconstruction.
the original invariant 100 Hz millipixel substep, and claw animation follows the
recovered 50/40/30/20/10 ms timer choices. Setting-specific repaint batching
remains a presentation-only reconstruction boundary.
The help screen is the original artwork in English, German, French, Italian,
or Spanish. As instructed on that screen, double-clicking its upper-left exit
+23 -3
View File
@@ -31,6 +31,7 @@ const CLAW_TRIGGER_RADIUS: f32 = 19.0;
// frame. Keeping that cadence independent of render rate makes captures
// deterministic on modern machines.
const CLAW_FRAME_SECONDS: f32 = 0.030;
const DETAIL_TIMER_SECONDS: [f32; 5] = [0.050, 0.040, 0.030, 0.020, 0.010];
const CLAW_TERMINAL_FRAMES: [u8; 4] = [1, 6, 7, 18];
const ORIGINAL_BALL_SPEED_PER_SECOND: f32 = 380.0;
@@ -225,6 +226,7 @@ pub struct Game {
trigger_contacts: [bool; 176],
object_active: [bool; 176],
target_effect: u8,
claw_frame_seconds: f32,
}
impl Game {
@@ -263,6 +265,7 @@ impl Game {
trigger_contacts: [false; 176],
object_active: initial_object_activity(),
target_effect: 0,
claw_frame_seconds: CLAW_FRAME_SECONDS,
}
}
@@ -315,11 +318,12 @@ impl Game {
self.player_entry = PlayerEntry::Closed;
}
pub fn update(&mut self, frame_time: f32, _detail: u8, controls: Controls) -> Vec<Event> {
pub fn update(&mut self, frame_time: f32, detail: u8, controls: Controls) -> Vec<Event> {
if self.finished {
return Vec::new();
}
self.last_collision_id = None;
self.claw_frame_seconds = DETAIL_TIMER_SECONDS[usize::from(detail.clamp(1, 5) - 1)];
let mut events = Vec::new();
let old_flippers = self.flippers;
self.flippers.left_raised = controls.left_flipper && !self.tilted;
@@ -909,8 +913,8 @@ impl Game {
return;
}
self.claw.frame_accumulator += dt;
while self.claw.active && self.claw.frame_accumulator >= CLAW_FRAME_SECONDS {
self.claw.frame_accumulator -= CLAW_FRAME_SECONDS;
while self.claw.active && self.claw.frame_accumulator >= self.claw_frame_seconds {
self.claw.frame_accumulator -= self.claw_frame_seconds;
self.advance_claw(events);
}
}
@@ -1581,6 +1585,22 @@ mod tests {
assert!(!game.claw.active);
}
#[test]
fn claw_frame_cadence_follows_the_five_original_timer_choices() {
for (detail, frame_seconds) in DETAIL_TIMER_SECONDS.into_iter().enumerate() {
let mut game = Game::new_with_seed(1, 7);
game.begin_claw_scenario(6);
let detail = u8::try_from(detail + 1).expect("five detail levels fit u8");
game.update(0.0, detail, Controls::default());
let mut events = Vec::new();
game.update_claw(frame_seconds - 0.001, &mut events);
assert_eq!(game.claw.frame, 10);
game.update_claw(0.001_1, &mut events);
assert_eq!(game.claw.frame, 9);
}
}
#[test]
fn claw_uses_the_original_circular_trigger_not_a_broad_rectangle() {
let mut game = Game::new(1);