Compare commits

...

3 Commits

Author SHA1 Message Date
Ryan Walters
968eb39b64 feat: fix emscripten browser logging, streamline console initialization and logging 2025-09-04 14:07:24 -05:00
Ryan Walters
0759019c8b fix: allow Window events, allows proper logical canvas resizing
You have no idea how much pain this has been causing me.
2025-09-04 13:26:08 -05:00
Ryan Walters
17188df729 refactor(test): remove dead code and consolidate test utilities 2025-09-04 11:53:29 -05:00
21 changed files with 330 additions and 580 deletions

View File

@@ -3,7 +3,7 @@ set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
# Regex to exclude files from coverage report, double escapes for Justfile + CLI
# You can use src\\\\..., but the filename alone is acceptable too
coverage_exclude_pattern := "src\\\\app.rs|audio.rs|src\\\\error.rs|platform\\\\emscripten.rs"
coverage_exclude_pattern := "src\\\\app\\.rs|audio\\.rs|src\\\\error\\.rs|platform\\\\emscripten\\.rs|bin\\\\.+\\.rs|main\\.rs|platform\\\\desktop\\.rs|platform\\\\tracing_buffer\\.rs|platform\\\\buffered_writer\\.rs|systems\\\\debug\\.rs|systems\\\\profiling\\.rs"
binary_extension := if os() == "windows" { ".exe" } else { "" }

View File

