mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-09 00:07:47 -06:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5109457fcd | ||
|
|
5497e4b0b9 | ||
| d72b6eec06 | |||
| ae42f6ead0 |
@@ -28,7 +28,6 @@ pub trait CommonPlatform {
|
|||||||
fn get_canvas_size(&self) -> Option<(u32, u32)>;
|
fn get_canvas_size(&self) -> Option<(u32, u32)>;
|
||||||
|
|
||||||
/// Loads raw asset data using the appropriate platform-specific method.
|
/// Loads raw asset data using the appropriate platform-specific method.
|
||||||
|
|
||||||
fn get_asset_bytes(&self, asset: Asset) -> Result<Cow<'static, [u8]>, AssetError>;
|
fn get_asset_bytes(&self, asset: Asset) -> Result<Cow<'static, [u8]>, AssetError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ pub enum CursorPosition {
|
|||||||
pub struct Bindings {
|
pub struct Bindings {
|
||||||
key_bindings: HashMap<Keycode, GameCommand>,
|
key_bindings: HashMap<Keycode, GameCommand>,
|
||||||
movement_keys: HashSet<Keycode>,
|
movement_keys: HashSet<Keycode>,
|
||||||
last_movement_key: Option<Keycode>,
|
pressed_movement_keys: Vec<Keycode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Bindings {
|
impl Default for Bindings {
|
||||||
@@ -67,11 +67,63 @@ impl Default for Bindings {
|
|||||||
Self {
|
Self {
|
||||||
key_bindings,
|
key_bindings,
|
||||||
movement_keys,
|
movement_keys,
|
||||||
last_movement_key: None,
|
pressed_movement_keys: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A simplified input event used for deterministic testing and logic reuse
|
||||||
|
/// without depending on SDL's event pump.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SimpleKeyEvent {
|
||||||
|
KeyDown(Keycode),
|
||||||
|
KeyUp(Keycode),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes a frame's worth of simplified key events and returns the resulting
|
||||||
|
/// `GameEvent`s that would be emitted by the input system for that frame.
|
||||||
|
///
|
||||||
|
/// This mirrors the behavior of `input_system` for keyboard-related logic:
|
||||||
|
/// - KeyDown emits the bound command immediately (movement or otherwise)
|
||||||
|
/// - Tracks pressed movement keys in order to continue movement on subsequent frames
|
||||||
|
/// - KeyUp removes movement keys; if another movement key remains, it resumes
|
||||||
|
pub fn process_simple_key_events(bindings: &mut Bindings, frame_events: &[SimpleKeyEvent]) -> Vec<GameEvent> {
|
||||||
|
let mut emitted_events = Vec::new();
|
||||||
|
let mut movement_key_pressed = false;
|
||||||
|
|
||||||
|
for event in frame_events {
|
||||||
|
match *event {
|
||||||
|
SimpleKeyEvent::KeyDown(key) => {
|
||||||
|
if let Some(command) = bindings.key_bindings.get(&key).copied() {
|
||||||
|
emitted_events.push(GameEvent::Command(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
if bindings.movement_keys.contains(&key) {
|
||||||
|
movement_key_pressed = true;
|
||||||
|
if !bindings.pressed_movement_keys.contains(&key) {
|
||||||
|
bindings.pressed_movement_keys.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SimpleKeyEvent::KeyUp(key) => {
|
||||||
|
if bindings.movement_keys.contains(&key) {
|
||||||
|
bindings.pressed_movement_keys.retain(|&k| k != key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !movement_key_pressed {
|
||||||
|
if let Some(&last_movement_key) = bindings.pressed_movement_keys.last() {
|
||||||
|
if let Some(command) = bindings.key_bindings.get(&last_movement_key).copied() {
|
||||||
|
emitted_events.push(GameEvent::Command(command));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitted_events
|
||||||
|
}
|
||||||
|
|
||||||
pub fn input_system(
|
pub fn input_system(
|
||||||
delta_time: Res<DeltaTime>,
|
delta_time: Res<DeltaTime>,
|
||||||
mut bindings: ResMut<Bindings>,
|
mut bindings: ResMut<Bindings>,
|
||||||
@@ -79,11 +131,14 @@ pub fn input_system(
|
|||||||
mut pump: NonSendMut<&'static mut EventPump>,
|
mut pump: NonSendMut<&'static mut EventPump>,
|
||||||
mut cursor: ResMut<CursorPosition>,
|
mut cursor: ResMut<CursorPosition>,
|
||||||
) {
|
) {
|
||||||
let mut movement_key_pressed = false;
|
|
||||||
let mut cursor_seen = false;
|
let mut cursor_seen = false;
|
||||||
|
// Collect all events for this frame.
|
||||||
|
let frame_events: Vec<Event> = pump.poll_iter().collect();
|
||||||
|
|
||||||
for event in pump.poll_iter() {
|
// Handle non-keyboard events inline and build a simplified keyboard event stream.
|
||||||
match event {
|
let mut simple_key_events = Vec::new();
|
||||||
|
for event in &frame_events {
|
||||||
|
match *event {
|
||||||
Event::Quit { .. } => {
|
Event::Quit { .. } => {
|
||||||
writer.write(GameEvent::Command(GameCommand::Exit));
|
writer.write(GameEvent::Command(GameCommand::Exit));
|
||||||
}
|
}
|
||||||
@@ -94,44 +149,28 @@ pub fn input_system(
|
|||||||
};
|
};
|
||||||
cursor_seen = true;
|
cursor_seen = true;
|
||||||
}
|
}
|
||||||
Event::KeyUp {
|
|
||||||
repeat: false,
|
|
||||||
keycode: Some(key),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
// If the last movement key was released, then forget it.
|
|
||||||
if let Some(last_movement_key) = bindings.last_movement_key {
|
|
||||||
if last_movement_key == key {
|
|
||||||
bindings.last_movement_key = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::KeyDown {
|
Event::KeyDown {
|
||||||
keycode: Some(key),
|
keycode: Some(key),
|
||||||
repeat: false,
|
repeat: false,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let command = bindings.key_bindings.get(&key).copied();
|
simple_key_events.push(SimpleKeyEvent::KeyDown(key));
|
||||||
if let Some(command) = command {
|
}
|
||||||
writer.write(GameEvent::Command(command));
|
Event::KeyUp {
|
||||||
}
|
keycode: Some(key),
|
||||||
|
repeat: false,
|
||||||
if bindings.movement_keys.contains(&key) {
|
..
|
||||||
movement_key_pressed = true;
|
} => {
|
||||||
bindings.last_movement_key = Some(key);
|
simple_key_events.push(SimpleKeyEvent::KeyUp(key));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(last_movement_key) = bindings.last_movement_key {
|
// Delegate keyboard handling to shared logic used by tests and production.
|
||||||
if !movement_key_pressed {
|
let emitted = process_simple_key_events(&mut bindings, &simple_key_events);
|
||||||
let command = bindings.key_bindings.get(&last_movement_key).copied();
|
for event in emitted {
|
||||||
if let Some(command) = command {
|
writer.write(event);
|
||||||
writer.write(GameEvent::Command(command));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (false, CursorPosition::Some { remaining_time, .. }) = (cursor_seen, &mut *cursor) {
|
if let (false, CursorPosition::Some { remaining_time, .. }) = (cursor_seen, &mut *cursor) {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use pacman::map::direction::Direction;
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_game_command_variants() {
|
fn test_game_command_variants() {
|
||||||
// Test that all GameCommand variants can be created
|
// Test that all GameCommand variants can be created
|
||||||
let commands = vec![
|
let commands = [
|
||||||
GameCommand::Exit,
|
GameCommand::Exit,
|
||||||
GameCommand::MovePlayer(Direction::Up),
|
GameCommand::MovePlayer(Direction::Up),
|
||||||
GameCommand::MovePlayer(Direction::Down),
|
GameCommand::MovePlayer(Direction::Down),
|
||||||
@@ -38,17 +38,6 @@ fn test_game_command_equality() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_game_command_copy_clone() {
|
|
||||||
let original = GameCommand::MovePlayer(Direction::Up);
|
|
||||||
let copied = original;
|
|
||||||
let cloned = original.clone();
|
|
||||||
|
|
||||||
assert_eq!(original, copied);
|
|
||||||
assert_eq!(original, cloned);
|
|
||||||
assert_eq!(copied, cloned);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_game_event_variants() {
|
fn test_game_event_variants() {
|
||||||
let command_event = GameEvent::Command(GameCommand::Exit);
|
let command_event = GameEvent::Command(GameCommand::Exit);
|
||||||
|
|||||||
38
tests/input.rs
Normal file
38
tests/input.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use pacman::events::{GameCommand, GameEvent};
|
||||||
|
use pacman::map::direction::Direction;
|
||||||
|
use pacman::systems::input::{process_simple_key_events, Bindings, SimpleKeyEvent};
|
||||||
|
use sdl2::keyboard::Keycode;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resumes_previous_direction_when_secondary_key_released() {
|
||||||
|
let mut bindings = Bindings::default();
|
||||||
|
|
||||||
|
// Frame 1: Press W (Up) => emits Move Up
|
||||||
|
let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::W)]);
|
||||||
|
assert!(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!(events.contains(&GameEvent::Command(GameCommand::MovePlayer(Direction::Right))));
|
||||||
|
|
||||||
|
// 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!(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();
|
||||||
|
|
||||||
|
// Frame 1: Press Left
|
||||||
|
let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::Left)]);
|
||||||
|
assert!(events.contains(&GameEvent::Command(GameCommand::MovePlayer(Direction::Left))));
|
||||||
|
|
||||||
|
// Frame 2: No input => continues Left
|
||||||
|
let events = process_simple_key_events(&mut bindings, &[]);
|
||||||
|
assert!(events.contains(&GameEvent::Command(GameCommand::MovePlayer(Direction::Left))));
|
||||||
|
|
||||||
|
// Frame 3: Release Left, no input remains => nothing emitted
|
||||||
|
let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyUp(Keycode::Left)]);
|
||||||
|
assert!(events.is_empty());
|
||||||
|
}
|
||||||
345
tests/item.rs
345
tests/item.rs
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user