test: add item testing

This commit is contained in:
2025-08-18 09:32:35 -05:00
parent ae42f6ead0
commit fc8f1cd417
3 changed files with 263 additions and 125 deletions

View File

@@ -70,6 +70,17 @@ impl EntityType {
_ => TraversalFlags::empty(), // Static entities don't traverse _ => TraversalFlags::empty(), // Static entities don't traverse
} }
} }
pub fn score_value(&self) -> Option<u32> {
match self {
EntityType::Pellet => Some(10),
EntityType::PowerPellet => Some(50),
_ => None,
}
}
pub fn is_collectible(&self) -> bool {
matches!(self, EntityType::Pellet | EntityType::PowerPellet)
}
} }
/// A component for entities that have a sprite, with a layer for ordering. /// A component for entities that have a sprite, with a layer for ordering.

View File

@@ -8,6 +8,17 @@ use crate::{
}, },
}; };
/// Determines if a collision between two entity types should be handled by the item system.
///
/// Returns `true` if one entity is a player and the other is a collectible item.
#[allow(dead_code)]
pub fn is_valid_item_collision(entity1: EntityType, entity2: EntityType) -> bool {
match (entity1, entity2) {
(EntityType::Player, entity) | (entity, EntityType::Player) => entity.is_collectible(),
_ => false,
}
}
pub fn item_system( pub fn item_system(
mut commands: Commands, mut commands: Commands,
mut collision_events: EventReader<GameEvent>, mut collision_events: EventReader<GameEvent>,
@@ -29,20 +40,17 @@ pub fn item_system(
// Get the item type and update score // Get the item type and update score
if let Ok((item_ent, entity_type)) = item_query.get(item_entity) { if let Ok((item_ent, entity_type)) = item_query.get(item_entity) {
match entity_type { if let Some(score_value) = entity_type.score_value() {
EntityType::Pellet => { score.0 += score_value;
score.0 += 10;
// Remove the collected item
commands.entity(item_ent).despawn();
// Trigger audio if appropriate
if entity_type.is_collectible() {
events.write(AudioEvent::PlayEat);
} }
EntityType::PowerPellet => {
score.0 += 50;
}
_ => continue,
} }
// Remove the collected item
commands.entity(item_ent).despawn();
events.write(AudioEvent::PlayEat);
} }
} }
} }

View File