@@ -128,7 +128,7 @@ impl Game {
EventType::TextInput,
EventType::TextEditing,
EventType::Display,
EventType::Window,
// EventType::Window,
EventType::MouseWheel,
// EventType::MouseMotion,
// EventType::MouseButtonDown, // Enable for desktop touch testing

View File

@@ -2,7 +2,7 @@
#![windows_subsystem = "windows"]
use crate::{app::App, constants::LOOP_TIME};
use tracing::{debug, info, warn};
use tracing::info;
mod app;
mod asset;
@@ -22,19 +22,9 @@ mod texture;
/// This function initializes SDL, the window, the game state, and then enters
/// the main game loop.
pub fn main() {
if platform::requires_console() {
// Setup buffered tracing subscriber that will buffer logs until console is ready
let switchable_writer = platform::tracing_buffer::setup_switchable_subscriber();
// Initialize platform-specific console
platform::init_console().expect("Could not initialize console");
// Now that console is initialized, flush buffered logs and switch to direct output
debug!("Switching to direct logging mode and flushing buffer...");
if let Err(error) = switchable_writer.switch_to_direct_mode() {
warn!("Failed to flush buffered logs to console: {error:?}");
}
}
// On Windows, this connects output streams to the console dynamically
// On Emscripten, this connects the subscriber to the browser console
platform::init_console().expect("Could not initialize console");
let mut app = App::new().expect("Could not create app");

View File

@@ -20,9 +20,13 @@ pub fn sleep(duration: Duration, focused: bool) {
pub fn init_console() -> Result<(), PlatformError> {
#[cfg(windows)]
{
use crate::platform::tracing_buffer::setup_switchable_subscriber;
use tracing::{debug, info};
use windows::Win32::System::Console::GetConsoleWindow;
// Setup buffered tracing subscriber that will buffer logs until console is ready
let switchable_writer = setup_switchable_subscriber();
// Check if we already have a console window
if unsafe { !GetConsoleWindow().0.is_null() } {
debug!("Already have a console window");
@@ -40,15 +44,19 @@ pub fn init_console() -> Result<(), PlatformError> {
attach_to_parent_console()?;
info!("Successfully attached to parent console");
}
// Now that console is initialized, flush buffered logs and switch to direct output
debug!("Switching to direct logging mode and flushing buffer...");
if let Err(error) = switchable_writer.switch_to_direct_mode() {
use tracing::warn;
warn!("Failed to flush buffered logs to console: {error:?}");
}
}
Ok(())
}
pub fn requires_console() -> bool {
cfg!(windows)
}
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
match asset {
Asset::Wav1 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/1.ogg"))),

View File

@@ -1,18 +1,21 @@
//! Emscripten platform implementation.
use std::borrow::Cow;
use std::time::Duration;
use crate::asset::Asset;
use crate::error::{AssetError, PlatformError};
use rand::{rngs::SmallRng, SeedableRng};
use sdl2::rwops::RWops;
use std::borrow::Cow;
use std::ffi::CString;
use std::io::{self, Read, Write};
use std::time::Duration;
// Emscripten FFI functions
#[allow(dead_code)]
extern "C" {
fn emscripten_get_now() -> f64;
fn emscripten_sleep(ms: u32);
fn emscripten_get_element_css_size(target: *const u8, width: *mut f64, height: *mut f64) -> i32;
// Standard C functions that Emscripten redirects to console
fn printf(format: *const u8, ...) -> i32;
}
pub fn sleep(duration: Duration, _focused: bool) {
@@ -22,11 +25,44 @@ pub fn sleep(duration: Duration, _focused: bool) {
}
pub fn init_console() -> Result<(), PlatformError> {
Ok(()) // No-op for Emscripten
use tracing_subscriber::{fmt, layer::SubscriberExt, EnvFilter};
// Set up a custom tracing subscriber that writes directly to emscripten console
let subscriber = tracing_subscriber::registry()
.with(
fmt::layer()
.with_writer(|| EmscriptenConsoleWriter)
.with_ansi(false)
.without_time()
.with_target(false),
)
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug")));
tracing::subscriber::set_global_default(subscriber)
.map_err(|e| PlatformError::ConsoleInit(format!("Failed to set tracing subscriber: {}", e)))?;
Ok(())
}
pub fn requires_console() -> bool {
false
/// A writer that outputs to the browser console via printf (redirected by emscripten)
struct EmscriptenConsoleWriter;
impl Write for EmscriptenConsoleWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if let Ok(s) = std::str::from_utf8(buf) {
if let Ok(cstr) = CString::new(s.trim_end_matches('\n')) {
let format_str = CString::new("%s\n").unwrap();
unsafe {
printf(format_str.as_ptr().cast(), cstr.as_ptr());
}
}
}
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[allow(dead_code)]
@@ -44,18 +80,13 @@ pub fn get_canvas_size() -> Option<(u32, u32)> {
}
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
use sdl2::rwops::RWops;
use std::io::Read;
let path = format!("assets/game/{}", asset.path());
let mut rwops = RWops::from_file(&path, "rb").map_err(|_| AssetError::NotFound(asset.path().to_string()))?;
let len = rwops.len().ok_or_else(|| AssetError::NotFound(asset.path().to_string()))?;
let mut buf = vec![0u8; len];
rwops
.read_exact(&mut buf)
.map_err(|e| AssetError::Io(std::io::Error::other(e)))?;
rwops.read_exact(&mut buf).map_err(|e| AssetError::Io(io::Error::other(e)))?;
Ok(Cow::Owned(buf))
}

View File

@@ -1,10 +1,12 @@
//! Platform abstraction layer for cross-platform functionality.
pub mod buffered_writer;
pub mod tracing_buffer;
#[cfg(not(target_os = "emscripten"))]
pub mod buffered_writer;
#[cfg(not(target_os = "emscripten"))]
mod desktop;
#[cfg(not(target_os = "emscripten"))]
pub mod tracing_buffer;
#[cfg(not(target_os = "emscripten"))]
pub use desktop::*;
#[cfg(target_os = "emscripten")]

View File

@@ -293,6 +293,9 @@ pub fn input_system(
simple_key_events.push(SimpleKeyEvent::KeyUp(key));
}
}
Event::RenderTargetsReset { .. } | Event::Window { .. } => {
// No-op
}
_ => {
tracing::warn!("Unhandled event, consider disabling: {:?}", event);
}

View File

@@ -1,57 +0,0 @@
// use glam::U16Vec2;
// use pacman::error::{AnimatedTextureError, GameError, TextureError};
// use pacman::texture::sprite::AtlasTile;
// use sdl2::pixels::Color;
// use smallvec::smallvec;
// fn mock_atlas_tile(id: u32) -> AtlasTile {
// AtlasTile {
// pos: U16Vec2::new(0, 0),
// size: U16Vec2::new(16, 16),
// color: Some(Color::RGB(id as u8, 0, 0)),
// }
// }
// #[test]
// fn test_animated_texture_creation_errors() {
// let tiles = smallvec![mock_atlas_tile(1), mock_atlas_tile(2)];
// assert!(matches!(
// AnimatedTexture::new(tiles.clone(), 0).unwrap_err(),
// GameError::Texture(TextureError::Animated(AnimatedTextureError::InvalidFrameDuration(0)))
// ));
// }
// #[test]
// fn test_animated_texture_advancement() {
// let tiles = smallvec![mock_atlas_tile(1), mock_atlas_tile(2), mock_atlas_tile(3)];
// let mut texture = AnimatedTexture::new(tiles, 10).unwrap();
// assert_eq!(texture.current_frame(), 0);
// texture.tick(25);
// assert_eq!(texture.current_frame(), 2);
// assert_eq!(texture.time_bank(), 5);
// }
// #[test]
// fn test_animated_texture_wrap_around() {
// let tiles = smallvec![mock_atlas_tile(1), mock_atlas_tile(2)];
// let mut texture = AnimatedTexture::new(tiles, 10).unwrap();
// texture.tick(10);
// assert_eq!(texture.current_frame(), 1);
// texture.tick(10);
// assert_eq!(texture.current_frame(), 0);
// }
// #[test]
// fn test_animated_texture_single_frame() {
// let tiles = smallvec![mock_atlas_tile(1)];
// let mut texture = AnimatedTexture::new(tiles, 10).unwrap();
// texture.tick(10);
// assert_eq!(texture.current_frame(), 0);
// assert_eq!(texture.current_tile().color.unwrap().r, 1);
// }

View File

@@ -1,19 +1,10 @@
use glam::U16Vec2;
use pacman::texture::blinking::BlinkingTexture;
use pacman::texture::sprite::AtlasTile;
use sdl2::pixels::Color;
fn mock_atlas_tile(id: u32) -> AtlasTile {
AtlasTile {
pos: U16Vec2::new(0, 0),
size: U16Vec2::new(16, 16),
color: Some(Color::RGB(id as u8, 0, 0)),
}
}
mod common;
#[test]
fn test_blinking_texture() {
let tile = mock_atlas_tile(1);
let tile = common::mock_atlas_tile(1);
let mut texture = BlinkingTexture::new(tile, 0.5);
assert!(texture.is_on());
@@ -30,7 +21,7 @@ fn test_blinking_texture() {
#[test]
fn test_blinking_texture_partial_duration() {
let tile = mock_atlas_tile(1);
let tile = common::mock_atlas_tile(1);
let mut texture = BlinkingTexture::new(tile, 0.5);
texture.tick(0.625);
@@ -40,7 +31,7 @@ fn test_blinking_texture_partial_duration() {
#[test]
fn test_blinking_texture_negative_time() {
let tile = mock_atlas_tile(1);
let tile = common::mock_atlas_tile(1);
let mut texture = BlinkingTexture::new(tile, 0.5);
texture.tick(-0.1);

View File

@@ -1,73 +1,7 @@
use bevy_ecs::{entity::Entity, event::Events, system::RunSystemOnce, world::World};
use bevy_ecs::system::RunSystemOnce;
use pacman::systems::{check_collision, collision_system, Collider, EntityType, GhostState, Position};
use pacman::{
error::GameError,
events::GameEvent,
map::builder::Map,
systems::{
check_collision, collision_system, Collider, EntityType, Ghost, GhostCollider, ItemCollider, NodeId, PacmanCollider,
Position,
},
};
fn create_test_world() -> World {
let mut world = World::new();
// Add required resources
world.insert_resource(Events::<GameEvent>::default());
world.insert_resource(Events::<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 }, Collider { size: 10.0 }, PacmanCollider))
.id()
}
fn spawn_test_item(world: &mut World) -> Entity {
world
.spawn((
Position::Stopped { node: 0 },
Collider { size: 8.0 },
ItemCollider,
EntityType::Pellet,
))
.id()
}
fn spawn_test_ghost(world: &mut World) -> Entity {
world
.spawn((
Position::Stopped { node: 0 },
Collider { size: 12.0 },
GhostCollider,
Ghost::Blinky,
EntityType::Ghost,
))
.id()
}
fn spawn_test_ghost_at_node(world: &mut World, node: usize) -> Entity {
world
.spawn((
Position::Stopped { node: node as NodeId },
Collider { size: 12.0 },
GhostCollider,
Ghost::Blinky,
EntityType::Ghost,
))
.id()
}
mod common;
#[test]
fn test_collider_collision_detection() {
@@ -81,7 +15,7 @@ fn test_collider_collision_detection() {
#[test]
fn test_check_collision_helper() {
let map = create_test_map();
let map = common::create_test_map();
let pos1 = Position::Stopped { node: 0 };
let pos2 = Position::Stopped { node: 0 }; // Same position
let collider1 = Collider { size: 10.0 };
@@ -101,9 +35,9 @@ fn test_check_collision_helper() {
#[test]
fn test_collision_system_pacman_item() {
let mut world = create_test_world();
let _pacman = spawn_test_pacman(&mut world);
let _item = spawn_test_item(&mut world);
let mut world = common::create_test_world();
let _pacman = common::spawn_test_pacman(&mut world, 0);
let _item = common::spawn_test_item(&mut world, 0, EntityType::Pellet);
// Run collision system - should not panic
world
@@ -113,9 +47,9 @@ fn test_collision_system_pacman_item() {
#[test]
fn test_collision_system_pacman_ghost() {
let mut world = create_test_world();
let _pacman = spawn_test_pacman(&mut world);
let _ghost = spawn_test_ghost(&mut world);
let mut world = common::create_test_world();
let _pacman = common::spawn_test_pacman(&mut world, 0);
let _ghost = common::spawn_test_ghost(&mut world, 0, GhostState::Normal);
// Run collision system - should not panic
world
@@ -125,9 +59,9 @@ fn test_collision_system_pacman_ghost() {
#[test]
fn test_collision_system_no_collision() {
let mut world = create_test_world();
let _pacman = spawn_test_pacman(&mut world);
let _ghost = spawn_test_ghost_at_node(&mut world, 1); // Different node
let mut world = common::create_test_world();
let _pacman = common::spawn_test_pacman(&mut world, 0);
let _ghost = common::spawn_test_ghost(&mut world, 1, GhostState::Normal); // Different node
// Run collision system - should not panic
world
@@ -137,10 +71,10 @@ fn test_collision_system_no_collision() {
#[test]
fn test_collision_system_multiple_entities() {
let mut world = create_test_world();
let _pacman = spawn_test_pacman(&mut world);
let _item = spawn_test_item(&mut world);
let _ghost = spawn_test_ghost(&mut world);
let mut world = common::create_test_world();
let _pacman = common::spawn_test_pacman(&mut world, 0);
let _item = common::spawn_test_item(&mut world, 0, EntityType::Pellet);
let _ghost = common::spawn_test_ghost(&mut world, 0, GhostState::Normal);
// Run collision system - should not panic
world

View File

@@ -1,12 +1,26 @@
#![allow(dead_code)]
use bevy_ecs::{entity::Entity, event::Events, world::World};
use glam::{U16Vec2, Vec2};
use pacman::{
asset::{get_asset_bytes, Asset},
constants::RAW_BOARD,
events::GameEvent,
game::ATLAS_FRAMES,
texture::sprite::{AtlasMapper, SpriteAtlas},
map::{
builder::Map,
direction::Direction,
graph::{Graph, Node},
},
systems::{
AudioEvent, AudioState, BufferedDirection, Collider, DebugState, DeltaTime, EntityType, Ghost, GhostCollider, GhostState,
GlobalState, ItemCollider, MovementModifiers, PacmanCollider, PlayerControlled, Position, ScoreResource, Velocity,
},
texture::sprite::{AtlasMapper, AtlasTile, SpriteAtlas},
};
use sdl2::{
image::LoadTexture,
pixels::Color,
render::{Canvas, TextureCreator},
video::{Window, WindowContext},
Sdl,
@@ -38,3 +52,122 @@ pub fn create_atlas(canvas: &mut sdl2::render::Canvas<sdl2::video::Window>) -> S
SpriteAtlas::new(texture, atlas_mapper)
}
/// Creates a simple test graph with 3 connected nodes for testing
pub fn create_test_graph() -> Graph {
let mut graph = Graph::new();
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),
});
graph.connect(node0, node1, false, None, Direction::Right).unwrap();
graph.connect(node0, node2, false, None, Direction::Down).unwrap();
graph
}
/// Creates a basic test world with required resources for ECS systems
pub fn create_test_world() -> World {
let mut world = World::new();
// Add required resources
world.insert_resource(Events::<GameEvent>::default());
world.insert_resource(Events::<pacman::error::GameError>::default());
world.insert_resource(Events::<AudioEvent>::default());
world.insert_resource(ScoreResource(0));
world.insert_resource(AudioState::default());
world.insert_resource(GlobalState { exit: false });
world.insert_resource(DebugState::default());
world.insert_resource(DeltaTime(1.0 / 60.0)); // 60 FPS
world.insert_resource(create_test_map());
world
}
/// Creates a test map using the default RAW_BOARD
pub fn create_test_map() -> Map {
Map::new(RAW_BOARD).expect("Failed to create test map")
}
/// Spawns a test Pac-Man entity at the specified node
pub fn spawn_test_pacman(world: &mut World, node: usize) -> Entity {
world
.spawn((
Position::Stopped { node: node as u16 },
Collider { size: 10.0 },
PacmanCollider,
EntityType::Player,
))
.id()
}
/// Spawns a controllable test player entity
pub fn spawn_test_player(world: &mut World, node: usize) -> Entity {
world
.spawn((
PlayerControlled,
Position::Stopped { node: node as u16 },
Velocity {
speed: 1.0,
direction: Direction::Right,
},
BufferedDirection::None,
EntityType::Player,
MovementModifiers::default(),
))
.id()
}
/// Spawns a test item entity at the specified node
pub fn spawn_test_item(world: &mut World, node: usize, item_type: EntityType) -> Entity {
world
.spawn((
Position::Stopped { node: node as u16 },
Collider { size: 8.0 },
ItemCollider,
item_type,
))
.id()
}
/// Spawns a test ghost entity at the specified node
pub fn spawn_test_ghost(world: &mut World, node: usize, ghost_state: GhostState) -> Entity {
world
.spawn((
Position::Stopped { node: node as u16 },
Collider { size: 12.0 },
GhostCollider,
Ghost::Blinky,
EntityType::Ghost,
ghost_state,
))
.id()
}
/// Sends a game event to the world
pub fn send_game_event(world: &mut World, event: GameEvent) {
let mut events = world.resource_mut::<Events<GameEvent>>();
events.send(event);
}
/// Sends a collision event between two entities
pub 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));
}
/// Creates a mock atlas tile for testing
pub fn mock_atlas_tile(id: u32) -> AtlasTile {
AtlasTile {
pos: U16Vec2::new(0, 0),
size: U16Vec2::new(16, 16),
color: Some(Color::RGB(id as u8, 0, 0)),
}
}

View File

@@ -1,19 +0,0 @@
use pacman::events::{GameCommand, GameEvent};
use pacman::map::direction::Direction;
#[test]
fn test_game_command_to_game_event_conversion_all_variants() {
let commands = vec![
GameCommand::Exit,
GameCommand::MovePlayer(Direction::Up),
GameCommand::ToggleDebug,
GameCommand::MuteAudio,
GameCommand::ResetLevel,
GameCommand::TogglePause,
];
for command in commands {
let event: GameEvent = command.into();
assert_eq!(event, GameEvent::Command(command));
}
}

View File

@@ -1,23 +1,7 @@
use pacman::map::direction::Direction;
use pacman::map::graph::{Graph, Node, TraversalFlags};
fn create_test_graph() -> Graph {
let mut graph = Graph::new();
let node1 = graph.add_node(Node {
position: glam::Vec2::new(0.0, 0.0),
});
let node2 = graph.add_node(Node {
position: glam::Vec2::new(16.0, 0.0),
});
let node3 = graph.add_node(Node {
position: glam::Vec2::new(0.0, 16.0),
});
graph.connect(node1, node2, false, None, Direction::Right).unwrap();
graph.connect(node1, node3, false, None, Direction::Down).unwrap();
graph
}
mod common;
#[test]
fn test_graph_basic_operations() {
@@ -124,14 +108,14 @@ fn should_error_on_negative_edge_distance() {
#[test]
fn should_error_on_duplicate_edge_without_replace() {
let mut graph = create_test_graph();
let mut graph = common::create_test_graph();
let result = graph.add_edge(0, 1, false, None, Direction::Right, TraversalFlags::ALL);
assert!(result.is_err());
}
#[test]
fn should_allow_replacing_an_edge() {
let mut graph = create_test_graph();
let mut graph = common::create_test_graph();
let result = graph.add_edge(0, 1, true, Some(42.0), Direction::Right, TraversalFlags::ALL);
assert!(result.is_ok());
@@ -141,7 +125,7 @@ fn should_allow_replacing_an_edge() {
#[test]
fn should_find_edge_between_nodes() {
let graph = create_test_graph();
let graph = common::create_test_graph();
let edge = graph.find_edge(0, 1);
assert!(edge.is_some());
assert_eq!(edge.unwrap().target, 1);

View File

@@ -1,26 +0,0 @@
use bevy_ecs::{event::Events, world::World};
use pacman::{error::GameError, systems::components::ScoreResource};
fn create_test_world() -> World {
let mut world = World::new();
// Add required resources
world.insert_resource(Events::<GameError>::default());
world.insert_resource(ScoreResource(1230)); // Test score
world
}
#[test]
fn test_hud_render_system_runs_without_error() {
let world = create_test_world();
// The HUD render system requires SDL2 resources that aren't available in tests,
// but we can at least verify it doesn't panic when called
// In a real test environment, we'd need to mock the SDL2 canvas and atlas
// For now, just verify the score resource is accessible
let score = world.resource::<ScoreResource>();
assert_eq!(score.0, 1230);
}

View File

@@ -1,13 +1,7 @@
use bevy_ecs::{entity::Entity, event::Events, system::RunSystemOnce, world::World};
use bevy_ecs::{entity::Entity, system::RunSystemOnce};
use pacman::systems::{is_valid_item_collision, item_system, EntityType, GhostState, Position, ScoreResource};
use pacman::{
events::GameEvent,
map::builder::Map,
systems::{
is_valid_item_collision, item_system, AudioEvent, AudioState, EntityType, Ghost, GhostCollider, GhostState, ItemCollider,
PacmanCollider, Position, ScoreResource,
},
};
mod common;
#[test]
fn test_calculate_score_for_item() {
@@ -44,62 +38,14 @@ fn test_is_valid_item_collision() {
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 spawn_test_ghost(world: &mut World, ghost_state: GhostState) -> Entity {
world
.spawn((
Position::Stopped { node: 2 },
Ghost::Blinky,
EntityType::Ghost,
GhostCollider,
ghost_state,
))
.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]
fn test_item_system_pellet_collection() {
let mut world = create_test_world();
let pacman = spawn_test_pacman(&mut world);
let pellet = spawn_test_item(&mut world, EntityType::Pellet);
let mut world = common::create_test_world();
let pacman = common::spawn_test_pacman(&mut world, 0);
let pellet = common::spawn_test_item(&mut world, 1, EntityType::Pellet);
// Send collision event
send_collision_event(&mut world, pacman, pellet);
common::send_collision_event(&mut world, pacman, pellet);
// Run the item system
world.run_system_once(item_system).expect("System should run successfully");
@@ -119,11 +65,11 @@ fn test_item_system_pellet_collection() {
#[test]
fn test_item_system_power_pellet_collection() {
let mut world = create_test_world();
let pacman = spawn_test_pacman(&mut world);
let power_pellet = spawn_test_item(&mut world, EntityType::PowerPellet);
let mut world = common::create_test_world();
let pacman = common::spawn_test_pacman(&mut world, 0);
let power_pellet = common::spawn_test_item(&mut world, 1, EntityType::PowerPellet);
send_collision_event(&mut world, pacman, power_pellet);
common::send_collision_event(&mut world, pacman, power_pellet);
world.run_system_once(item_system).expect("System should run successfully");
@@ -142,16 +88,16 @@ fn test_item_system_power_pellet_collection() {
#[test]
fn test_item_system_multiple_collections() {
let mut world = create_test_world();
let pacman = spawn_test_pacman(&mut world);
let pellet1 = spawn_test_item(&mut world, EntityType::Pellet);
let pellet2 = spawn_test_item(&mut world, EntityType::Pellet);
let power_pellet = spawn_test_item(&mut world, EntityType::PowerPellet);
let mut world = common::create_test_world();
let pacman = common::spawn_test_pacman(&mut world, 0);
let pellet1 = common::spawn_test_item(&mut world, 1, EntityType::Pellet);
let pellet2 = common::spawn_test_item(&mut world, 2, EntityType::Pellet);
let power_pellet = common::spawn_test_item(&mut world, 3, 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);
common::send_collision_event(&mut world, pacman, pellet1);
common::send_collision_event(&mut world, pacman, pellet2);
common::send_collision_event(&mut world, pacman, power_pellet);
world.run_system_once(item_system).expect("System should run successfully");
@@ -176,8 +122,8 @@ fn test_item_system_multiple_collections() {
#[test]
fn test_item_system_ignores_non_item_collisions() {
let mut world = create_test_world();
let pacman = spawn_test_pacman(&mut world);
let mut world = common::create_test_world();
let pacman = common::spawn_test_pacman(&mut world, 0);
// Create a ghost entity (not an item)
let ghost = world.spawn((Position::Stopped { node: 2 }, EntityType::Ghost)).id();
@@ -186,7 +132,7 @@ fn test_item_system_ignores_non_item_collisions() {
let initial_score = world.resource::<ScoreResource>().0;
// Send collision event between pacman and ghost
send_collision_event(&mut world, pacman, ghost);
common::send_collision_event(&mut world, pacman, ghost);
world.run_system_once(item_system).expect("System should run successfully");
@@ -205,9 +151,9 @@ fn test_item_system_ignores_non_item_collisions() {
#[test]
fn test_item_system_no_collision_events() {
let mut world = create_test_world();
let _pacman = spawn_test_pacman(&mut world);
let _pellet = spawn_test_item(&mut world, EntityType::Pellet);
let mut world = common::create_test_world();
let _pacman = common::spawn_test_pacman(&mut world, 0);
let _pellet = common::spawn_test_item(&mut world, 1, EntityType::Pellet);
let initial_score = world.resource::<ScoreResource>().0;
@@ -227,13 +173,13 @@ fn test_item_system_no_collision_events() {
#[test]
fn test_item_system_collision_with_missing_entity() {
let mut world = create_test_world();
let pacman = spawn_test_pacman(&mut world);
let mut world = common::create_test_world();
let pacman = common::spawn_test_pacman(&mut world, 0);
// Create a fake entity ID that doesn't exist
let fake_entity = Entity::from_raw(999);
send_collision_event(&mut world, pacman, fake_entity);
common::send_collision_event(&mut world, pacman, fake_entity);
// System should handle gracefully and not crash
world
@@ -247,15 +193,15 @@ fn test_item_system_collision_with_missing_entity() {
#[test]
fn test_item_system_preserves_existing_score() {
let mut world = create_test_world();
let mut world = common::create_test_world();
// Set initial score
world.insert_resource(ScoreResource(100));
let pacman = spawn_test_pacman(&mut world);
let pellet = spawn_test_item(&mut world, EntityType::Pellet);
let pacman = common::spawn_test_pacman(&mut world, 0);
let pellet = common::spawn_test_item(&mut world, 1, EntityType::Pellet);
send_collision_event(&mut world, pacman, pellet);
common::send_collision_event(&mut world, pacman, pellet);
world.run_system_once(item_system).expect("System should run successfully");
@@ -266,17 +212,17 @@ fn test_item_system_preserves_existing_score() {
#[test]
fn test_power_pellet_does_not_affect_ghosts_in_eyes_state() {
let mut world = create_test_world();
let pacman = spawn_test_pacman(&mut world);
let power_pellet = spawn_test_item(&mut world, EntityType::PowerPellet);
let mut world = common::create_test_world();
let pacman = common::spawn_test_pacman(&mut world, 0);
let power_pellet = common::spawn_test_item(&mut world, 1, EntityType::PowerPellet);
// Spawn a ghost in Eyes state (returning to ghost house)
let eyes_ghost = spawn_test_ghost(&mut world, GhostState::Eyes);
let eyes_ghost = common::spawn_test_ghost(&mut world, 2, GhostState::Eyes);
// Spawn a ghost in Normal state
let normal_ghost = spawn_test_ghost(&mut world, GhostState::Normal);
let normal_ghost = common::spawn_test_ghost(&mut world, 3, GhostState::Normal);
send_collision_event(&mut world, pacman, power_pellet);
common::send_collision_event(&mut world, pacman, power_pellet);
world.run_system_once(item_system).expect("System should run successfully");

View File

@@ -34,61 +34,3 @@ fn test_map_node_positions() {
assert_eq!(node.position, expected_pos);
}
}
// #[test]
// fn test_generate_items() {
// use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
// use std::collections::HashMap;
// let map = Map::new(RAW_BOARD).unwrap();
// // Create a minimal atlas for testing
// let mut frames = HashMap::new();
// frames.insert(
// "maze/pellet.png".to_string(),
// MapperFrame {
// x: 0,
// y: 0,
// width: 8,
// height: 8,
// },
// );
// frames.insert(
// "maze/energizer.png".to_string(),
// MapperFrame {
// x: 8,
// y: 0,
// width: 8,
// height: 8,
// },
// );
// let mapper = AtlasMapper { frames };
// let texture = unsafe { std::mem::transmute::<usize, Texture<'static>>(0usize) };
// let atlas = SpriteAtlas::new(texture, mapper);
// let items = map.generate_items(&atlas).unwrap();
// // Verify we have items
// assert!(!items.is_empty());
// // Count different types
// let pellet_count = items
// .iter()
// .filter(|item| matches!(item.item_type, pacman::entity::item::ItemType::Pellet))
// .count();
// let energizer_count = items
// .iter()
// .filter(|item| matches!(item.item_type, pacman::entity::item::ItemType::Energizer))
// .count();
// // Should have both types
// assert_eq!(pellet_count, 240);
// assert_eq!(energizer_count, 4);
// // All items should be uncollected initially
// assert!(items.iter().all(|item| !item.is_collected()));
// // All items should have valid node indices
// assert!(items.iter().all(|item| item.node_index < map.graph.node_count()));
// }

View File

@@ -1,28 +1,8 @@
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
}
mod common;
#[test]
fn test_position_is_at_node() {
@@ -127,7 +107,7 @@ fn test_position_tick_overshoot_with_overflow() {
#[test]
fn test_position_get_pixel_position_stopped() {
let graph = create_test_graph();
let graph = common::create_test_graph();
let pos = Position::Stopped { node: 0 };
let pixel_pos = pos.get_pixel_position(&graph).unwrap();
@@ -141,7 +121,7 @@ fn test_position_get_pixel_position_stopped() {
#[test]
fn test_position_get_pixel_position_moving() {
let graph = create_test_graph();
let graph = common::create_test_graph();
let pos = Position::Moving {
from: 0,
to: 1,

View File

@@ -1,63 +1,17 @@
use bevy_ecs::{entity::Entity, event::Events, system::RunSystemOnce, world::World};
use bevy_ecs::{event::Events, system::RunSystemOnce};
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, MovementModifiers, PlayerControlled, Position, Velocity,
EntityType, GlobalState, 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,
MovementModifiers::default(),
))
.id()
}
fn send_game_event(world: &mut World, command: GameCommand) {
let mut events = world.resource_mut::<Events<GameEvent>>();
events.send(GameEvent::Command(command));
}
mod common;
#[test]
fn test_can_traverse_player_on_all_edges() {
@@ -155,17 +109,13 @@ fn test_entity_type_traversal_flags() {
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);
let mut world = common::create_test_world();
let _player = common::spawn_test_player(&mut world, 0);
// Send move command
send_game_event(&mut world, GameCommand::MovePlayer(Direction::Up));
common::send_game_event(&mut world, GameEvent::Command(GameCommand::MovePlayer(Direction::Up)));
// Run the system
world
@@ -190,11 +140,11 @@ fn test_player_control_system_move_command() {
#[test]
fn test_player_control_system_exit_command() {
let mut world = create_test_world();
let _player = spawn_test_player(&mut world);
let mut world = common::create_test_world();
let _player = common::spawn_test_player(&mut world, 0);
// Send exit command
send_game_event(&mut world, GameCommand::Exit);
common::send_game_event(&mut world, GameEvent::Command(GameCommand::Exit));
// Run the system
world
@@ -208,11 +158,11 @@ fn test_player_control_system_exit_command() {
#[test]
fn test_player_control_system_toggle_debug() {
let mut world = create_test_world();
let _player = spawn_test_player(&mut world);
let mut world = common::create_test_world();
let _player = common::spawn_test_player(&mut world, 0);
// Send toggle debug command
send_game_event(&mut world, GameCommand::ToggleDebug);
common::send_game_event(&mut world, GameEvent::Command(GameCommand::ToggleDebug));
// Run the system
world
@@ -226,11 +176,11 @@ fn test_player_control_system_toggle_debug() {
#[test]
fn test_player_control_system_mute_audio() {
let mut world = create_test_world();
let _player = spawn_test_player(&mut world);
let mut world = common::create_test_world();
let _player = common::spawn_test_player(&mut world, 0);
// Send mute audio command
send_game_event(&mut world, GameCommand::MuteAudio);
common::send_game_event(&mut world, GameEvent::Command(GameCommand::MuteAudio));
// Run the system
world
@@ -243,7 +193,7 @@ fn test_player_control_system_mute_audio() {
// 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);
common::send_game_event(&mut world, GameEvent::Command(GameCommand::MuteAudio));
world
.run_system_once(player_control_system)
.expect("System should run successfully");
@@ -255,10 +205,10 @@ fn test_player_control_system_mute_audio() {
#[test]
fn test_player_control_system_no_player_entity() {
let mut world = create_test_world();
let mut world = common::create_test_world();
// Don't spawn a player entity
send_game_event(&mut world, GameCommand::MovePlayer(Direction::Up));
common::send_game_event(&mut world, GameEvent::Command(GameCommand::MovePlayer(Direction::Up)));
// Run the system - should write an error
world
@@ -272,8 +222,8 @@ fn test_player_control_system_no_player_entity() {
#[test]
fn test_player_movement_system_buffered_direction_expires() {
let mut world = create_test_world();
let player = spawn_test_player(&mut world);
let mut world = common::create_test_world();
let player = common::spawn_test_player(&mut world, 0);
// Set a buffered direction with short time
world.entity_mut(player).insert(BufferedDirection::Some {
@@ -305,8 +255,8 @@ fn test_player_movement_system_buffered_direction_expires() {
#[test]
fn test_player_movement_system_start_moving_from_stopped() {
let mut world = create_test_world();
let _player = spawn_test_player(&mut world);
let mut world = common::create_test_world();
let _player = common::spawn_test_player(&mut world, 0);
// Player starts at node 0, facing right (towards node 1)
// Should start moving when system runs
@@ -330,8 +280,8 @@ fn test_player_movement_system_start_moving_from_stopped() {
#[test]
fn test_player_movement_system_buffered_direction_change() {
let mut world = create_test_world();
let player = spawn_test_player(&mut world);
let mut world = common::create_test_world();
let player = common::spawn_test_player(&mut world, 0);
// Set a buffered direction to go down (towards node 2)
world.entity_mut(player).insert(BufferedDirection::Some {
@@ -361,8 +311,8 @@ fn test_player_movement_system_buffered_direction_change() {
#[test]
fn test_player_movement_system_no_valid_edge() {
let mut world = create_test_world();
let player = spawn_test_player(&mut world);
let mut world = common::create_test_world();
let player = common::spawn_test_player(&mut world, 0);
// Set velocity to direction with no edge
world.entity_mut(player).insert(Velocity {
@@ -386,8 +336,8 @@ fn test_player_movement_system_no_valid_edge() {
#[test]
fn test_player_movement_system_continue_moving() {
let mut world = create_test_world();
let player = spawn_test_player(&mut world);
let mut world = common::create_test_world();
let player = common::spawn_test_player(&mut world, 0);
// Set player to already be moving
world.entity_mut(player).insert(Position::Moving {
@@ -414,17 +364,13 @@ fn test_player_movement_system_continue_moving() {
}
}
// ============================================================================
// Integration Tests
// ============================================================================
#[test]
fn test_full_player_input_to_movement_flow() {
let mut world = create_test_world();
let _player = spawn_test_player(&mut world);
let mut world = common::create_test_world();
let _player = common::spawn_test_player(&mut world, 0);
// Send move command
send_game_event(&mut world, GameCommand::MovePlayer(Direction::Down));
common::send_game_event(&mut world, GameEvent::Command(GameCommand::MovePlayer(Direction::Down)));
// Run control system to process input
world
@@ -454,11 +400,11 @@ fn test_full_player_input_to_movement_flow() {
#[test]
fn test_buffered_direction_timing() {
let mut world = create_test_world();
let _player = spawn_test_player(&mut world);
let mut world = common::create_test_world();
let _player = common::spawn_test_player(&mut world, 0);
// Send move command
send_game_event(&mut world, GameCommand::MovePlayer(Direction::Up));
common::send_game_event(&mut world, GameEvent::Command(GameCommand::MovePlayer(Direction::Up)));
world
.run_system_once(player_control_system)
.expect("System should run successfully");
@@ -493,21 +439,21 @@ fn test_buffered_direction_timing() {
#[test]
fn test_multiple_rapid_direction_changes() {
let mut world = create_test_world();
let _player = spawn_test_player(&mut world);
let mut world = common::create_test_world();
let _player = common::spawn_test_player(&mut world, 0);
// Send multiple rapid direction changes
send_game_event(&mut world, GameCommand::MovePlayer(Direction::Up));
common::send_game_event(&mut world, GameEvent::Command(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));
common::send_game_event(&mut world, GameEvent::Command(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));
common::send_game_event(&mut world, GameEvent::Command(GameCommand::MovePlayer(Direction::Left)));
world
.run_system_once(player_control_system)
.expect("System should run successfully");
@@ -526,15 +472,15 @@ fn test_multiple_rapid_direction_changes() {
#[test]
fn test_player_state_persistence_across_systems() {
let mut world = create_test_world();
let _player = spawn_test_player(&mut world);
let mut world = common::create_test_world();
let _player = common::spawn_test_player(&mut world, 0);
// 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);
common::send_game_event(&mut world, GameEvent::Command(GameCommand::ToggleDebug));
world
.run_system_once(player_control_system)
.expect("System should run successfully");
@@ -542,7 +488,7 @@ fn test_player_state_persistence_across_systems() {
// Clear events and mute audio
world.resource_mut::<Events<GameEvent>>().clear();
send_game_event(&mut world, GameCommand::MuteAudio);
common::send_game_event(&mut world, GameEvent::Command(GameCommand::MuteAudio));
world
.run_system_once(player_control_system)
.expect("System should run successfully");
@@ -550,7 +496,7 @@ fn test_player_state_persistence_across_systems() {
// Clear events and move player
world.resource_mut::<Events<GameEvent>>().clear();
send_game_event(&mut world, GameCommand::MovePlayer(Direction::Down));
common::send_game_event(&mut world, GameEvent::Command(GameCommand::MovePlayer(Direction::Down)));
world
.run_system_once(player_control_system)
.expect("System should run successfully");

View File

@@ -1,14 +1,12 @@
use glam::U16Vec2;
use pacman::texture::sprite::{AtlasMapper, AtlasTile, MapperFrame, SpriteAtlas};
use pacman::texture::sprite::{AtlasMapper, AtlasTile, MapperFrame};
use sdl2::pixels::Color;
use std::collections::HashMap;
fn mock_texture() -> sdl2::render::Texture {
unsafe { std::mem::transmute(0usize) }
}
mod common;
#[test]
fn test_sprite_atlas_basic() {
fn test_atlas_mapper_frame_lookup() {
let mut frames = HashMap::new();
frames.insert(
"test".to_string(),
@@ -19,19 +17,17 @@ fn test_sprite_atlas_basic() {
);
let mapper = AtlasMapper { frames };
let texture = mock_texture();
let atlas = SpriteAtlas::new(texture, mapper);
let tile = atlas.get_tile("test");
assert!(tile.is_some());
let tile = tile.unwrap();
assert_eq!(tile.pos, glam::U16Vec2::new(10, 20));
assert_eq!(tile.size, glam::U16Vec2::new(32, 64));
assert_eq!(tile.color, None);
// Test direct frame lookup
let frame = mapper.frames.get("test");
assert!(frame.is_some());
let frame = frame.unwrap();
assert_eq!(frame.pos, U16Vec2::new(10, 20));
assert_eq!(frame.size, U16Vec2::new(32, 64));
}
#[test]
fn test_sprite_atlas_multiple_tiles() {
fn test_atlas_mapper_multiple_frames() {
let mut frames = HashMap::new();
frames.insert(
"tile1".to_string(),
@@ -49,27 +45,12 @@ fn test_sprite_atlas_multiple_tiles() {
);
let mapper = AtlasMapper { frames };
let texture = mock_texture();
let atlas = SpriteAtlas::new(texture, mapper);
assert_eq!(atlas.tiles_count(), 2);
assert!(atlas.has_tile("tile1"));
assert!(atlas.has_tile("tile2"));
assert!(!atlas.has_tile("tile3"));
assert!(atlas.get_tile("nonexistent").is_none());
}
#[test]
fn test_sprite_atlas_color() {
let mapper = AtlasMapper { frames: HashMap::new() };
let texture = mock_texture();
let mut atlas = SpriteAtlas::new(texture, mapper);
assert_eq!(atlas.default_color(), None);
let color = Color::RGB(255, 0, 0);
atlas.set_color(color);
assert_eq!(atlas.default_color(), Some(color));
assert_eq!(mapper.frames.len(), 2);
assert!(mapper.frames.contains_key("tile1"));
assert!(mapper.frames.contains_key("tile2"));
assert!(!mapper.frames.contains_key("tile3"));
assert!(mapper.frames.get("nonexistent").is_none());
}
#[test]

View File

@@ -1,9 +1,9 @@
use pacman::texture::{sprite::SpriteAtlas, text::TextTexture};
use crate::common::create_atlas;
mod common;
use common::create_atlas;
/// Helper function to get all characters that should be in the atlas
fn get_all_chars() -> String {
let mut chars = Vec::new();

View File

@@ -1,19 +0,0 @@
use pacman::platform::tracing_buffer::SwitchableWriter;
use std::io::Write;
#[test]
fn test_switchable_writer_buffering() {
let mut writer = SwitchableWriter::default();
// Write some data while in buffered mode
writer.write_all(b"Hello, ").unwrap();
writer.write_all(b"world!").unwrap();
writer.write_all(b"This is buffered content.\n").unwrap();
// Switch to direct mode (this should flush to stdout and show buffer size)
// In a real test we can't easily capture stdout, so we'll just verify it doesn't panic
writer.switch_to_direct_mode().unwrap();
// Write more data in direct mode
writer.write_all(b"Direct output after flush\n").unwrap();
}