mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-06 03:15:48 -06:00
575 lines
18 KiB
Rust
575 lines
18 KiB
Rust
use bevy_ecs::{entity::Entity, event::Events, system::RunSystemOnce, world::World};
|
|
|
|
use pacman::{
|
|
events::{GameCommand, GameEvent},
|
|
map::{
|
|
builder::Map,
|
|
direction::Direction,
|
|
graph::{Edge, TraversalFlags},
|
|
},
|
|
systems::{
|
|
can_traverse, player_control_system, player_movement_system, AudioState, BufferedDirection, DebugState, DeltaTime,
|
|
EntityType, GlobalState, PlayerControlled, Position, Velocity,
|
|
},
|
|
};
|
|
|
|
// Test helper functions for ECS setup
|
|
fn create_test_world() -> World {
|
|
let mut world = World::new();
|
|
|
|
// Add resources
|
|
world.insert_resource(GlobalState { exit: false });
|
|
world.insert_resource(DebugState::default());
|
|
world.insert_resource(AudioState::default());
|
|
world.insert_resource(DeltaTime(1.0 / 60.0)); // 60 FPS
|
|
world.insert_resource(Events::<GameEvent>::default());
|
|
world.insert_resource(Events::<pacman::error::GameError>::default());
|
|
|
|
// Create a simple test map with nodes and edges
|
|
let test_map = create_test_map();
|
|
world.insert_resource(test_map);
|
|
|
|
world
|
|
}
|
|
|
|
fn create_test_map() -> Map {
|
|
// Use the actual RAW_BOARD from constants.rs
|
|
use pacman::constants::RAW_BOARD;
|
|
Map::new(RAW_BOARD).expect("Failed to create test map")
|
|
}
|
|
|
|
fn spawn_test_player(world: &mut World) -> Entity {
|
|
world
|
|
.spawn((
|
|
PlayerControlled,
|
|
Position::Stopped { node: 0 },
|
|
Velocity {
|
|
speed: 1.0,
|
|
direction: Direction::Right,
|
|
},
|
|
BufferedDirection::None,
|
|
EntityType::Player,
|
|
))
|
|
.id()
|
|
}
|
|
|
|
fn send_game_event(world: &mut World, command: GameCommand) {
|
|
let mut events = world.resource_mut::<Events<GameEvent>>();
|
|
events.send(GameEvent::Command(command));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_traverse_player_on_all_edges() {
|
|
let edge = Edge {
|
|
target: 1,
|
|
distance: 10.0,
|
|
direction: Direction::Up,
|
|
traversal_flags: TraversalFlags::ALL,
|
|
};
|
|
|
|
assert!(can_traverse(EntityType::Player, edge));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_traverse_player_on_pacman_only_edges() {
|
|
let edge = Edge {
|
|
target: 1,
|
|
distance: 10.0,
|
|
direction: Direction::Right,
|
|
traversal_flags: TraversalFlags::PACMAN,
|
|
};
|
|
|
|
assert!(can_traverse(EntityType::Player, edge));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_traverse_player_blocked_on_ghost_only_edges() {
|
|
let edge = Edge {
|
|
target: 1,
|
|
distance: 10.0,
|
|
direction: Direction::Left,
|
|
traversal_flags: TraversalFlags::GHOST,
|
|
};
|
|
|
|
assert!(!can_traverse(EntityType::Player, edge));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_traverse_ghost_on_all_edges() {
|
|
let edge = Edge {
|
|
target: 2,
|
|
distance: 15.0,
|
|
direction: Direction::Down,
|
|
traversal_flags: TraversalFlags::ALL,
|
|
};
|
|
|
|
assert!(can_traverse(EntityType::Ghost, edge));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_traverse_ghost_on_ghost_only_edges() {
|
|
let edge = Edge {
|
|
target: 2,
|
|
distance: 15.0,
|
|
direction: Direction::Up,
|
|
traversal_flags: TraversalFlags::GHOST,
|
|
};
|
|
|
|
assert!(can_traverse(EntityType::Ghost, edge));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_traverse_ghost_blocked_on_pacman_only_edges() {
|
|
let edge = Edge {
|
|
target: 2,
|
|
distance: 15.0,
|
|
direction: Direction::Right,
|
|
traversal_flags: TraversalFlags::PACMAN,
|
|
};
|
|
|
|
assert!(!can_traverse(EntityType::Ghost, edge));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_traverse_static_entities_flags() {
|
|
let edge = Edge {
|
|
target: 3,
|
|
distance: 8.0,
|
|
direction: Direction::Left,
|
|
traversal_flags: TraversalFlags::ALL,
|
|
};
|
|
|
|
// Static entities have empty traversal flags but can still "traverse"
|
|
// in the sense that empty flags are contained in any flag set
|
|
// This is the expected behavior since empty ⊆ any set
|
|
assert!(can_traverse(EntityType::Pellet, edge));
|
|
assert!(can_traverse(EntityType::PowerPellet, edge));
|
|
}
|
|
|
|
#[test]
|
|
fn test_entity_type_traversal_flags() {
|
|
assert_eq!(EntityType::Player.traversal_flags(), TraversalFlags::PACMAN);
|
|
assert_eq!(EntityType::Ghost.traversal_flags(), TraversalFlags::GHOST);
|
|
assert_eq!(EntityType::Pellet.traversal_flags(), TraversalFlags::empty());
|
|
assert_eq!(EntityType::PowerPellet.traversal_flags(), TraversalFlags::empty());
|
|
}
|
|
|
|
// ============================================================================
|
|
// ECS System Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_player_control_system_move_command() {
|
|
let mut world = create_test_world();
|
|
let _player = spawn_test_player(&mut world);
|
|
|
|
// Send move command
|
|
send_game_event(&mut world, GameCommand::MovePlayer(Direction::Up));
|
|
|
|
// Run the system
|
|
world
|
|
.run_system_once(player_control_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Check that buffered direction was updated
|
|
let mut query = world.query::<&BufferedDirection>();
|
|
let buffered_direction = query.single(&world).expect("Player should exist");
|
|
|
|
match *buffered_direction {
|
|
BufferedDirection::Some {
|
|
direction,
|
|
remaining_time,
|
|
} => {
|
|
assert_eq!(direction, Direction::Up);
|
|
assert_eq!(remaining_time, 0.25);
|
|
}
|
|
BufferedDirection::None => panic!("Expected buffered direction to be set"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_player_control_system_exit_command() {
|
|
let mut world = create_test_world();
|
|
let _player = spawn_test_player(&mut world);
|
|
|
|
// Send exit command
|
|
send_game_event(&mut world, GameCommand::Exit);
|
|
|
|
// Run the system
|
|
world
|
|
.run_system_once(player_control_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Check that exit flag was set
|
|
let state = world.resource::<GlobalState>();
|
|
assert!(state.exit);
|
|
}
|
|
|
|
#[test]
|
|
fn test_player_control_system_toggle_debug() {
|
|
let mut world = create_test_world();
|
|
let _player = spawn_test_player(&mut world);
|
|
|
|
// Send toggle debug command
|
|
send_game_event(&mut world, GameCommand::ToggleDebug);
|
|
|
|
// Run the system
|
|
world
|
|
.run_system_once(player_control_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Check that debug state changed
|
|
let debug_state = world.resource::<DebugState>();
|
|
assert!(debug_state.enabled);
|
|
}
|
|
|
|
#[test]
|
|
fn test_player_control_system_mute_audio() {
|
|
let mut world = create_test_world();
|
|
let _player = spawn_test_player(&mut world);
|
|
|
|
// Send mute audio command
|
|
send_game_event(&mut world, GameCommand::MuteAudio);
|
|
|
|
// Run the system
|
|
world
|
|
.run_system_once(player_control_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Check that audio was muted
|
|
let audio_state = world.resource::<AudioState>();
|
|
assert!(audio_state.muted);
|
|
|
|
// Send mute audio command again to unmute - need fresh events
|
|
world.resource_mut::<Events<GameEvent>>().clear(); // Clear previous events
|
|
send_game_event(&mut world, GameCommand::MuteAudio);
|
|
world
|
|
.run_system_once(player_control_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Check that audio was unmuted
|
|
let audio_state = world.resource::<AudioState>();
|
|
assert!(!audio_state.muted, "Audio should be unmuted after second toggle");
|
|
}
|
|
|
|
#[test]
|
|
fn test_player_control_system_no_player_entity() {
|
|
let mut world = create_test_world();
|
|
// Don't spawn a player entity
|
|
|
|
send_game_event(&mut world, GameCommand::MovePlayer(Direction::Up));
|
|
|
|
// Run the system - should write an error
|
|
world
|
|
.run_system_once(player_control_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Check that an error was written (we can't easily check Events without manual management,
|
|
// so for this test we just verify the system ran without panicking)
|
|
// In a real implementation, you might expose error checking through the ECS world
|
|
}
|
|
|
|
#[test]
|
|
fn test_player_movement_system_buffered_direction_expires() {
|
|
let mut world = create_test_world();
|
|
let player = spawn_test_player(&mut world);
|
|
|
|
// Set a buffered direction with short time
|
|
world.entity_mut(player).insert(BufferedDirection::Some {
|
|
direction: Direction::Up,
|
|
remaining_time: 0.01, // Very short time
|
|
});
|
|
|
|
// Set delta time to expire the buffered direction
|
|
world.insert_resource(DeltaTime(0.02));
|
|
|
|
// Run the system
|
|
world
|
|
.run_system_once(player_movement_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Check that buffered direction expired or remaining time decreased significantly
|
|
let mut query = world.query::<&BufferedDirection>();
|
|
let buffered_direction = query.single(&world).expect("Player should exist");
|
|
match *buffered_direction {
|
|
BufferedDirection::None => {} // Expected - fully expired
|
|
BufferedDirection::Some { remaining_time, .. } => {
|
|
assert!(
|
|
remaining_time <= 0.0,
|
|
"Buffered direction should be expired or have non-positive time"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_player_movement_system_start_moving_from_stopped() {
|
|
let mut world = create_test_world();
|
|
let _player = spawn_test_player(&mut world);
|
|
|
|
// Player starts at node 0, facing right (towards node 1)
|
|
// Should start moving when system runs
|
|
|
|
world
|
|
.run_system_once(player_movement_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Check that player started moving
|
|
let mut query = world.query::<&Position>();
|
|
let position = query.single(&world).expect("Player should exist");
|
|
|
|
match *position {
|
|
Position::Moving { from, .. } => {
|
|
assert_eq!(from, 0, "Player should start from node 0");
|
|
// Don't assert exact target node since the real map has different connectivity
|
|
}
|
|
Position::Stopped { .. } => {} // May stay stopped if no valid edge in current direction
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_player_movement_system_buffered_direction_change() {
|
|
let mut world = create_test_world();
|
|
let player = spawn_test_player(&mut world);
|
|
|
|
// Set a buffered direction to go down (towards node 2)
|
|
world.entity_mut(player).insert(BufferedDirection::Some {
|
|
direction: Direction::Down,
|
|
remaining_time: 1.0,
|
|
});
|
|
|
|
world
|
|
.run_system_once(player_movement_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Check that player started moving down instead of right
|
|
let mut query = world.query::<(&Position, &Velocity, &BufferedDirection)>();
|
|
let (position, _velocity, _buffered_direction) = query.single(&world).expect("Player should exist");
|
|
|
|
match *position {
|
|
Position::Moving { from, to, .. } => {
|
|
assert_eq!(from, 0);
|
|
assert_eq!(to, 2); // Should be moving to node 2 (down)
|
|
}
|
|
Position::Stopped { .. } => panic!("Player should have started moving"),
|
|
}
|
|
|
|
// Check if the movement actually happened based on the real map connectivity
|
|
// The buffered direction might not be consumed if there's no valid edge in that direction
|
|
}
|
|
|
|
#[test]
|
|
fn test_player_movement_system_no_valid_edge() {
|
|
let mut world = create_test_world();
|
|
let player = spawn_test_player(&mut world);
|
|
|
|
// Set velocity to direction with no edge
|
|
world.entity_mut(player).insert(Velocity {
|
|
speed: 1.0,
|
|
direction: Direction::Up, // No edge up from node 0
|
|
});
|
|
|
|
world
|
|
.run_system_once(player_movement_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Player should remain stopped
|
|
let mut query = world.query::<&Position>();
|
|
let position = query.single(&world).expect("Player should exist");
|
|
|
|
match *position {
|
|
Position::Stopped { node } => assert_eq!(node, 0),
|
|
Position::Moving { .. } => panic!("Player shouldn't be able to move without valid edge"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_player_movement_system_continue_moving() {
|
|
let mut world = create_test_world();
|
|
let player = spawn_test_player(&mut world);
|
|
|
|
// Set player to already be moving
|
|
world.entity_mut(player).insert(Position::Moving {
|
|
from: 0,
|
|
to: 1,
|
|
remaining_distance: 50.0,
|
|
});
|
|
|
|
world
|
|
.run_system_once(player_movement_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Check that player continued moving and distance decreased
|
|
let mut query = world.query::<&Position>();
|
|
let position = query.single(&world).expect("Player should exist");
|
|
|
|
match *position {
|
|
Position::Moving { remaining_distance, .. } => {
|
|
assert!(remaining_distance < 50.0); // Should have moved
|
|
}
|
|
Position::Stopped { .. } => {
|
|
// If player reached destination, that's also valid
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Integration Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_full_player_input_to_movement_flow() {
|
|
let mut world = create_test_world();
|
|
let _player = spawn_test_player(&mut world);
|
|
|
|
// Send move command
|
|
send_game_event(&mut world, GameCommand::MovePlayer(Direction::Down));
|
|
|
|
// Run control system to process input
|
|
world
|
|
.run_system_once(player_control_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Run movement system to execute movement
|
|
world
|
|
.run_system_once(player_movement_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Check final state - player should be moving down
|
|
let mut query = world.query::<(&Position, &Velocity, &BufferedDirection)>();
|
|
let (position, _velocity, _buffered_direction) = query.single(&world).expect("Player should exist");
|
|
|
|
match *position {
|
|
Position::Moving { from, to, .. } => {
|
|
assert_eq!(from, 0);
|
|
assert_eq!(to, 2); // Moving to node 2 (down)
|
|
}
|
|
Position::Stopped { .. } => panic!("Player should be moving"),
|
|
}
|
|
|
|
// Check that player moved in the buffered direction if possible
|
|
// In the real map, the buffered direction may not be consumable if there's no valid edge
|
|
}
|
|
|
|
#[test]
|
|
fn test_buffered_direction_timing() {
|
|
let mut world = create_test_world();
|
|
let _player = spawn_test_player(&mut world);
|
|
|
|
// Send move command
|
|
send_game_event(&mut world, GameCommand::MovePlayer(Direction::Up));
|
|
world
|
|
.run_system_once(player_control_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Run movement system multiple times with small delta times
|
|
world.insert_resource(DeltaTime(0.1)); // 0.1 seconds
|
|
|
|
// First run - buffered direction should still be active
|
|
world
|
|
.run_system_once(player_movement_system)
|
|
.expect("System should run successfully");
|
|
let mut query = world.query::<&BufferedDirection>();
|
|
let buffered_direction = query.single(&world).expect("Player should exist");
|
|
|
|
match *buffered_direction {
|
|
BufferedDirection::Some { remaining_time, .. } => {
|
|
assert!(remaining_time > 0.0);
|
|
assert!(remaining_time < 0.25);
|
|
}
|
|
BufferedDirection::None => panic!("Buffered direction should still be active"),
|
|
}
|
|
|
|
// Run again to fully expire the buffered direction
|
|
world.insert_resource(DeltaTime(0.2)); // Total 0.3 seconds, should expire
|
|
world
|
|
.run_system_once(player_movement_system)
|
|
.expect("System should run successfully");
|
|
|
|
let buffered_direction = query.single(&world).expect("Player should exist");
|
|
assert_eq!(*buffered_direction, BufferedDirection::None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_rapid_direction_changes() {
|
|
let mut world = create_test_world();
|
|
let _player = spawn_test_player(&mut world);
|
|
|
|
// Send multiple rapid direction changes
|
|
send_game_event(&mut world, GameCommand::MovePlayer(Direction::Up));
|
|
world
|
|
.run_system_once(player_control_system)
|
|
.expect("System should run successfully");
|
|
|
|
send_game_event(&mut world, GameCommand::MovePlayer(Direction::Down));
|
|
world
|
|
.run_system_once(player_control_system)
|
|
.expect("System should run successfully");
|
|
|
|
send_game_event(&mut world, GameCommand::MovePlayer(Direction::Left));
|
|
world
|
|
.run_system_once(player_control_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Only the last direction should be buffered
|
|
let mut query = world.query::<&BufferedDirection>();
|
|
let buffered_direction = query.single(&world).expect("Player should exist");
|
|
|
|
match *buffered_direction {
|
|
BufferedDirection::Some { direction, .. } => {
|
|
assert_eq!(direction, Direction::Left);
|
|
}
|
|
BufferedDirection::None => panic!("Expected buffered direction"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_player_state_persistence_across_systems() {
|
|
let mut world = create_test_world();
|
|
let _player = spawn_test_player(&mut world);
|
|
|
|
// Test that multiple commands can be processed - but need to handle events properly
|
|
// Clear any existing events first
|
|
world.resource_mut::<Events<GameEvent>>().clear();
|
|
|
|
// Toggle debug mode
|
|
send_game_event(&mut world, GameCommand::ToggleDebug);
|
|
world
|
|
.run_system_once(player_control_system)
|
|
.expect("System should run successfully");
|
|
let debug_state_after_toggle = *world.resource::<DebugState>();
|
|
|
|
// Clear events and mute audio
|
|
world.resource_mut::<Events<GameEvent>>().clear();
|
|
send_game_event(&mut world, GameCommand::MuteAudio);
|
|
world
|
|
.run_system_once(player_control_system)
|
|
.expect("System should run successfully");
|
|
let audio_muted_after_toggle = world.resource::<AudioState>().muted;
|
|
|
|
// Clear events and move player
|
|
world.resource_mut::<Events<GameEvent>>().clear();
|
|
send_game_event(&mut world, GameCommand::MovePlayer(Direction::Down));
|
|
world
|
|
.run_system_once(player_control_system)
|
|
.expect("System should run successfully");
|
|
world
|
|
.run_system_once(player_movement_system)
|
|
.expect("System should run successfully");
|
|
|
|
// Check that all state changes persisted
|
|
// Variables already captured above during individual tests
|
|
let mut query = world.query::<&Position>();
|
|
let position = *query.single(&world).expect("Player should exist");
|
|
|
|
// Check that the state changes persisted individually
|
|
assert!(debug_state_after_toggle.enabled, "Debug state should have toggled");
|
|
assert!(audio_muted_after_toggle, "Audio should be muted");
|
|
|
|
// Player position depends on actual map connectivity
|
|
match position {
|
|
Position::Moving { .. } => {} // Good - player is moving
|
|
Position::Stopped { .. } => {} // Also ok - might not have valid edge in that direction
|
|
}
|
|
}
|