diff --git a/tdkpin-rs/RECONSTRUCTION.md b/tdkpin-rs/RECONSTRUCTION.md index 573b60f..545a99c 100644 --- a/tdkpin-rs/RECONSTRUCTION.md +++ b/tdkpin-rs/RECONSTRUCTION.md @@ -22,7 +22,7 @@ implementation. | Artwork | Exact | All 34 custom DIB images, three standard bitmaps, icon, and palette derivatives are preserved in `assets/original/`. The game uses the original 640x460 table, loading, help, ball, wheel, robot, magnet, media, and diamond frames. | | Audio | Exact samples | All 16 mono PCM WAV resources are embedded unchanged. Their trigger roles were recovered from resource use and gameplay context. | | Help and languages | Exact | Original resource images 1001-1005 are displayed directly. | -| Playfield collision layout | Recovered | The active static line segments are transcribed from the original 175-object registration table. The original sideways coordinates are transformed with `screen = (y, 478 - x)`. The lower apron is split around the visible center drain; moving flippers and round bumpers use equivalent native Rust bodies. | +| 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. Object 174 is omitted because the original overwrites it with the live ball every frame. Moving flippers use equivalent native Rust bodies. | | Physics arithmetic | Reimplemented | The Win16 fixed-point/timer engine is replaced by deterministic fixed-step floating-point integration. Restitution and impulses are tuned to the recovered table but are not instruction-for-instruction equivalents. | | Rules | Behaviorally recovered | Player count, controls, wheel holes, magnetic saves, robot grip, four-position ball lock, target banks, increasing bumper value, nine-part TDK diamond, permanent double scoring for a completed diamond, KByte media progression, and media extra balls follow the original help and code paths. | | Numeric scoring | Partly inferred | Visible 2000-6000 target values and recovered registration values are preserved. Some bumper, bank-completion, robot, wheel, lock, and media thresholds are best-evidence reconstructions because the decompiler did not recover meaningful names or a clean rule table. | @@ -66,4 +66,5 @@ decoded, build-ready subset; it does not replace that evidence archive. - `assets.rs`: compile-time original asset embedding; - `game.rs`: fixed-step game state, recovered walls, rules, and scoring; - `geometry.rs`: segment/circle collision primitives; +- `table.rs`: source-traceable collision objects recovered from the Win16 table; - `persistence.rs`: platform paths, settings, high scores, and legacy import. diff --git a/tdkpin-rs/src/app.rs b/tdkpin-rs/src/app.rs index 84c4929..64002d1 100644 --- a/tdkpin-rs/src/app.rs +++ b/tdkpin-rs/src/app.rs @@ -2,6 +2,7 @@ use crate::{ assets::Assets, game::{Controls, Event, Game}, persistence::{HighScore, Language, Persistence, SavedData, insert_high_score}, + table::BUMPERS, }; use macroquad::prelude::*; @@ -460,13 +461,10 @@ impl App { }, ); } - for (index, center) in [vec2(207.0, 109.0), vec2(167.0, 148.0), vec2(219.0, 167.0)] - .into_iter() - .enumerate() - { + for (index, bumper) in BUMPERS.into_iter().enumerate() { if game.bumper_flash[index] > 0.0 { - draw_circle_lines(center.x, center.y, 17.0, 3.0, WHITE); - draw_circle_lines(center.x, center.y, 13.0, 2.0, YELLOW); + draw_circle_lines(bumper.center.x, bumper.center.y, 17.0, 3.0, WHITE); + draw_circle_lines(bumper.center.x, bumper.center.y, 13.0, 2.0, YELLOW); } } if game.player().diamond_segments > 0 { @@ -503,8 +501,8 @@ impl App { } for (start, end) in [ - (vec2(103.0, 394.0), vec2(58.0, 374.0)), - (vec2(211.0, 394.0), vec2(256.0, 374.0)), + (vec2(103.0, 397.0), vec2(58.0, 374.0)), + (vec2(210.0, 397.0), vec2(256.0, 374.0)), ] { draw_line(start.x, start.y, end.x, end.y, 22.0, BLACK); draw_circle(start.x, start.y, 11.0, BLACK); diff --git a/tdkpin-rs/src/game.rs b/tdkpin-rs/src/game.rs index 8831961..ef3cf59 100644 --- a/tdkpin-rs/src/game.rs +++ b/tdkpin-rs/src/game.rs @@ -1,6 +1,8 @@ -use crate::geometry::{Segment, circle_collision, segment_collision}; +use crate::{ + geometry::{Segment, circle_collision, segment_collision}, + table::{BUMPERS, PASSIVE_CIRCLES, WALLS}, +}; use macroquad::prelude::{Vec2, vec2}; -use std::sync::LazyLock; const BALL_RADIUS: f32 = 7.0; const GRAVITY: f32 = 135.0; @@ -228,59 +230,91 @@ impl Game { return; } - self.ball.velocity.y += GRAVITY * dt; - self.ball.velocity *= 1.0 - dt * 0.055; - self.ball.velocity = self.ball.velocity.clamp_length_max(MAX_SPEED); - self.ball.position += self.ball.velocity * dt; + let travel = self.ball.velocity.length() * dt; + let maximum_step_travel = BALL_RADIUS * 0.45; + let mut substeps = 1_u8; + while f32::from(substeps) * maximum_step_travel < travel && substeps < 12 { + substeps += 1; + } + let substep = dt / f32::from(substeps); + for _ in 0..substeps { + self.ball.velocity.y += GRAVITY * substep; + self.ball.velocity *= 1.0 - substep * 0.055; + self.ball.velocity = self.ball.velocity.clamp_length_max(MAX_SPEED); + self.ball.position += self.ball.velocity * substep; - for wall in table_walls() { - segment_collision( + let mut drained = false; + for wall in WALLS { + // Object 25 is the one-way shooter stop. It catches a returning + // ball, but must not reflect an upward launch from the line. + if wall.id == 25 && self.ball.velocity.y < 0.0 { + continue; + } + let hit = segment_collision( + &mut self.ball.position, + &mut self.ball.velocity, + BALL_RADIUS, + wall.segment, + ); + if hit && wall.id == 2 { + drained = true; + break; + } + } + if drained { + self.drain(events); + return; + } + + for circle in PASSIVE_CIRCLES { + debug_assert_ne!(circle.id, 174, "the live ball is not a static obstacle"); + circle_collision( + &mut self.ball.position, + &mut self.ball.velocity, + BALL_RADIUS, + circle.center, + circle.radius, + 0.0, + ); + } + + let left_flipper = flipper_segment(vec2(103.0, 397.0), self.left_flipper_angle); + let right_flipper = flipper_segment(vec2(210.0, 397.0), self.right_flipper_angle); + if segment_collision( &mut self.ball.position, &mut self.ball.velocity, - BALL_RADIUS, - *wall, - ); - } - - let left_flipper = flipper_segment(vec2(103.0, 394.0), self.left_flipper_angle); - let right_flipper = flipper_segment(vec2(211.0, 394.0), self.right_flipper_angle); - if segment_collision( - &mut self.ball.position, - &mut self.ball.velocity, - BALL_RADIUS + 2.0, - left_flipper, - ) && self.left_flipper_angle > -2.55 - { - self.ball.velocity += vec2(-20.0, -115.0); - } - if segment_collision( - &mut self.ball.position, - &mut self.ball.velocity, - BALL_RADIUS + 2.0, - right_flipper, - ) && self.right_flipper_angle < -0.58 - { - self.ball.velocity += vec2(20.0, -115.0); - } - - for (index, center) in [vec2(207.0, 109.0), vec2(167.0, 148.0), vec2(219.0, 167.0)] - .into_iter() - .enumerate() - { - let hit = circle_collision( + BALL_RADIUS + 2.0, + left_flipper, + ) && self.left_flipper_angle > -2.55 + { + self.ball.velocity += vec2(-20.0, -115.0); + } + if segment_collision( &mut self.ball.position, &mut self.ball.velocity, - BALL_RADIUS, - center, - 14.0, - 105.0, - ); - if hit && !self.tilted && self.bumper_cooldown <= 0.0 { - self.add_score(self.player().bumper_value); - self.bonus = self.bonus.saturating_add(100); - self.bumper_cooldown = 0.08; - self.bumper_flash[index] = 0.16; - events.push(Event::Bumper); + BALL_RADIUS + 2.0, + right_flipper, + ) && self.right_flipper_angle < -0.58 + { + self.ball.velocity += vec2(20.0, -115.0); + } + + for (index, bumper) in BUMPERS.into_iter().enumerate() { + let hit = circle_collision( + &mut self.ball.position, + &mut self.ball.velocity, + BALL_RADIUS, + bumper.center, + bumper.radius, + 105.0, + ); + if hit && !self.tilted && self.bumper_cooldown <= 0.0 { + self.add_score(self.player().bumper_value); + self.bonus = self.bonus.saturating_add(100); + self.bumper_cooldown = 0.08; + self.bumper_flash[index] = 0.16; + events.push(Event::Bumper); + } } } @@ -487,8 +521,8 @@ impl Game { pub fn flippers(&self) -> (Segment, Segment) { ( - flipper_segment(vec2(103.0, 394.0), self.left_flipper_angle), - flipper_segment(vec2(211.0, 394.0), self.right_flipper_angle), + flipper_segment(vec2(103.0, 397.0), self.left_flipper_angle), + flipper_segment(vec2(210.0, 397.0), self.right_flipper_angle), ) } } @@ -498,117 +532,6 @@ fn flipper_segment(pivot: Vec2, angle: f32) -> Segment { } #[allow(clippy::too_many_lines)] -fn table_walls() -> &'static [Segment] { - static WALLS: LazyLock> = LazyLock::new(|| { - vec![ - Segment::new(Vec2::new(125.0, 21.0), Vec2::new(7.0, 61.0), 0.82), - Segment::new(Vec2::new(300.0, 3.0), Vec2::new(10.0, 3.0), 0.82), - Segment::new(Vec2::new(120.0, 6.0), Vec2::new(120.0, 26.0), 0.82), - Segment::new(Vec2::new(195.0, 26.0), Vec2::new(195.0, 6.0), 0.82), - Segment::new(Vec2::new(134.0, 33.0), Vec2::new(134.0, 16.0), 0.82), - Segment::new(Vec2::new(148.0, 11.0), Vec2::new(148.0, 33.0), 0.82), - Segment::new(Vec2::new(164.0, 12.0), Vec2::new(165.0, 33.0), 0.82), - Segment::new(Vec2::new(180.0, 11.0), Vec2::new(179.0, 33.0), 0.82), - Segment::new(Vec2::new(304.0, 64.0), Vec2::new(183.0, 21.0), 0.82), - Segment::new(Vec2::new(298.0, 417.0), Vec2::new(298.0, 313.0), 0.82), - Segment::new(Vec2::new(298.0, 336.0), Vec2::new(310.0, 336.0), 0.82), - Segment::new(Vec2::new(292.0, 374.0), Vec2::new(291.0, 342.0), 0.82), - Segment::new(Vec2::new(281.0, 378.0), Vec2::new(300.0, 369.0), 0.82), - Segment::new(Vec2::new(275.0, 343.0), Vec2::new(290.0, 382.0), 0.82), - Segment::new(Vec2::new(248.0, 364.0), Vec2::new(264.0, 339.0), 0.82), - Segment::new(Vec2::new(257.0, 387.0), Vec2::new(249.0, 364.0), 0.82), - Segment::new(Vec2::new(277.0, 429.0), Vec2::new(257.0, 387.0), 0.82), - Segment::new(Vec2::new(287.0, 446.0), Vec2::new(277.0, 427.0), 0.82), - Segment::new(Vec2::new(303.0, 430.0), Vec2::new(283.0, 436.0), 0.82), - Segment::new(Vec2::new(320.0, 410.0), Vec2::new(302.0, 430.0), 0.82), - Segment::new(Vec2::new(320.0, 21.0), Vec2::new(320.0, 410.0), 0.82), - Segment::new(Vec2::new(307.0, 437.0), Vec2::new(316.0, 430.0), 0.82), - Segment::new(Vec2::new(296.0, 442.0), Vec2::new(307.0, 437.0), 0.82), - Segment::new(Vec2::new(285.0, 443.0), Vec2::new(296.0, 442.0), 0.82), - // Preserve the recovered lower apron on either side of the center drain. - Segment::new(Vec2::new(112.0, 443.0), Vec2::new(141.0, 443.0), 0.82), - Segment::new(Vec2::new(173.0, 443.0), Vec2::new(285.0, 443.0), 0.82), - Segment::new(Vec2::new(99.0, 438.0), Vec2::new(121.0, 444.0), 0.82), - Segment::new(Vec2::new(78.0, 427.0), Vec2::new(100.0, 438.0), 0.82), - Segment::new(Vec2::new(66.0, 409.0), Vec2::new(79.0, 428.0), 0.82), - Segment::new(Vec2::new(57.0, 385.0), Vec2::new(68.0, 413.0), 0.82), - Segment::new(Vec2::new(59.0, 304.0), Vec2::new(59.0, 396.0), 0.82), - Segment::new(Vec2::new(67.0, 288.0), Vec2::new(58.0, 311.0), 0.82), - Segment::new(Vec2::new(41.0, 268.0), Vec2::new(64.0, 279.0), 0.82), - Segment::new(Vec2::new(22.0, 293.0), Vec2::new(31.0, 272.0), 0.82), - Segment::new(Vec2::new(22.0, 447.0), Vec2::new(22.0, 293.0), 0.82), - Segment::new(Vec2::new(11.0, 445.0), Vec2::new(23.0, 445.0), 0.82), - Segment::new(Vec2::new(13.0, 288.0), Vec2::new(13.0, 448.0), 0.82), - Segment::new(Vec2::new(32.0, 228.0), Vec2::new(12.0, 293.0), 0.82), - Segment::new(Vec2::new(10.0, 158.0), Vec2::new(33.0, 227.0), 0.82), - Segment::new(Vec2::new(12.0, 51.0), Vec2::new(12.0, 197.0), 0.82), - Segment::new(Vec2::new(37.0, 111.0), Vec2::new(37.0, 166.0), 0.82), - Segment::new(Vec2::new(110.0, 74.0), Vec2::new(37.0, 111.0), 0.82), - Segment::new(Vec2::new(19.0, 73.0), Vec2::new(92.0, 50.0), 0.82), - Segment::new(Vec2::new(19.0, 167.0), Vec2::new(19.0, 73.0), 0.82), - Segment::new(Vec2::new(141.0, 45.0), Vec2::new(112.0, 72.0), 0.82), - Segment::new(Vec2::new(97.0, 47.0), Vec2::new(130.0, 31.0), 0.82), - Segment::new(Vec2::new(0.0, 433.0), Vec2::new(7.0, 478.0), 0.82), - Segment::new(Vec2::new(292.0, 73.0), Vec2::new(292.0, 167.0), 0.82), - Segment::new(Vec2::new(221.0, 50.0), Vec2::new(292.0, 73.0), 0.82), - Segment::new(Vec2::new(275.0, 111.0), Vec2::new(205.0, 74.0), 0.82), - Segment::new(Vec2::new(275.0, 166.0), Vec2::new(275.0, 111.0), 0.82), - Segment::new(Vec2::new(177.0, 31.0), Vec2::new(216.0, 47.0), 0.82), - Segment::new(Vec2::new(201.0, 72.0), Vec2::new(172.0, 45.0), 0.82), - Segment::new(Vec2::new(40.0, 257.0), Vec2::new(73.0, 273.0), 0.82), - Segment::new(Vec2::new(73.0, 273.0), Vec2::new(67.0, 285.0), 0.82), - Segment::new(Vec2::new(34.0, 269.0), Vec2::new(40.0, 257.0), 0.82), - Segment::new(Vec2::new(292.0, 231.0), Vec2::new(292.0, 205.0), 0.82), - Segment::new(Vec2::new(292.0, 205.0), Vec2::new(302.0, 205.0), 0.82), - Segment::new(Vec2::new(302.0, 231.0), Vec2::new(292.0, 231.0), 0.82), - Segment::new(Vec2::new(292.0, 247.0), Vec2::new(292.0, 222.0), 0.82), - Segment::new(Vec2::new(292.0, 222.0), Vec2::new(302.0, 222.0), 0.82), - Segment::new(Vec2::new(302.0, 247.0), Vec2::new(292.0, 247.0), 0.82), - Segment::new(Vec2::new(292.0, 261.0), Vec2::new(292.0, 237.0), 0.82), - Segment::new(Vec2::new(292.0, 237.0), Vec2::new(302.0, 237.0), 0.82), - Segment::new(Vec2::new(302.0, 261.0), Vec2::new(292.0, 261.0), 0.82), - Segment::new(Vec2::new(292.0, 277.0), Vec2::new(292.0, 252.0), 0.82), - Segment::new(Vec2::new(292.0, 252.0), Vec2::new(302.0, 252.0), 0.82), - Segment::new(Vec2::new(302.0, 276.0), Vec2::new(292.0, 276.0), 0.82), - Segment::new(Vec2::new(292.0, 291.0), Vec2::new(292.0, 268.0), 0.82), - Segment::new(Vec2::new(292.0, 269.0), Vec2::new(302.0, 268.0), 0.82), - Segment::new(Vec2::new(302.0, 295.0), Vec2::new(292.0, 290.0), 0.82), - Segment::new(Vec2::new(148.0, 254.0), Vec2::new(203.0, 236.0), 0.82), - Segment::new(Vec2::new(215.0, 261.0), Vec2::new(158.0, 280.0), 0.82), - Segment::new(Vec2::new(187.0, 232.0), Vec2::new(209.0, 225.0), 0.82), - Segment::new(Vec2::new(209.0, 225.0), Vec2::new(214.0, 240.0), 0.82), - Segment::new(Vec2::new(191.0, 244.0), Vec2::new(187.0, 232.0), 0.82), - Segment::new(Vec2::new(169.0, 239.0), Vec2::new(198.0, 229.0), 0.82), - Segment::new(Vec2::new(198.0, 229.0), Vec2::new(202.0, 241.0), 0.82), - Segment::new(Vec2::new(173.0, 251.0), Vec2::new(169.0, 239.0), 0.82), - Segment::new(Vec2::new(153.0, 245.0), Vec2::new(182.0, 235.0), 0.82), - Segment::new(Vec2::new(182.0, 235.0), Vec2::new(186.0, 247.0), 0.82), - Segment::new(Vec2::new(157.0, 257.0), Vec2::new(153.0, 245.0), 0.82), - Segment::new(Vec2::new(139.0, 252.0), Vec2::new(163.0, 241.0), 0.82), - Segment::new(Vec2::new(163.0, 241.0), Vec2::new(167.0, 253.0), 0.82), - Segment::new(Vec2::new(144.0, 267.0), Vec2::new(139.0, 252.0), 0.82), - Segment::new(Vec2::new(84.0, 318.0), Vec2::new(105.0, 329.0), 0.82), - Segment::new(Vec2::new(105.0, 329.0), Vec2::new(101.0, 339.0), 0.82), - Segment::new(Vec2::new(80.0, 328.0), Vec2::new(84.0, 318.0), 0.82), - Segment::new(Vec2::new(87.0, 327.0), Vec2::new(111.0, 339.0), 0.82), - Segment::new(Vec2::new(77.0, 370.0), Vec2::new(77.0, 331.0), 0.82), - Segment::new(Vec2::new(87.0, 372.0), Vec2::new(77.0, 370.0), 0.82), - Segment::new(Vec2::new(110.0, 337.0), Vec2::new(87.0, 372.0), 0.82), - Segment::new(Vec2::new(184.0, 426.0), Vec2::new(172.0, 403.0), 0.82), - Segment::new(Vec2::new(193.0, 396.0), Vec2::new(205.0, 417.0), 0.82), - Segment::new(Vec2::new(213.0, 426.0), Vec2::new(201.0, 403.0), 0.82), - Segment::new(Vec2::new(222.0, 396.0), Vec2::new(234.0, 417.0), 0.82), - Segment::new(Vec2::new(242.0, 426.0), Vec2::new(230.0, 403.0), 0.82), - Segment::new(Vec2::new(251.0, 396.0), Vec2::new(263.0, 417.0), 0.82), - Segment::new(Vec2::new(291.0, 205.0), Vec2::new(298.0, 187.0), 0.82), - Segment::new(Vec2::new(298.0, 378.0), Vec2::new(298.0, 58.0), 0.82), - Segment::new(Vec2::new(298.0, 288.0), Vec2::new(298.0, 211.0), 0.82), - Segment::new(Vec2::new(298.0, 313.0), Vec2::new(291.0, 290.0), 0.82), - ] - }); - &WALLS -} - #[cfg(test)] mod tests { use super::*; @@ -661,21 +584,29 @@ mod tests { fn stalled_ball_search_restores_motion() { let mut game = Game::new(1); game.ball.in_launcher = false; - game.ball.position = vec2(220.0, 437.0); + game.ball.position = vec2(250.0, 300.0); game.ball.velocity = Vec2::ZERO; + game.stalled_for = BALL_SEARCH_DELAY - 1.0 / 120.0; - let mut events = Vec::new(); - for _ in 0..600 { - events.extend(game.update(1.0 / 120.0, 3, Controls::default())); - if events.contains(&Event::BallSearch) { - break; - } - } + let events = game.update(1.0 / 120.0, 3, Controls::default()); assert!(events.contains(&Event::BallSearch)); assert!(game.ball.velocity.y < 0.0); } + #[test] + fn fast_ball_cannot_tunnel_through_the_top_rail() { + let mut game = Game::new(1); + game.ball.in_launcher = false; + game.ball.position = vec2(270.0, 30.0); + game.ball.velocity = vec2(0.0, -MAX_SPEED); + + game.fixed_update(1.0 / 30.0, &mut Vec::new()); + + assert!(game.ball.position.y >= 15.0 + BALL_RADIUS - 0.01); + assert!(game.ball.velocity.y > 0.0); + } + #[test] fn tilt_latches_and_disables_flippers_and_scoring() { let mut game = Game::new(1); diff --git a/tdkpin-rs/src/main.rs b/tdkpin-rs/src/main.rs index 391bb3f..e7b0d21 100644 --- a/tdkpin-rs/src/main.rs +++ b/tdkpin-rs/src/main.rs @@ -3,6 +3,7 @@ mod assets; mod game; mod geometry; mod persistence; +mod table; use app::App; use macroquad::prelude::*; diff --git a/tdkpin-rs/src/table.rs b/tdkpin-rs/src/table.rs new file mode 100644 index 0000000..25ba5bc --- /dev/null +++ b/tdkpin-rs/src/table.rs @@ -0,0 +1,231 @@ +//! Collision primitives recovered from the original Win16 object table. +//! +//! `FUN_1000_34ab` registers objects 1 through 175 through +//! `FUN_1008_0138`. The latter reveals the coordinate conversion: ordinary +//! objects use `(param_34, param_32 - 20)` and `(param_30, param_28 - 20)`; +//! relative objects accumulate those values from the preceding endpoint. +//! The old port used `478 - x`, which mirrored the complete table vertically. + +use crate::geometry::Segment; +use macroquad::prelude::Vec2; + +const WALL_BOUNCE: f32 = 0.82; + +#[derive(Clone, Copy, Debug)] +pub struct TableSegment { + pub id: u8, + pub segment: Segment, +} + +impl TableSegment { + const fn new(id: u8, start: Vec2, end: Vec2) -> Self { + Self { + id, + segment: Segment::new(start, end, WALL_BOUNCE), + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct StaticCircle { + pub id: u8, + pub center: Vec2, + pub radius: f32, +} + +impl StaticCircle { + const fn new(id: u8, center: Vec2, radius: f32) -> Self { + Self { id, center, radius } + } +} + +/// Active type-2 objects after later registrations overwrite objects 169 and +/// 170. Object 2 is the drain sensor and object 25 is the shooter stop; callers +/// apply their special behavior after detecting contact. +pub const WALLS: &[TableSegment] = &[ + TableSegment::new(1, Vec2::new(7.0, 397.0), Vec2::new(125.0, 437.0)), + TableSegment::new(2, Vec2::new(10.0, 455.0), Vec2::new(300.0, 455.0)), + TableSegment::new(3, Vec2::new(134.0, 442.0), Vec2::new(134.0, 425.0)), + TableSegment::new(5, Vec2::new(148.0, 425.0), Vec2::new(148.0, 447.0)), + TableSegment::new(7, Vec2::new(165.0, 425.0), Vec2::new(164.0, 446.0)), + TableSegment::new(9, Vec2::new(179.0, 425.0), Vec2::new(180.0, 447.0)), + TableSegment::new(10, Vec2::new(183.0, 437.0), Vec2::new(304.0, 394.0)), + TableSegment::new(11, Vec2::new(298.0, 145.0), Vec2::new(298.0, 41.0)), + TableSegment::new(12, Vec2::new(310.0, 122.0), Vec2::new(298.0, 122.0)), + TableSegment::new(14, Vec2::new(291.0, 116.0), Vec2::new(292.0, 84.0)), + TableSegment::new(15, Vec2::new(300.0, 89.0), Vec2::new(281.0, 80.0)), + TableSegment::new(16, Vec2::new(290.0, 76.0), Vec2::new(275.0, 115.0)), + TableSegment::new(18, Vec2::new(264.0, 119.0), Vec2::new(248.0, 94.0)), + TableSegment::new(19, Vec2::new(249.0, 94.0), Vec2::new(257.0, 71.0)), + TableSegment::new(20, Vec2::new(257.0, 71.0), Vec2::new(277.0, 29.0)), + TableSegment::new(21, Vec2::new(277.0, 31.0), Vec2::new(287.0, 12.0)), + TableSegment::new(22, Vec2::new(283.0, 22.0), Vec2::new(303.0, 28.0)), + TableSegment::new(23, Vec2::new(302.0, 28.0), Vec2::new(320.0, 48.0)), + TableSegment::new(24, Vec2::new(320.0, 48.0), Vec2::new(320.0, 437.0)), + TableSegment::new(25, Vec2::new(315.0, 413.0), Vec2::new(332.0, 413.0)), + TableSegment::new(27, Vec2::new(328.0, 58.0), Vec2::new(326.0, 47.0)), + TableSegment::new(28, Vec2::new(326.0, 47.0), Vec2::new(322.0, 37.0)), + TableSegment::new(29, Vec2::new(322.0, 37.0), Vec2::new(316.0, 28.0)), + TableSegment::new(30, Vec2::new(316.0, 28.0), Vec2::new(307.0, 21.0)), + TableSegment::new(31, Vec2::new(307.0, 21.0), Vec2::new(296.0, 16.0)), + TableSegment::new(32, Vec2::new(296.0, 16.0), Vec2::new(285.0, 15.0)), + TableSegment::new(33, Vec2::new(285.0, 15.0), Vec2::new(112.0, 15.0)), + TableSegment::new(34, Vec2::new(121.0, 14.0), Vec2::new(99.0, 20.0)), + TableSegment::new(35, Vec2::new(100.0, 20.0), Vec2::new(78.0, 31.0)), + TableSegment::new(36, Vec2::new(79.0, 30.0), Vec2::new(66.0, 49.0)), + TableSegment::new(37, Vec2::new(68.0, 45.0), Vec2::new(57.0, 73.0)), + TableSegment::new(38, Vec2::new(59.0, 62.0), Vec2::new(59.0, 154.0)), + TableSegment::new(39, Vec2::new(58.0, 147.0), Vec2::new(67.0, 170.0)), + TableSegment::new(41, Vec2::new(64.0, 179.0), Vec2::new(41.0, 190.0)), + TableSegment::new(43, Vec2::new(31.0, 186.0), Vec2::new(22.0, 165.0)), + TableSegment::new(44, Vec2::new(22.0, 165.0), Vec2::new(22.0, 11.0)), + TableSegment::new(45, Vec2::new(23.0, 13.0), Vec2::new(11.0, 13.0)), + TableSegment::new(46, Vec2::new(13.0, 10.0), Vec2::new(13.0, 170.0)), + TableSegment::new(47, Vec2::new(12.0, 165.0), Vec2::new(32.0, 230.0)), + TableSegment::new(49, Vec2::new(33.0, 231.0), Vec2::new(10.0, 300.0)), + TableSegment::new(50, Vec2::new(12.0, 261.0), Vec2::new(12.0, 407.0)), + TableSegment::new(55, Vec2::new(59.0, 287.0), Vec2::new(107.0, 359.0)), + TableSegment::new(57, Vec2::new(49.0, 294.0), Vec2::new(2.0, 268.0)), + TableSegment::new(59, Vec2::new(44.0, 287.0), Vec2::new(44.0, 242.0)), + TableSegment::new(61, Vec2::new(37.0, 292.0), Vec2::new(37.0, 347.0)), + TableSegment::new(62, Vec2::new(37.0, 347.0), Vec2::new(110.0, 384.0)), + TableSegment::new(63, Vec2::new(92.0, 408.0), Vec2::new(19.0, 385.0)), + TableSegment::new(64, Vec2::new(19.0, 385.0), Vec2::new(19.0, 291.0)), + TableSegment::new(66, Vec2::new(112.0, 386.0), Vec2::new(141.0, 413.0)), + TableSegment::new(68, Vec2::new(130.0, 427.0), Vec2::new(97.0, 411.0)), + TableSegment::new(70, Vec2::new(267.0, 292.0), Vec2::new(267.0, 337.0)), + TableSegment::new(72, Vec2::new(261.0, 299.0), Vec2::new(214.0, 321.0)), + TableSegment::new(74, Vec2::new(252.0, 289.0), Vec2::new(302.0, 218.0)), + TableSegment::new(76, Vec2::new(292.0, 291.0), Vec2::new(292.0, 385.0)), + TableSegment::new(77, Vec2::new(292.0, 385.0), Vec2::new(221.0, 408.0)), + TableSegment::new(78, Vec2::new(205.0, 384.0), Vec2::new(275.0, 347.0)), + TableSegment::new(79, Vec2::new(275.0, 347.0), Vec2::new(275.0, 292.0)), + TableSegment::new(81, Vec2::new(216.0, 411.0), Vec2::new(177.0, 427.0)), + TableSegment::new(83, Vec2::new(172.0, 413.0), Vec2::new(201.0, 386.0)), + TableSegment::new(84, Vec2::new(73.0, 185.0), Vec2::new(40.0, 201.0)), + TableSegment::new(85, Vec2::new(67.0, 173.0), Vec2::new(73.0, 185.0)), + TableSegment::new(86, Vec2::new(40.0, 201.0), Vec2::new(34.0, 189.0)), + TableSegment::new(90, Vec2::new(292.0, 253.0), Vec2::new(292.0, 227.0)), + TableSegment::new(91, Vec2::new(302.0, 253.0), Vec2::new(292.0, 253.0)), + TableSegment::new(92, Vec2::new(292.0, 227.0), Vec2::new(302.0, 227.0)), + TableSegment::new(93, Vec2::new(292.0, 236.0), Vec2::new(292.0, 211.0)), + TableSegment::new(94, Vec2::new(302.0, 236.0), Vec2::new(292.0, 236.0)), + TableSegment::new(95, Vec2::new(292.0, 211.0), Vec2::new(302.0, 211.0)), + TableSegment::new(96, Vec2::new(292.0, 221.0), Vec2::new(292.0, 197.0)), + TableSegment::new(97, Vec2::new(302.0, 221.0), Vec2::new(292.0, 221.0)), + TableSegment::new(98, Vec2::new(292.0, 197.0), Vec2::new(302.0, 197.0)), + TableSegment::new(99, Vec2::new(292.0, 206.0), Vec2::new(292.0, 181.0)), + TableSegment::new(100, Vec2::new(302.0, 206.0), Vec2::new(292.0, 206.0)), + TableSegment::new(101, Vec2::new(292.0, 182.0), Vec2::new(302.0, 182.0)), + TableSegment::new(102, Vec2::new(292.0, 190.0), Vec2::new(292.0, 167.0)), + TableSegment::new(103, Vec2::new(302.0, 190.0), Vec2::new(292.0, 189.0)), + TableSegment::new(104, Vec2::new(292.0, 168.0), Vec2::new(302.0, 163.0)), + TableSegment::new(105, Vec2::new(203.0, 222.0), Vec2::new(148.0, 204.0)), + TableSegment::new(107, Vec2::new(158.0, 178.0), Vec2::new(215.0, 197.0)), + TableSegment::new(109, Vec2::new(209.0, 233.0), Vec2::new(187.0, 226.0)), + TableSegment::new(110, Vec2::new(214.0, 218.0), Vec2::new(209.0, 233.0)), + TableSegment::new(111, Vec2::new(187.0, 226.0), Vec2::new(191.0, 214.0)), + TableSegment::new(112, Vec2::new(198.0, 229.0), Vec2::new(169.0, 219.0)), + TableSegment::new(113, Vec2::new(202.0, 217.0), Vec2::new(198.0, 229.0)), + TableSegment::new(114, Vec2::new(169.0, 219.0), Vec2::new(173.0, 207.0)), + TableSegment::new(115, Vec2::new(182.0, 223.0), Vec2::new(153.0, 213.0)), + TableSegment::new(116, Vec2::new(186.0, 211.0), Vec2::new(182.0, 223.0)), + TableSegment::new(117, Vec2::new(153.0, 213.0), Vec2::new(157.0, 201.0)), + TableSegment::new(118, Vec2::new(163.0, 217.0), Vec2::new(139.0, 206.0)), + TableSegment::new(119, Vec2::new(167.0, 205.0), Vec2::new(163.0, 217.0)), + TableSegment::new(120, Vec2::new(139.0, 206.0), Vec2::new(144.0, 191.0)), + TableSegment::new(121, Vec2::new(105.0, 129.0), Vec2::new(84.0, 140.0)), + TableSegment::new(122, Vec2::new(101.0, 119.0), Vec2::new(105.0, 129.0)), + TableSegment::new(123, Vec2::new(84.0, 140.0), Vec2::new(80.0, 130.0)), + TableSegment::new(124, Vec2::new(111.0, 119.0), Vec2::new(87.0, 131.0)), + TableSegment::new(126, Vec2::new(77.0, 127.0), Vec2::new(77.0, 88.0)), + TableSegment::new(127, Vec2::new(77.0, 88.0), Vec2::new(87.0, 86.0)), + TableSegment::new(128, Vec2::new(87.0, 86.0), Vec2::new(110.0, 121.0)), + TableSegment::new(156, Vec2::new(172.0, 55.0), Vec2::new(184.0, 32.0)), + TableSegment::new(158, Vec2::new(205.0, 41.0), Vec2::new(193.0, 62.0)), + TableSegment::new(160, Vec2::new(201.0, 55.0), Vec2::new(213.0, 32.0)), + TableSegment::new(162, Vec2::new(234.0, 41.0), Vec2::new(222.0, 62.0)), + TableSegment::new(164, Vec2::new(230.0, 55.0), Vec2::new(242.0, 32.0)), + TableSegment::new(166, Vec2::new(263.0, 41.0), Vec2::new(251.0, 62.0)), + TableSegment::new(168, Vec2::new(298.0, 271.0), Vec2::new(291.0, 253.0)), + TableSegment::new(169, Vec2::new(328.0, 422.0), Vec2::new(328.0, 58.0)), + TableSegment::new(170, Vec2::new(298.0, 400.0), Vec2::new(298.0, 80.0)), + TableSegment::new(171, Vec2::new(298.0, 247.0), Vec2::new(298.0, 170.0)), + TableSegment::new(173, Vec2::new(291.0, 168.0), Vec2::new(298.0, 145.0)), +]; + +/// Active type-1 solid circles, excluding the three scored bumpers and object +/// 174, whose position is overwritten every frame for the live ball. +pub const PASSIVE_CIRCLES: &[StaticCircle] = &[ + StaticCircle::new(4, Vec2::new(141.0, 426.0), 8.0), + StaticCircle::new(8, Vec2::new(172.0, 426.0), 8.0), + StaticCircle::new(13, Vec2::new(298.0, 116.0), 7.0), + StaticCircle::new(17, Vec2::new(269.0, 114.0), 7.0), + StaticCircle::new(26, Vec2::new(257.0, 95.0), 7.0), + StaticCircle::new(40, Vec2::new(60.0, 173.0), 8.0), + StaticCircle::new(42, Vec2::new(38.0, 183.0), 8.0), + StaticCircle::new(48, Vec2::new(25.0, 231.0), 8.0), + StaticCircle::new(54, Vec2::new(53.0, 292.0), 8.0), + StaticCircle::new(56, Vec2::new(50.0, 287.0), 8.0), + StaticCircle::new(58, Vec2::new(52.0, 287.0), 8.0), + StaticCircle::new(60, Vec2::new(29.0, 294.0), 10.0), + StaticCircle::new(65, Vec2::new(103.0, 397.0), 15.0), + StaticCircle::new(67, Vec2::new(134.0, 419.0), 9.0), + StaticCircle::new(69, Vec2::new(260.0, 292.0), 8.0), + StaticCircle::new(71, Vec2::new(260.0, 291.0), 8.0), + StaticCircle::new(73, Vec2::new(261.0, 291.0), 8.0), + StaticCircle::new(75, Vec2::new(284.0, 292.0), 9.0), + StaticCircle::new(80, Vec2::new(210.0, 397.0), 15.0), + StaticCircle::new(82, Vec2::new(179.0, 419.0), 9.0), + StaticCircle::new(106, Vec2::new(156.0, 192.0), 14.0), + StaticCircle::new(108, Vec2::new(210.0, 210.0), 14.0), + StaticCircle::new(125, Vec2::new(84.0, 126.0), 8.0), + StaticCircle::new(134, Vec2::new(124.0, 76.0), 20.0), + StaticCircle::new(135, Vec2::new(100.0, 58.0), 17.0), + StaticCircle::new(136, Vec2::new(133.0, 48.0), 17.0), + StaticCircle::new(137, Vec2::new(154.0, 76.0), 17.0), + StaticCircle::new(138, Vec2::new(133.0, 105.0), 17.0), + StaticCircle::new(139, Vec2::new(100.0, 94.0), 17.0), + StaticCircle::new(155, Vec2::new(183.0, 59.0), 11.0), + StaticCircle::new(157, Vec2::new(195.0, 36.0), 11.0), + StaticCircle::new(159, Vec2::new(212.0, 59.0), 11.0), + StaticCircle::new(161, Vec2::new(224.0, 36.0), 11.0), + StaticCircle::new(163, Vec2::new(241.0, 59.0), 11.0), + StaticCircle::new(165, Vec2::new(253.0, 36.0), 11.0), + StaticCircle::new(167, Vec2::new(299.0, 248.0), 9.0), + StaticCircle::new(172, Vec2::new(299.0, 171.0), 9.0), +]; + +pub const BUMPERS: [StaticCircle; 3] = [ + StaticCircle::new(51, Vec2::new(165.0, 148.0), 23.0), + StaticCircle::new(52, Vec2::new(206.0, 110.0), 23.0), + StaticCircle::new(53, Vec2::new(219.0, 165.0), 23.0), +]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn recovered_object_inventory_is_complete() { + assert_eq!(WALLS.len(), 109); + assert_eq!(PASSIVE_CIRCLES.len(), 37); + assert_eq!(BUMPERS.len(), 3); + } + + #[test] + fn recovered_landmarks_match_the_visible_table() { + let left_flipper_post = PASSIVE_CIRCLES + .iter() + .find(|circle| circle.id == 65) + .expect("object 65 should be the left flipper post"); + assert_eq!(left_flipper_post.center, Vec2::new(103.0, 397.0)); + + let shooter_wall = WALLS + .iter() + .find(|wall| wall.id == 169) + .expect("object 169 should be the outside shooter wall"); + assert_eq!(shooter_wall.segment.start, Vec2::new(328.0, 422.0)); + assert_eq!(shooter_wall.segment.end, Vec2::new(328.0, 58.0)); + } +}