From 471b118efde1437f8957a2a95069efb755036598 Mon Sep 17 00:00:00 2001 From: Xevion Date: Mon, 18 Aug 2025 00:04:07 -0500 Subject: [PATCH] test: add tests for item systems & movement types --- tests/item.rs | 159 +++++++++++++++++++++++++++++++++++++ tests/movement.rs | 195 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 tests/item.rs create mode 100644 tests/movement.rs diff --git a/tests/item.rs b/tests/item.rs new file mode 100644 index 0000000..36c5705 --- /dev/null +++ b/tests/item.rs @@ -0,0 +1,159 @@ +use pacman::systems::components::EntityType; + +// Helper functions that extract the core scoring logic from item_system +// This allows us to test the business rules without ECS complexity + +fn calculate_score_for_item(entity_type: EntityType) -> Option { + match entity_type { + EntityType::Pellet => Some(10), + EntityType::PowerPellet => Some(50), + _ => None, + } +} + +fn is_collectible_item(entity_type: EntityType) -> bool { + matches!(entity_type, EntityType::Pellet | EntityType::PowerPellet) +} + +fn should_trigger_audio_on_collection(entity_type: EntityType) -> bool { + is_collectible_item(entity_type) +} + +#[test] +fn test_pellet_scoring() { + assert_eq!(calculate_score_for_item(EntityType::Pellet), Some(10)); +} + +#[test] +fn test_power_pellet_scoring() { + assert_eq!(calculate_score_for_item(EntityType::PowerPellet), Some(50)); +} + +#[test] +fn test_non_collectible_items_no_score() { + assert_eq!(calculate_score_for_item(EntityType::Player), None); + assert_eq!(calculate_score_for_item(EntityType::Ghost), None); +} + +#[test] +fn test_collectible_item_detection() { + assert!(is_collectible_item(EntityType::Pellet)); + assert!(is_collectible_item(EntityType::PowerPellet)); + assert!(!is_collectible_item(EntityType::Player)); + assert!(!is_collectible_item(EntityType::Ghost)); +} + +#[test] +fn test_audio_trigger_for_collectibles() { + assert!(should_trigger_audio_on_collection(EntityType::Pellet)); + assert!(should_trigger_audio_on_collection(EntityType::PowerPellet)); + assert!(!should_trigger_audio_on_collection(EntityType::Player)); + assert!(!should_trigger_audio_on_collection(EntityType::Ghost)); +} + +#[test] +fn test_score_progression() { + // Test that power pellets are worth more than regular pellets + let pellet_score = calculate_score_for_item(EntityType::Pellet).unwrap(); + let power_pellet_score = calculate_score_for_item(EntityType::PowerPellet).unwrap(); + + assert!(power_pellet_score > pellet_score); + assert_eq!(power_pellet_score / pellet_score, 5); // Power pellets are worth 5x regular pellets +} + +#[test] +fn test_entity_type_variants() { + // Test all EntityType variants to ensure they're handled appropriately + let all_types = vec![ + EntityType::Player, + EntityType::Ghost, + EntityType::Pellet, + EntityType::PowerPellet, + ]; + + let mut collectible_count = 0; + let mut non_collectible_count = 0; + + for entity_type in all_types { + if is_collectible_item(entity_type) { + collectible_count += 1; + // All collectible items should have a score + assert!(calculate_score_for_item(entity_type).is_some()); + } else { + non_collectible_count += 1; + // Non-collectible items should not have a score + assert!(calculate_score_for_item(entity_type).is_none()); + } + } + + // Verify we have the expected number of each type + assert_eq!(collectible_count, 2); // Pellet and PowerPellet + assert_eq!(non_collectible_count, 2); // Player and Ghost +} + +#[test] +fn test_score_accumulation() { + // Test score accumulation logic (simulating multiple collections) + let mut total_score = 0u32; + + // Collect some items + let collected_items = vec![ + EntityType::Pellet, + EntityType::Pellet, + EntityType::PowerPellet, + EntityType::Pellet, + EntityType::PowerPellet, + ]; + + for item in collected_items { + if let Some(score) = calculate_score_for_item(item) { + total_score += score; + } + } + + // Expected: 3 pellets (30) + 2 power pellets (100) = 130 + assert_eq!(total_score, 130); +} + +#[test] +fn test_collision_filtering_logic() { + // Test the logic for determining valid collision pairs + // This mirrors the logic in item_system that checks entity types + + let test_cases = vec![ + (EntityType::Player, EntityType::Pellet, true), + (EntityType::Player, EntityType::PowerPellet, true), + (EntityType::Player, EntityType::Ghost, false), // Not handled by item system + (EntityType::Player, EntityType::Player, false), // Not a valid collision + (EntityType::Ghost, EntityType::Pellet, false), // Ghosts don't collect items + (EntityType::Pellet, EntityType::PowerPellet, false), // Items don't interact + ]; + + for (entity1, entity2, should_be_valid) in test_cases { + let is_valid_item_collision = (entity1 == EntityType::Player && is_collectible_item(entity2)) + || (entity2 == EntityType::Player && is_collectible_item(entity1)); + + assert_eq!( + is_valid_item_collision, should_be_valid, + "Failed for collision between {:?} and {:?}", + entity1, entity2 + ); + } +} + +#[test] +fn test_item_collection_side_effects() { + // Test that collecting items should trigger the expected side effects + let collectible_items = vec![EntityType::Pellet, EntityType::PowerPellet]; + + for item in collectible_items { + // Should provide score + assert!(calculate_score_for_item(item).is_some()); + + // Should trigger audio + assert!(should_trigger_audio_on_collection(item)); + + // Should be marked as collectible + assert!(is_collectible_item(item)); + } +} diff --git a/tests/movement.rs b/tests/movement.rs new file mode 100644 index 0000000..8ad531e --- /dev/null +++ b/tests/movement.rs @@ -0,0 +1,195 @@ +use glam::Vec2; +use pacman::map::direction::Direction; +use pacman::map::graph::{Graph, Node}; +use pacman::systems::movement::{BufferedDirection, Position, Velocity}; + +fn create_test_graph() -> Graph { + let mut graph = Graph::new(); + + // Add a few test nodes + let node0 = graph.add_node(Node { + position: Vec2::new(0.0, 0.0), + }); + let node1 = graph.add_node(Node { + position: Vec2::new(16.0, 0.0), + }); + let node2 = graph.add_node(Node { + position: Vec2::new(0.0, 16.0), + }); + + // Connect them + graph.connect(node0, node1, false, None, Direction::Right).unwrap(); + graph.connect(node0, node2, false, None, Direction::Down).unwrap(); + + graph +} + +#[test] +fn test_position_is_at_node() { + let stopped_pos = Position::Stopped { node: 0 }; + let moving_pos = Position::Moving { + from: 0, + to: 1, + remaining_distance: 8.0, + }; + + assert!(stopped_pos.is_at_node()); + assert!(!moving_pos.is_at_node()); +} + +#[test] +fn test_position_current_node() { + let stopped_pos = Position::Stopped { node: 5 }; + let moving_pos = Position::Moving { + from: 3, + to: 7, + remaining_distance: 12.0, + }; + + assert_eq!(stopped_pos.current_node(), 5); + assert_eq!(moving_pos.current_node(), 3); +} + +#[test] +fn test_position_tick_no_movement_when_stopped() { + let mut pos = Position::Stopped { node: 0 }; + let result = pos.tick(5.0); + + assert!(result.is_none()); + assert_eq!(pos, Position::Stopped { node: 0 }); +} + +#[test] +fn test_position_tick_no_movement_when_zero_distance() { + let mut pos = Position::Moving { + from: 0, + to: 1, + remaining_distance: 10.0, + }; + let result = pos.tick(0.0); + + assert!(result.is_none()); + assert_eq!( + pos, + Position::Moving { + from: 0, + to: 1, + remaining_distance: 10.0, + } + ); +} + +#[test] +fn test_position_tick_partial_movement() { + let mut pos = Position::Moving { + from: 0, + to: 1, + remaining_distance: 10.0, + }; + let result = pos.tick(3.0); + + assert!(result.is_none()); + assert_eq!( + pos, + Position::Moving { + from: 0, + to: 1, + remaining_distance: 7.0, + } + ); +} + +#[test] +fn test_position_tick_exact_arrival() { + let mut pos = Position::Moving { + from: 0, + to: 1, + remaining_distance: 5.0, + }; + let result = pos.tick(5.0); + + assert!(result.is_none()); + assert_eq!(pos, Position::Stopped { node: 1 }); +} + +#[test] +fn test_position_tick_overshoot_with_overflow() { + let mut pos = Position::Moving { + from: 0, + to: 1, + remaining_distance: 3.0, + }; + let result = pos.tick(8.0); + + assert_eq!(result, Some(5.0)); + assert_eq!(pos, Position::Stopped { node: 1 }); +} + +#[test] +fn test_position_get_pixel_position_stopped() { + let graph = create_test_graph(); + let pos = Position::Stopped { node: 0 }; + + let pixel_pos = pos.get_pixel_position(&graph).unwrap(); + let expected = Vec2::new( + 0.0 + pacman::constants::BOARD_PIXEL_OFFSET.x as f32, + 0.0 + pacman::constants::BOARD_PIXEL_OFFSET.y as f32, + ); + + assert_eq!(pixel_pos, expected); +} + +#[test] +fn test_position_get_pixel_position_moving() { + let graph = create_test_graph(); + let pos = Position::Moving { + from: 0, + to: 1, + remaining_distance: 8.0, // Halfway through a 16-unit edge + }; + + let pixel_pos = pos.get_pixel_position(&graph).unwrap(); + // Should be halfway between (0,0) and (16,0), so at (8,0) plus offset + let expected = Vec2::new( + 8.0 + pacman::constants::BOARD_PIXEL_OFFSET.x as f32, + 0.0 + pacman::constants::BOARD_PIXEL_OFFSET.y as f32, + ); + + assert_eq!(pixel_pos, expected); +} + +#[test] +fn test_velocity_basic_properties() { + let velocity = Velocity { + speed: 2.5, + direction: Direction::Up, + }; + + assert_eq!(velocity.speed, 2.5); + assert_eq!(velocity.direction, Direction::Up); +} + +#[test] +fn test_buffered_direction_none() { + let buffered = BufferedDirection::None; + assert_eq!(buffered, BufferedDirection::None); +} + +#[test] +fn test_buffered_direction_some() { + let buffered = BufferedDirection::Some { + direction: Direction::Left, + remaining_time: 0.5, + }; + + if let BufferedDirection::Some { + direction, + remaining_time, + } = buffered + { + assert_eq!(direction, Direction::Left); + assert_eq!(remaining_time, 0.5); + } else { + panic!("Expected BufferedDirection::Some"); + } +}