@@ -1,159 +1,278 @@
use pacman::systems::components::EntityType; use bevy_ecs::{event::Events, prelude::*, system::RunSystemOnce, world::World};
// Helper functions that extract the core scoring logic from item_system use pacman::{
// This allows us to test the business rules without ECS complexity events::GameEvent,
map::builder::Map,
systems::{
audio::AudioEvent,
components::{AudioState, EntityType, ItemCollider, PacmanCollider, ScoreResource},
item::{is_valid_item_collision, item_system},
movement::Position,
},
};
fn calculate_score_for_item(entity_type: EntityType) -> Option<u32> { #[test]
match entity_type { fn test_calculate_score_for_item() {
EntityType::Pellet => Some(10), assert!(EntityType::Pellet.score_value() < EntityType::PowerPellet.score_value());
EntityType::PowerPellet => Some(50), assert!(EntityType::Pellet.score_value().is_some());
_ => None, assert!(EntityType::PowerPellet.score_value().is_some());
} assert!(EntityType::Pellet.score_value().unwrap() < EntityType::PowerPellet.score_value().unwrap());
} assert!(EntityType::Player.score_value().is_none());
assert!(EntityType::Ghost.score_value().is_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] #[test]
fn test_pellet_scoring() { fn test_is_collectible_item() {
assert_eq!(calculate_score_for_item(EntityType::Pellet), Some(10)); // Collectible
assert!(EntityType::Pellet.is_collectible());
assert!(EntityType::PowerPellet.is_collectible());
// Non-collectible
assert!(!EntityType::Player.is_collectible());
assert!(!EntityType::Ghost.is_collectible());
} }
#[test] #[test]
fn test_power_pellet_scoring() { fn test_is_valid_item_collision() {
assert_eq!(calculate_score_for_item(EntityType::PowerPellet), Some(50)); // Player-item collisions should be valid
assert!(is_valid_item_collision(EntityType::Player, EntityType::Pellet));
assert!(is_valid_item_collision(EntityType::Player, EntityType::PowerPellet));
assert!(is_valid_item_collision(EntityType::Pellet, EntityType::Player));
assert!(is_valid_item_collision(EntityType::PowerPellet, EntityType::Player));
// Non-player-item collisions should be invalid
assert!(!is_valid_item_collision(EntityType::Player, EntityType::Ghost));
assert!(!is_valid_item_collision(EntityType::Ghost, EntityType::Pellet));
assert!(!is_valid_item_collision(EntityType::Pellet, EntityType::PowerPellet));
assert!(!is_valid_item_collision(EntityType::Player, EntityType::Player));
}
fn create_test_world() -> World {
let mut world = World::new();
// Add required resources
world.insert_resource(ScoreResource(0));
world.insert_resource(AudioState::default());
world.insert_resource(Events::<GameEvent>::default());
world.insert_resource(Events::<AudioEvent>::default());
world.insert_resource(Events::<pacman::error::GameError>::default());
// Add a minimal test map
world.insert_resource(create_test_map());
world
}
fn create_test_map() -> Map {
use pacman::constants::RAW_BOARD;
Map::new(RAW_BOARD).expect("Failed to create test map")
}
fn spawn_test_pacman(world: &mut World) -> Entity {
world
.spawn((Position::Stopped { node: 0 }, EntityType::Player, PacmanCollider))
.id()
}
fn spawn_test_item(world: &mut World, item_type: EntityType) -> Entity {
world.spawn((Position::Stopped { node: 1 }, item_type, ItemCollider)).id()
}
fn send_collision_event(world: &mut World, entity1: Entity, entity2: Entity) {
let mut events = world.resource_mut::<Events<GameEvent>>();
events.send(GameEvent::Collision(entity1, entity2));
} }
#[test] #[test]
fn test_non_collectible_items_no_score() { fn test_item_system_pellet_collection() {
assert_eq!(calculate_score_for_item(EntityType::Player), None); let mut world = create_test_world();
assert_eq!(calculate_score_for_item(EntityType::Ghost), None); let pacman = spawn_test_pacman(&mut world);
let pellet = spawn_test_item(&mut world, EntityType::Pellet);
// Send collision event
send_collision_event(&mut world, pacman, pellet);
// Run the item system
world.run_system_once(item_system).expect("System should run successfully");
// Check that score was updated
let score = world.resource::<ScoreResource>();
assert_eq!(score.0, 10);
// Check that the pellet was despawned (query should return empty)
let item_count = world
.query::<&EntityType>()
.iter(&world)
.filter(|&entity_type| matches!(entity_type, EntityType::Pellet))
.count();
assert_eq!(item_count, 0);
} }
#[test] #[test]
fn test_collectible_item_detection() { fn test_item_system_power_pellet_collection() {
assert!(is_collectible_item(EntityType::Pellet)); let mut world = create_test_world();
assert!(is_collectible_item(EntityType::PowerPellet)); let pacman = spawn_test_pacman(&mut world);
assert!(!is_collectible_item(EntityType::Player)); let power_pellet = spawn_test_item(&mut world, EntityType::PowerPellet);
assert!(!is_collectible_item(EntityType::Ghost));
send_collision_event(&mut world, pacman, power_pellet);
world.run_system_once(item_system).expect("System should run successfully");
// Check that score was updated with power pellet value
let score = world.resource::<ScoreResource>();
assert_eq!(score.0, 50);
// Check that the power pellet was despawned (query should return empty)
let item_count = world
.query::<&EntityType>()
.iter(&world)
.filter(|&entity_type| matches!(entity_type, EntityType::PowerPellet))
.count();
assert_eq!(item_count, 0);
} }
#[test] #[test]
fn test_audio_trigger_for_collectibles() { fn test_item_system_multiple_collections() {
assert!(should_trigger_audio_on_collection(EntityType::Pellet)); let mut world = create_test_world();
assert!(should_trigger_audio_on_collection(EntityType::PowerPellet)); let pacman = spawn_test_pacman(&mut world);
assert!(!should_trigger_audio_on_collection(EntityType::Player)); let pellet1 = spawn_test_item(&mut world, EntityType::Pellet);
assert!(!should_trigger_audio_on_collection(EntityType::Ghost)); let pellet2 = spawn_test_item(&mut world, EntityType::Pellet);
let power_pellet = spawn_test_item(&mut world, EntityType::PowerPellet);
// Send multiple collision events
send_collision_event(&mut world, pacman, pellet1);
send_collision_event(&mut world, pacman, pellet2);
send_collision_event(&mut world, pacman, power_pellet);
world.run_system_once(item_system).expect("System should run successfully");
// Check final score: 2 pellets (20) + 1 power pellet (50) = 70
let score = world.resource::<ScoreResource>();
assert_eq!(score.0, 70);
// Check that all items were despawned
let pellet_count = world
.query::<&EntityType>()
.iter(&world)
.filter(|&entity_type| matches!(entity_type, EntityType::Pellet))
.count();
let power_pellet_count = world
.query::<&EntityType>()
.iter(&world)
.filter(|&entity_type| matches!(entity_type, EntityType::PowerPellet))
.count();
assert_eq!(pellet_count, 0);
assert_eq!(power_pellet_count, 0);
} }
#[test] #[test]
fn test_score_progression() { fn test_item_system_ignores_non_item_collisions() {
// Test that power pellets are worth more than regular pellets let mut world = create_test_world();
let pellet_score = calculate_score_for_item(EntityType::Pellet).unwrap(); let pacman = spawn_test_pacman(&mut world);
let power_pellet_score = calculate_score_for_item(EntityType::PowerPellet).unwrap();
assert!(power_pellet_score > pellet_score); // Create a ghost entity (not an item)
assert_eq!(power_pellet_score / pellet_score, 5); // Power pellets are worth 5x regular pellets let ghost = world.spawn((Position::Stopped { node: 2 }, EntityType::Ghost)).id();
// Initial score
let initial_score = world.resource::<ScoreResource>().0;
// Send collision event between pacman and ghost
send_collision_event(&mut world, pacman, ghost);
world.run_system_once(item_system).expect("System should run successfully");
// Score should remain unchanged
let score = world.resource::<ScoreResource>();
assert_eq!(score.0, initial_score);
// Ghost should still exist (not despawned)
let ghost_count = world
.query::<&EntityType>()
.iter(&world)
.filter(|&entity_type| matches!(entity_type, EntityType::Ghost))
.count();
assert_eq!(ghost_count, 1);
} }
#[test] #[test]
fn test_entity_type_variants() { fn test_item_system_wrong_collision_order() {
// Test all EntityType variants to ensure they're handled appropriately let mut world = create_test_world();
let all_types = vec![ let pacman = spawn_test_pacman(&mut world);
EntityType::Player, let pellet = spawn_test_item(&mut world, EntityType::Pellet);
EntityType::Ghost,
EntityType::Pellet,
EntityType::PowerPellet,
];
let mut collectible_count = 0; // Send collision event with entities in reverse order
let mut non_collectible_count = 0; send_collision_event(&mut world, pellet, pacman);
for entity_type in all_types { world.run_system_once(item_system).expect("System should run successfully");
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 // Should still work correctly
assert_eq!(collectible_count, 2); // Pellet and PowerPellet let score = world.resource::<ScoreResource>();
assert_eq!(non_collectible_count, 2); // Player and Ghost assert_eq!(score.0, 10);
let pellet_count = world
.query::<&EntityType>()
.iter(&world)
.filter(|&entity_type| matches!(entity_type, EntityType::Pellet))
.count();
assert_eq!(pellet_count, 0);
} }
#[test] #[test]
fn test_score_accumulation() { fn test_item_system_no_collision_events() {
// Test score accumulation logic (simulating multiple collections) let mut world = create_test_world();
let mut total_score = 0u32; let _pacman = spawn_test_pacman(&mut world);
let _pellet = spawn_test_item(&mut world, EntityType::Pellet);
// Collect some items let initial_score = world.resource::<ScoreResource>().0;
let collected_items = vec![
EntityType::Pellet,
EntityType::Pellet,
EntityType::PowerPellet,
EntityType::Pellet,
EntityType::PowerPellet,
];
for item in collected_items { // Run system without any collision events
if let Some(score) = calculate_score_for_item(item) { world.run_system_once(item_system).expect("System should run successfully");
total_score += score;
}
}
// Expected: 3 pellets (30) + 2 power pellets (100) = 130 // Nothing should change
assert_eq!(total_score, 130); let score = world.resource::<ScoreResource>();
assert_eq!(score.0, initial_score);
let pellet_count = world
.query::<&EntityType>()
.iter(&world)
.filter(|&entity_type| matches!(entity_type, EntityType::Pellet))
.count();
assert_eq!(pellet_count, 1);
} }
#[test] #[test]
fn test_collision_filtering_logic() { fn test_item_system_collision_with_missing_entity() {
// Test the logic for determining valid collision pairs let mut world = create_test_world();
// This mirrors the logic in item_system that checks entity types let pacman = spawn_test_pacman(&mut world);
let test_cases = vec![ // Create a fake entity ID that doesn't exist
(EntityType::Player, EntityType::Pellet, true), let fake_entity = Entity::from_raw(999);
(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 { send_collision_event(&mut world, pacman, fake_entity);
let is_valid_item_collision = (entity1 == EntityType::Player && is_collectible_item(entity2))
|| (entity2 == EntityType::Player && is_collectible_item(entity1));
assert_eq!( // System should handle gracefully and not crash
is_valid_item_collision, should_be_valid, world
"Failed for collision between {:?} and {:?}", .run_system_once(item_system)
entity1, entity2 .expect("System should handle missing entities gracefully");
);
} // Score should remain unchanged
let score = world.resource::<ScoreResource>();
assert_eq!(score.0, 0);
} }
#[test] #[test]
fn test_item_collection_side_effects() { fn test_item_system_preserves_existing_score() {
// Test that collecting items should trigger the expected side effects let mut world = create_test_world();
let collectible_items = vec![EntityType::Pellet, EntityType::PowerPellet];
for item in collectible_items { // Set initial score
// Should provide score world.insert_resource(ScoreResource(100));
assert!(calculate_score_for_item(item).is_some());
// Should trigger audio let pacman = spawn_test_pacman(&mut world);
assert!(should_trigger_audio_on_collection(item)); let pellet = spawn_test_item(&mut world, EntityType::Pellet);
// Should be marked as collectible send_collision_event(&mut world, pacman, pellet);
assert!(is_collectible_item(item));
} world.run_system_once(item_system).expect("System should run successfully");
// Score should be initial + pellet value
let score = world.resource::<ScoreResource>();
assert_eq!(score.0, 110);
} }