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
This commit is contained in:
+4
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<Self, Self::Err> {
|
||||
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<u64, String> {
|
||||
}
|
||||
|
||||
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<Snapshot>,
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user