fmt: just fmt (rust only)
Build TDK Pinball / build (macos-latest) (push) Canceled after 0s
Build TDK Pinball / build (ubuntu-latest) (push) Canceled after 0s
Build TDK Pinball / build (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-29 09:57:39 +02:00
parent ee58525011
commit c2f1443436
10 changed files with 386 additions and 328 deletions
+78 -53
View File
@@ -1,24 +1,67 @@
use std::path::Path;
use macroquad::prelude::*;
use crate::{ use crate::{
assets::Assets, assets::Assets,
game::{Controls, Event, Game, Nudge}, game::{Controls, Event, Game, Nudge},
persistence::{ persistence::{
HighScore, Language, Persistence, SavedData, insert_high_score, qualifies_high_score, HighScore,
Language,
Persistence,
SavedData,
insert_high_score,
qualifies_high_score,
}, },
}; };
use macroquad::prelude::*;
use std::path::Path;
const WIDTH: f32 = 640.0; const WIDTH: f32 = 640.0;
const HEIGHT: f32 = 460.0; const HEIGHT: f32 = 460.0;
const DETAIL_TIMER_SECONDS: [f32; 5] = [0.050, 0.040, 0.030, 0.020, 0.010]; const DETAIL_TIMER_SECONDS: [f32; 5] = [0.050, 0.040, 0.030, 0.020, 0.010];
const ADD_PLAYER_KEYS: [KeyCode; 3] = [KeyCode::KpAdd, KeyCode::RightBracket, KeyCode::Equal]; const ADD_PLAYER_KEYS: [KeyCode; 3] = [KeyCode::KpAdd, KeyCode::RightBracket, KeyCode::Equal];
const TARGET_POSITIONS: [[(f32, f32); 5]; 6] = [ const TARGET_POSITIONS: [[(f32, f32); 5]; 6] = [
[(9.0, 41.0), (23.0, 11.0), (56.0, 14.0), (63.0, 47.0), (35.0, 63.0)], [
[(11.0, 45.0), (18.0, 13.0), (52.0, 11.0), (63.0, 42.0), (39.0, 64.0)], (9.0, 41.0),
[(13.0, 50.0), (15.0, 17.0), (46.0, 9.0), (64.0, 37.0), (44.0, 62.0)], (23.0, 11.0),
[(17.0, 55.0), (11.0, 23.0), (39.0, 7.0), (64.0, 30.0), (50.0, 59.0)], (56.0, 14.0),
[(21.0, 60.0), (9.0, 28.0), (35.0, 7.0), (62.0, 25.0), (54.0, 56.0)], (63.0, 47.0),
[(8.0, 35.0), (26.0, 8.0), (59.0, 18.0), (59.0, 50.0), (27.0, 61.0)], (35.0, 63.0),
],
[
(11.0, 45.0),
(18.0, 13.0),
(52.0, 11.0),
(63.0, 42.0),
(39.0, 64.0),
],
[
(13.0, 50.0),
(15.0, 17.0),
(46.0, 9.0),
(64.0, 37.0),
(44.0, 62.0),
],
[
(17.0, 55.0),
(11.0, 23.0),
(39.0, 7.0),
(64.0, 30.0),
(50.0, 59.0),
],
[
(21.0, 60.0),
(9.0, 28.0),
(35.0, 7.0),
(62.0, 25.0),
(54.0, 56.0),
],
[
(8.0, 35.0),
(26.0, 8.0),
(59.0, 18.0),
(59.0, 50.0),
(27.0, 61.0),
],
]; ];
const BUMPER_VALUE_REGIONS: [(i32, i32, i32, i32); 5] = [ const BUMPER_VALUE_REGIONS: [(i32, i32, i32, i32); 5] = [
(103, 304, 121, 323), (103, 304, 121, 323),
@@ -116,11 +159,7 @@ impl AttractAnimation {
fn target_active(self, index: usize) -> bool { fn target_active(self, index: usize) -> bool {
let index = u32::try_from(index).unwrap_or(0); let index = u32::try_from(index).unwrap_or(0);
let first_activation = if index == 0 { let first_activation = if index == 0 { 32 } else { index * 2 };
32
} else {
index * 2
};
self.tick >= first_activation && self.tick.wrapping_sub(index * 2) % 32 < 16 self.tick >= first_activation && self.tick.wrapping_sub(index * 2) % 32 < 16
} }
@@ -137,11 +176,7 @@ impl AttractAnimation {
fn record_strip_active(self, index: usize) -> bool { fn record_strip_active(self, index: usize) -> bool {
let index = u32::try_from(index).unwrap_or(0); let index = u32::try_from(index).unwrap_or(0);
let first_activation = if index == 0 { let first_activation = if index == 0 { 40 } else { index * 4 };
40
} else {
index * 4
};
self.tick >= first_activation && self.tick.wrapping_sub(index * 4) % 40 < 20 self.tick >= first_activation && self.tick.wrapping_sub(index * 4) % 40 < 20
} }
@@ -151,11 +186,7 @@ impl AttractAnimation {
fn item_index(self) -> Option<usize> { fn item_index(self) -> Option<usize> {
let phase = (self.tick % 144) / 8; let phase = (self.tick % 144) / 8;
let item = if phase < 10 { let item = if phase < 10 { phase } else { 18 - phase };
phase
} else {
18 - phase
};
(item != 0).then(|| usize::try_from(item - 1).unwrap_or(0)) (item != 0).then(|| usize::try_from(item - 1).unwrap_or(0))
} }
@@ -495,7 +526,8 @@ impl App {
self.name.pop(); self.name.pop();
} }
let click = is_mouse_button_pressed(MouseButton::Left).then(Self::logical_mouse); let click = is_mouse_button_pressed(MouseButton::Left).then(Self::logical_mouse);
let clicked_ok = click.is_some_and(|point| Rect::new(255.0, 256.0, 60.0, 23.0).contains(point)); let clicked_ok =
click.is_some_and(|point| Rect::new(255.0, 256.0, 60.0, 23.0).contains(point));
let clicked_cancel = let clicked_cancel =
click.is_some_and(|point| Rect::new(324.0, 256.0, 60.0, 23.0).contains(point)); click.is_some_and(|point| Rect::new(324.0, 256.0, 60.0, 23.0).contains(point));
if is_key_pressed(KeyCode::Escape) || clicked_cancel { if is_key_pressed(KeyCode::Escape) || clicked_cancel {
@@ -574,11 +606,8 @@ impl App {
} }
if self.attract.magnetic_records_active() { if self.attract.magnetic_records_active() {
for (x, y, width, height) in [ for (x, y, width, height) in [(151, 389, 13, 49), (11, 344, 13, 49), (291, 346, 13, 49)]
(151, 389, 13, 49), {
(11, 344, 13, 49),
(291, 346, 13, 49),
] {
self.draw_active_table_region(x, y, width, height); self.draw_active_table_region(x, y, width, height);
} }
} }
@@ -605,12 +634,7 @@ impl App {
} }
if let Some(item) = self.attract.item_index() { if let Some(item) = self.attract.item_index() {
draw_texture( draw_texture(&self.assets.diamond[item], 123.0, 300.0, WHITE);
&self.assets.diamond[item],
123.0,
300.0,
WHITE,
);
} }
self.draw_intro_marquee(); self.draw_intro_marquee();
} }
@@ -929,7 +953,15 @@ impl App {
} else if (66..=128).contains(&frame) { } else if (66..=128).contains(&frame) {
draw_texture_region(&self.assets.wheel, 79.0, 33.0, 91.0, 90.0, 180.0, 90.0); draw_texture_region(&self.assets.wheel, 79.0, 33.0, 91.0, 90.0, 180.0, 90.0);
if frame < 83 { if frame < 83 {
draw_texture_region(wide_a, 79.0, ((82 - frame) * 2) as f32, 91.0, 90.0, 0.0, 0.0); draw_texture_region(
wide_a,
79.0,
((82 - frame) * 2) as f32,
91.0,
90.0,
0.0,
0.0,
);
} else { } else {
let height = (128 - frame) * 2; let height = (128 - frame) * 2;
draw_texture_region( draw_texture_region(
@@ -1122,14 +1154,7 @@ impl App {
draw_centered("SETTINGS", 122.0, 30, BLACK); draw_centered("SETTINGS", 122.0, 30, BLACK);
let rows = [ let rows = [
format!("GRAPHICS DETAIL / SPEED: {}", self.saved.settings.speed), format!("GRAPHICS DETAIL / SPEED: {}", self.saved.settings.speed),
format!( format!("SOUND: {}", if self.sounds_enabled { "ON" } else { "OFF" }),
"SOUND: {}",
if self.sounds_enabled {
"ON"
} else {
"OFF"
}
),
format!("LANGUAGE: {}", self.saved.settings.language.label()), format!("LANGUAGE: {}", self.saved.settings.language.label()),
]; ];
for (index, row) in rows.iter().enumerate() { for (index, row) in rows.iter().enumerate() {
@@ -1181,7 +1206,13 @@ impl App {
draw_rectangle(x, y, 270.0, 124.0, Color::from_rgba(192, 192, 192, 255)); draw_rectangle(x, y, 270.0, 124.0, Color::from_rgba(192, 192, 192, 255));
draw_rectangle_lines(x, y, 270.0, 124.0, 2.0, WHITE); draw_rectangle_lines(x, y, 270.0, 124.0, 2.0, WHITE);
draw_rectangle_lines(x + 2.0, y + 2.0, 266.0, 120.0, 2.0, DARKGRAY); draw_rectangle_lines(x + 2.0, y + 2.0, 266.0, 120.0, 2.0, DARKGRAY);
draw_rectangle(x + 4.0, y + 4.0, 262.0, 20.0, Color::from_rgba(0, 0, 128, 255)); draw_rectangle(
x + 4.0,
y + 4.0,
262.0,
20.0,
Color::from_rgba(0, 0, 128, 255),
);
draw_text( draw_text(
"Congratulations! This is a Top Ten Score!", "Congratulations! This is a Top Ten Score!",
x + 9.0, x + 9.0,
@@ -1319,13 +1350,7 @@ mod tests {
#[test] #[test]
fn attract_timer_uses_the_selected_detail_callback() { fn attract_timer_uses_the_selected_detail_callback() {
for (detail, interval) in [ for (detail, interval) in [(1, 0.050), (2, 0.040), (3, 0.030), (4, 0.020), (5, 0.010)] {
(1, 0.050),
(2, 0.040),
(3, 0.030),
(4, 0.020),
(5, 0.010),
] {
let mut animation = AttractAnimation::default(); let mut animation = AttractAnimation::default();
animation.update(interval - 0.001, detail); animation.update(interval - 0.001, detail);
assert_eq!(animation.tick, 0); assert_eq!(animation.tick, 0);
+2 -1
View File
@@ -33,7 +33,8 @@ impl Assets {
let inactive_table = texture(include_bytes!("../assets/original/images/dat_00998.png")); let inactive_table = texture(include_bytes!("../assets/original/images/dat_00998.png"));
let loading = texture(include_bytes!("../assets/original/images/dat_00995.png")); let loading = texture(include_bytes!("../assets/original/images/dat_00995.png"));
let loading_progress = texture(include_bytes!("../assets/original/images/dat_00994.png")); let loading_progress = texture(include_bytes!("../assets/original/images/dat_00994.png"));
let highscore_background = texture(include_bytes!("../assets/original/images/dat_00993.png")); let highscore_background =
texture(include_bytes!("../assets/original/images/dat_00993.png"));
let help = [ let help = [
texture(include_bytes!("../assets/original/images/dat_01001.png")), texture(include_bytes!("../assets/original/images/dat_01001.png")),
texture(include_bytes!("../assets/original/images/dat_01002.png")), texture(include_bytes!("../assets/original/images/dat_01002.png")),
+5 -4
View File
@@ -77,15 +77,16 @@ mod tests {
fn zero_bounds_still_advance_the_seed() { fn zero_bounds_still_advance_the_seed() {
let mut random = BorlandRandom::new(7); let mut random = BorlandRandom::new(7);
assert_eq!(random.below(0), 0); assert_eq!(random.below(0), 0);
assert_eq!(random.seed(), 7_u32.wrapping_mul(MULTIPLIER).wrapping_add(1)); assert_eq!(
random.seed(),
7_u32.wrapping_mul(MULTIPLIER).wrapping_add(1)
);
} }
#[test] #[test]
fn unit_interval_is_the_exact_unsigned_seed_fraction() { fn unit_interval_is_the_exact_unsigned_seed_fraction() {
let mut random = BorlandRandom::new(0xfedc_ba98); let mut random = BorlandRandom::new(0xfedc_ba98);
let expected_seed = 0xfedc_ba98_u32 let expected_seed = 0xfedc_ba98_u32.wrapping_mul(MULTIPLIER).wrapping_add(1);
.wrapping_mul(MULTIPLIER)
.wrapping_add(1);
assert_eq!( assert_eq!(
random.unit_interval().to_bits(), random.unit_interval().to_bits(),
(f64::from(expected_seed) / TWO_TO_32).to_bits() (f64::from(expected_seed) / TWO_TO_32).to_bits()
+66 -25
View File
@@ -5,8 +5,7 @@ use crate::{original_physics::MilliVec, real48::Real48};
const SEARCH_RADIUS: i32 = 54_000; const SEARCH_RADIUS: i32 = 54_000;
const ONE: Real48 = Real48::from_bytes([0x81, 0, 0, 0, 0, 0]); const ONE: Real48 = Real48::from_bytes([0x81, 0, 0, 0, 0, 0]);
const TWO: Real48 = Real48::from_bytes([0x82, 0, 0, 0, 0, 0]); const TWO: Real48 = Real48::from_bytes([0x82, 0, 0, 0, 0, 0]);
const TWO_FIFTHS: Real48 = const TWO_FIFTHS: Real48 = Real48::from_bytes([0x7f, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]);
Real48::from_bytes([0x7f, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]);
const THOUSAND: Real48 = Real48::from_bytes([0x8a, 0, 0, 0, 0, 0x7a]); const THOUSAND: Real48 = Real48::from_bytes([0x8a, 0, 0, 0, 0, 0x7a]);
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -171,10 +170,7 @@ fn penetration(geometry: Geometry, delta: i32, ball: MilliVec) -> i32 {
.wrapping_add(43_000) .wrapping_add(43_000)
.wrapping_sub(geometry.pivot.y), .wrapping_sub(geometry.pivot.y),
); );
geometry geometry.pivot.y.wrapping_sub(
.pivot
.y
.wrapping_sub(
Real48::from_i32(numerator) Real48::from_i32(numerator)
.divide(Real48::from_i32(edge_dx)) .divide(Real48::from_i32(edge_dx))
.round_i32(), .round_i32(),
@@ -299,8 +295,14 @@ mod tests {
let velocities = [ let velocities = [
MilliVec { x: 0, y: 2_000 }, MilliVec { x: 0, y: 2_000 },
MilliVec { x: 1_000, y: 2_000 }, MilliVec { x: 1_000, y: 2_000 },
MilliVec { x: -1_000, y: 2_000 }, MilliVec {
MilliVec { x: 2_700, y: -2_700 }, x: -1_000,
y: 2_000,
},
MilliVec {
x: 2_700,
y: -2_700,
},
]; ];
let mut hash = 14_695_981_039_346_656_037_u64; let mut hash = 14_695_981_039_346_656_037_u64;
let mut hits = 0_u32; let mut hits = 0_u32;
@@ -315,10 +317,10 @@ mod tests {
Real48::from_bytes([0x80, 0, 0, 0, 0, 0]), Real48::from_bytes([0x80, 0, 0, 0, 0, 0]),
3_800, 3_800,
); );
let (hit, velocity, movement) = response.map_or( let (hit, velocity, movement) = response
(0_u32, input_velocity, MilliVec::default()), .map_or((0_u32, input_velocity, MilliVec::default()), |response| {
|response| (1, response.velocity, response.movement), (1, response.velocity, response.movement)
); });
hits += hit; hits += hit;
for value in [ for value in [
i32_bits(x), i32_bits(x),
@@ -342,40 +344,73 @@ mod tests {
fn four_direction_vectors_match_the_reconstructed_c_harness() { fn four_direction_vectors_match_the_reconstructed_c_harness() {
let cases = [ let cases = [
( (
MilliVec { x: 104_000, y: 384_000 }, MilliVec {
x: 104_000,
y: 384_000,
},
MilliVec { x: 1_000, y: 2_000 }, MilliVec { x: 1_000, y: 2_000 },
-1, -1,
FlipperSide::Left, FlipperSide::Left,
Real48::from_bytes([0x80, 0, 0, 0, 0, 0]), Real48::from_bytes([0x80, 0, 0, 0, 0, 0]),
MilliVec { x: 1_266, y: 1_511 }, MilliVec { x: 1_266, y: 1_511 },
MilliVec { x: -6_703, y: -8_000 }, MilliVec {
x: -6_703,
y: -8_000,
},
), ),
( (
MilliVec { x: 209_000, y: 419_000 }, MilliVec {
MilliVec { x: -1_000, y: 2_000 }, x: 209_000,
y: 419_000,
},
MilliVec {
x: -1_000,
y: 2_000,
},
1, 1,
FlipperSide::Right, FlipperSide::Right,
Real48::from_bytes([0x80, 0, 0, 0, 0, 0x40]), Real48::from_bytes([0x80, 0, 0, 0, 0, 0x40]),
MilliVec { x: -737, y: 2_263 }, MilliVec { x: -737, y: 2_263 },
MilliVec { x: -5_578, y: 17_127 }, MilliVec {
x: -5_578,
y: 17_127,
},
), ),
( (
MilliVec { x: 104_000, y: 421_000 }, MilliVec {
x: 104_000,
y: 421_000,
},
MilliVec { x: 1_000, y: 2_000 }, MilliVec { x: 1_000, y: 2_000 },
1, 1,
FlipperSide::Left, FlipperSide::Left,
Real48::from_bytes([0x7f, 0, 0, 0, 0, 0]), Real48::from_bytes([0x7f, 0, 0, 0, 0, 0]),
MilliVec { x: 622, y: 2_320 }, MilliVec { x: 622, y: 2_320 },
MilliVec { x: 5_414, y: 20_193 }, MilliVec {
x: 5_414,
y: 20_193,
},
), ),
( (
MilliVec { x: 209_000, y: 385_000 }, MilliVec {
MilliVec { x: -1_000, y: 2_000 }, x: 209_000,
y: 385_000,
},
MilliVec {
x: -1_000,
y: 2_000,
},
-1, -1,
FlipperSide::Right, FlipperSide::Right,
Real48::ZERO, Real48::ZERO,
MilliVec { x: -1_280, y: 1_560 }, MilliVec {
MilliVec { x: 7_385, y: -9_000 }, x: -1_280,
y: 1_560,
},
MilliVec {
x: 7_385,
y: -9_000,
},
), ),
]; ];
for (ball, velocity, delta, side, response, expected_velocity, expected_movement) in cases { for (ball, velocity, delta, side, response, expected_velocity, expected_movement) in cases {
@@ -398,7 +433,10 @@ mod tests {
fn boundary_vectors_match_the_live_original_binary() { fn boundary_vectors_match_the_live_original_binary() {
for (ball, velocity, delta, side, expected_velocity, expected_movement) in [ for (ball, velocity, delta, side, expected_velocity, expected_movement) in [
( (
MilliVec { x: 100_000, y: 370_000 }, MilliVec {
x: 100_000,
y: 370_000,
},
MilliVec { x: 1_000, y: 2_000 }, MilliVec { x: 1_000, y: 2_000 },
-1, -1,
FlipperSide::Left, FlipperSide::Left,
@@ -406,7 +444,10 @@ mod tests {
MilliVec::default(), MilliVec::default(),
), ),
( (
MilliVec { x: 209_000, y: 419_000 }, MilliVec {
x: 209_000,
y: 419_000,
},
MilliVec { x: 1_000, y: 2_000 }, MilliVec { x: 1_000, y: 2_000 },
1, 1,
FlipperSide::Right, FlipperSide::Right,
+112 -142
View File
@@ -1,20 +1,34 @@
use macroquad::prelude::{Rect, Vec2, vec2};
use crate::{ use crate::{
borland_random::BorlandRandom, borland_random::BorlandRandom,
flipper_physics::{FlipperSide, moving_flipper_response}, flipper_physics::{FlipperSide, moving_flipper_response},
geometry::Segment, geometry::Segment,
original_physics::{ original_physics::{
CollisionMaterial, CollisionResponse, GRAVITY_MILLI_PER_STEP, CollisionMaterial,
MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec, StaticCollisionCandidate, CollisionResponse,
ball_collision_response, capture_collision_candidate, circle_collision_candidate_at, GRAVITY_MILLI_PER_STEP,
line_collision_candidate_at, milli_distance, path_intersects_circle, MAXIMUM_SPEED_MILLI_PER_STEP,
MilliVec,
StaticCollisionCandidate,
ball_collision_response,
capture_collision_candidate,
circle_collision_candidate_at,
line_collision_candidate_at,
milli_distance,
path_intersects_circle,
}, },
real48::Real48, real48::Real48,
table::{ table::{
BUMPERS, EFFECT_SENSOR, LOCK_HOLES, PASSIVE_CIRCLES, TARGET_SENSORS, WALLS, BUMPERS,
EFFECT_SENSOR,
LOCK_HOLES,
PASSIVE_CIRCLES,
SPECIAL_HOLE_SENSOR, SPECIAL_HOLE_SENSOR,
TARGET_SENSORS,
WALLS,
}, },
}; };
use macroquad::prelude::{Rect, Vec2, vec2};
const LEFT_FLIPPER_RAISED_TIP: Vec2 = Vec2::new(133.0, 377.0); const LEFT_FLIPPER_RAISED_TIP: Vec2 = Vec2::new(133.0, 377.0);
const RIGHT_FLIPPER_RAISED_TIP: Vec2 = Vec2::new(181.0, 376.0); const RIGHT_FLIPPER_RAISED_TIP: Vec2 = Vec2::new(181.0, 376.0);
@@ -483,7 +497,8 @@ impl Game {
let variation = self.random.below(3_800) / 40; let variation = self.random.below(3_800) / 40;
launch_velocity = -MAXIMUM_SPEED_MILLI_PER_STEP + i32::from(variation); launch_velocity = -MAXIMUM_SPEED_MILLI_PER_STEP + i32::from(variation);
} else if launch_velocity > -2_280 { } else if launch_velocity > -2_280 {
launch_velocity = launch_velocity.wrapping_sub(i32::from(self.random.below(3_800) / 40)); launch_velocity =
launch_velocity.wrapping_sub(i32::from(self.random.below(3_800) / 40));
} }
self.ball.in_launcher = false; self.ball.in_launcher = false;
self.ball.velocity = MilliVec { self.ball.velocity = MilliVec {
@@ -516,12 +531,10 @@ impl Game {
} else if self.tilted { } else if self.tilted {
self.flipper_release_latch[1] = true; self.flipper_release_latch[1] = true;
} }
self.flipper_inputs.left_raised = controls.left_flipper self.flipper_inputs.left_raised =
&& !self.tilted controls.left_flipper && !self.tilted && !self.flipper_release_latch[0];
&& !self.flipper_release_latch[0]; self.flipper_inputs.right_raised =
self.flipper_inputs.right_raised = controls.right_flipper controls.right_flipper && !self.tilted && !self.flipper_release_latch[1];
&& !self.tilted
&& !self.flipper_release_latch[1];
if self.ball.in_launcher && !self.tilted { if self.ball.in_launcher && !self.tilted {
if controls.launch_down { if controls.launch_down {
@@ -576,10 +589,7 @@ impl Game {
self.score_mode = ScoreMode::Multiball; self.score_mode = ScoreMode::Multiball;
} }
for _ in 0..substeps { for _ in 0..substeps {
if self.fixed_update(events) if self.fixed_update(events) || self.finished || self.claw.ball_suspended {
|| self.finished
|| self.claw.ball_suspended
{
break; break;
} }
} }
@@ -609,18 +619,14 @@ impl Game {
} }
} }
self.pending_flipper_edges[0] = match ( self.pending_flipper_edges[0] =
self.flipper_inputs.left_raised, match (self.flipper_inputs.left_raised, self.flippers.left_raised) {
self.flippers.left_raised,
) {
(true, false) => -1, (true, false) => -1,
(false, true) => 1, (false, true) => 1,
_ => 0, _ => 0,
}; };
self.pending_flipper_edges[1] = match ( self.pending_flipper_edges[1] =
self.flipper_inputs.right_raised, match (self.flipper_inputs.right_raised, self.flippers.right_raised) {
self.flippers.right_raised,
) {
(true, false) => -1, (true, false) => -1,
(false, true) => 1, (false, true) => 1,
_ => 0, _ => 0,
@@ -706,7 +712,11 @@ impl Game {
match nudge { match nudge {
Nudge::Left | Nudge::Right => { Nudge::Left | Nudge::Right => {
let amount = (50 - i32::from(self.random.below(20))) * SCALAR; let amount = (50 - i32::from(self.random.below(20))) * SCALAR;
let signed = if nudge == Nudge::Left { -amount } else { amount }; let signed = if nudge == Nudge::Left {
-amount
} else {
amount
};
velocity.x = velocity.x.wrapping_add(signed); velocity.x = velocity.x.wrapping_add(signed);
} }
Nudge::Center => { Nudge::Center => {
@@ -726,12 +736,10 @@ impl Game {
} }
if let Some(secondary) = &mut self.secondary_ball { if let Some(secondary) = &mut self.secondary_ball {
let slot_impulse = match nudge { let slot_impulse = match nudge {
Nudge::Left | Nudge::Right => { Nudge::Left | Nudge::Right => MilliVec {
MilliVec {
x: if nudge == Nudge::Left { -600 } else { 600 }, x: if nudge == Nudge::Left { -600 } else { 600 },
y: 0, y: 0,
} },
}
Nudge::Center => { Nudge::Center => {
let horizontal = (i32::from(self.random.below(21)) - 10) * SCALAR; let horizontal = (i32::from(self.random.below(21)) - 10) * SCALAR;
let vertical = -(i32::from(self.random.below(100)) + 50) * SCALAR; let vertical = -(i32::from(self.random.below(100)) + 50) * SCALAR;
@@ -1070,13 +1078,7 @@ impl Game {
best = Some((id, false, candidate)); best = Some((id, false, candidate));
} }
ball.velocity = velocity.to_velocity_per_second(); ball.velocity = velocity.to_velocity_per_second();
( (best, SensorScanResult { action }, predicted)
best,
SensorScanResult {
action,
},
predicted,
)
} }
fn find_static_collision_candidate_in_range( fn find_static_collision_candidate_in_range(
@@ -1317,12 +1319,7 @@ impl Game {
collided || scan.action == BallAction::Suspend collided || scan.action == BallAction::Suspend
} }
fn apply_bumper_rule( fn apply_bumper_rule(&mut self, object_id: u8, auxiliary_fired: bool, events: &mut Vec<Event>) {
&mut self,
object_id: u8,
auxiliary_fired: bool,
events: &mut Vec<Event>,
) {
if self.tilted || !BUMPERS.iter().any(|bumper| bumper.id == object_id) { if self.tilted || !BUMPERS.iter().any(|bumper| bumper.id == object_id) {
return; return;
} }
@@ -1504,11 +1501,7 @@ impl Game {
if !self.tilted if !self.tilted
&& (TARGET_SENSORS.into_iter().any(|sensor| { && (TARGET_SENSORS.into_iter().any(|sensor| {
self.object_active[usize::from(sensor.id)] self.object_active[usize::from(sensor.id)]
&& trigger_broadphase_contains( && trigger_broadphase_contains(predicted_position, sensor.center, sensor.radius)
predicted_position,
sensor.center,
sensor.radius,
)
}) || self.object_active[usize::from(EFFECT_SENSOR.id)] }) || self.object_active[usize::from(EFFECT_SENSOR.id)]
&& trigger_broadphase_contains( && trigger_broadphase_contains(
predicted_position, predicted_position,
@@ -1549,8 +1542,7 @@ impl Game {
} }
} }
if let Some(completed_action) = if let Some(completed_action) = self.check_lock_holes(
self.check_lock_holes(
ball, ball,
ball_number, ball_number,
ball_count, ball_count,
@@ -1558,19 +1550,11 @@ impl Game {
predicted_position, predicted_position,
&mut capture_candidate, &mut capture_candidate,
events, events,
) ) {
{
action = completed_action; action = completed_action;
} }
self.check_target_sensors( self.check_target_sensors(ball, old_position, movement_velocity, 140..=147, events);
ball, if let Some(completed_action) = self.check_special_hole(
old_position,
movement_velocity,
140..=147,
events,
);
if let Some(completed_action) =
self.check_special_hole(
ball, ball,
ball_number, ball_number,
ball_count, ball_count,
@@ -1578,27 +1562,12 @@ impl Game {
predicted_position, predicted_position,
&mut capture_candidate, &mut capture_candidate,
events, events,
) ) {
{
action = completed_action; action = completed_action;
} }
self.check_effect_sensor( self.check_effect_sensor(ball, ball_number, old_position, movement_velocity, events);
ball, self.check_target_sensors(ball, old_position, movement_velocity, 150..=152, events);
ball_number, SensorScanResult { action }
old_position,
movement_velocity,
events,
);
self.check_target_sensors(
ball,
old_position,
movement_velocity,
150..=152,
events,
);
SensorScanResult {
action,
}
} }
#[allow(clippy::cast_possible_truncation, clippy::too_many_arguments)] #[allow(clippy::cast_possible_truncation, clippy::too_many_arguments)]
@@ -1858,7 +1827,8 @@ impl Game {
self.players[self.current_player].secondary_score = 0; self.players[self.current_player].secondary_score = 0;
} }
7 if self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)] != 0 7 if self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)] != 0
&& self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)] != ball_number && self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)]
!= ball_number
&& self.secondary_ball.is_none() && self.secondary_ball.is_none()
&& matches!( && matches!(
self.multiball_state, self.multiball_state,
@@ -1999,10 +1969,7 @@ impl Game {
.round_i32() .round_i32()
.wrapping_neg(); .wrapping_neg();
let predicted = old_position.add(*velocity); let predicted = old_position.add(*velocity);
if predicted.x < min_x if predicted.x < min_x || predicted.x > max_x || predicted.y < min_y || predicted.y > max_y
|| predicted.x > max_x
|| predicted.y < min_y
|| predicted.y > max_y
{ {
self.object_active[object_id] = false; self.object_active[object_id] = false;
} }
@@ -2225,7 +2192,10 @@ impl Game {
fn special_respawn_pending(&self) -> bool { fn special_respawn_pending(&self) -> bool {
self.secondary_ball.is_none() self.secondary_ball.is_none()
&& self.score_mode == ScoreMode::Normal && self.score_mode == ScoreMode::Normal
&& matches!(self.multiball_state, MultiballState::Ready | MultiballState::Active) && matches!(
self.multiball_state,
MultiballState::Ready | MultiballState::Active
)
} }
fn save_current_rule_state(&mut self) { fn save_current_rule_state(&mut self) {
@@ -2336,16 +2306,14 @@ fn claw_release(frame: u8) -> (Vec2, Vec2) {
fn apply_flipper_response_to_ball(ball: &mut Ball, delta: i32, side: FlipperSide) { fn apply_flipper_response_to_ball(ball: &mut Ball, delta: i32, side: FlipperSide) {
let position = MilliVec::from_position(ball.position); let position = MilliVec::from_position(ball.position);
let velocity = MilliVec::from_velocity_per_second(ball.velocity); let velocity = MilliVec::from_velocity_per_second(ball.velocity);
if let Some(response) = if let Some(response) = moving_flipper_response(
moving_flipper_response(
position, position,
velocity, velocity,
delta, delta,
side, side,
Real48::from_bytes([0x80, 0, 0, 0, 0, 0]), Real48::from_bytes([0x80, 0, 0, 0, 0, 0]),
MAXIMUM_SPEED_MILLI_PER_STEP, MAXIMUM_SPEED_MILLI_PER_STEP,
) ) {
{
ball.position = position.add(response.movement).to_position(); ball.position = position.add(response.movement).to_position();
ball.velocity = response.velocity.to_velocity_per_second(); ball.velocity = response.velocity.to_velocity_per_second();
} }
@@ -2379,11 +2347,7 @@ mod tests {
game.ball.in_launcher = false; game.ball.in_launcher = false;
game.ball.position = center; game.ball.position = center;
game.ball.velocity = Vec2::ZERO; game.ball.velocity = Vec2::ZERO;
game.check_sensor_objects( game.check_sensor_objects(MilliVec::from_position(center), MilliVec::default(), events);
MilliVec::from_position(center),
MilliVec::default(),
events,
);
} }
} }
@@ -2397,7 +2361,8 @@ mod tests {
) -> CaptureStep { ) -> CaptureStep {
let mut ball = game.ball; let mut ball = game.ball;
let mut capture_candidate = None; let mut capture_candidate = None;
let predicted_position = previous_position.add(MilliVec::from_velocity_per_second(ball.velocity)); let predicted_position =
previous_position.add(MilliVec::from_velocity_per_second(ball.velocity));
let result = game.capture_record_step( let result = game.capture_record_step(
&mut ball, &mut ball,
1, 1,
@@ -2456,8 +2421,8 @@ mod tests {
game.object_active[usize::from(wall.id)] = true; game.object_active[usize::from(wall.id)] = true;
game.ball.in_launcher = false; game.ball.in_launcher = false;
game.ball.position = midpoint - normal; game.ball.position = midpoint - normal;
game.ball.velocity = MilliVec::from_velocity_per_second(normal * 200.0) game.ball.velocity =
.to_velocity_per_second(); MilliVec::from_velocity_per_second(normal * 200.0).to_velocity_per_second();
game.fixed_update(&mut Vec::new()); game.fixed_update(&mut Vec::new());
if game.last_collision_id != Some(wall.id) { if game.last_collision_id != Some(wall.id) {
missed_walls.push(wall.id); missed_walls.push(wall.id);
@@ -2478,7 +2443,10 @@ mod tests {
} }
} }
assert!(missed_walls.is_empty(), "missed production wall records: {missed_walls:?}"); assert!(
missed_walls.is_empty(),
"missed production wall records: {missed_walls:?}"
);
assert!( assert!(
missed_circles.is_empty(), missed_circles.is_empty(),
"missed production circle records: {missed_circles:?}" "missed production circle records: {missed_circles:?}"
@@ -2869,9 +2837,11 @@ mod tests {
assert_eq!(special.multiball_state, MultiballState::Unavailable); assert_eq!(special.multiball_state, MultiballState::Unavailable);
assert_eq!(special.record_contacts[148], 0); assert_eq!(special.record_contacts[148], 0);
assert!(!special_events.contains(&Event::Drain)); assert!(!special_events.contains(&Event::Drain));
assert!(!special_events assert!(
!special_events
.iter() .iter()
.any(|event| event.sound_resource().is_some())); .any(|event| event.sound_resource().is_some())
);
} }
#[test] #[test]
@@ -3179,10 +3149,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
MilliVec::from_velocity_per_second(game.ball.velocity), MilliVec::from_velocity_per_second(game.ball.velocity),
MilliVec { MilliVec { x: 0, y: 3_055 }
x: 0,
y: 3_055,
}
); );
assert_eq!(game.special_hole_gate, SpecialHoleGate::Suppressed); assert_eq!(game.special_hole_gate, SpecialHoleGate::Suppressed);
@@ -3255,10 +3222,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
MilliVec::from_velocity_per_second(game.ball.velocity), MilliVec::from_velocity_per_second(game.ball.velocity),
MilliVec { MilliVec { x: -302, y: -1_809 }
x: -302,
y: -1_809,
}
); );
assert_eq!(game.multiball_state, MultiballState::Unavailable); assert_eq!(game.multiball_state, MultiballState::Unavailable);
assert_eq!(game.special_hole_gate, SpecialHoleGate::Suppressed); assert_eq!(game.special_hole_gate, SpecialHoleGate::Suppressed);
@@ -3297,9 +3261,7 @@ mod tests {
spin: Real48::ZERO, spin: Real48::ZERO,
capture_age: 0, capture_age: 0,
}; };
let old_position = MilliVec::from_position( let old_position = MilliVec::from_position(sensor.center - vec2(sensor.radius + 1.0, 0.0));
sensor.center - vec2(sensor.radius + 1.0, 0.0),
);
let current_position = MilliVec::from_position(sensor.center); let current_position = MilliVec::from_position(sensor.center);
let movement = MilliVec { let movement = MilliVec {
x: current_position.x.wrapping_sub(old_position.x), x: current_position.x.wrapping_sub(old_position.x),
@@ -3391,7 +3353,10 @@ mod tests {
BallAction::Keep BallAction::Keep
); );
assert_eq!(game.secondary_ball.map(|ball| ball.position), Some(secondary.position)); assert_eq!(
game.secondary_ball.map(|ball| ball.position),
Some(secondary.position)
);
assert_eq!(game.multiball_state, MultiballState::Active); assert_eq!(game.multiball_state, MultiballState::Active);
assert_eq!(game.target_effect, 0); assert_eq!(game.target_effect, 0);
} }
@@ -3511,7 +3476,6 @@ mod tests {
assert!(game.secondary_ball.is_none()); assert!(game.secondary_ball.is_none());
assert_eq!(game.player().balls, 3); assert_eq!(game.player().balls, 3);
assert_eq!(game.score_mode, ScoreMode::Normal); assert_eq!(game.score_mode, ScoreMode::Normal);
} }
#[test] #[test]
@@ -3605,10 +3569,7 @@ mod tests {
assert!(game.fixed_update(&mut events)); assert!(game.fixed_update(&mut events));
assert_eq!(game.last_collision_id, Some(57)); assert_eq!(game.last_collision_id, Some(57));
assert_eq!( assert_eq!(game.ball.spin.bytes(), [0x83, 0x5d, 0x8f, 0xc2, 0xf5, 0xa8]);
game.ball.spin.bytes(),
[0x83, 0x5d, 0x8f, 0xc2, 0xf5, 0xa8]
);
assert_eq!( assert_eq!(
MilliVec::from_position(game.ball.position), MilliVec::from_position(game.ball.position),
MilliVec { MilliVec {
@@ -3645,10 +3606,7 @@ mod tests {
assert!(game.fixed_update(&mut events)); assert!(game.fixed_update(&mut events));
assert_eq!(game.last_collision_id, Some(72)); assert_eq!(game.last_collision_id, Some(72));
assert_eq!( assert_eq!(game.ball.spin.bytes(), [0x83, 0x86, 0xeb, 0x51, 0xb8, 0x2e]);
game.ball.spin.bytes(),
[0x83, 0x86, 0xeb, 0x51, 0xb8, 0x2e]
);
assert_eq!( assert_eq!(
MilliVec::from_position(game.ball.position), MilliVec::from_position(game.ball.position),
MilliVec { MilliVec {
@@ -3658,7 +3616,10 @@ mod tests {
); );
assert_eq!( assert_eq!(
MilliVec::from_velocity_per_second(game.ball.velocity), MilliVec::from_velocity_per_second(game.ball.velocity),
MilliVec { x: -2_095, y: 1_669 } MilliVec {
x: -2_095,
y: 1_669
}
); );
} }
@@ -3928,8 +3889,7 @@ mod tests {
#[test] #[test]
fn raised_flipper_tips_use_their_asymmetric_swept_bounds() { fn raised_flipper_tips_use_their_asymmetric_swept_bounds() {
for (object_id, center) in [(67_u8, vec2(133.0, 377.0)), (82, vec2(181.0, 376.0))] for (object_id, center) in [(67_u8, vec2(133.0, 377.0)), (82, vec2(181.0, 376.0))] {
{
let mut game = Game::new(1); let mut game = Game::new(1);
game.object_active.fill(false); game.object_active.fill(false);
game.object_active[usize::from(object_id)] = true; game.object_active[usize::from(object_id)] = true;
@@ -4012,7 +3972,13 @@ mod tests {
let velocity_after_edge = game.ball.velocity; let velocity_after_edge = game.ball.velocity;
assert_eq!(game.ball.position, vec2(138.124, 355.0)); assert_eq!(game.ball.position, vec2(138.124, 355.0));
let raw_velocity = MilliVec::from_velocity_per_second(velocity_after_edge); let raw_velocity = MilliVec::from_velocity_per_second(velocity_after_edge);
assert_eq!(raw_velocity, MilliVec { x: 2_209, y: -5_891 }); assert_eq!(
raw_velocity,
MilliVec {
x: 2_209,
y: -5_891
}
);
assert_eq!(game.pending_flipper_edges, [0, 0]); assert_eq!(game.pending_flipper_edges, [0, 0]);
game.apply_flipper_kicks(); game.apply_flipper_kicks();
@@ -4102,20 +4068,20 @@ mod tests {
#[test] #[test]
fn tilt_counter_decays_once_per_selected_detail_callback() { fn tilt_counter_decays_once_per_selected_detail_callback() {
for (detail, interval) in [ for (detail, interval) in [(1, 0.050), (2, 0.040), (3, 0.030), (4, 0.020), (5, 0.010)] {
(1, 0.050),
(2, 0.040),
(3, 0.030),
(4, 0.020),
(5, 0.010),
] {
let mut game = Game::new(1); let mut game = Game::new(1);
game.tilt_counter = 2; game.tilt_counter = 2;
game.update(interval - 0.001, detail, Controls::default()); game.update(interval - 0.001, detail, Controls::default());
assert_eq!(game.tilt_counter, 2, "detail {detail} decayed before its callback"); assert_eq!(
game.tilt_counter, 2,
"detail {detail} decayed before its callback"
);
game.update(0.001_1, detail, Controls::default()); game.update(0.001_1, detail, Controls::default());
assert_eq!(game.tilt_counter, 1, "detail {detail} did not decay at its callback"); assert_eq!(
game.tilt_counter, 1,
"detail {detail} did not decay at its callback"
);
} }
} }
@@ -4462,11 +4428,7 @@ mod tests {
5 5
); );
complete_stationary_type_three_capture( complete_stationary_type_three_capture(&mut game, SPECIAL_HOLE_SENSOR.center, &mut events);
&mut game,
SPECIAL_HOLE_SENSOR.center,
&mut events,
);
assert!(game.wheel_holes.iter().all(|filled| *filled)); assert!(game.wheel_holes.iter().all(|filled| *filled));
assert_eq!(game.multiball_state, MultiballState::Ready); assert_eq!(game.multiball_state, MultiballState::Ready);
assert_eq!(game.record_contacts[148], 2); assert_eq!(game.record_contacts[148], 2);
@@ -4696,7 +4658,11 @@ mod tests {
assert_eq!(game.panel_frame, None); assert_eq!(game.panel_frame, None);
assert_eq!(game.wheel_holes, [false; 5]); assert_eq!(game.wheel_holes, [false; 5]);
assert!(game.record_contacts[129..=133].iter().all(|contact| *contact == 0)); assert!(
game.record_contacts[129..=133]
.iter()
.all(|contact| *contact == 0)
);
assert_eq!( assert_eq!(
events, events,
[ [
@@ -4758,7 +4724,11 @@ mod tests {
game.record_countdowns[51] = 5; game.record_countdowns[51] = 5;
game.timer_tick(0.030, 0, &mut Vec::new()); game.timer_tick(0.030, 0, &mut Vec::new());
assert_eq!(game.record_countdown(51), 5, "launcher idle pauses countdowns"); assert_eq!(
game.record_countdown(51),
5,
"launcher idle pauses countdowns"
);
game.ball.in_launcher = false; game.ball.in_launcher = false;
for expected in (0..5).rev() { for expected in (0..5).rev() {
+23 -30
View File
@@ -73,11 +73,7 @@ impl CollisionMaterial {
} }
} }
pub const fn circle( pub const fn circle(normal_rebound: f64, tangent_coupling: f64, normal_kick: f64) -> Self {
normal_rebound: f64,
tangent_coupling: f64,
normal_kick: f64,
) -> Self {
Self { Self {
normal_rebound, normal_rebound,
tangent_coupling, tangent_coupling,
@@ -173,8 +169,7 @@ impl MilliVec {
// `maximum / speed` is mathematically equivalent, but can differ by // `maximum / speed` is mathematically equivalent, but can differ by
// one millipixel because each Real48 operation is rounded // one millipixel because each Real48 operation is rounded
// independently. // independently.
let excess = Real48::from_i32(speed.wrapping_sub(maximum)) let excess = Real48::from_i32(speed.wrapping_sub(maximum)).divide(Real48::from_i32(speed));
.divide(Real48::from_i32(speed));
self.x = self self.x = self
.x .x
.wrapping_sub(Real48::from_i32(self.x).multiply(excess).round_i32()); .wrapping_sub(Real48::from_i32(self.x).multiply(excess).round_i32());
@@ -262,11 +257,7 @@ fn tangent_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, leng
.round_i32() .round_i32()
} }
fn cross_at_endpoint( fn cross_at_endpoint(point: MilliVec, current: MilliVec, predicted: MilliVec) -> i32 {
point: MilliVec,
current: MilliVec,
predicted: MilliVec,
) -> i32 {
predicted predicted
.x .x
.wrapping_sub(point.x) .wrapping_sub(point.x)
@@ -383,9 +374,7 @@ pub fn line_collision_candidate_at(
let vertical_units = normal_y.divide(THOUSAND); let vertical_units = normal_y.divide(THOUSAND);
let distance = Real48::from_i32(old_position.x.wrapping_sub(start.x)) let distance = Real48::from_i32(old_position.x.wrapping_sub(start.x))
.multiply(vertical_units) .multiply(vertical_units)
.subtract( .subtract(Real48::from_i32(old_position.y.wrapping_sub(start.y)).multiply(horizontal_units))
Real48::from_i32(old_position.y.wrapping_sub(start.y)).multiply(horizontal_units),
)
.divide(length_units); .divide(length_units);
let distance = if distance.compare(ZERO).is_lt() { let distance = if distance.compare(ZERO).is_lt() {
ZERO.subtract(distance) ZERO.subtract(distance)
@@ -525,10 +514,7 @@ pub fn capture_collision_candidate(
let radius_milli = (radius * 1_000.0).round() as i32; let radius_milli = (radius * 1_000.0).round() as i32;
let delta = MilliVec { let delta = MilliVec {
x: center.x.wrapping_sub(old_position.x), x: center.x.wrapping_sub(old_position.x),
y: center y: center.y.wrapping_sub(old_position.y).wrapping_sub(2_000),
.y
.wrapping_sub(old_position.y)
.wrapping_sub(2_000),
}; };
let surface_distance = milli_distance(delta).wrapping_sub(radius_milli); let surface_distance = milli_distance(delta).wrapping_sub(radius_milli);
let normal_x = Real48::from_i32(center.y.wrapping_sub(old_position.y)); let normal_x = Real48::from_i32(center.y.wrapping_sub(old_position.y));
@@ -541,9 +527,7 @@ pub fn capture_collision_candidate(
return None; return None;
} }
let normal_velocity = normal_velocity(velocity, normal_x, normal_y, length); let normal_velocity = normal_velocity(velocity, normal_x, normal_y, length);
if normal_velocity >= 0 if normal_velocity >= 0 || normal_velocity.wrapping_abs() < surface_distance.wrapping_abs() {
|| normal_velocity.wrapping_abs() < surface_distance.wrapping_abs()
{
return None; return None;
} }
Some(StaticCollisionCandidate { Some(StaticCollisionCandidate {
@@ -681,8 +665,14 @@ mod tests {
#[test] #[test]
fn float_views_roundtrip_every_gameplay_velocity_millipixel() { fn float_views_roundtrip_every_gameplay_velocity_millipixel() {
for value in -10_000..=10_000 { for value in -10_000..=10_000 {
let milli = MilliVec { x: value, y: -value }; let milli = MilliVec {
assert_eq!(MilliVec::from_velocity_per_second(milli.to_velocity_per_second()), milli); x: value,
y: -value,
};
assert_eq!(
MilliVec::from_velocity_per_second(milli.to_velocity_per_second()),
milli
);
} }
} }
@@ -697,8 +687,7 @@ mod tests {
if speed <= maximum { if speed <= maximum {
continue; continue;
} }
let excess = Real48::from_i32(speed - maximum) let excess = Real48::from_i32(speed - maximum).divide(Real48::from_i32(speed));
.divide(Real48::from_i32(speed));
let expected = MilliVec { let expected = MilliVec {
x: x.wrapping_sub(Real48::from_i32(x).multiply(excess).round_i32()), x: x.wrapping_sub(Real48::from_i32(x).multiply(excess).round_i32()),
y: y.wrapping_sub(Real48::from_i32(y).multiply(excess).round_i32()), y: y.wrapping_sub(Real48::from_i32(y).multiply(excess).round_i32()),
@@ -716,7 +705,10 @@ mod tests {
break; break;
} }
} }
assert!(mismatches.is_empty(), "speed-clamp mismatches: {mismatches:?}"); assert!(
mismatches.is_empty(),
"speed-clamp mismatches: {mismatches:?}"
);
} }
#[test] #[test]
@@ -783,7 +775,9 @@ mod tests {
assert_eq!(candidate.surface_distance, -800); assert_eq!(candidate.surface_distance, -800);
assert_ne!( assert_ne!(
candidate.resolve(MilliVec { x: 1_000, y: 0 }, Real48::ZERO).velocity, candidate
.resolve(MilliVec { x: 1_000, y: 0 }, Real48::ZERO)
.velocity,
MilliVec { x: 1_000, y: 0 } MilliVec { x: 1_000, y: 0 }
); );
} }
@@ -887,8 +881,7 @@ mod tests {
fn surface_distance_orders_candidate_contacts() { fn surface_distance_orders_candidate_contacts() {
let old = MilliVec::default(); let old = MilliVec::default();
let velocity = MilliVec { x: 10_000, y: 0 }; let velocity = MilliVec { x: 10_000, y: 0 };
let near = let near = line_collision_response(
line_collision_response(
old, old,
velocity, velocity,
vec2(2.0, 1.0), vec2(2.0, 1.0),
+12 -6
View File
@@ -1,6 +1,7 @@
use std::{fs, io, path::PathBuf};
use directories::ProjectDirs; use directories::ProjectDirs;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{fs, io, path::PathBuf};
const ORIGINAL_HIGHSCORES: &[u8] = include_bytes!("../assets/original/HISCORES.DAT"); const ORIGINAL_HIGHSCORES: &[u8] = include_bytes!("../assets/original/HISCORES.DAT");
const ORIGINAL_INI: &str = include_str!("../../original/TDKPIN.INI"); const ORIGINAL_INI: &str = include_str!("../../original/TDKPIN.INI");
@@ -154,7 +155,10 @@ pub fn parse_original_settings(text: &str) -> Settings {
_ => Language::English, _ => Language::English,
}; };
} else if key.trim().eq_ignore_ascii_case("Speed") { } else if key.trim().eq_ignore_ascii_case("Speed") {
settings.speed = u8::try_from(value).ok().filter(|speed| (1..=5).contains(speed)).unwrap_or(3); settings.speed = u8::try_from(value)
.ok()
.filter(|speed| (1..=5).contains(speed))
.unwrap_or(3);
} else if key.trim().eq_ignore_ascii_case("Sound") { } else if key.trim().eq_ignore_ascii_case("Sound") {
settings.sounds = value != 0; settings.sounds = value != 0;
} }
@@ -249,14 +253,16 @@ mod tests {
let settings = parse_original_settings(ORIGINAL_INI); let settings = parse_original_settings(ORIGINAL_INI);
assert_eq!(settings.speed, 3); assert_eq!(settings.speed, 3);
assert_eq!(settings.language, Language::German); assert_eq!(settings.language, Language::German);
assert!(settings.sounds, "the original reads singular Sound, not Sounds"); assert!(
settings.sounds,
"the original reads singular Sound, not Sounds"
);
} }
#[test] #[test]
fn original_ini_values_are_case_insensitive_and_clamped() { fn original_ini_values_are_case_insensitive_and_clamped() {
let settings = parse_original_settings( let settings =
"[settings]\nlanguage=5\nSPEED=9\nSound=0\nSounds=1\n", parse_original_settings("[settings]\nlanguage=5\nSPEED=9\nSound=0\nSounds=1\n");
);
assert_eq!(settings.speed, 3); assert_eq!(settings.speed, 3);
assert_eq!(settings.language, Language::Spanish); assert_eq!(settings.language, Language::Spanish);
assert!(!settings.sounds); assert!(!settings.sounds);
+7 -13
View File
@@ -167,8 +167,7 @@ impl Real48 {
} }
let mut numerator = self.significand(); let mut numerator = self.significand();
let denominator = right.significand(); let denominator = right.significand();
let mut exponent = let mut exponent = i16::from(self.exponent()) - i16::from(right.exponent()) + EXPONENT_BIAS;
i16::from(self.exponent()) - i16::from(right.exponent()) + EXPONENT_BIAS;
if numerator < denominator { if numerator < denominator {
numerator <<= 1; numerator <<= 1;
exponent -= 1; exponent -= 1;
@@ -254,16 +253,8 @@ impl Real48 {
assert!(highest_bit < 32, "Real48 integer overflow"); assert!(highest_bit < 32, "Real48 integer overflow");
let significand = self.significand(); let significand = self.significand();
let shift = i16::try_from(FRACTION_BITS).unwrap_or(39) - highest_bit; let shift = i16::try_from(FRACTION_BITS).unwrap_or(39) - highest_bit;
let mut magnitude = if shift >= 64 { let mut magnitude = if shift >= 64 { 0 } else { significand >> shift };
0 if round && shift > 0 && shift <= 40 && (significand >> (shift - 1)) & 1 != 0 {
} else {
significand >> shift
};
if round
&& shift > 0
&& shift <= 40
&& (significand >> (shift - 1)) & 1 != 0
{
magnitude += 1; magnitude += 1;
} }
let limit = if self.negative() { let limit = if self.negative() {
@@ -344,7 +335,10 @@ mod tests {
assert_eq!(HALF.round_i32(), 1); assert_eq!(HALF.round_i32(), 1);
assert_eq!(Real48::from_bytes([0x80, 0, 0, 0, 0, 0x80]).round_i32(), -1); assert_eq!(Real48::from_bytes([0x80, 0, 0, 0, 0, 0x80]).round_i32(), -1);
assert_eq!(ONE.compare(TWO), Ordering::Less); assert_eq!(ONE.compare(TWO), Ordering::Less);
assert_eq!(Real48::from_i32(-2).compare(Real48::from_i32(-1)), Ordering::Less); assert_eq!(
Real48::from_i32(-2).compare(Real48::from_i32(-1)),
Ordering::Less
);
assert_eq!(TWO.sqrt().bytes(), [0x81, 0xfa, 0x33, 0xf3, 0x04, 0x35]); assert_eq!(TWO.sqrt().bytes(), [0x81, 0xfa, 0x33, 0xf3, 0x04, 0x35]);
assert_eq!( assert_eq!(
ONE_AND_HALF.sqrt().bytes(), ONE_AND_HALF.sqrt().bytes(),
+33 -10
View File
@@ -1,11 +1,13 @@
use crate::game::{ClawSpriteBank, Controls, Event, Game};
use serde::Serialize;
use std::{ use std::{
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
}; };
use serde::Serialize;
use crate::game::{ClawSpriteBank, Controls, Event, Game};
pub const SIMULATION_HZ: u32 = 120; pub const SIMULATION_HZ: u32 = 120;
const SIMULATION_HZ_F64: f64 = 120.0; const SIMULATION_HZ_F64: f64 = 120.0;
const SIMULATION_DT: f32 = 1.0 / 120.0; const SIMULATION_DT: f32 = 1.0 / 120.0;
@@ -376,9 +378,7 @@ fn controls_for(scenario: Scenario, step: u64) -> Controls {
| Scenario::Claw1 | Scenario::Claw1
| Scenario::Claw6 | Scenario::Claw6
| Scenario::Claw7 | Scenario::Claw7
| Scenario::Claw18 => { | Scenario::Claw18 => Controls::default(),
Controls::default()
}
} }
} }
@@ -507,9 +507,24 @@ mod tests {
.iter() .iter()
.flat_map(|snapshot| snapshot.events.iter().map(String::as_str)) .flat_map(|snapshot| snapshot.events.iter().map(String::as_str))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!(events.iter().filter(|event| **event == "StopSound").count(), 3); assert_eq!(
assert_eq!(events.iter().filter(|event| **event == "Sound(2013)").count(), 4); events.iter().filter(|event| **event == "StopSound").count(),
assert_eq!(events.iter().filter(|event| **event == "Sound(2012)").count(), 2); 3
);
assert_eq!(
events
.iter()
.filter(|event| **event == "Sound(2013)")
.count(),
4
);
assert_eq!(
events
.iter()
.filter(|event| **event == "Sound(2012)")
.count(),
2
);
} }
#[test] #[test]
@@ -521,7 +536,10 @@ mod tests {
simulation.advance_to(22); simulation.advance_to(22);
assert_eq!(simulation.game.target_rotation_state, Some(6)); assert_eq!(simulation.game.target_rotation_state, Some(6));
assert_eq!(simulation.game.wheel_holes, [false, true, false, false, true]); assert_eq!(
simulation.game.wheel_holes,
[false, true, false, false, true]
);
simulation.advance_to(26); simulation.advance_to(26);
assert_eq!(simulation.game.target_rotation_state, None); assert_eq!(simulation.game.target_rotation_state, None);
} }
@@ -561,7 +579,12 @@ mod tests {
.filter(|event| **event == "ClawRelease") .filter(|event| **event == "ClawRelease")
.count(); .count();
assert!(captures >= 1, "{} must enter the claw", scenario.name()); assert!(captures >= 1, "{} must enter the claw", scenario.name());
assert_eq!(releases, 1, "{} must release its seeded capture", scenario.name()); assert_eq!(
releases,
1,
"{} must release its seeded capture",
scenario.name()
);
assert!(captures >= releases); assert!(captures >= releases);
assert!(simulation.game.ball.position.is_finite()); assert!(simulation.game.ball.position.is_finite());
assert!(simulation.game.ball.velocity.is_finite()); assert!(simulation.game.ball.velocity.is_finite());
+6 -2
View File
@@ -9,9 +9,10 @@
//! each object's bounds by `param_26`. They must not be enlarged again by the //! each object's bounds by `param_26`. They must not be enlarged again by the
//! radius of the rendered ball. //! radius of the rendered ball.
use crate::geometry::Segment;
use macroquad::prelude::Vec2; use macroquad::prelude::Vec2;
use crate::geometry::Segment;
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct TableSegment { pub struct TableSegment {
pub id: u8, pub id: u8,
@@ -425,7 +426,10 @@ mod tests {
.iter() .iter()
.find(|wall| wall.id == id) .find(|wall| wall.id == id)
.expect("relative line record must be present"); .expect("relative line record must be present");
assert_eq!((wall.segment.start, wall.segment.end), (expected_start, expected_end)); assert_eq!(
(wall.segment.start, wall.segment.end),
(expected_start, expected_end)
);
} }
for (id, expected_center) in [ for (id, expected_center) in [
(54, Vec2::new(53.0, 292.0)), (54, Vec2::new(53.0, 292.0)),