From 4b84460c93951da96df67895c12559480e4e2ea2 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sun, 23 Aug 2026 17:49:04 +0200 Subject: [PATCH] fix(game): restore the 281-frame panel sequence Replace the immediate five-hole cleanup with the original 281-callback panel state machine. Suspend physics, clear contact/item state at the recovered boundaries, and emit WAVE 2013, WAVE 2012, and stop events at frames 1/45/58/66/129/159/222/230/238 before resuming after cleanup. Decode DAT600's four AND/OR mask-image pairs into transparent native textures and render all nine phase formulas with the original source rectangles, destination offsets, variable heights, and opaque wheel copies. Add a `panel` simulation scenario for deterministic trace and framebuffer validation. Test Plan: - `cargo test --all-targets` -- passed, 72 tests - `cargo clippy --all-targets -- -D warnings` -- passed - `rumdl check CHANGELOG.md README.md RECONSTRUCTION.md` -- passed - rendered panel frames 1, 66, 159, 238, and completion -- visually verified - `git diff --cached --check` -- passed --- tdkpin-rs/CHANGELOG.md | 3 + tdkpin-rs/README.md | 4 +- tdkpin-rs/RECONSTRUCTION.md | 2 +- tdkpin-rs/src/app.rs | 147 ++++++++++++++++++++++++++++++++++++ tdkpin-rs/src/assets.rs | 52 ++++++++++++- tdkpin-rs/src/game.rs | 69 ++++++++++++++++- tdkpin-rs/src/simulation.rs | 37 ++++++++- 7 files changed, 303 insertions(+), 11 deletions(-) diff --git a/tdkpin-rs/CHANGELOG.md b/tdkpin-rs/CHANGELOG.md index 547633b..8a48a7e 100644 --- a/tdkpin-rs/CHANGELOG.md +++ b/tdkpin-rs/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to ### Fixed +- Restore the complete 281-callback DAT600 panel sequence after the fifth lock + hole, including physics suspension, contact/item cleanup, all nine image + phases, and the original WAVE 2013/2012/stop boundaries. - Remove the invented attract-screen instruction panel and stop assigning the original F2/F3 keys to modern overlays. The idle view is the original table; optional settings/high-score viewers move to F10/F9. diff --git a/tdkpin-rs/README.md b/tdkpin-rs/README.md index 68645d3..65297ee 100644 --- a/tdkpin-rs/README.md +++ b/tdkpin-rs/README.md @@ -44,8 +44,8 @@ cargo run -- --simulate claw-6 --at 0.15 \ ``` Use `--step N` instead of `--at SECONDS` to reproduce one exact update. The -available scenarios are `autoplay`, `launcher`, `flippers`, `claw-1`, `claw-6`, -`claw-7`, and `claw-18`; `--seed N` fixes random choices. `autoplay` charges +available scenarios are `autoplay`, `launcher`, `flippers`, `panel`, `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. diff --git a/tdkpin-rs/RECONSTRUCTION.md b/tdkpin-rs/RECONSTRUCTION.md index 3dd9826..45fc65b 100644 --- a/tdkpin-rs/RECONSTRUCTION.md +++ b/tdkpin-rs/RECONSTRUCTION.md @@ -25,7 +25,7 @@ implementation. | Playfield collision layout | Recovered | All 109 active type-2 line objects and 40 static active type-1 circles are transcribed from the original 175-object registration table. The registration routine converts its sideways inputs with `screen = (y, x - 20)` and accumulates explicitly relative objects. Type-2 records retain every recovered Real48 normal/tangent response pair and registered one-sided orientation. Type-1 records retain their swept-circle radius, radial rebound, tangent coupling, and bumper kick. Each flipper uses its exact two line records plus moving tip circle in both positions. Moving-flipper contact ports `1000:7ed9` rather than fitting live samples: delta-specific pivots/edges, integer cross gates, radial/penetration calculations, response-record gain, and position/velocity publication are tested against all four C harness directions and the raised release geometry. Object 174 is overwritten with the live first ball and Rust handles its ball-to-ball role directly. | | Ball launcher and nudge input | Recovered | The initial 32-bit fixed-point coordinates decode to `(325, 413)` in the right shooter lane. Each Down keydown subtracts `15*50 = 750` millipixels, release subtracts another `15*100 = 1500`, and the result follows the recovered randomized `-3800` lower and `-2280` weak upper clamp branches. The ten decoration frames use the same strict 750-millipixel thresholds. Left Shift and keypad 3 apply their directional `(50-Random(20))*15` impulses; Space uses the recovered Real48 horizontal factor and `(60-Random(20))*15` vertical impulse. Each nudge adds 25 to the wrapping 16-bit tilt counter, compares it with `30+Random(10)`, and the detail timer decrements a nonzero counter once per callback. | | Physics arithmetic | Recovered Real48 core | Production movement uses the original 10 ms millipixel substep, `+15` vertical acceleration, Real48 `3800/speed` clamp, type-2 distance/cross gates, type-1 midpoint normal, surface-distance candidate ordering, persistent Real48 spin, and the common impulse response. Dynamic records 174/175 transfer normal impulse to the other ball before applying `normal_velocity-1000` to the moving ball. The original Borland seed update, high-word `Random(n)`, and normalized Real48 random register result drive launcher variation, effects, claw terminals, magnetic fields, and trigger response. Type-4 triggers retain a separate transient entry flag; type-3 captures retain per-player 16-bit contact words, deep-inside pull/hold progression to age 300, first-contact sound, and `99`/`2` completion sentinels. Zero-spin C harnesses and retained-spin Wine probes are tested separately rather than conflated. | -| Rules | Recovered gameplay paths | Player count, controls, the five three-line bumper-value groups, four three-line TDK-diamond groups, five doubling-value lock holes, wheel-reset target, seven-way effect selector/consumer including multiball, permanent double scoring, and four exact media/extra-ball thresholds follow original help/code paths, globals, and object flags. The ninth diamond pays the original 24,464 completion value; the following completed bank enables double scoring, and later completions add 100,000 to the per-player secondary score. Turn changes mirror the original save/load of all 175 collision record states: wheel/top targets, active/contact slots, selected effect, and multiball readiness remain attached to their player. Claw contact and all initially active type-4 targets use recovered records. The top three targets score 500 each and independently enable the left, center, or right magnetic field record; each field pulls the ball upward until it exits and then deactivates. The claw state machine and release table have live differential coverage for all four random terminals. | +| Rules | Recovered gameplay paths | Player count, controls, the five three-line bumper-value groups, four three-line TDK-diamond groups, five doubling-value lock holes, wheel-reset target, seven-way effect selector/consumer including multiball, permanent double scoring, and four exact media/extra-ball thresholds follow original help/code paths, globals, and object flags. The ninth diamond pays the original 24,464 completion value; the following completed bank enables double scoring, and later completions add 100,000 to the per-player secondary score. Completing all five lock holes suspends physics for the full 281-callback DAT600 panel animation, clears contact/item state at the recovered boundaries, and uses the exact WAVE 2013/2012/stop sequence. Turn changes mirror the original save/load of all 175 collision record states: wheel/top targets, active/contact slots, selected effect, and multiball readiness remain attached to their player. Claw contact and all initially active type-4 targets use recovered records. The top three targets score 500 each and independently enable the left, center, or right magnetic field record; each field pulls the ball upward until it exits and then deactivates. The claw state machine and release table have live differential coverage for all four random terminals. | | Numeric scoring | Recovered gameplay values | Static scores come from the initialized 175-object ledger. Dynamic bumper progression, target-bank completion, diamond awards, 10k-160k lock bonuses, 310k transfer, six effect values, multiball mode, and all four media thresholds are transcribed from `1000:b476`, `1000:c4e1`, `1000:bc36`, and live state probes. Lock and effect awards share the original per-player secondary score and display multiplier; the fifth hole transfers and clears it, increments the multiplier, and grants the recovered ball award. Score mutation uses the original 32-bit wrapping behavior, and each add operation can advance at most one media threshold. | | High scores | Recovered visible flow; portable storage | The original 276-byte table is decoded as ten `IWIK`-XOR-obfuscated little-endian scores plus ten 22-byte names. Each player is checked immediately when their own last ball is lost; qualifying scores use the original signed-high/unsigned-low comparison and a `TDK Pinball Player`-prefilled name screen before the table is shown and play resumes. Persisted updates use portable JSON rather than rewriting the Win16 file. | | Configuration | Behaviorally compatible | Sound, language, and five detail levels are retained. Storage moves from a local Win16 INI file to the platform user-data directory. | diff --git a/tdkpin-rs/src/app.rs b/tdkpin-rs/src/app.rs index e27ed87..d10db07 100644 --- a/tdkpin-rs/src/app.rs +++ b/tdkpin-rs/src/app.rs @@ -216,6 +216,10 @@ impl App { } fn play_event(&self, event: Event) { + if event == Event::StopSound { + self.assets.stop_all_sounds(); + return; + } if let Some(resource) = event.sound_resource() { self.assets.play(resource, self.saved.settings.sounds); } @@ -363,6 +367,10 @@ impl App { return; }; + if let Some(frame) = game.panel_frame { + self.draw_panel_animation(frame); + } + if game.wheel_animation > 0.0 { let frame = animation_frame(f64::from(0.65 - game.wheel_animation), 18.0, 5); draw_texture_ex( @@ -508,6 +516,120 @@ impl App { } } + #[allow(clippy::cast_precision_loss, clippy::too_many_lines)] + fn draw_panel_animation(&self, frame: u16) { + draw_texture_region(&self.assets.active_table, 79.0, 0.0, 91.0, 123.0, 79.0, 0.0); + let frame = i32::from(frame); + let narrow_a = &self.assets.panel_pair_narrow_a; + let narrow_b = &self.assets.panel_pair_narrow_b; + let wide_a = &self.assets.panel_pair_wide_a; + let wide_b = &self.assets.panel_pair_wide_b; + + if (1..=44).contains(&frame) { + draw_texture_region( + narrow_a, + 99.0, + 0.0, + 45.0, + (frame * 2) as f32, + 0.0, + ((45 - frame) * 2 - 1) as f32, + ); + } else if (45..=57).contains(&frame) { + draw_texture_region(narrow_a, 99.0, 0.0, 45.0, 88.0, 0.0, 0.0); + } else if (58..=65).contains(&frame) { + draw_texture_region(narrow_b, 99.0, 0.0, 45.0, 88.0, 0.0, 0.0); + } else if (66..=128).contains(&frame) { + draw_texture_region(&self.assets.wheel, 79.0, 33.0, 91.0, 90.0, 180.0, 90.0); + if frame < 83 { + draw_texture_region(wide_a, 79.0, ((82 - frame) * 2) as f32, 91.0, 90.0, 0.0, 0.0); + } else { + let height = (128 - frame) * 2; + draw_texture_region( + wide_a, + 79.0, + 0.0, + 91.0, + height as f32, + 0.0, + ((frame - 82) * 2) as f32, + ); + } + if frame < 111 { + let height = (110 - frame) * 2; + draw_texture_region( + narrow_b, + 99.0, + 0.0, + 45.0, + height as f32, + 0.0, + ((frame - 66) * 2) as f32, + ); + } + } else if (129..=158).contains(&frame) { + draw_texture_region(&self.assets.wheel, 79.0, 33.0, 91.0, 90.0, 180.0, 90.0); + } else if (159..=221).contains(&frame) { + draw_texture_region(&self.assets.wheel, 79.0, 33.0, 91.0, 90.0, 180.0, 90.0); + if frame < 204 { + let height = (frame - 159) * 2; + draw_texture_region( + wide_b, + 79.0, + 0.0, + 91.0, + height as f32, + 0.0, + ((204 - frame) * 2) as f32, + ); + } else { + draw_texture_region( + wide_b, + 79.0, + ((frame - 204) * 2) as f32, + 91.0, + 90.0, + 0.0, + 0.0, + ); + } + if frame > 176 { + if frame == 221 { + draw_texture_region(narrow_b, 99.0, 0.0, 45.0, 88.0, 0.0, 0.0); + } else { + let height = (frame - 177) * 2; + draw_texture_region( + narrow_b, + 99.0, + 0.0, + 45.0, + height as f32, + 0.0, + ((222 - frame) * 2 - 1) as f32, + ); + } + } + } else if (222..=229).contains(&frame) { + draw_texture_region(wide_b, 79.0, 33.0, 91.0, 90.0, 0.0, 0.0); + draw_texture_region(narrow_b, 99.0, 0.0, 45.0, 88.0, 0.0, 0.0); + } else if (230..=237).contains(&frame) { + draw_texture_region(wide_b, 79.0, 33.0, 91.0, 90.0, 0.0, 0.0); + draw_texture_region(narrow_a, 99.0, 0.0, 45.0, 88.0, 0.0, 0.0); + } else if (238..=281).contains(&frame) { + draw_texture_region(wide_b, 79.0, 33.0, 91.0, 90.0, 0.0, 0.0); + let height = (281 - frame) * 2; + draw_texture_region( + narrow_a, + 99.0, + 0.0, + 45.0, + height as f32, + 0.0, + ((frame - 238) * 2) as f32, + ); + } + } + #[allow(clippy::cast_precision_loss)] fn draw_displays(&self, game: &Game) { for region in [ @@ -751,6 +873,31 @@ fn draw_panel(x: f32, y: f32, width: f32, height: f32) { draw_rectangle_lines(x + 4.0, y + 4.0, width - 8.0, height - 8.0, 2.0, DARKGRAY); } +fn draw_texture_region( + texture: &Texture2D, + destination_x: f32, + destination_y: f32, + width: f32, + height: f32, + source_x: f32, + source_y: f32, +) { + if width <= 0.0 || height <= 0.0 { + return; + } + draw_texture_ex( + texture, + destination_x, + destination_y, + WHITE, + DrawTextureParams { + dest_size: Some(vec2(width, height)), + source: Some(Rect::new(source_x, source_y, width, height)), + ..Default::default() + }, + ); +} + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn animation_frame(elapsed: f64, frames_per_second: f64, frame_count: usize) -> usize { ((elapsed.max(0.0) * frames_per_second) as usize) % frame_count diff --git a/tdkpin-rs/src/assets.rs b/tdkpin-rs/src/assets.rs index 89148ec..7101479 100644 --- a/tdkpin-rs/src/assets.rs +++ b/tdkpin-rs/src/assets.rs @@ -1,5 +1,5 @@ use macroquad::{ - audio::{PlaySoundParams, Sound, load_sound_from_bytes, play_sound}, + audio::{PlaySoundParams, Sound, load_sound_from_bytes, play_sound, stop_sound}, prelude::{FilterMode, Image, Texture2D}, }; @@ -11,6 +11,10 @@ pub struct Assets { pub media: [Texture2D; 4], pub diamond: [Texture2D; 9], pub wheel: Texture2D, + pub panel_pair_narrow_a: Texture2D, + pub panel_pair_narrow_b: Texture2D, + pub panel_pair_wide_a: Texture2D, + pub panel_pair_wide_b: Texture2D, pub robot: Texture2D, pub plunger: Texture2D, pub ball: Texture2D, @@ -48,7 +52,12 @@ impl Assets { texture(include_bytes!("../assets/original/images/dat_00808.png")), texture(include_bytes!("../assets/original/images/dat_00809.png")), ]; - let wheel = texture(include_bytes!("../assets/original/images/dat_00600.png")); + let wheel_bytes = include_bytes!("../assets/original/images/dat_00600.png"); + let wheel = texture(wheel_bytes); + let panel_pair_narrow_a = masked_atlas_pair(wheel_bytes, 0, 45, 45, 90); + let panel_pair_narrow_b = masked_atlas_pair(wheel_bytes, 90, 135, 45, 90); + let panel_pair_wide_a = masked_atlas_pair(wheel_bytes, 271, 362, 91, 90); + let panel_pair_wide_b = masked_atlas_pair(wheel_bytes, 453, 362, 91, 90); let robot = texture(include_bytes!("../assets/original/images/dat_00900.png")); let plunger = texture(include_bytes!("../assets/original/images/dat_00901.png")); let ball = masked_texture( @@ -139,6 +148,10 @@ impl Assets { media, diamond, wheel, + panel_pair_narrow_a, + panel_pair_narrow_b, + panel_pair_wide_a, + panel_pair_wide_b, robot, plunger, ball, @@ -161,6 +174,12 @@ impl Assets { ); } } + + pub fn stop_all_sounds(&self) { + for (_, sound) in &self.sounds { + stop_sound(sound); + } + } } fn texture(bytes: &[u8]) -> Texture2D { @@ -200,3 +219,32 @@ fn monochrome_texture(bytes: &[u8]) -> Texture2D { texture.set_filter(FilterMode::Nearest); texture } + +fn masked_atlas_pair( + bytes: &[u8], + image_x: usize, + mask_x: usize, + width: usize, + height: usize, +) -> Texture2D { + let atlas = Image::from_file_with_format(bytes, None).expect("embedded DAT600 must decode"); + let atlas_width = usize::from(atlas.width); + let mut output = Image { + bytes: vec![0; width * height * 4], + width: u16::try_from(width).unwrap_or(0), + height: u16::try_from(height).unwrap_or(0), + }; + for y in 0..height { + for x in 0..width { + let output_index = (y * width + x) * 4; + let image_index = ((y + 90) * atlas_width + image_x + x) * 4; + let mask_index = ((y + 90) * atlas_width + mask_x + x) * 4; + output.bytes[output_index..output_index + 3] + .copy_from_slice(&atlas.bytes[image_index..image_index + 3]); + output.bytes[output_index + 3] = u8::MAX - atlas.bytes[mask_index]; + } + } + let texture = Texture2D::from_image(&output); + texture.set_filter(FilterMode::Nearest); + texture +} diff --git a/tdkpin-rs/src/game.rs b/tdkpin-rs/src/game.rs index 413f56f..9192502 100644 --- a/tdkpin-rs/src/game.rs +++ b/tdkpin-rs/src/game.rs @@ -66,6 +66,7 @@ pub enum Event { Tilt, Drain, HighScoreCandidate(u32), + StopSound, Sound(u16), } @@ -87,7 +88,8 @@ impl Event { | Self::Nudge | Self::Tilt | Self::Drain - | Self::HighScoreCandidate(_) => None, + | Self::HighScoreCandidate(_) + | Self::StopSound => None, } } } @@ -267,6 +269,7 @@ pub struct Game { pub claw: Claw, pub bumper_flash: [f32; 3], pub wheel_animation: f32, + pub panel_frame: Option, pub nudge_shake: f32, pub launcher_charge: f32, pub finished: bool, @@ -310,6 +313,7 @@ impl Game { claw: Claw::default(), bumper_flash: [0.0; 3], wheel_animation: 0.0, + panel_frame: None, nudge_shake: 0.0, launcher_charge: 0.0, finished: false, @@ -452,7 +456,8 @@ impl Game { fn timer_tick(&mut self, elapsed: f32, substeps: u8, events: &mut Vec) { self.update_claw(elapsed, events); - if !self.claw.ball_suspended { + let panel_active = self.update_panel_completion(events); + if !self.claw.ball_suspended && !panel_active { for _ in 0..substeps { self.fixed_update(STEP_SECONDS, events); self.advance_secondary_ball(events); @@ -488,6 +493,31 @@ impl Game { self.apply_flipper_kicks(); } + fn update_panel_completion(&mut self, events: &mut Vec) -> bool { + let Some(frame) = self.panel_frame else { + return false; + }; + if frame >= 281 { + self.panel_frame = None; + self.wheel_holes.fill(false); + return true; + } + if frame == 1 { + for record_id in 129..=133 { + self.record_contacts[record_id] = 0; + } + } + let next = frame + 1; + self.panel_frame = Some(next); + match next { + 1 | 66 | 159 | 238 => events.push(Event::Sound(2013)), + 45 | 129 | 222 => events.push(Event::StopSound), + 58 | 230 => events.push(Event::Sound(2012)), + _ => {} + } + true + } + fn advance_tilt_counter(&mut self, elapsed: f32) { self.tilt_counter_accumulator += elapsed; while self.tilt_counter_accumulator >= self.claw_frame_seconds { @@ -1025,6 +1055,7 @@ impl Game { player.secondary_score = 0; player.score_multiplier = player.score_multiplier.wrapping_add(1); player.extra_balls = player.extra_balls.wrapping_add(1); + self.panel_frame = Some(0); } self.reset_ball_to_launcher(); events.push(Event::Lock); @@ -1354,6 +1385,7 @@ impl Game { self.tilt_counter_accumulator = 0.0; self.bumper_flash.fill(0.0); self.wheel_animation = 0.0; + self.panel_frame = None; self.claw = Claw::default(); self.flipper_inputs = Flippers::default(); self.flippers = Flippers::default(); @@ -2410,6 +2442,39 @@ mod tests { assert_eq!(game.capture_age, 0); } + #[test] + fn panel_completion_uses_all_281_frames_and_original_sound_boundaries() { + let mut game = Game::new(1); + game.panel_frame = Some(0); + game.wheel_holes.fill(true); + for record_id in 129..=133 { + game.record_contacts[record_id] = 99; + } + let mut events = Vec::new(); + + for _ in 0..282 { + game.timer_tick(0.030, 0, &mut events); + } + + assert_eq!(game.panel_frame, None); + assert_eq!(game.wheel_holes, [false; 5]); + assert!(game.record_contacts[129..=133].iter().all(|contact| *contact == 0)); + assert_eq!( + events, + [ + Event::Sound(2013), + Event::StopSound, + Event::Sound(2012), + Event::Sound(2013), + Event::StopSound, + Event::Sound(2013), + Event::StopSound, + Event::Sound(2012), + Event::Sound(2013), + ] + ); + } + #[test] fn claw_release_table_decodes_the_original_thousandth_pixel_coordinates() { for (frame, expected_position, expected_velocity) in [ diff --git a/tdkpin-rs/src/simulation.rs b/tdkpin-rs/src/simulation.rs index 3eeb391..8794908 100644 --- a/tdkpin-rs/src/simulation.rs +++ b/tdkpin-rs/src/simulation.rs @@ -16,6 +16,7 @@ pub enum Scenario { Autoplay, Launcher, Flippers, + Panel, Claw1, Claw6, Claw7, @@ -28,6 +29,7 @@ impl Scenario { Self::Autoplay => "autoplay", Self::Launcher => "launcher", Self::Flippers => "flippers", + Self::Panel => "panel", Self::Claw1 => "claw-1", Self::Claw6 => "claw-6", Self::Claw7 => "claw-7", @@ -41,7 +43,7 @@ impl Scenario { Self::Claw6 => Some(6), Self::Claw7 => Some(7), Self::Claw18 => Some(18), - Self::Autoplay | Self::Launcher | Self::Flippers => None, + Self::Autoplay | Self::Launcher | Self::Flippers | Self::Panel => None, } } } @@ -54,12 +56,13 @@ impl FromStr for Scenario { "autoplay" => Ok(Self::Autoplay), "launcher" => Ok(Self::Launcher), "flippers" => Ok(Self::Flippers), + "panel" => Ok(Self::Panel), "claw-1" => Ok(Self::Claw1), "claw-6" => Ok(Self::Claw6), "claw-7" => Ok(Self::Claw7), "claw-18" => Ok(Self::Claw18), _ => Err(format!( - "unknown scenario {value:?}; expected autoplay, launcher, flippers, claw-1, claw-6, claw-7, or claw-18" + "unknown scenario {value:?}; expected autoplay, launcher, flippers, panel, claw-1, claw-6, claw-7, or claw-18" )), } } @@ -143,7 +146,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: autoplay, 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, panel, claw-1, claw-6, claw-7, claw-18" } #[derive(Debug, Serialize, PartialEq)] @@ -157,6 +160,7 @@ pub struct Snapshot { pub flippers: FlipperSnapshot, pub launcher_charge: f32, pub collision_id: Option, + pub panel_frame: Option, pub events: Vec, } @@ -194,6 +198,10 @@ pub struct Simulation { impl Simulation { pub fn new(scenario: Scenario, seed: u32) -> Self { let mut game = Game::new_with_seed(1, seed); + if scenario == Scenario::Panel { + game.panel_frame = Some(0); + game.wheel_holes.fill(true); + } let initial_events = scenario .claw_terminal() .map_or_else(Vec::new, |terminal| game.begin_claw_scenario(terminal)); @@ -274,6 +282,7 @@ impl Simulation { }, launcher_charge: self.game.launcher_charge, collision_id: self.game.last_collision_id, + panel_frame: self.game.panel_frame, events: events .into_iter() .map(|event| format!("{event:?}")) @@ -317,7 +326,7 @@ fn controls_for(scenario: Scenario, step: u64) -> Controls { ..Controls::default() } } - Scenario::Claw1 | Scenario::Claw6 | Scenario::Claw7 | Scenario::Claw18 => { + Scenario::Panel | Scenario::Claw1 | Scenario::Claw6 | Scenario::Claw7 | Scenario::Claw18 => { Controls::default() } } @@ -432,6 +441,26 @@ mod tests { assert!(!simulation.game.flippers.left_raised); } + #[test] + fn panel_scenario_runs_all_frames_and_cleans_up() { + let mut simulation = Simulation::new(Scenario::Panel, 1); + simulation.advance_to(4); + assert_eq!(simulation.game.panel_frame, Some(1)); + assert_eq!(simulation.trace[4].events, ["Sound(2013)"]); + + simulation.advance_to(1_020); + assert_eq!(simulation.game.panel_frame, None); + assert_eq!(simulation.game.wheel_holes, [false; 5]); + let events = simulation + .trace + .iter() + .flat_map(|snapshot| snapshot.events.iter().map(String::as_str)) + .collect::>(); + assert_eq!(events.iter().filter(|event| **event == "StopSound").count(), 3); + assert_eq!(events.iter().filter(|event| **event == "Sound(2013)").count(), 4); + assert_eq!(events.iter().filter(|event| **event == "Sound(2012)").count(), 2); + } + #[test] fn every_claw_scenario_completes_its_seeded_capture() { for scenario in [