diff --git a/src/map/builder.rs b/src/map/builder.rs index 64028b8..cc89614 100644 --- a/src/map/builder.rs +++ b/src/map/builder.rs @@ -359,12 +359,7 @@ impl Map { + IVec2::from(Direction::Left.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0), }, ) - .map_err(|e| { - MapError::InvalidConfig(format!( - "Failed to connect left tunnel entrance to left tunnel hidden node: {}", - e - )) - })? + .expect("Failed to connect left tunnel entrance to left tunnel hidden node") }; // Create the right tunnel nodes @@ -384,12 +379,7 @@ impl Map { + IVec2::from(Direction::Right.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0), }, ) - .map_err(|e| { - MapError::InvalidConfig(format!( - "Failed to connect right tunnel entrance to right tunnel hidden node: {}", - e - )) - })? + .expect("Failed to connect right tunnel entrance to right tunnel hidden node") }; // Connect the left tunnel hidden node to the right tunnel hidden node @@ -401,12 +391,7 @@ impl Map { Some(0.0), Direction::Left, ) - .map_err(|e| { - MapError::InvalidConfig(format!( - "Failed to connect left tunnel hidden node to right tunnel hidden node: {}", - e - )) - })?; + .expect("Failed to connect left tunnel hidden node to right tunnel hidden node"); Ok(()) } diff --git a/src/systems/input.rs b/src/systems/input.rs index a9e6b51..cd4646f 100644 --- a/src/systems/input.rs +++ b/src/systems/input.rs @@ -20,10 +20,10 @@ use crate::{ }; // Touch input constants -const TOUCH_DIRECTION_THRESHOLD: f32 = 10.0; -const TOUCH_EASING_DISTANCE_THRESHOLD: f32 = 1.0; -const MAX_TOUCH_MOVEMENT_SPEED: f32 = 100.0; -const TOUCH_EASING_FACTOR: f32 = 1.5; +pub const TOUCH_DIRECTION_THRESHOLD: f32 = 10.0; +pub const TOUCH_EASING_DISTANCE_THRESHOLD: f32 = 1.0; +pub const MAX_TOUCH_MOVEMENT_SPEED: f32 = 100.0; +pub const TOUCH_EASING_FACTOR: f32 = 1.5; #[derive(Resource, Default, Debug, Copy, Clone)] pub enum CursorPosition { @@ -35,7 +35,7 @@ pub enum CursorPosition { }, } -#[derive(Resource, Default, Debug)] +#[derive(Resource, Default, Debug, Clone)] pub struct TouchState { pub active_touch: Option, } @@ -160,7 +160,7 @@ pub fn process_simple_key_events(bindings: &mut Bindings, frame_events: &[Simple } /// Calculates the primary direction from a 2D vector delta -fn calculate_direction_from_delta(delta: Vec2) -> Direction { +pub fn calculate_direction_from_delta(delta: Vec2) -> Direction { if delta.x.abs() > delta.y.abs() { if delta.x > 0.0 { Direction::Right @@ -179,7 +179,7 @@ fn calculate_direction_from_delta(delta: Vec2) -> Direction { /// This slowly moves the start_pos towards the current_pos, with the speed /// decreasing as the distance gets smaller. The maximum movement speed is capped. /// Returns the delta vector and its length for reuse by the caller. -fn update_touch_reference_position(touch_data: &mut TouchData, delta_time: f32) -> (Vec2, f32) { +pub fn update_touch_reference_position(touch_data: &mut TouchData, delta_time: f32) -> (Vec2, f32) { // Calculate the vector from start to current position let delta = touch_data.current_pos - touch_data.start_pos; let distance = delta.length(); @@ -220,16 +220,6 @@ pub fn input_system( // Collect all events for this frame. let frame_events: SmallVec<[Event; 3]> = pump.poll_iter().collect(); - // Warn if the smallvec was heap allocated due to exceeding stack capacity - #[cfg(debug_assertions)] - if frame_events.len() > frame_events.capacity() { - tracing::warn!( - "More than {} events in a frame, consider adjusting stack capacity: {:?}", - frame_events.capacity(), - frame_events - ); - } - // Handle non-keyboard events inline and build a simplified keyboard event stream. let mut simple_key_events: SmallVec<[SimpleKeyEvent; 3]> = smallvec![]; for event in &frame_events { diff --git a/tests/asset.rs b/tests/asset.rs index 35e7532..66f5448 100644 --- a/tests/asset.rs +++ b/tests/asset.rs @@ -20,6 +20,6 @@ fn all_asset_paths_exist() { fn asset_paths_are_non_empty() { for asset in Asset::iter() { let path = asset.path(); - assert!(!path.is_empty(), "Asset path for {:?} should not be empty", asset); + assert_that(&path.is_empty()).is_false(); } } diff --git a/tests/game.rs b/tests/game.rs index 472d08e..539d140 100644 --- a/tests/game.rs +++ b/tests/game.rs @@ -1,16 +1,15 @@ -use pacman::error::GameResult; +use pacman::error::{GameError, GameResult}; use pacman::game::Game; -use sdl2; +use speculoos::prelude::*; mod common; use common::setup_sdl; -/// Test that runs the game for 30 seconds at 60 FPS without sleep #[test] fn test_game_30_seconds_60fps() -> GameResult<()> { - let (canvas, texture_creator, _sdl_context) = setup_sdl().map_err(|e| pacman::error::GameError::Sdl(e))?; - let ttf_context = sdl2::ttf::init().map_err(|e| pacman::error::GameError::Sdl(e.to_string()))?; + let (canvas, texture_creator, _sdl_context) = setup_sdl().map_err(GameError::Sdl)?; + let ttf_context = sdl2::ttf::init().map_err(GameError::Sdl)?; let event_pump = _sdl_context .event_pump() .map_err(|e| pacman::error::GameError::Sdl(e.to_string()))?; @@ -43,8 +42,8 @@ fn test_game_30_seconds_60fps() -> GameResult<()> { /// Test that runs the game for 30 seconds with variable frame timing #[test] fn test_game_30_seconds_variable_timing() -> GameResult<()> { - let (canvas, texture_creator, _sdl_context) = setup_sdl().map_err(|e| pacman::error::GameError::Sdl(e))?; - let ttf_context = sdl2::ttf::init().map_err(|e| pacman::error::GameError::Sdl(e.to_string()))?; + let (canvas, texture_creator, _sdl_context) = setup_sdl().map_err(GameError::Sdl)?; + let ttf_context = sdl2::ttf::init().map_err(|e| GameError::Sdl(e.to_string()))?; let event_pump = _sdl_context .event_pump() .map_err(|e| pacman::error::GameError::Sdl(e.to_string()))?; @@ -75,11 +74,6 @@ fn test_game_30_seconds_variable_timing() -> GameResult<()> { frame_count += 1; } - assert!( - total_time >= target_time, - "Should have run for at least {} seconds, but ran for {}s", - target_time, - total_time - ); + assert_that(&total_time).is_greater_than_or_equal_to(target_time); Ok(()) } diff --git a/tests/input.rs b/tests/input.rs index 536cc9b..c8e90c8 100644 --- a/tests/input.rs +++ b/tests/input.rs @@ -1,39 +1,321 @@ +use glam::Vec2; use pacman::events::{GameCommand, GameEvent}; use pacman::map::direction::Direction; -use pacman::systems::input::{process_simple_key_events, Bindings, SimpleKeyEvent}; +use pacman::systems::input::{ + calculate_direction_from_delta, process_simple_key_events, update_touch_reference_position, Bindings, CursorPosition, + SimpleKeyEvent, TouchData, TouchState, TOUCH_DIRECTION_THRESHOLD, TOUCH_EASING_DISTANCE_THRESHOLD, +}; use sdl2::keyboard::Keycode; use speculoos::prelude::*; -#[test] -fn resumes_previous_direction_when_secondary_key_released() { - let mut bindings = Bindings::default(); +// Test modules for better organization +mod keyboard_tests { + use super::*; - // Frame 1: Press W (Up) => emits Move Up - let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::W)]); - assert_that(&events.contains(&GameEvent::Command(GameCommand::MovePlayer(Direction::Up)))).is_true(); + #[test] + fn key_down_emits_bound_command() { + let mut bindings = Bindings::default(); + let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::W)]); + assert_that(&events).contains(GameEvent::Command(GameCommand::MovePlayer(Direction::Up))); + } - // Frame 2: Press D (Right) => emits Move Right - let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::D)]); - assert_that(&events.contains(&GameEvent::Command(GameCommand::MovePlayer(Direction::Right)))).is_true(); + #[test] + fn key_down_emits_non_movement_commands() { + let mut bindings = Bindings::default(); + let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::P)]); + assert_that(&events).contains(GameEvent::Command(GameCommand::TogglePause)); + } - // Frame 3: Release D, no new key this frame => should continue previous key W (Up) - let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyUp(Keycode::D)]); - assert_that(&events.contains(&GameEvent::Command(GameCommand::MovePlayer(Direction::Up)))).is_true(); + #[test] + fn unbound_key_emits_nothing() { + let mut bindings = Bindings::default(); + let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::Z)]); + assert_that(&events).is_empty(); + } + + #[test] + fn movement_key_held_continues_across_frames() { + let mut bindings = Bindings::default(); + process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::Left)]); + let events = process_simple_key_events(&mut bindings, &[]); + assert_that(&events).contains(GameEvent::Command(GameCommand::MovePlayer(Direction::Left))); + } + + #[test] + fn releasing_movement_key_stops_continuation() { + let mut bindings = Bindings::default(); + process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::Up)]); + let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyUp(Keycode::Up)]); + assert_that(&events).is_empty(); + } + + #[test] + fn multiple_movement_keys_resumes_previous_when_current_released() { + let mut bindings = Bindings::default(); + process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::W)]); + process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::D)]); + let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyUp(Keycode::D)]); + assert_that(&events).contains(GameEvent::Command(GameCommand::MovePlayer(Direction::Up))); + } } -#[test] -fn holds_last_pressed_key_across_frames_when_no_new_input() { - let mut bindings = Bindings::default(); +mod direction_calculation_tests { + use super::*; - // Frame 1: Press Left - let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::Left)]); - assert_that(&events.contains(&GameEvent::Command(GameCommand::MovePlayer(Direction::Left)))).is_true(); + #[test] + fn prioritizes_horizontal_movement() { + let test_cases = vec![ + (Vec2::new(6.0, 5.0), Direction::Right), + (Vec2::new(-6.0, 5.0), Direction::Left), + ]; - // Frame 2: No input => continues Left - let events = process_simple_key_events(&mut bindings, &[]); - assert_that(&events.contains(&GameEvent::Command(GameCommand::MovePlayer(Direction::Left)))).is_true(); + for (delta, expected) in test_cases { + assert_that(&calculate_direction_from_delta(delta)).is_equal_to(expected); + } + } - // Frame 3: Release Left, no input remains => nothing emitted - let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyUp(Keycode::Left)]); - assert_that(&events.is_empty()).is_true(); + #[test] + fn uses_vertical_when_dominant() { + let test_cases = vec![ + (Vec2::new(3.0, 10.0), Direction::Down), + (Vec2::new(3.0, -10.0), Direction::Up), + ]; + + for (delta, expected) in test_cases { + assert_that(&calculate_direction_from_delta(delta)).is_equal_to(expected); + } + } + + #[test] + fn handles_zero_delta() { + let delta = Vec2::ZERO; + // Should default to Up when both components are zero + assert_that(&calculate_direction_from_delta(delta)).is_equal_to(Direction::Up); + } + + #[test] + fn handles_equal_magnitudes() { + // When x and y have equal absolute values, should prioritize vertical + let delta = Vec2::new(5.0, 5.0); + assert_that(&calculate_direction_from_delta(delta)).is_equal_to(Direction::Down); + + let delta = Vec2::new(-5.0, 5.0); + assert_that(&calculate_direction_from_delta(delta)).is_equal_to(Direction::Down); + } +} + +mod touch_easing_tests { + use super::*; + + #[test] + fn easing_within_threshold_does_nothing() { + let mut touch_data = TouchData::new(0, Vec2::new(100.0, 100.0)); + touch_data.current_pos = Vec2::new(100.0 + TOUCH_EASING_DISTANCE_THRESHOLD - 0.1, 100.0); + + let (_delta, distance) = update_touch_reference_position(&mut touch_data, 0.016); + + assert_that(&distance).is_less_than(TOUCH_EASING_DISTANCE_THRESHOLD); + assert_that(&touch_data.start_pos).is_equal_to(Vec2::new(100.0, 100.0)); + } + + #[test] + fn easing_beyond_threshold_moves_towards_target() { + let mut touch_data = TouchData::new(0, Vec2::new(100.0, 100.0)); + touch_data.current_pos = Vec2::new(150.0, 100.0); + + let original_start_pos = touch_data.start_pos; + let (_delta, distance) = update_touch_reference_position(&mut touch_data, 0.016); + + assert_that(&distance).is_greater_than(TOUCH_EASING_DISTANCE_THRESHOLD); + assert_that(&touch_data.start_pos.x).is_greater_than(original_start_pos.x); + assert_that(&touch_data.start_pos.x).is_less_than(touch_data.current_pos.x); + } + + #[test] + fn easing_overshoot_sets_to_target() { + let mut touch_data = TouchData::new(0, Vec2::new(100.0, 100.0)); + touch_data.current_pos = Vec2::new(101.0, 100.0); + + let (_delta, _distance) = update_touch_reference_position(&mut touch_data, 10.0); + + assert_that(&touch_data.start_pos).is_equal_to(touch_data.current_pos); + } + + #[test] + fn easing_returns_correct_delta() { + let mut touch_data = TouchData::new(0, Vec2::new(100.0, 100.0)); + touch_data.current_pos = Vec2::new(120.0, 110.0); + + let (delta, distance) = update_touch_reference_position(&mut touch_data, 0.016); + + let expected_delta = Vec2::new(20.0, 10.0); + let expected_distance = expected_delta.length(); + + assert_that(&delta).is_equal_to(expected_delta); + assert_that(&distance).is_equal_to(expected_distance); + } +} + +// Integration tests for the full input system +mod integration_tests { + use super::*; + + fn mouse_motion_event(x: i32, y: i32) -> sdl2::event::Event { + sdl2::event::Event::MouseMotion { + x, + y, + xrel: 0, + yrel: 0, + mousestate: sdl2::mouse::MouseState::from_sdl_state(0), + which: 0, + window_id: 0, + timestamp: 0, + } + } + + fn mouse_button_down_event(x: i32, y: i32) -> sdl2::event::Event { + sdl2::event::Event::MouseButtonDown { + x, + y, + mouse_btn: sdl2::mouse::MouseButton::Left, + clicks: 1, + which: 0, + window_id: 0, + timestamp: 0, + } + } + + fn mouse_button_up_event(x: i32, y: i32) -> sdl2::event::Event { + sdl2::event::Event::MouseButtonUp { + x, + y, + mouse_btn: sdl2::mouse::MouseButton::Left, + clicks: 1, + which: 0, + window_id: 0, + timestamp: 0, + } + } + + // Simplified helper for testing SDL integration + fn run_input_system_with_events(events: Vec, delta_time: f32) -> (CursorPosition, TouchState) { + use bevy_ecs::{event::Events, system::RunSystemOnce, world::World}; + use pacman::systems::components::DeltaTime; + use pacman::systems::input::input_system; + + let sdl_context = sdl2::init().expect("Failed to initialize SDL"); + let event_subsystem = sdl_context.event().expect("Failed to get event subsystem"); + let event_pump = sdl_context.event_pump().expect("Failed to create event pump"); + + let mut world = World::new(); + world.insert_resource(Events::::default()); + world.insert_resource(DeltaTime { + seconds: delta_time, + ticks: 1, + }); + world.insert_resource(Bindings::default()); + world.insert_resource(CursorPosition::None); + world.insert_resource(TouchState::default()); + world.insert_non_send_resource(event_pump); + + // Inject events into SDL's event queue + for event in events { + event_subsystem.push_event(event).expect("Failed to push event"); + } + + // Run the real input system + world + .run_system_once(input_system) + .expect("Input system should run successfully"); + + let cursor = *world.resource::(); + let touch_state = world.resource::().clone(); + + (cursor, touch_state) + } + + #[test] + fn mouse_motion_updates_cursor_position() { + let events = vec![mouse_motion_event(100, 200)]; + let (cursor, _touch_state) = run_input_system_with_events(events, 0.016); + + match cursor { + CursorPosition::Some { + position, + remaining_time, + } => { + assert_that(&position).is_equal_to(Vec2::new(100.0, 200.0)); + assert_that(&remaining_time).is_equal_to(0.20); + } + CursorPosition::None => panic!("Expected cursor position to be set"), + } + } + + #[test] + fn mouse_button_down_starts_touch() { + let events = vec![mouse_button_down_event(150, 250)]; + let (_cursor, touch_state) = run_input_system_with_events(events, 0.016); + + assert_that(&touch_state.active_touch).is_some(); + if let Some(touch_data) = &touch_state.active_touch { + assert_that(&touch_data.finger_id).is_equal_to(0); + assert_that(&touch_data.start_pos).is_equal_to(Vec2::new(150.0, 250.0)); + } + } + + #[test] + fn mouse_button_up_ends_touch() { + let events = vec![mouse_button_down_event(150, 250), mouse_button_up_event(150, 250)]; + let (_cursor, touch_state) = run_input_system_with_events(events, 0.016); + + assert_that(&touch_state.active_touch).is_none(); + } +} + +// Touch direction tests +mod touch_direction_tests { + use super::*; + + #[test] + fn movement_above_threshold_emits_direction() { + let mut touch_data = TouchData::new(1, Vec2::new(100.0, 100.0)); + touch_data.current_pos = Vec2::new(100.0 + TOUCH_DIRECTION_THRESHOLD + 5.0, 100.0); + + let (delta, distance) = update_touch_reference_position(&mut touch_data, 0.016); + + assert_that(&distance).is_greater_than_or_equal_to(TOUCH_DIRECTION_THRESHOLD); + let direction = calculate_direction_from_delta(delta); + assert_that(&direction).is_equal_to(Direction::Right); + } + + #[test] + fn movement_below_threshold_no_direction() { + let mut touch_data = TouchData::new(1, Vec2::new(100.0, 100.0)); + touch_data.current_pos = Vec2::new(100.0 + TOUCH_DIRECTION_THRESHOLD - 1.0, 100.0); + + let (_delta, distance) = update_touch_reference_position(&mut touch_data, 0.016); + + assert_that(&distance).is_less_than(TOUCH_DIRECTION_THRESHOLD); + } + + #[test] + fn all_directions_work_correctly() { + let test_cases = vec![ + (Vec2::new(TOUCH_DIRECTION_THRESHOLD + 5.0, 0.0), Direction::Right), + (Vec2::new(-TOUCH_DIRECTION_THRESHOLD - 5.0, 0.0), Direction::Left), + (Vec2::new(0.0, TOUCH_DIRECTION_THRESHOLD + 5.0), Direction::Down), + (Vec2::new(0.0, -TOUCH_DIRECTION_THRESHOLD - 5.0), Direction::Up), + ]; + + for (offset, expected_direction) in test_cases { + let mut touch_data = TouchData::new(1, Vec2::new(100.0, 100.0)); + touch_data.current_pos = Vec2::new(100.0, 100.0) + offset; + + let (delta, distance) = update_touch_reference_position(&mut touch_data, 0.016); + + assert_that(&distance).is_greater_than_or_equal_to(TOUCH_DIRECTION_THRESHOLD); + let direction = calculate_direction_from_delta(delta); + assert_that(&direction).is_equal_to(expected_direction); + } + } } diff --git a/tests/map_builder.rs b/tests/map_builder.rs index 90da547..8ad3649 100644 --- a/tests/map_builder.rs +++ b/tests/map_builder.rs @@ -1,10 +1,11 @@ use glam::Vec2; use pacman::constants::{CELL_SIZE, RAW_BOARD}; use pacman::map::builder::Map; +use pacman::map::graph::TraversalFlags; use speculoos::prelude::*; #[test] -fn test_map_creation() { +fn test_map_creation_success() { let map = Map::new(RAW_BOARD).unwrap(); assert_that(&map.graph.nodes().count()).is_greater_than(0); @@ -22,7 +23,7 @@ fn test_map_creation() { } #[test] -fn test_map_node_positions() { +fn test_map_node_positions_accuracy() { let map = Map::new(RAW_BOARD).unwrap(); for (grid_pos, &node_id) in &map.grid_to_node { @@ -35,3 +36,54 @@ fn test_map_node_positions() { assert_that(&node.position).is_equal_to(expected_pos); } } + +#[test] +fn test_start_positions_are_valid() { + let map = Map::new(RAW_BOARD).unwrap(); + let positions = &map.start_positions; + + // All start positions should exist in the graph + assert_that(&map.graph.get_node(positions.pacman)).is_some(); + assert_that(&map.graph.get_node(positions.blinky)).is_some(); + assert_that(&map.graph.get_node(positions.pinky)).is_some(); + assert_that(&map.graph.get_node(positions.inky)).is_some(); + assert_that(&map.graph.get_node(positions.clyde)).is_some(); +} + +#[test] +fn test_ghost_house_has_ghost_only_entrance() { + let map = Map::new(RAW_BOARD).unwrap(); + + // Find the house entrance node + let house_entrance = map.start_positions.blinky; + + // Check that there's a ghost-only connection from the house entrance + let mut has_ghost_only_connection = false; + for edge in map.graph.adjacency_list[house_entrance as usize].edges() { + if edge.traversal_flags == TraversalFlags::GHOST { + has_ghost_only_connection = true; + break; + } + } + assert_that(&has_ghost_only_connection).is_true(); +} + +#[test] +fn test_tunnel_connections_exist() { + let map = Map::new(RAW_BOARD).unwrap(); + + // Find tunnel nodes by looking for nodes with zero-distance connections + let mut has_tunnel_connection = false; + for intersection in &map.graph.adjacency_list { + for edge in intersection.edges() { + if edge.distance == 0.0f32 { + has_tunnel_connection = true; + break; + } + } + if has_tunnel_connection { + break; + } + } + assert_that(&has_tunnel_connection).is_true(); +}