From 7d28818e2345ae790e127fcb94bdc8edd4c00b55 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sat, 22 Aug 2026 21:33:29 +0200 Subject: [PATCH] test(game): exercise two minutes of autoplay Add a seeded autoplay scenario that performs a deliberate full-strength charge for every launcher ball and operates both flippers from live ball position. Turn the two-minute run into an end-to-end test covering repeated launches, flipper edges, bumpers, targets, lock holes, claw capture/release, and drains while rejecting non-finite state. Document autoplay as the long-run validation entry point and record the expanded runtime coverage boundary. Test Plan: - `cargo test --all-targets` -- 48 passed - `cargo clippy --all-targets -- -D warnings` -- passed - `cargo build --profile production` -- passed - Windows GNU and macOS x86_64 `cargo check` -- passed - `git diff --cached --check` -- passed --- tdkpin-rs/README.md | 6 ++-- tdkpin-rs/RECONSTRUCTION.md | 4 +++ tdkpin-rs/src/simulation.rs | 67 ++++++++++++++++++++++++++++++++++--- 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/tdkpin-rs/README.md b/tdkpin-rs/README.md index c040416..07fec0f 100644 --- a/tdkpin-rs/README.md +++ b/tdkpin-rs/README.md @@ -44,8 +44,10 @@ cargo run -- --simulate claw-6 --at 0.15 \ ``` Use `--step N` instead of `--at SECONDS` to reproduce one exact update. The -available scenarios are `launcher`, `flippers`, `claw-1`, `claw-6`, `claw-7`, -and `claw-18`; `--seed N` fixes random choices for later seeded scenarios. +available scenarios are `autoplay`, `launcher`, `flippers`, `claw-1`, `claw-6`, +`claw-7`, and `claw-18`; `--seed N` fixes random choices. `autoplay` charges +each ball and operates the flippers from live ball position for long end-to-end +validation runs. ## Original and modern controls diff --git a/tdkpin-rs/RECONSTRUCTION.md b/tdkpin-rs/RECONSTRUCTION.md index 00a2cad..e4ccff3 100644 --- a/tdkpin-rs/RECONSTRUCTION.md +++ b/tdkpin-rs/RECONSTRUCTION.md @@ -66,6 +66,10 @@ decoded, build-ready subset; it does not replace that evidence archive. terminal scenarios are driven by a 120 Hz validation clock while production physics accumulates the recovered 100 Hz substep. Traces include the last collision-object id and can export the logical 640x460 render target. +- End-to-end gameplay coverage: seeded autoplay charges each launcher ball and + operates both flippers from ball position. The two-minute acceptance run + covers repeated launches, both flippers, targets, bumpers, lock holes, a claw + capture/release pair, and drains while checking every state for finite values. - Semantic boundary: no claim is made that every trajectory or score tick is bit-identical to the 16-bit executable. Remaining numeric inference is listed above instead of being presented as proven parity. diff --git a/tdkpin-rs/src/simulation.rs b/tdkpin-rs/src/simulation.rs index 0d08de5..c13b111 100644 --- a/tdkpin-rs/src/simulation.rs +++ b/tdkpin-rs/src/simulation.rs @@ -13,6 +13,7 @@ const MAX_SIMULATION_STEPS: u64 = 72_000; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Scenario { + Autoplay, Launcher, Flippers, Claw1, @@ -24,6 +25,7 @@ pub enum Scenario { impl Scenario { pub const fn name(self) -> &'static str { match self { + Self::Autoplay => "autoplay", Self::Launcher => "launcher", Self::Flippers => "flippers", Self::Claw1 => "claw-1", @@ -39,7 +41,7 @@ impl Scenario { Self::Claw6 => Some(6), Self::Claw7 => Some(7), Self::Claw18 => Some(18), - Self::Launcher | Self::Flippers => None, + Self::Autoplay | Self::Launcher | Self::Flippers => None, } } } @@ -49,6 +51,7 @@ impl FromStr for Scenario { fn from_str(value: &str) -> Result { match value { + "autoplay" => Ok(Self::Autoplay), "launcher" => Ok(Self::Launcher), "flippers" => Ok(Self::Flippers), "claw-1" => Ok(Self::Claw1), @@ -56,7 +59,7 @@ impl FromStr for Scenario { "claw-7" => Ok(Self::Claw7), "claw-18" => Ok(Self::Claw18), _ => Err(format!( - "unknown scenario {value:?}; expected launcher, flippers, claw-1, claw-6, claw-7, or claw-18" + "unknown scenario {value:?}; expected autoplay, launcher, flippers, claw-1, claw-6, claw-7, or claw-18" )), } } @@ -140,7 +143,7 @@ fn seconds_to_step(value: &str) -> Result { } pub const fn usage() -> &'static str { - "Usage:\n tdkpin-rs\n tdkpin-rs --simulate SCENARIO [--at SECONDS | --step N] [--screenshot FILE.png] [--trace FILE.json] [--seed N]\n\nScenarios: launcher, flippers, claw-1, claw-6, claw-7, claw-18" + "Usage:\n tdkpin-rs\n tdkpin-rs --simulate SCENARIO [--at SECONDS | --step N] [--screenshot FILE.png] [--trace FILE.json] [--seed N]\n\nScenarios: autoplay, launcher, flippers, claw-1, claw-6, claw-7, claw-18" } #[derive(Debug, Serialize, PartialEq)] @@ -184,6 +187,7 @@ pub struct Simulation { step: u64, game: Game, trace: Vec, + autoplay_launcher_frames: u32, } impl Simulation { @@ -197,6 +201,7 @@ impl Simulation { step: 0, game, trace: Vec::new(), + autoplay_launcher_frames: 0, }; simulation.record(initial_events); simulation @@ -204,7 +209,11 @@ impl Simulation { pub fn advance_to(&mut self, target_step: u64) { while self.step < target_step { - let controls = controls_for(self.scenario, self.step); + let controls = if self.scenario == Scenario::Autoplay { + self.autoplay_controls() + } else { + controls_for(self.scenario, self.step) + }; let events = self.game.update(SIMULATION_DT, 3, controls); self.step += 1; self.record(events); @@ -263,10 +272,31 @@ impl Simulation { .collect(), }); } + + fn autoplay_controls(&mut self) -> Controls { + if self.game.ball.in_launcher { + let launch_down = self.autoplay_launcher_frames < 132; + self.autoplay_launcher_frames += 1; + Controls { + launch_down, + ..Controls::default() + } + } else { + self.autoplay_launcher_frames = 0; + Controls { + left_flipper: self.game.ball.position.y > 340.0 + && self.game.ball.position.x < 175.0, + right_flipper: self.game.ball.position.y > 340.0 + && self.game.ball.position.x > 140.0, + ..Controls::default() + } + } + } } fn controls_for(scenario: Scenario, step: u64) -> Controls { match scenario { + Scenario::Autoplay => unreachable!("autoplay controls need mutable simulation state"), Scenario::Launcher => Controls { launch_down: step < u64::from(SIMULATION_HZ), ..Controls::default() @@ -333,6 +363,35 @@ mod tests { assert_eq!(first.trace, second.trace); } + #[test] + fn autoplay_exercises_two_minutes_of_complete_gameplay() { + let mut simulation = Simulation::new(Scenario::Autoplay, 7); + simulation.advance_to(u64::from(SIMULATION_HZ) * 120); + let event_count = |name: &str| { + simulation + .trace + .iter() + .flat_map(|snapshot| &snapshot.events) + .filter(|event| event.as_str() == name) + .count() + }; + + assert!(event_count("Launch") >= 3); + assert!(event_count("FlipperMove") >= 10); + assert!(event_count("Bumper") >= 1); + assert!(event_count("Target") >= 1); + assert!(event_count("Lock") >= 1); + assert!(event_count("ClawCapture") >= 1); + assert_eq!(event_count("ClawCapture"), event_count("ClawRelease")); + assert!(event_count("Drain") >= 1); + assert!(simulation.trace.iter().all(|snapshot| { + snapshot.ball.x.is_finite() + && snapshot.ball.y.is_finite() + && snapshot.ball.velocity_x.is_finite() + && snapshot.ball.velocity_y.is_finite() + })); + } + #[test] fn launcher_scenario_holds_for_one_second_then_releases() { let mut simulation = Simulation::new(Scenario::Launcher, 1);