From 3c50bfeab61e3d2770ce9a8ed64f7cfed05dc4b0 Mon Sep 17 00:00:00 2001 From: Ryan Walters Date: Fri, 5 Sep 2025 19:20:58 -0500 Subject: [PATCH] refactor: add ticks to DeltaTime, rewrite Blinking system for tick-based calculations with absolute calculations, rewrite Blinking/Direction tests --- Cargo.lock | 75 ++++++++++++++ Cargo.toml | 1 + src/constants.rs | 4 +- src/game.rs | 4 +- src/systems/blinking.rs | 54 ++++++++--- src/systems/components.rs | 30 +++++- src/systems/ghost.rs | 4 +- src/systems/input.rs | 4 +- src/systems/player.rs | 4 +- src/systems/render.rs | 4 +- src/texture/blinking.rs | 43 +++++--- tests/blinking.rs | 199 ++++++++++++++++++++++++++++++++++---- tests/common/mod.rs | 5 +- tests/direction.rs | 52 +++++++--- tests/player.rs | 6 +- 15 files changed, 413 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c462e8..9bd2b7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -570,12 +570,76 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -618,6 +682,7 @@ dependencies = [ "serde", "serde_json", "smallvec", + "speculoos", "spin_sleep", "strum", "strum_macros", @@ -965,6 +1030,16 @@ dependencies = [ "serde", ] +[[package]] +name = "speculoos" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c84ba5fa63b0de837c0d3cef5373ac1c3c6342053b7f446a210a1dde79a034" +dependencies = [ + "num", + "serde_json", +] + [[package]] name = "spin" version = "0.9.8" diff --git a/Cargo.toml b/Cargo.toml index f0f15a8..869c717 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ libc = "0.2.175" # TODO: Describe why this is required. [dev-dependencies] pretty_assertions = "1.4.1" +speculoos = "0.13.0" [build-dependencies] phf = { version = "0.13.1", features = ["macros"] } diff --git a/src/constants.rs b/src/constants.rs index 9b07865..0d8eeba 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -72,8 +72,8 @@ pub mod collider { pub mod ui { /// Debug font size in points pub const DEBUG_FONT_SIZE: u16 = 12; - /// Power pellet blink rate in seconds - pub const POWER_PELLET_BLINK_RATE: f32 = 0.2; + /// Power pellet blink rate in ticks (at 60 FPS, 12 ticks = 0.2 seconds) + pub const POWER_PELLET_BLINK_RATE: u32 = 12; } /// Map tile types that define gameplay behavior and collision properties. diff --git a/src/game.rs b/src/game.rs index 0601ce6..38ee8b6 100644 --- a/src/game.rs +++ b/src/game.rs @@ -372,7 +372,7 @@ impl Game { world.insert_resource(SystemTimings::default()); world.insert_resource(Timing::default()); world.insert_resource(Bindings::default()); - world.insert_resource(DeltaTime(0f32)); + world.insert_resource(DeltaTime { seconds: 0.0, ticks: 0 }); world.insert_resource(RenderDirty::default()); world.insert_resource(DebugState::default()); world.insert_resource(AudioState::default()); @@ -633,7 +633,7 @@ impl Game { /// /// `true` if the game should terminate (exit command received), `false` to continue pub fn tick(&mut self, dt: f32) -> bool { - self.world.insert_resource(DeltaTime(dt)); + self.world.insert_resource(DeltaTime { seconds: dt, ticks: 1 }); // Note: We don't need to read the current tick here since we increment it after running systems diff --git a/src/systems/blinking.rs b/src/systems/blinking.rs index fbe698d..8c8ef07 100644 --- a/src/systems/blinking.rs +++ b/src/systems/blinking.rs @@ -12,20 +12,24 @@ use crate::systems::{ #[derive(Component, Debug)] pub struct Blinking { - pub timer: f32, - pub interval: f32, + pub tick_timer: u32, + pub interval_ticks: u32, } impl Blinking { - pub fn new(interval: f32) -> Self { - Self { timer: 0.0, interval } + pub fn new(interval_ticks: u32) -> Self { + Self { + tick_timer: 0, + interval_ticks, + } } } /// Updates blinking entities by toggling their visibility at regular intervals. /// /// This system manages entities that have both `Blinking` and `Renderable` components, -/// accumulating time and toggling visibility when the specified interval is reached. +/// accumulating ticks and toggling visibility when the specified interval is reached. +/// Uses integer arithmetic for deterministic behavior. #[allow(clippy::type_complexity)] pub fn blinking_system( mut commands: Commands, @@ -42,22 +46,40 @@ pub fn blinking_system( continue; } - // Increase the timer by the delta time - blinking.timer += time.0; + // Increase the timer by the delta ticks + blinking.tick_timer += time.ticks; - // If the timer is less than the interval, there's nothing to do yet - if blinking.timer < blinking.interval { + // Handle zero interval case (immediate toggling) + if blinking.interval_ticks == 0 { + if time.ticks > 0 { + if hidden { + commands.entity(entity).remove::(); + } else { + commands.entity(entity).insert(Hidden); + } + } continue; } - // Subtract the interval (allows for the timer to retain partial interval progress) - blinking.timer -= blinking.interval; + // Calculate how many complete intervals have passed + let complete_intervals = blinking.tick_timer / blinking.interval_ticks; - // Toggle the Hidden component - if hidden { - commands.entity(entity).remove::(); - } else { - commands.entity(entity).insert(Hidden); + // If no complete intervals have passed, there's nothing to do yet + if complete_intervals == 0 { + continue; + } + + // Update the timer to the remainder after complete intervals + blinking.tick_timer %= blinking.interval_ticks; + + // Toggle the Hidden component for each complete interval + // Since toggling twice is a no-op, we only need to toggle if the count is odd + if complete_intervals % 2 == 1 { + if hidden { + commands.entity(entity).remove::(); + } else { + commands.entity(entity).insert(Hidden); + } } } } diff --git a/src/systems/components.rs b/src/systems/components.rs index 47f7502..489083e 100644 --- a/src/systems/components.rs +++ b/src/systems/components.rs @@ -162,7 +162,35 @@ pub struct GlobalState { pub struct ScoreResource(pub u32); #[derive(Resource)] -pub struct DeltaTime(pub f32); +pub struct DeltaTime { + /// Floating-point delta time in seconds + pub seconds: f32, + /// Integer tick delta (usually 1, but can be different for testing) + pub ticks: u32, +} + +#[allow(dead_code)] +impl DeltaTime { + /// Creates a new DeltaTime from a floating-point delta time in seconds + /// + /// While this method exists as a helper, it does not mean that seconds and ticks are interchangeable. + pub fn from_seconds(seconds: f32) -> Self { + Self { + seconds, + ticks: (seconds * 60.0).round() as u32, + } + } + + /// Creates a new DeltaTime from an integer tick delta + /// + /// While this method exists as a helper, it does not mean that seconds and ticks are interchangeable. + pub fn from_ticks(ticks: u32) -> Self { + Self { + seconds: ticks as f32 / 60.0, + ticks, + } + } +} /// Movement modifiers that can affect Pac-Man's speed or handling. #[derive(Component, Debug, Clone, Copy)] diff --git a/src/systems/ghost.rs b/src/systems/ghost.rs index 7af363b..1d705ee 100644 --- a/src/systems/ghost.rs +++ b/src/systems/ghost.rs @@ -25,7 +25,7 @@ pub fn ghost_movement_system( mut ghosts: Query<(&Ghost, &mut Velocity, &mut Position), Without>, ) { for (_ghost, mut velocity, mut position) in ghosts.iter_mut() { - let mut distance = velocity.speed * 60.0 * delta_time.0; + let mut distance = velocity.speed * 60.0 * delta_time.seconds; loop { match *position { Position::Stopped { node: current_node } => { @@ -111,7 +111,7 @@ pub fn eaten_ghost_system( } } Position::Moving { to, .. } => { - let distance = velocity.speed * 60.0 * delta_time.0; + let distance = velocity.speed * 60.0 * delta_time.seconds; if let Some(_overflow) = position.tick(distance) { // Reached target node, check if we're at ghost house center if to == ghost_house_center { diff --git a/src/systems/input.rs b/src/systems/input.rs index fd4f655..a9e6b51 100644 --- a/src/systems/input.rs +++ b/src/systems/input.rs @@ -319,7 +319,7 @@ pub fn input_system( // Update touch reference position with easing if let Some(ref mut touch_data) = touch_state.active_touch { // Apply easing to the reference position and get the delta for direction calculation - let (delta, distance) = update_touch_reference_position(touch_data, delta_time.0); + let (delta, distance) = update_touch_reference_position(touch_data, delta_time.seconds); // Check for direction based on updated reference position if distance >= TOUCH_DIRECTION_THRESHOLD { @@ -336,7 +336,7 @@ pub fn input_system( } if let (false, CursorPosition::Some { remaining_time, .. }) = (cursor_seen, &mut *cursor) { - *remaining_time -= delta_time.0; + *remaining_time -= delta_time.seconds; if *remaining_time <= 0.0 { *cursor = CursorPosition::None; } diff --git a/src/systems/player.rs b/src/systems/player.rs index f691403..71a6d76 100644 --- a/src/systems/player.rs +++ b/src/systems/player.rs @@ -99,12 +99,12 @@ pub fn player_movement_system( } else { *buffered_direction = BufferedDirection::Some { direction, - remaining_time: remaining_time - delta_time.0, + remaining_time: remaining_time - delta_time.seconds, }; } } - let mut distance = velocity.speed * modifiers.speed_multiplier * 60.0 * delta_time.0; + let mut distance = velocity.speed * modifiers.speed_multiplier * 60.0 * delta_time.seconds; loop { match *position { diff --git a/src/systems/render.rs b/src/systems/render.rs index 05054c2..31c5851 100644 --- a/src/systems/render.rs +++ b/src/systems/render.rs @@ -55,7 +55,7 @@ pub fn directional_render_system( dt: Res, mut query: Query<(&Position, &Velocity, &mut DirectionalAnimation, &mut Renderable)>, ) { - let ticks = (dt.0 * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec + let ticks = (dt.seconds * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec for (position, velocity, mut anim, mut renderable) in query.iter_mut() { let stopped = matches!(position, Position::Stopped { .. }); @@ -90,7 +90,7 @@ pub fn directional_render_system( /// /// This system handles entities that use LinearAnimation component for simple frame cycling. pub fn linear_render_system(dt: Res, mut query: Query<(&mut LinearAnimation, &mut Renderable)>) { - let ticks = (dt.0 * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec + let ticks = (dt.seconds * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec for (mut anim, mut renderable) in query.iter_mut() { // Tick animation diff --git a/src/texture/blinking.rs b/src/texture/blinking.rs index 3ed8f2a..10e72f7 100644 --- a/src/texture/blinking.rs +++ b/src/texture/blinking.rs @@ -4,25 +4,42 @@ use crate::texture::sprite::AtlasTile; #[derive(Clone)] pub struct BlinkingTexture { tile: AtlasTile, - blink_duration: f32, - time_bank: f32, + interval_ticks: u32, + tick_timer: u32, is_on: bool, } impl BlinkingTexture { - pub fn new(tile: AtlasTile, blink_duration: f32) -> Self { + pub fn new(tile: AtlasTile, interval_ticks: u32) -> Self { Self { tile, - blink_duration, - time_bank: 0.0, + interval_ticks, + tick_timer: 0, is_on: true, } } - pub fn tick(&mut self, dt: f32) { - self.time_bank += dt; - if self.time_bank >= self.blink_duration { - self.time_bank -= self.blink_duration; + pub fn tick(&mut self, delta_ticks: u32) { + self.tick_timer += delta_ticks; + + // Handle zero interval case (immediate toggling) + if self.interval_ticks == 0 { + // With zero interval, any positive ticks should toggle + if delta_ticks > 0 { + self.is_on = !self.is_on; + } + return; + } + + // Calculate how many complete intervals have passed + let complete_intervals = self.tick_timer / self.interval_ticks; + + // Update the timer to the remainder after complete intervals + self.tick_timer %= self.interval_ticks; + + // Toggle for each complete interval, but since toggling twice is a no-op, + // we only need to toggle if the count is odd + if complete_intervals % 2 == 1 { self.is_on = !self.is_on; } } @@ -36,11 +53,11 @@ impl BlinkingTexture { } // Helper methods for testing - pub fn time_bank(&self) -> f32 { - self.time_bank + pub fn tick_timer(&self) -> u32 { + self.tick_timer } - pub fn blink_duration(&self) -> f32 { - self.blink_duration + pub fn interval_ticks(&self) -> u32 { + self.interval_ticks } } diff --git a/tests/blinking.rs b/tests/blinking.rs index 0f91891..47395be 100644 --- a/tests/blinking.rs +++ b/tests/blinking.rs @@ -1,40 +1,205 @@ use pacman::texture::blinking::BlinkingTexture; +use speculoos::prelude::*; mod common; #[test] fn test_blinking_texture() { let tile = common::mock_atlas_tile(1); - let mut texture = BlinkingTexture::new(tile, 0.5); + let mut texture = BlinkingTexture::new(tile, 30); // 30 ticks = 0.5 seconds at 60 FPS - assert!(texture.is_on()); + assert_that(&texture.is_on()).is_true(); - texture.tick(0.5); - assert!(!texture.is_on()); + texture.tick(30); + assert_that(&texture.is_on()).is_false(); - texture.tick(0.5); - assert!(texture.is_on()); + texture.tick(30); + assert_that(&texture.is_on()).is_true(); - texture.tick(0.5); - assert!(!texture.is_on()); + texture.tick(30); + assert_that(&texture.is_on()).is_false(); } #[test] fn test_blinking_texture_partial_duration() { let tile = common::mock_atlas_tile(1); - let mut texture = BlinkingTexture::new(tile, 0.5); + let mut texture = BlinkingTexture::new(tile, 30); // 30 ticks - texture.tick(0.625); - assert!(!texture.is_on()); - assert_eq!(texture.time_bank(), 0.125); + texture.tick(37); // 37 ticks, should complete 1 interval (30 ticks) with 7 remaining + assert_that(&texture.is_on()).is_false(); + assert_that(&texture.tick_timer()).is_equal_to(7); } #[test] -fn test_blinking_texture_negative_time() { +fn test_blinking_texture_zero_interval() { let tile = common::mock_atlas_tile(1); - let mut texture = BlinkingTexture::new(tile, 0.5); + let mut texture = BlinkingTexture::new(tile, 0); - texture.tick(-0.1); - assert!(texture.is_on()); - assert_eq!(texture.time_bank(), -0.1); + assert_that(&texture.is_on()).is_true(); + assert_that(&texture.interval_ticks()).is_equal_to(0); + + // With zero interval, any positive ticks should toggle + texture.tick(1); + assert_that(&texture.is_on()).is_false(); + assert_that(&texture.tick_timer()).is_equal_to(1); + + texture.tick(1); + assert_that(&texture.is_on()).is_true(); + assert_that(&texture.tick_timer()).is_equal_to(2); +} + +#[test] +fn test_blinking_texture_multiple_cycles() { + let tile = common::mock_atlas_tile(1); + let mut texture = BlinkingTexture::new(tile, 15); // 15 ticks + + // Test multiple complete cycles + for i in 0..10 { + let expected_state = i % 2 == 0; + assert_that(&texture.is_on()).is_equal_to(expected_state); + texture.tick(15); + } +} + +#[test] +fn test_blinking_texture_accumulated_ticks() { + let tile = common::mock_atlas_tile(1); + let mut texture = BlinkingTexture::new(tile, 60); // 60 ticks = 1 second at 60 FPS + + // Test that ticks accumulate correctly + texture.tick(18); // 18 ticks + assert_that(&texture.tick_timer()).is_equal_to(18); + assert_that(&texture.is_on()).is_true(); + + texture.tick(18); // 36 total ticks + assert_that(&texture.tick_timer()).is_equal_to(36); + assert_that(&texture.is_on()).is_true(); + + texture.tick(18); // 54 total ticks + assert_that(&texture.tick_timer()).is_equal_to(54); + assert_that(&texture.is_on()).is_true(); + + texture.tick(12); // 66 total ticks, should complete 1 interval (60 ticks) with 6 remaining + assert_that(&texture.tick_timer()).is_equal_to(6); + assert_that(&texture.is_on()).is_false(); +} + +#[test] +fn test_blinking_texture_large_tick_step() { + let tile = common::mock_atlas_tile(1); + let mut texture = BlinkingTexture::new(tile, 30); // 30 ticks + + // Test with a tick step larger than the interval + texture.tick(90); // 90 ticks, should complete 3 intervals (90/30 = 3) + assert_that(&texture.is_on()).is_false(); // 3 toggles: true -> false -> true -> false + assert_that(&texture.tick_timer()).is_equal_to(0); // 90 % 30 = 0 + + texture.tick(60); // 60 ticks, should complete 2 intervals (60/30 = 2) + assert_that(&texture.is_on()).is_false(); // 2 more toggles: false -> true -> false (no change) + assert_that(&texture.tick_timer()).is_equal_to(0); // 60 % 30 = 0 +} + +#[test] +fn test_blinking_texture_small_steps() { + let tile = common::mock_atlas_tile(1); + let mut texture = BlinkingTexture::new(tile, 6); // 6 ticks + + // Test with small tick steps + for _ in 0..6 { + texture.tick(1); + } + + // After 6 ticks, should have completed one cycle + assert_that(&texture.tick_timer()).is_equal_to(0); + assert_that(&texture.is_on()).is_false(); +} + +#[test] +fn test_blinking_texture_clone() { + let tile = common::mock_atlas_tile(1); + let mut texture1 = BlinkingTexture::new(tile, 30); + let texture2 = texture1.clone(); + + // Both should have the same initial state + assert_that(&texture1.is_on()).is_equal_to(texture2.is_on()); + assert_that(&texture1.tick_timer()).is_equal_to(texture2.tick_timer()); + assert_that(&texture1.interval_ticks()).is_equal_to(texture2.interval_ticks()); + + // Modifying one shouldn't affect the other + texture1.tick(18); + assert_that(&texture1.tick_timer()).is_not_equal_to(texture2.tick_timer()); + assert_that(&texture2.tick_timer()).is_equal_to(0); +} + +#[test] +fn test_blinking_texture_edge_cases() { + let tile = common::mock_atlas_tile(1); + let mut texture = BlinkingTexture::new(tile, 60); // 60 ticks + + // Test exactly at the interval + texture.tick(60); + assert_that(&texture.is_on()).is_false(); + assert_that(&texture.tick_timer()).is_equal_to(0); + + // Test just under the interval + texture.tick(59); + assert_that(&texture.is_on()).is_false(); + assert_that(&texture.tick_timer()).is_equal_to(59); + + // Test just over the interval + texture.tick(2); + assert_that(&texture.is_on()).is_true(); + assert_that(&texture.tick_timer()).is_equal_to(1); +} + +#[test] +fn test_blinking_texture_very_small_interval() { + let tile = common::mock_atlas_tile(1); + let mut texture = BlinkingTexture::new(tile, 1); // 1 tick + + // Test with very small interval + texture.tick(1); + assert_that(&texture.is_on()).is_false(); + assert_that(&texture.tick_timer()).is_equal_to(0); + + texture.tick(1); + assert_that(&texture.is_on()).is_true(); + assert_that(&texture.tick_timer()).is_equal_to(0); +} + +#[test] +fn test_blinking_texture_very_large_interval() { + let tile = common::mock_atlas_tile(1); + let mut texture = BlinkingTexture::new(tile, 60000); // 60000 ticks = 1000 seconds at 60 FPS + + // Test with very large interval + texture.tick(30000); + assert_that(&texture.is_on()).is_true(); + assert_that(&texture.tick_timer()).is_equal_to(30000); + + texture.tick(30000); + assert_that(&texture.is_on()).is_false(); + assert_that(&texture.tick_timer()).is_equal_to(0); +} + +#[test] +fn test_blinking_texture_multiple_toggles_no_op() { + let tile = common::mock_atlas_tile(1); + let mut texture = BlinkingTexture::new(tile, 10); // 10 ticks + + // Test that multiple toggles work correctly (key feature) + // 2x ticks than the interval should do nothing at all, because toggling twice is a no-op + texture.tick(20); // 20 ticks = 2 complete intervals + assert_that(&texture.is_on()).is_true(); // Should still be true (2 toggles = no-op) + assert_that(&texture.tick_timer()).is_equal_to(0); + + // 3x ticks should toggle once (3 toggles = 1 toggle) + texture.tick(30); // 30 ticks = 3 complete intervals + assert_that(&texture.is_on()).is_false(); // Should be false (3 toggles = 1 toggle) + assert_that(&texture.tick_timer()).is_equal_to(0); + + // 4x ticks should do nothing (4 toggles = no-op) + texture.tick(40); // 40 ticks = 4 complete intervals + assert_that(&texture.is_on()).is_false(); // Should still be false (4 toggles = no-op) + assert_that(&texture.tick_timer()).is_equal_to(0); } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 18dd82b..54d0ccd 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -85,7 +85,10 @@ pub fn create_test_world() -> World { world.insert_resource(AudioState::default()); world.insert_resource(GlobalState { exit: false }); world.insert_resource(DebugState::default()); - world.insert_resource(DeltaTime(1.0 / 60.0)); // 60 FPS + world.insert_resource(DeltaTime { + seconds: 1.0 / 60.0, + ticks: 1, + }); // 60 FPS world.insert_resource(create_test_map()); world diff --git a/tests/direction.rs b/tests/direction.rs index 895483e..2025fd4 100644 --- a/tests/direction.rs +++ b/tests/direction.rs @@ -1,5 +1,5 @@ -use glam::I8Vec2; use pacman::map::direction::*; +use speculoos::prelude::*; #[test] fn test_direction_opposite() { @@ -11,21 +11,47 @@ fn test_direction_opposite() { ]; for (dir, expected) in test_cases { - assert_eq!(dir.opposite(), expected); + assert_that(&dir.opposite()).is_equal_to(expected); } } #[test] -fn test_direction_as_ivec2() { - let test_cases = [ - (Direction::Up, -I8Vec2::Y), - (Direction::Down, I8Vec2::Y), - (Direction::Left, -I8Vec2::X), - (Direction::Right, I8Vec2::X), - ]; - - for (dir, expected) in test_cases { - assert_eq!(dir.as_ivec2(), expected); - assert_eq!(I8Vec2::from(dir), expected); +fn test_direction_opposite_symmetry() { + // Test that opposite() is symmetric: opposite(opposite(d)) == d + for &dir in &Direction::DIRECTIONS { + assert_that(&dir.opposite().opposite()).is_equal_to(dir); } } + +#[test] +fn test_direction_opposite_exhaustive() { + // Test that every direction has a unique opposite + let mut opposites = std::collections::HashSet::new(); + for &dir in &Direction::DIRECTIONS { + let opposite = dir.opposite(); + assert_that(&opposites.insert(opposite)).is_true(); + } + assert_that(&opposites).has_length(4); +} + +#[test] +fn test_direction_as_usize_exhaustive() { + // Test that as_usize() returns unique values for all directions + let mut usizes = std::collections::HashSet::new(); + for &dir in &Direction::DIRECTIONS { + let usize_val = dir.as_usize(); + assert_that(&usizes.insert(usize_val)).is_true(); + } + assert_that(&usizes).has_length(4); +} + +#[test] +fn test_direction_as_ivec2_exhaustive() { + // Test that as_ivec2() returns unique values for all directions + let mut ivec2s = std::collections::HashSet::new(); + for &dir in &Direction::DIRECTIONS { + let ivec2_val = dir.as_ivec2(); + assert_that(&ivec2s.insert(ivec2_val)).is_true(); + } + assert_that(&ivec2s).has_length(4); +} diff --git a/tests/player.rs b/tests/player.rs index 5ab5ede..f9789a2 100644 --- a/tests/player.rs +++ b/tests/player.rs @@ -232,7 +232,7 @@ fn test_player_movement_system_buffered_direction_expires() { }); // Set delta time to expire the buffered direction - world.insert_resource(DeltaTime(0.02)); + world.insert_resource(DeltaTime::from_seconds(0.02)); // Run the system world @@ -410,7 +410,7 @@ fn test_buffered_direction_timing() { .expect("System should run successfully"); // Run movement system multiple times with small delta times - world.insert_resource(DeltaTime(0.1)); // 0.1 seconds + world.insert_resource(DeltaTime::from_seconds(0.1)); // 0.1 seconds // First run - buffered direction should still be active world @@ -428,7 +428,7 @@ fn test_buffered_direction_timing() { } // Run again to fully expire the buffered direction - world.insert_resource(DeltaTime(0.2)); // Total 0.3 seconds, should expire + world.insert_resource(DeltaTime::from_seconds(0.2)); // Total 0.3 seconds, should expire world .run_system_once(player_movement_system) .expect("System should run successfully");