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::{
assets::Assets,
game::{Controls, Event, Game, Nudge},
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 HEIGHT: f32 = 460.0;
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 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)],
[(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)],
[
(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),
],
[
(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] = [
(103, 304, 121, 323),
@@ -116,11 +159,7 @@ impl AttractAnimation {
fn target_active(self, index: usize) -> bool {
let index = u32::try_from(index).unwrap_or(0);
let first_activation = if index == 0 {
32
} else {
index * 2
};
let first_activation = if index == 0 { 32 } else { index * 2 };
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 {
let index = u32::try_from(index).unwrap_or(0);
let first_activation = if index == 0 {
40
} else {
index * 4
};
let first_activation = if index == 0 { 40 } else { index * 4 };
self.tick >= first_activation && self.tick.wrapping_sub(index * 4) % 40 < 20
}
@@ -151,11 +186,7 @@ impl AttractAnimation {
fn item_index(self) -> Option<usize> {
let phase = (self.tick % 144) / 8;
let item = if phase < 10 {
phase
} else {
18 - phase
};
let item = if phase < 10 { phase } else { 18 - phase };
(item != 0).then(|| usize::try_from(item - 1).unwrap_or(0))
}
@@ -495,7 +526,8 @@ impl App {
self.name.pop();
}
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 =
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 {
@@ -574,11 +606,8 @@ impl App {
}
if self.attract.magnetic_records_active() {
for (x, y, width, height) in [
(151, 389, 13, 49),
(11, 344, 13, 49),
(291, 346, 13, 49),
] {
for (x, y, width, height) in [(151, 389, 13, 49), (11, 344, 13, 49), (291, 346, 13, 49)]
{
self.draw_active_table_region(x, y, width, height);
}
}
@@ -605,12 +634,7 @@ impl App {
}
if let Some(item) = self.attract.item_index() {
draw_texture(
&self.assets.diamond[item],
123.0,
300.0,
WHITE,
);
draw_texture(&self.assets.diamond[item], 123.0, 300.0, WHITE);
}
self.draw_intro_marquee();
}
@@ -929,7 +953,15 @@ impl App {
} 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);
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(
@@ -1122,14 +1154,7 @@ impl App {
draw_centered("SETTINGS", 122.0, 30, BLACK);
let rows = [
format!("GRAPHICS DETAIL / SPEED: {}", self.saved.settings.speed),
format!(
"SOUND: {}",
if self.sounds_enabled {
"ON"
} else {
"OFF"
}
),
format!("SOUND: {}", if self.sounds_enabled { "ON" } else { "OFF" }),
format!("LANGUAGE: {}", self.saved.settings.language.label()),
];
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_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(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(
"Congratulations! This is a Top Ten Score!",
x + 9.0,
@@ -1319,13 +1350,7 @@ mod tests {
#[test]
fn attract_timer_uses_the_selected_detail_callback() {
for (detail, interval) in [
(1, 0.050),
(2, 0.040),
(3, 0.030),
(4, 0.020),
(5, 0.010),
] {
for (detail, interval) in [(1, 0.050), (2, 0.040), (3, 0.030), (4, 0.020), (5, 0.010)] {
let mut animation = AttractAnimation::default();
animation.update(interval - 0.001, detail);
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 loading = texture(include_bytes!("../assets/original/images/dat_00995.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 = [
texture(include_bytes!("../assets/original/images/dat_01001.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() {
let mut random = BorlandRandom::new(7);
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]
fn unit_interval_is_the_exact_unsigned_seed_fraction() {
let mut random = BorlandRandom::new(0xfedc_ba98);
let expected_seed = 0xfedc_ba98_u32
.wrapping_mul(MULTIPLIER)
.wrapping_add(1);
let expected_seed = 0xfedc_ba98_u32.wrapping_mul(MULTIPLIER).wrapping_add(1);
assert_eq!(
random.unit_interval().to_bits(),
(f64::from(expected_seed) / TWO_TO_32).to_bits()
+70 -29
View File
@@ -5,8 +5,7 @@ use crate::{original_physics::MilliVec, real48::Real48};
const SEARCH_RADIUS: i32 = 54_000;
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_FIFTHS: Real48 =
Real48::from_bytes([0x7f, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]);
const TWO_FIFTHS: Real48 = Real48::from_bytes([0x7f, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]);
const THOUSAND: Real48 = Real48::from_bytes([0x8a, 0, 0, 0, 0, 0x7a]);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -171,14 +170,11 @@ fn penetration(geometry: Geometry, delta: i32, ball: MilliVec) -> i32 {
.wrapping_add(43_000)
.wrapping_sub(geometry.pivot.y),
);
geometry
.pivot
.y
.wrapping_sub(
Real48::from_i32(numerator)
.divide(Real48::from_i32(edge_dx))
.round_i32(),
)
geometry.pivot.y.wrapping_sub(
Real48::from_i32(numerator)
.divide(Real48::from_i32(edge_dx))
.round_i32(),
)
} else {
geometry.positive_edge.y.wrapping_add(43_000)
}
@@ -299,8 +295,14 @@ mod tests {
let velocities = [
MilliVec { x: 0, y: 2_000 },
MilliVec { x: 1_000, y: 2_000 },
MilliVec { x: -1_000, y: 2_000 },
MilliVec { x: 2_700, y: -2_700 },
MilliVec {
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 hits = 0_u32;
@@ -315,10 +317,10 @@ mod tests {
Real48::from_bytes([0x80, 0, 0, 0, 0, 0]),
3_800,
);
let (hit, velocity, movement) = response.map_or(
(0_u32, input_velocity, MilliVec::default()),
|response| (1, response.velocity, response.movement),
);
let (hit, velocity, movement) = response
.map_or((0_u32, input_velocity, MilliVec::default()), |response| {
(1, response.velocity, response.movement)
});
hits += hit;
for value in [
i32_bits(x),
@@ -342,40 +344,73 @@ mod tests {
fn four_direction_vectors_match_the_reconstructed_c_harness() {
let cases = [
(
MilliVec { x: 104_000, y: 384_000 },
MilliVec {
x: 104_000,
y: 384_000,
},
MilliVec { x: 1_000, y: 2_000 },
-1,
FlipperSide::Left,
Real48::from_bytes([0x80, 0, 0, 0, 0, 0]),
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 { x: -1_000, y: 2_000 },
MilliVec {
x: 209_000,
y: 419_000,
},
MilliVec {
x: -1_000,
y: 2_000,
},
1,
FlipperSide::Right,
Real48::from_bytes([0x80, 0, 0, 0, 0, 0x40]),
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 },
1,
FlipperSide::Left,
Real48::from_bytes([0x7f, 0, 0, 0, 0, 0]),
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 { x: -1_000, y: 2_000 },
MilliVec {
x: 209_000,
y: 385_000,
},
MilliVec {
x: -1_000,
y: 2_000,
},
-1,
FlipperSide::Right,
Real48::ZERO,
MilliVec { x: -1_280, y: 1_560 },
MilliVec { x: 7_385, y: -9_000 },
MilliVec {
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 {
@@ -398,7 +433,10 @@ mod tests {
fn boundary_vectors_match_the_live_original_binary() {
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 },
-1,
FlipperSide::Left,
@@ -406,7 +444,10 @@ mod tests {
MilliVec::default(),
),
(
MilliVec { x: 209_000, y: 419_000 },
MilliVec {
x: 209_000,
y: 419_000,
},
MilliVec { x: 1_000, y: 2_000 },
1,
FlipperSide::Right,
+142 -172
View File
@@ -1,20 +1,34 @@
use macroquad::prelude::{Rect, Vec2, vec2};
use crate::{
borland_random::BorlandRandom,
flipper_physics::{FlipperSide, moving_flipper_response},
geometry::Segment,
original_physics::{
CollisionMaterial, CollisionResponse, GRAVITY_MILLI_PER_STEP,
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,
CollisionMaterial,
CollisionResponse,
GRAVITY_MILLI_PER_STEP,
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,
table::{
BUMPERS, EFFECT_SENSOR, LOCK_HOLES, PASSIVE_CIRCLES, TARGET_SENSORS, WALLS,
BUMPERS,
EFFECT_SENSOR,
LOCK_HOLES,
PASSIVE_CIRCLES,
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 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;
launch_velocity = -MAXIMUM_SPEED_MILLI_PER_STEP + i32::from(variation);
} 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.velocity = MilliVec {
@@ -516,12 +531,10 @@ impl Game {
} else if self.tilted {
self.flipper_release_latch[1] = true;
}
self.flipper_inputs.left_raised = controls.left_flipper
&& !self.tilted
&& !self.flipper_release_latch[0];
self.flipper_inputs.right_raised = controls.right_flipper
&& !self.tilted
&& !self.flipper_release_latch[1];
self.flipper_inputs.left_raised =
controls.left_flipper && !self.tilted && !self.flipper_release_latch[0];
self.flipper_inputs.right_raised =
controls.right_flipper && !self.tilted && !self.flipper_release_latch[1];
if self.ball.in_launcher && !self.tilted {
if controls.launch_down {
@@ -576,10 +589,7 @@ impl Game {
self.score_mode = ScoreMode::Multiball;
}
for _ in 0..substeps {
if self.fixed_update(events)
|| self.finished
|| self.claw.ball_suspended
{
if self.fixed_update(events) || self.finished || self.claw.ball_suspended {
break;
}
}
@@ -609,22 +619,18 @@ impl Game {
}
}
self.pending_flipper_edges[0] = match (
self.flipper_inputs.left_raised,
self.flippers.left_raised,
) {
(true, false) => -1,
(false, true) => 1,
_ => 0,
};
self.pending_flipper_edges[1] = match (
self.flipper_inputs.right_raised,
self.flippers.right_raised,
) {
(true, false) => -1,
(false, true) => 1,
_ => 0,
};
self.pending_flipper_edges[0] =
match (self.flipper_inputs.left_raised, self.flippers.left_raised) {
(true, false) => -1,
(false, true) => 1,
_ => 0,
};
self.pending_flipper_edges[1] =
match (self.flipper_inputs.right_raised, self.flippers.right_raised) {
(true, false) => -1,
(false, true) => 1,
_ => 0,
};
self.flippers = self.flipper_inputs;
for edge in self.pending_flipper_edges {
if edge != 0 {
@@ -706,7 +712,11 @@ impl Game {
match nudge {
Nudge::Left | Nudge::Right => {
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);
}
Nudge::Center => {
@@ -726,12 +736,10 @@ impl Game {
}
if let Some(secondary) = &mut self.secondary_ball {
let slot_impulse = match nudge {
Nudge::Left | Nudge::Right => {
MilliVec {
x: if nudge == Nudge::Left { -600 } else { 600 },
y: 0,
}
}
Nudge::Left | Nudge::Right => MilliVec {
x: if nudge == Nudge::Left { -600 } else { 600 },
y: 0,
},
Nudge::Center => {
let horizontal = (i32::from(self.random.below(21)) - 10) * SCALAR;
let vertical = -(i32::from(self.random.below(100)) + 50) * SCALAR;
@@ -1070,13 +1078,7 @@ impl Game {
best = Some((id, false, candidate));
}
ball.velocity = velocity.to_velocity_per_second();
(
best,
SensorScanResult {
action,
},
predicted,
)
(best, SensorScanResult { action }, predicted)
}
fn find_static_collision_candidate_in_range(
@@ -1317,12 +1319,7 @@ impl Game {
collided || scan.action == BallAction::Suspend
}
fn apply_bumper_rule(
&mut self,
object_id: u8,
auxiliary_fired: bool,
events: &mut Vec<Event>,
) {
fn apply_bumper_rule(&mut self, object_id: u8, auxiliary_fired: bool, events: &mut Vec<Event>) {
if self.tilted || !BUMPERS.iter().any(|bumper| bumper.id == object_id) {
return;
}
@@ -1504,11 +1501,7 @@ impl Game {
if !self.tilted
&& (TARGET_SENSORS.into_iter().any(|sensor| {
self.object_active[usize::from(sensor.id)]
&& trigger_broadphase_contains(
predicted_position,
sensor.center,
sensor.radius,
)
&& trigger_broadphase_contains(predicted_position, sensor.center, sensor.radius)
}) || self.object_active[usize::from(EFFECT_SENSOR.id)]
&& trigger_broadphase_contains(
predicted_position,
@@ -1549,56 +1542,32 @@ impl Game {
}
}
if let Some(completed_action) =
self.check_lock_holes(
ball,
ball_number,
ball_count,
old_position,
predicted_position,
&mut capture_candidate,
events,
)
{
action = completed_action;
}
self.check_target_sensors(
ball,
old_position,
movement_velocity,
140..=147,
events,
);
if let Some(completed_action) =
self.check_special_hole(
ball,
ball_number,
ball_count,
old_position,
predicted_position,
&mut capture_candidate,
events,
)
{
action = completed_action;
}
self.check_effect_sensor(
if let Some(completed_action) = self.check_lock_holes(
ball,
ball_number,
ball_count,
old_position,
movement_velocity,
predicted_position,
&mut capture_candidate,
events,
);
self.check_target_sensors(
ball,
old_position,
movement_velocity,
150..=152,
events,
);
SensorScanResult {
action,
) {
action = completed_action;
}
self.check_target_sensors(ball, old_position, movement_velocity, 140..=147, events);
if let Some(completed_action) = self.check_special_hole(
ball,
ball_number,
ball_count,
old_position,
predicted_position,
&mut capture_candidate,
events,
) {
action = completed_action;
}
self.check_effect_sensor(ball, ball_number, 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)]
@@ -1858,7 +1827,8 @@ impl Game {
self.players[self.current_player].secondary_score = 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()
&& matches!(
self.multiball_state,
@@ -1999,10 +1969,7 @@ impl Game {
.round_i32()
.wrapping_neg();
let predicted = old_position.add(*velocity);
if predicted.x < min_x
|| predicted.x > max_x
|| predicted.y < min_y
|| predicted.y > max_y
if predicted.x < min_x || predicted.x > max_x || predicted.y < min_y || predicted.y > max_y
{
self.object_active[object_id] = false;
}
@@ -2225,7 +2192,10 @@ impl Game {
fn special_respawn_pending(&self) -> bool {
self.secondary_ball.is_none()
&& 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) {
@@ -2336,16 +2306,14 @@ fn claw_release(frame: u8) -> (Vec2, Vec2) {
fn apply_flipper_response_to_ball(ball: &mut Ball, delta: i32, side: FlipperSide) {
let position = MilliVec::from_position(ball.position);
let velocity = MilliVec::from_velocity_per_second(ball.velocity);
if let Some(response) =
moving_flipper_response(
position,
velocity,
delta,
side,
Real48::from_bytes([0x80, 0, 0, 0, 0, 0]),
MAXIMUM_SPEED_MILLI_PER_STEP,
)
{
if let Some(response) = moving_flipper_response(
position,
velocity,
delta,
side,
Real48::from_bytes([0x80, 0, 0, 0, 0, 0]),
MAXIMUM_SPEED_MILLI_PER_STEP,
) {
ball.position = position.add(response.movement).to_position();
ball.velocity = response.velocity.to_velocity_per_second();
}
@@ -2379,11 +2347,7 @@ mod tests {
game.ball.in_launcher = false;
game.ball.position = center;
game.ball.velocity = Vec2::ZERO;
game.check_sensor_objects(
MilliVec::from_position(center),
MilliVec::default(),
events,
);
game.check_sensor_objects(MilliVec::from_position(center), MilliVec::default(), events);
}
}
@@ -2397,7 +2361,8 @@ mod tests {
) -> CaptureStep {
let mut ball = game.ball;
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(
&mut ball,
1,
@@ -2456,8 +2421,8 @@ mod tests {
game.object_active[usize::from(wall.id)] = true;
game.ball.in_launcher = false;
game.ball.position = midpoint - normal;
game.ball.velocity = MilliVec::from_velocity_per_second(normal * 200.0)
.to_velocity_per_second();
game.ball.velocity =
MilliVec::from_velocity_per_second(normal * 200.0).to_velocity_per_second();
game.fixed_update(&mut Vec::new());
if game.last_collision_id != Some(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!(
missed_circles.is_empty(),
"missed production circle records: {missed_circles:?}"
@@ -2869,9 +2837,11 @@ mod tests {
assert_eq!(special.multiball_state, MultiballState::Unavailable);
assert_eq!(special.record_contacts[148], 0);
assert!(!special_events.contains(&Event::Drain));
assert!(!special_events
.iter()
.any(|event| event.sound_resource().is_some()));
assert!(
!special_events
.iter()
.any(|event| event.sound_resource().is_some())
);
}
#[test]
@@ -3179,10 +3149,7 @@ mod tests {
);
assert_eq!(
MilliVec::from_velocity_per_second(game.ball.velocity),
MilliVec {
x: 0,
y: 3_055,
}
MilliVec { x: 0, y: 3_055 }
);
assert_eq!(game.special_hole_gate, SpecialHoleGate::Suppressed);
@@ -3255,10 +3222,7 @@ mod tests {
);
assert_eq!(
MilliVec::from_velocity_per_second(game.ball.velocity),
MilliVec {
x: -302,
y: -1_809,
}
MilliVec { x: -302, y: -1_809 }
);
assert_eq!(game.multiball_state, MultiballState::Unavailable);
assert_eq!(game.special_hole_gate, SpecialHoleGate::Suppressed);
@@ -3297,9 +3261,7 @@ mod tests {
spin: Real48::ZERO,
capture_age: 0,
};
let old_position = MilliVec::from_position(
sensor.center - vec2(sensor.radius + 1.0, 0.0),
);
let old_position = MilliVec::from_position(sensor.center - vec2(sensor.radius + 1.0, 0.0));
let current_position = MilliVec::from_position(sensor.center);
let movement = MilliVec {
x: current_position.x.wrapping_sub(old_position.x),
@@ -3391,7 +3353,10 @@ mod tests {
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.target_effect, 0);
}
@@ -3511,7 +3476,6 @@ mod tests {
assert!(game.secondary_ball.is_none());
assert_eq!(game.player().balls, 3);
assert_eq!(game.score_mode, ScoreMode::Normal);
}
#[test]
@@ -3605,10 +3569,7 @@ mod tests {
assert!(game.fixed_update(&mut events));
assert_eq!(game.last_collision_id, Some(57));
assert_eq!(
game.ball.spin.bytes(),
[0x83, 0x5d, 0x8f, 0xc2, 0xf5, 0xa8]
);
assert_eq!(game.ball.spin.bytes(), [0x83, 0x5d, 0x8f, 0xc2, 0xf5, 0xa8]);
assert_eq!(
MilliVec::from_position(game.ball.position),
MilliVec {
@@ -3645,10 +3606,7 @@ mod tests {
assert!(game.fixed_update(&mut events));
assert_eq!(game.last_collision_id, Some(72));
assert_eq!(
game.ball.spin.bytes(),
[0x83, 0x86, 0xeb, 0x51, 0xb8, 0x2e]
);
assert_eq!(game.ball.spin.bytes(), [0x83, 0x86, 0xeb, 0x51, 0xb8, 0x2e]);
assert_eq!(
MilliVec::from_position(game.ball.position),
MilliVec {
@@ -3658,7 +3616,10 @@ mod tests {
);
assert_eq!(
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]
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);
game.object_active.fill(false);
game.object_active[usize::from(object_id)] = true;
@@ -4012,7 +3972,13 @@ mod tests {
let velocity_after_edge = game.ball.velocity;
assert_eq!(game.ball.position, vec2(138.124, 355.0));
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]);
game.apply_flipper_kicks();
@@ -4102,20 +4068,20 @@ mod tests {
#[test]
fn tilt_counter_decays_once_per_selected_detail_callback() {
for (detail, interval) in [
(1, 0.050),
(2, 0.040),
(3, 0.030),
(4, 0.020),
(5, 0.010),
] {
for (detail, interval) in [(1, 0.050), (2, 0.040), (3, 0.030), (4, 0.020), (5, 0.010)] {
let mut game = Game::new(1);
game.tilt_counter = 2;
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());
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
);
complete_stationary_type_three_capture(
&mut game,
SPECIAL_HOLE_SENSOR.center,
&mut events,
);
complete_stationary_type_three_capture(&mut game, SPECIAL_HOLE_SENSOR.center, &mut events);
assert!(game.wheel_holes.iter().all(|filled| *filled));
assert_eq!(game.multiball_state, MultiballState::Ready);
assert_eq!(game.record_contacts[148], 2);
@@ -4696,7 +4658,11 @@ mod tests {
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!(
game.record_contacts[129..=133]
.iter()
.all(|contact| *contact == 0)
);
assert_eq!(
events,
[
@@ -4758,7 +4724,11 @@ mod tests {
game.record_countdowns[51] = 5;
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;
for expected in (0..5).rev() {
+31 -38
View File
@@ -73,11 +73,7 @@ impl CollisionMaterial {
}
}
pub const fn circle(
normal_rebound: f64,
tangent_coupling: f64,
normal_kick: f64,
) -> Self {
pub const fn circle(normal_rebound: f64, tangent_coupling: f64, normal_kick: f64) -> Self {
Self {
normal_rebound,
tangent_coupling,
@@ -173,8 +169,7 @@ impl MilliVec {
// `maximum / speed` is mathematically equivalent, but can differ by
// one millipixel because each Real48 operation is rounded
// independently.
let excess = Real48::from_i32(speed.wrapping_sub(maximum))
.divide(Real48::from_i32(speed));
let excess = Real48::from_i32(speed.wrapping_sub(maximum)).divide(Real48::from_i32(speed));
self.x = self
.x
.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()
}
fn cross_at_endpoint(
point: MilliVec,
current: MilliVec,
predicted: MilliVec,
) -> i32 {
fn cross_at_endpoint(point: MilliVec, current: MilliVec, predicted: MilliVec) -> i32 {
predicted
.x
.wrapping_sub(point.x)
@@ -383,9 +374,7 @@ pub fn line_collision_candidate_at(
let vertical_units = normal_y.divide(THOUSAND);
let distance = Real48::from_i32(old_position.x.wrapping_sub(start.x))
.multiply(vertical_units)
.subtract(
Real48::from_i32(old_position.y.wrapping_sub(start.y)).multiply(horizontal_units),
)
.subtract(Real48::from_i32(old_position.y.wrapping_sub(start.y)).multiply(horizontal_units))
.divide(length_units);
let distance = if distance.compare(ZERO).is_lt() {
ZERO.subtract(distance)
@@ -525,10 +514,7 @@ pub fn capture_collision_candidate(
let radius_milli = (radius * 1_000.0).round() as i32;
let delta = MilliVec {
x: center.x.wrapping_sub(old_position.x),
y: center
.y
.wrapping_sub(old_position.y)
.wrapping_sub(2_000),
y: center.y.wrapping_sub(old_position.y).wrapping_sub(2_000),
};
let surface_distance = milli_distance(delta).wrapping_sub(radius_milli);
let normal_x = Real48::from_i32(center.y.wrapping_sub(old_position.y));
@@ -541,9 +527,7 @@ pub fn capture_collision_candidate(
return None;
}
let normal_velocity = normal_velocity(velocity, normal_x, normal_y, length);
if normal_velocity >= 0
|| normal_velocity.wrapping_abs() < surface_distance.wrapping_abs()
{
if normal_velocity >= 0 || normal_velocity.wrapping_abs() < surface_distance.wrapping_abs() {
return None;
}
Some(StaticCollisionCandidate {
@@ -681,8 +665,14 @@ mod tests {
#[test]
fn float_views_roundtrip_every_gameplay_velocity_millipixel() {
for value in -10_000..=10_000 {
let milli = MilliVec { x: value, y: -value };
assert_eq!(MilliVec::from_velocity_per_second(milli.to_velocity_per_second()), milli);
let milli = MilliVec {
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 {
continue;
}
let excess = Real48::from_i32(speed - maximum)
.divide(Real48::from_i32(speed));
let excess = Real48::from_i32(speed - maximum).divide(Real48::from_i32(speed));
let expected = MilliVec {
x: x.wrapping_sub(Real48::from_i32(x).multiply(excess).round_i32()),
y: y.wrapping_sub(Real48::from_i32(y).multiply(excess).round_i32()),
@@ -716,7 +705,10 @@ mod tests {
break;
}
}
assert!(mismatches.is_empty(), "speed-clamp mismatches: {mismatches:?}");
assert!(
mismatches.is_empty(),
"speed-clamp mismatches: {mismatches:?}"
);
}
#[test]
@@ -783,7 +775,9 @@ mod tests {
assert_eq!(candidate.surface_distance, -800);
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 }
);
}
@@ -887,16 +881,15 @@ mod tests {
fn surface_distance_orders_candidate_contacts() {
let old = MilliVec::default();
let velocity = MilliVec { x: 10_000, y: 0 };
let near =
line_collision_response(
old,
velocity,
vec2(2.0, 1.0),
vec2(2.0, -1.0),
CollisionMaterial::line(0.6, 0.1),
Real48::ZERO,
)
.expect("near rail should be crossed");
let near = line_collision_response(
old,
velocity,
vec2(2.0, 1.0),
vec2(2.0, -1.0),
CollisionMaterial::line(0.6, 0.1),
Real48::ZERO,
)
.expect("near rail should be crossed");
let far = line_collision_response(
old,
velocity,
+12 -6
View File
@@ -1,6 +1,7 @@
use std::{fs, io, path::PathBuf};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::{fs, io, path::PathBuf};
const ORIGINAL_HIGHSCORES: &[u8] = include_bytes!("../assets/original/HISCORES.DAT");
const ORIGINAL_INI: &str = include_str!("../../original/TDKPIN.INI");
@@ -154,7 +155,10 @@ pub fn parse_original_settings(text: &str) -> Settings {
_ => Language::English,
};
} 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") {
settings.sounds = value != 0;
}
@@ -249,14 +253,16 @@ mod tests {
let settings = parse_original_settings(ORIGINAL_INI);
assert_eq!(settings.speed, 3);
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]
fn original_ini_values_are_case_insensitive_and_clamped() {
let settings = parse_original_settings(
"[settings]\nlanguage=5\nSPEED=9\nSound=0\nSounds=1\n",
);
let settings =
parse_original_settings("[settings]\nlanguage=5\nSPEED=9\nSound=0\nSounds=1\n");
assert_eq!(settings.speed, 3);
assert_eq!(settings.language, Language::Spanish);
assert!(!settings.sounds);
+7 -13
View File
@@ -167,8 +167,7 @@ impl Real48 {
}
let mut numerator = self.significand();
let denominator = right.significand();
let mut exponent =
i16::from(self.exponent()) - i16::from(right.exponent()) + EXPONENT_BIAS;
let mut exponent = i16::from(self.exponent()) - i16::from(right.exponent()) + EXPONENT_BIAS;
if numerator < denominator {
numerator <<= 1;
exponent -= 1;
@@ -254,16 +253,8 @@ impl Real48 {
assert!(highest_bit < 32, "Real48 integer overflow");
let significand = self.significand();
let shift = i16::try_from(FRACTION_BITS).unwrap_or(39) - highest_bit;
let mut magnitude = if shift >= 64 {
0
} else {
significand >> shift
};
if round
&& shift > 0
&& shift <= 40
&& (significand >> (shift - 1)) & 1 != 0
{
let mut magnitude = if shift >= 64 { 0 } else { significand >> shift };
if round && shift > 0 && shift <= 40 && (significand >> (shift - 1)) & 1 != 0 {
magnitude += 1;
}
let limit = if self.negative() {
@@ -344,7 +335,10 @@ mod tests {
assert_eq!(HALF.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!(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!(
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::{
fs,
path::{Path, PathBuf},
str::FromStr,
};
use serde::Serialize;
use crate::game::{ClawSpriteBank, Controls, Event, Game};
pub const SIMULATION_HZ: u32 = 120;
const SIMULATION_HZ_F64: f64 = 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::Claw6
| Scenario::Claw7
| Scenario::Claw18 => {
Controls::default()
}
| Scenario::Claw18 => Controls::default(),
}
}
@@ -507,9 +507,24 @@ mod tests {
.iter()
.flat_map(|snapshot| snapshot.events.iter().map(String::as_str))
.collect::<Vec<_>>();
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);
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]
@@ -521,7 +536,10 @@ mod tests {
simulation.advance_to(22);
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);
assert_eq!(simulation.game.target_rotation_state, None);
}
@@ -561,7 +579,12 @@ mod tests {
.filter(|event| **event == "ClawRelease")
.count();
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!(simulation.game.ball.position.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
//! radius of the rendered ball.
use crate::geometry::Segment;
use macroquad::prelude::Vec2;
use crate::geometry::Segment;
#[derive(Clone, Copy, Debug)]
pub struct TableSegment {
pub id: u8,
@@ -425,7 +426,10 @@ mod tests {
.iter()
.find(|wall| wall.id == id)
.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 [
(54, Vec2::new(53.0, 292.0)),