From c1c5dae6f207d117446a347e83f5518b9ebf4b4e Mon Sep 17 00:00:00 2001 From: Xevion Date: Tue, 12 Aug 2025 14:40:48 -0500 Subject: [PATCH] refactor: restructure game logic and state management into separate modules - Moved game logic from `game.rs` to `game/mod.rs` and `game/state.rs` for better organization. - Updated `App` to utilize the new `Game` struct and its state management. - Refactored error handling - Removed unused audio subsystem references --- src/app.rs | 8 +- src/asset.rs | 13 +- src/entity/ghost.rs | 6 +- src/entity/item.rs | 22 +- src/entity/pacman.rs | 6 +- src/error.rs | 57 +++-- src/game.rs | 435 ------------------------------------- src/game/mod.rs | 327 ++++++++++++++++++++++++++++ src/game/state.rs | 135 ++++++++++++ src/map/builder.rs | 34 +-- src/map/parser.rs | 13 +- src/platform/desktop.rs | 5 +- src/platform/emscripten.rs | 5 +- src/platform/mod.rs | 14 +- src/texture/animated.rs | 20 +- src/texture/directional.rs | 6 +- src/texture/sprite.rs | 26 ++- tests/animated.rs | 7 +- tests/game.rs | 11 - tests/map_builder.rs | 15 +- tests/parser.rs | 3 +- 21 files changed, 577 insertions(+), 591 deletions(-) delete mode 100644 src/game.rs create mode 100644 src/game/mod.rs create mode 100644 src/game/state.rs diff --git a/src/app.rs b/src/app.rs index a63c859..21648f3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -30,9 +30,9 @@ impl App { let sdl_context: &'static Sdl = Box::leak(Box::new(sdl2::init().map_err(|e| GameError::Sdl(e.to_string()))?)); let video_subsystem: &'static VideoSubsystem = Box::leak(Box::new(sdl_context.video().map_err(|e| GameError::Sdl(e.to_string()))?)); - let audio_subsystem: &'static AudioSubsystem = + let _audio_subsystem: &'static AudioSubsystem = Box::leak(Box::new(sdl_context.audio().map_err(|e| GameError::Sdl(e.to_string()))?)); - let ttf_context: &'static Sdl2TtfContext = + let _ttf_context: &'static Sdl2TtfContext = Box::leak(Box::new(sdl2::ttf::init().map_err(|e| GameError::Sdl(e.to_string()))?)); let event_pump: &'static mut EventPump = Box::leak(Box::new(sdl_context.event_pump().map_err(|e| GameError::Sdl(e.to_string()))?)); @@ -58,7 +58,7 @@ impl App { let texture_creator: &'static TextureCreator = Box::leak(Box::new(canvas.texture_creator())); - let mut game = Game::new(texture_creator, ttf_context, audio_subsystem)?; + let mut game = Game::new(texture_creator)?; // game.audio.set_mute(cfg!(debug_assertions)); let mut backbuffer = texture_creator @@ -119,7 +119,7 @@ impl App { keycode: Some(Keycode::Space), .. } => { - self.game.debug_mode = !self.game.debug_mode; + self.game.toggle_debug_mode(); } Event::KeyDown { keycode: Some(key), .. } => { self.game.keyboard_event(key); diff --git a/src/asset.rs b/src/asset.rs index 1a1c140..391fdd4 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -3,18 +3,6 @@ //! On desktop, assets are embedded using include_bytes!; on Emscripten, assets are loaded from the filesystem. use std::borrow::Cow; -use std::io; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum AssetError { - #[error("IO error: {0}")] - Io(#[from] io::Error), - #[error("Asset not found: {0}")] - NotFound(String), - #[error("Invalid asset format: {0}")] - InvalidFormat(String), -} #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Asset { @@ -44,6 +32,7 @@ impl Asset { mod imp { use super::*; + use crate::error::AssetError; use crate::platform::get_platform; pub fn get_asset_bytes(asset: Asset) -> Result, AssetError> { diff --git a/src/entity/ghost.rs b/src/entity/ghost.rs index 28735cc..0b3ec0e 100644 --- a/src/entity/ghost.rs +++ b/src/entity/ghost.rs @@ -164,10 +164,8 @@ impl Ghost { })?, ]; - textures[direction.as_usize()] = - Some(AnimatedTexture::new(moving_tiles, 0.2).map_err(|e| GameError::Texture(TextureError::Animated(e)))?); - stopped_textures[direction.as_usize()] = - Some(AnimatedTexture::new(stopped_tiles, 0.1).map_err(|e| GameError::Texture(TextureError::Animated(e)))?); + textures[direction.as_usize()] = Some(AnimatedTexture::new(moving_tiles, 0.2)?); + stopped_textures[direction.as_usize()] = Some(AnimatedTexture::new(stopped_tiles, 0.1)?); } Ok(Self { diff --git a/src/entity/item.rs b/src/entity/item.rs index d8cc31c..8d9788e 100644 --- a/src/entity/item.rs +++ b/src/entity/item.rs @@ -1,7 +1,7 @@ use crate::{ constants, entity::{collision::Collidable, graph::Graph}, - error::EntityError, + error::{EntityError, GameResult}, texture::sprite::{Sprite, SpriteAtlas}, }; use sdl2::render::{Canvas, RenderTarget}; @@ -95,16 +95,18 @@ impl Item { self.item_type.get_score() } - pub fn render(&self, canvas: &mut Canvas, atlas: &mut SpriteAtlas, graph: &Graph) -> anyhow::Result<()> { - if !self.collected { - let node = graph - .get_node(self.node_index) - .ok_or(EntityError::NodeNotFound(self.node_index))?; - let position = node.position + constants::BOARD_PIXEL_OFFSET.as_vec2(); - self.sprite.render(canvas, atlas, position) - } else { - Ok(()) + pub fn render(&self, canvas: &mut Canvas, atlas: &mut SpriteAtlas, graph: &Graph) -> GameResult<()> { + if self.collected { + return Ok(()); } + + let node = graph + .get_node(self.node_index) + .ok_or(EntityError::NodeNotFound(self.node_index))?; + let position = node.position + constants::BOARD_PIXEL_OFFSET.as_vec2(); + + self.sprite.render(canvas, atlas, position)?; + Ok(()) } } diff --git a/src/entity/pacman.rs b/src/entity/pacman.rs index 6d6e63b..861b192 100644 --- a/src/entity/pacman.rs +++ b/src/entity/pacman.rs @@ -98,10 +98,8 @@ impl Pacman { let stopped_tiles = vec![SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_b.png")) .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound(format!("{moving_prefix}_b.png"))))?]; - textures[direction.as_usize()] = - Some(AnimatedTexture::new(moving_tiles, 0.08).map_err(|e| GameError::Texture(TextureError::Animated(e)))?); - stopped_textures[direction.as_usize()] = - Some(AnimatedTexture::new(stopped_tiles, 0.1).map_err(|e| GameError::Texture(TextureError::Animated(e)))?); + textures[direction.as_usize()] = Some(AnimatedTexture::new(moving_tiles, 0.08)?); + stopped_textures[direction.as_usize()] = Some(AnimatedTexture::new(stopped_tiles, 0.1)?); } Ok(Self { diff --git a/src/error.rs b/src/error.rs index 993fe78..2a85b60 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,22 +3,22 @@ //! This module defines all error types used throughout the application, //! providing a consistent error handling approach. -use thiserror::Error; +use std::io; /// Main error type for the Pac-Man game. /// /// This is the primary error type that should be used in public APIs. /// It can represent any error that can occur during game operation. -#[derive(Error, Debug)] +#[derive(thiserror::Error, Debug)] pub enum GameError { #[error("Asset error: {0}")] - Asset(#[from] crate::asset::AssetError), + Asset(#[from] AssetError), #[error("Platform error: {0}")] - Platform(#[from] crate::platform::PlatformError), + Platform(#[from] PlatformError), #[error("Map parsing error: {0}")] - MapParse(#[from] crate::map::parser::ParseError), + MapParse(#[from] ParseError), #[error("Map error: {0}")] Map(#[from] MapError), @@ -36,26 +36,49 @@ pub enum GameError { Sdl(String), #[error("IO error: {0}")] - Io(#[from] std::io::Error), + Io(#[from] io::Error), #[error("Serialization error: {0}")] Serialization(#[from] serde_json::Error), #[error("Invalid state: {0}")] InvalidState(String), +} - #[error("Resource not found: {0}")] +#[derive(thiserror::Error, Debug)] +pub enum AssetError { + #[error("IO error: {0}")] + Io(#[from] io::Error), + #[error("Asset not found: {0}")] NotFound(String), +} - #[error("Configuration error: {0}")] - Config(String), +/// Platform-specific errors. +#[derive(thiserror::Error, Debug)] +#[allow(dead_code)] +pub enum PlatformError { + #[error("Console initialization failed: {0}")] + ConsoleInit(String), + #[error("Platform-specific error: {0}")] + Other(String), +} + +/// Error type for map parsing operations. +#[derive(thiserror::Error, Debug)] +pub enum ParseError { + #[error("Unknown character in board: {0}")] + UnknownCharacter(char), + #[error("House door must have exactly 2 positions, found {0}")] + InvalidHouseDoorCount(usize), + #[error("Map parsing failed: {0}")] + ParseFailed(String), } /// Errors related to texture operations. -#[derive(Error, Debug)] +#[derive(thiserror::Error, Debug)] pub enum TextureError { #[error("Animated texture error: {0}")] - Animated(#[from] crate::texture::animated::AnimatedTextureError), + Animated(#[from] AnimatedTextureError), #[error("Failed to load texture: {0}")] LoadFailed(String), @@ -70,8 +93,14 @@ pub enum TextureError { RenderFailed(String), } +#[derive(thiserror::Error, Debug)] +pub enum AnimatedTextureError { + #[error("Frame duration must be positive, got {0}")] + InvalidFrameDuration(f32), +} + /// Errors related to entity operations. -#[derive(Error, Debug)] +#[derive(thiserror::Error, Debug)] pub enum EntityError { #[error("Node not found in graph: {0}")] NodeNotFound(usize), @@ -87,11 +116,11 @@ pub enum EntityError { } /// Errors related to game state operations. -#[derive(Error, Debug)] +#[derive(thiserror::Error, Debug)] pub enum GameStateError {} /// Errors related to map operations. -#[derive(Error, Debug)] +#[derive(thiserror::Error, Debug)] pub enum MapError { #[error("Node not found: {0}")] NodeNotFound(usize), diff --git a/src/game.rs b/src/game.rs deleted file mode 100644 index 140ba3d..0000000 --- a/src/game.rs +++ /dev/null @@ -1,435 +0,0 @@ -//! This module contains the main game logic and state. - -use glam::{UVec2, Vec2}; -use rand::{rngs::SmallRng, Rng, SeedableRng}; -use sdl2::{ - image::LoadTexture, - keyboard::Keycode, - pixels::Color, - render::{Canvas, RenderTarget, Texture, TextureCreator}, - video::WindowContext, -}; - -use crate::error::{EntityError, GameError, GameResult, TextureError}; - -use crate::{ - asset::{get_asset_bytes, Asset}, - audio::Audio, - constants::{CELL_SIZE, RAW_BOARD}, - entity::{ - collision::{Collidable, CollisionSystem, EntityId}, - ghost::{Ghost, GhostType}, - item::Item, - pacman::Pacman, - r#trait::Entity, - }, - map::Map, - texture::{ - sprite::{AtlasMapper, AtlasTile, SpriteAtlas}, - text::TextTexture, - }, -}; - -/// The main game state. -/// -/// Contains all the information necessary to run the game, including -/// the game state, rendering resources, and audio. -pub struct Game { - pub score: u32, - pub map: Map, - pub pacman: Pacman, - pub ghosts: Vec, - pub items: Vec, - pub debug_mode: bool, - - // Collision system - collision_system: CollisionSystem, - pacman_id: EntityId, - ghost_ids: Vec, - item_ids: Vec, - - // Rendering resources - atlas: SpriteAtlas, - map_texture: AtlasTile, - text_texture: TextTexture, - - // Audio - pub audio: Audio, -} - -impl Game { - pub fn new( - texture_creator: &'static TextureCreator, - _ttf_context: &sdl2::ttf::Sdl2TtfContext, - _audio_subsystem: &sdl2::AudioSubsystem, - ) -> GameResult { - let map = Map::new(RAW_BOARD)?; - - let pacman_start_pos = map - .find_starting_position(0) - .ok_or_else(|| GameError::NotFound("Pac-Man starting position".to_string()))?; - let pacman_start_node = *map - .grid_to_node - .get(&glam::IVec2::new(pacman_start_pos.x as i32, pacman_start_pos.y as i32)) - .ok_or_else(|| GameError::NotFound("Pac-Man starting position not found in graph".to_string()))?; - - let atlas_bytes = get_asset_bytes(Asset::Atlas)?; - let atlas_texture = Box::leak(Box::new(texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| { - if e.to_string().contains("format") || e.to_string().contains("unsupported") { - GameError::Texture(TextureError::InvalidFormat(format!("Unsupported texture format: {e}"))) - } else { - GameError::Texture(TextureError::LoadFailed(e.to_string())) - } - })?)); - let atlas_json = get_asset_bytes(Asset::AtlasJson)?; - let atlas_mapper: AtlasMapper = serde_json::from_slice(&atlas_json)?; - let atlas = SpriteAtlas::new(unsafe { std::mem::transmute_copy(atlas_texture) }, atlas_mapper); - - let mut map_texture = SpriteAtlas::get_tile(&atlas, "maze/full.png") - .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/full.png".to_string())))?; - map_texture.color = Some(Color::RGB(0x20, 0x20, 0xf9)); - - let text_texture = TextTexture::new(1.0); - let audio = Audio::new(); - let pacman = Pacman::new(&map.graph, pacman_start_node, &atlas)?; - - // Generate items (pellets and energizers) - let items = map.generate_items(&atlas)?; - - // Create ghosts at random positions - let mut ghosts = Vec::new(); - let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde]; - let mut rng = SmallRng::from_os_rng(); - - if map.graph.node_count() == 0 { - return Err(GameError::Config("Game map has no nodes - invalid configuration".to_string())); - // TODO: This is a bug, we should handle this better - } - - for &ghost_type in &ghost_types { - // Find a random node for the ghost to start at - let random_node = rng.random_range(0..map.graph.node_count()); - let ghost = Ghost::new(&map.graph, random_node, ghost_type, &atlas)?; - ghosts.push(ghost); - } - - // Initialize collision system - let mut collision_system = CollisionSystem::default(); - - // Register Pac-Man - let pacman_id = collision_system.register_entity(pacman.position()); - - // Register items - let mut item_ids = Vec::new(); - for item in &items { - let item_id = collision_system.register_entity(item.position()); - item_ids.push(item_id); - } - - // Register ghosts - let mut ghost_ids = Vec::new(); - for ghost in &ghosts { - let ghost_id = collision_system.register_entity(ghost.position()); - ghost_ids.push(ghost_id); - } - - Ok(Game { - score: 0, - map, - pacman, - ghosts, - items, - debug_mode: false, - collision_system, - pacman_id, - ghost_ids, - item_ids, - map_texture, - text_texture, - audio, - atlas, - }) - } - - pub fn keyboard_event(&mut self, keycode: Keycode) { - self.pacman.handle_key(keycode); - - if keycode == Keycode::M { - self.audio.set_mute(!self.audio.is_muted()); - } - - if keycode == Keycode::R { - if let Err(e) = self.reset_game_state() { - tracing::error!("Failed to reset game state: {}", e); - } - } - } - - /// Resets the game state, randomizing ghost positions and resetting Pac-Man - fn reset_game_state(&mut self) -> GameResult<()> { - // Reset Pac-Man to starting position - let pacman_start_pos = self - .map - .find_starting_position(0) - .ok_or_else(|| GameError::NotFound("Pac-Man starting position".to_string()))?; - let pacman_start_node = *self - .map - .grid_to_node - .get(&glam::IVec2::new(pacman_start_pos.x as i32, pacman_start_pos.y as i32)) - .ok_or_else(|| GameError::NotFound("Pac-Man starting position not found in graph".to_string()))?; - - self.pacman = Pacman::new(&self.map.graph, pacman_start_node, &self.atlas)?; - - // Reset items - self.items = self.map.generate_items(&self.atlas)?; - - // Randomize ghost positions - let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde]; - let mut rng = SmallRng::from_os_rng(); - - for (i, ghost) in self.ghosts.iter_mut().enumerate() { - let random_node = rng.random_range(0..self.map.graph.node_count()); - *ghost = Ghost::new(&self.map.graph, random_node, ghost_types[i], &self.atlas)?; - } - - // Reset collision system - self.collision_system = CollisionSystem::default(); - - // Re-register Pac-Man - self.pacman_id = self.collision_system.register_entity(self.pacman.position()); - - // Re-register items - self.item_ids.clear(); - for item in &self.items { - let item_id = self.collision_system.register_entity(item.position()); - self.item_ids.push(item_id); - } - - // Re-register ghosts - self.ghost_ids.clear(); - for ghost in &self.ghosts { - let ghost_id = self.collision_system.register_entity(ghost.position()); - self.ghost_ids.push(ghost_id); - } - - Ok(()) - } - - pub fn tick(&mut self, dt: f32) { - self.pacman.tick(dt, &self.map.graph); - - // Update all ghosts - for ghost in &mut self.ghosts { - ghost.tick(dt, &self.map.graph); - } - - // Update collision system positions - self.update_collision_positions(); - - // Check for collisions - self.check_collisions(); - } - - fn update_collision_positions(&mut self) { - // Update Pac-Man's position - self.collision_system.update_position(self.pacman_id, self.pacman.position()); - - // Update ghost positions - for (ghost, &ghost_id) in self.ghosts.iter().zip(&self.ghost_ids) { - self.collision_system.update_position(ghost_id, ghost.position()); - } - } - - fn check_collisions(&mut self) { - // Check Pac-Man vs Items - let potential_collisions = self.collision_system.potential_collisions(&self.pacman.position()); - - for entity_id in potential_collisions { - if entity_id != self.pacman_id { - // Check if this is an item collision - if let Some(item_index) = self.find_item_by_id(entity_id) { - let item = &mut self.items[item_index]; - if !item.is_collected() { - item.collect(); - self.score += item.get_score(); - self.audio.eat(); - - // Handle energizer effects - if matches!(item.item_type, crate::entity::item::ItemType::Energizer) { - // TODO: Make ghosts frightened - tracing::info!("Energizer collected! Ghosts should become frightened."); - } - } - } - - // Check if this is a ghost collision - if let Some(_ghost_index) = self.find_ghost_by_id(entity_id) { - // TODO: Handle Pac-Man being eaten by ghost - tracing::info!("Pac-Man collided with ghost!"); - } - } - } - } - - fn find_item_by_id(&self, entity_id: EntityId) -> Option { - self.item_ids.iter().position(|&id| id == entity_id) - } - - fn find_ghost_by_id(&self, entity_id: EntityId) -> Option { - self.ghost_ids.iter().position(|&id| id == entity_id) - } - - pub fn draw(&mut self, canvas: &mut Canvas, backbuffer: &mut Texture) -> GameResult<()> { - canvas - .with_texture_canvas(backbuffer, |canvas| { - canvas.set_draw_color(Color::BLACK); - canvas.clear(); - self.map.render(canvas, &mut self.atlas, &mut self.map_texture); - - // Render all items - for item in &self.items { - if let Err(e) = item.render(canvas, &mut self.atlas, &self.map.graph) { - tracing::error!("Failed to render item: {}", e); - } - } - - // Render all ghosts - for ghost in &self.ghosts { - if let Err(e) = ghost.render(canvas, &mut self.atlas, &self.map.graph) { - tracing::error!("Failed to render ghost: {}", e); - } - } - - if let Err(e) = self.pacman.render(canvas, &mut self.atlas, &self.map.graph) { - tracing::error!("Failed to render pacman: {}", e); - } - }) - .map_err(|e| GameError::Sdl(e.to_string()))?; - - Ok(()) - } - - pub fn present_backbuffer( - &mut self, - canvas: &mut Canvas, - backbuffer: &Texture, - cursor_pos: glam::Vec2, - ) -> GameResult<()> { - canvas - .copy(backbuffer, None, None) - .map_err(|e| GameError::Sdl(e.to_string()))?; - if self.debug_mode { - if let Err(e) = self - .map - .debug_render_with_cursor(canvas, &mut self.text_texture, &mut self.atlas, cursor_pos) - { - tracing::error!("Failed to render debug cursor: {}", e); - } - self.render_pathfinding_debug(canvas)?; - } - self.draw_hud(canvas)?; - canvas.present(); - Ok(()) - } - - /// Renders pathfinding debug lines from each ghost to Pac-Man. - /// - /// Each ghost's path is drawn in its respective color with a small offset - /// to prevent overlapping lines. - fn render_pathfinding_debug(&self, canvas: &mut Canvas) -> GameResult<()> { - let pacman_node = self.pacman.current_node_id(); - - for ghost in self.ghosts.iter() { - if let Ok(path) = ghost.calculate_path_to_target(&self.map.graph, pacman_node) { - if path.len() < 2 { - continue; // Skip if path is too short - } - - // Set the ghost's color - canvas.set_draw_color(ghost.debug_color()); - - // Calculate offset based on ghost index to prevent overlapping lines - // let offset = (i as f32) * 2.0 - 3.0; // Offset range: -3.0 to 3.0 - - // Calculate a consistent offset direction for the entire path - // let first_node = self.map.graph.get_node(path[0]).unwrap(); - // let last_node = self.map.graph.get_node(path[path.len() - 1]).unwrap(); - - // Use the overall direction from start to end to determine the perpendicular offset - let offset = match ghost.ghost_type { - GhostType::Blinky => Vec2::new(0.25, 0.5), - GhostType::Pinky => Vec2::new(-0.25, -0.25), - GhostType::Inky => Vec2::new(0.5, -0.5), - GhostType::Clyde => Vec2::new(-0.5, 0.25), - } * 5.0; - - // Calculate offset positions for all nodes using the same perpendicular direction - let mut offset_positions = Vec::new(); - for &node_id in &path { - let node = self - .map - .graph - .get_node(node_id) - .ok_or(GameError::Entity(EntityError::NodeNotFound(node_id)))?; - let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2(); - offset_positions.push(pos + offset); - } - - // Draw lines between the offset positions - for window in offset_positions.windows(2) { - if let (Some(from), Some(to)) = (window.first(), window.get(1)) { - // Skip if the distance is too far (used for preventing lines between tunnel portals) - if from.distance_squared(*to) > (CELL_SIZE * 16).pow(2) as f32 { - continue; - } - - // Draw the line - canvas - .draw_line((from.x as i32, from.y as i32), (to.x as i32, to.y as i32)) - .map_err(|e| GameError::Sdl(e.to_string()))?; - } - } - } - } - - Ok(()) - } - - fn draw_hud(&mut self, canvas: &mut Canvas) -> GameResult<()> { - let lives = 3; - let score_text = format!("{:02}", self.score); - let x_offset = 4; - let y_offset = 2; - let lives_offset = 3; - let score_offset = 7 - (score_text.len() as i32); - self.text_texture.set_scale(1.0); - if let Err(e) = self.text_texture.render( - canvas, - &mut self.atlas, - &format!("{lives}UP HIGH SCORE "), - UVec2::new(8 * lives_offset as u32 + x_offset, y_offset), - ) { - tracing::error!("Failed to render HUD text: {}", e); - } - if let Err(e) = self.text_texture.render( - canvas, - &mut self.atlas, - &score_text, - UVec2::new(8 * score_offset as u32 + x_offset, 8 + y_offset), - ) { - tracing::error!("Failed to render score text: {}", e); - } - - // Display FPS information in top-left corner - // let fps_text = format!("FPS: {:.1} (1s) / {:.1} (10s)", self.fps_1s, self.fps_10s); - // self.render_text_on( - // canvas, - // &*texture_creator, - // &fps_text, - // IVec2::new(10, 10), - // Color::RGB(255, 255, 0), // Yellow color for FPS display - // ); - - Ok(()) - } -} diff --git a/src/game/mod.rs b/src/game/mod.rs new file mode 100644 index 0000000..3bd38f8 --- /dev/null +++ b/src/game/mod.rs @@ -0,0 +1,327 @@ +//! This module contains the main game logic and state. + +use glam::{UVec2, Vec2}; +use rand::{rngs::SmallRng, Rng, SeedableRng}; +use sdl2::{ + keyboard::Keycode, + pixels::Color, + render::{Canvas, RenderTarget, Texture, TextureCreator}, + video::WindowContext, +}; + +use crate::error::{EntityError, GameError, GameResult}; + +use crate::entity::{ + collision::{Collidable, CollisionSystem, EntityId}, + ghost::{Ghost, GhostType}, + pacman::Pacman, + r#trait::Entity, +}; + +pub mod state; +use state::GameState; + +/// The `Game` struct is the main entry point for the game. +/// +/// It contains the game's state and logic, and is responsible for +/// handling user input, updating the game state, and rendering the game. +pub struct Game { + state: GameState, +} + +impl Game { + pub fn new(texture_creator: &'static TextureCreator) -> GameResult { + let state = GameState::new(texture_creator)?; + + Ok(Game { state }) + } + + pub fn keyboard_event(&mut self, keycode: Keycode) { + self.state.pacman.handle_key(keycode); + + if keycode == Keycode::M { + self.state.audio.set_mute(!self.state.audio.is_muted()); + } + + if keycode == Keycode::R { + if let Err(e) = self.reset_game_state() { + tracing::error!("Failed to reset game state: {}", e); + } + } + } + + /// Resets the game state, randomizing ghost positions and resetting Pac-Man + fn reset_game_state(&mut self) -> GameResult<()> { + let pacman_start_node = self.state.map.start_positions.pacman; + self.state.pacman = Pacman::new(&self.state.map.graph, pacman_start_node, &self.state.atlas)?; + + // Reset items + self.state.items = self.state.map.generate_items(&self.state.atlas)?; + + // Randomize ghost positions + let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde]; + let mut rng = SmallRng::from_os_rng(); + + for (i, ghost) in self.state.ghosts.iter_mut().enumerate() { + let random_node = rng.random_range(0..self.state.map.graph.node_count()); + *ghost = Ghost::new(&self.state.map.graph, random_node, ghost_types[i], &self.state.atlas)?; + } + + // Reset collision system + self.state.collision_system = CollisionSystem::default(); + + // Re-register Pac-Man + self.state.pacman_id = self.state.collision_system.register_entity(self.state.pacman.position()); + + // Re-register items + self.state.item_ids.clear(); + for item in &self.state.items { + let item_id = self.state.collision_system.register_entity(item.position()); + self.state.item_ids.push(item_id); + } + + // Re-register ghosts + self.state.ghost_ids.clear(); + for ghost in &self.state.ghosts { + let ghost_id = self.state.collision_system.register_entity(ghost.position()); + self.state.ghost_ids.push(ghost_id); + } + + Ok(()) + } + + pub fn tick(&mut self, dt: f32) { + self.state.pacman.tick(dt, &self.state.map.graph); + + // Update all ghosts + for ghost in &mut self.state.ghosts { + ghost.tick(dt, &self.state.map.graph); + } + + // Update collision system positions + self.update_collision_positions(); + + // Check for collisions + self.check_collisions(); + } + + /// Toggles the debug mode on and off. + /// + /// When debug mode is enabled, the game will render additional information + /// that is useful for debugging, such as the collision grid and entity paths. + pub fn toggle_debug_mode(&mut self) { + self.state.debug_mode = !self.state.debug_mode; + } + + fn update_collision_positions(&mut self) { + // Update Pac-Man's position + self.state + .collision_system + .update_position(self.state.pacman_id, self.state.pacman.position()); + + // Update ghost positions + for (ghost, &ghost_id) in self.state.ghosts.iter().zip(&self.state.ghost_ids) { + self.state.collision_system.update_position(ghost_id, ghost.position()); + } + } + + fn check_collisions(&mut self) { + // Check Pac-Man vs Items + let potential_collisions = self + .state + .collision_system + .potential_collisions(&self.state.pacman.position()); + + for entity_id in potential_collisions { + if entity_id != self.state.pacman_id { + // Check if this is an item collision + if let Some(item_index) = self.find_item_by_id(entity_id) { + let item = &mut self.state.items[item_index]; + if !item.is_collected() { + item.collect(); + self.state.score += item.get_score(); + self.state.audio.eat(); + + // Handle energizer effects + if matches!(item.item_type, crate::entity::item::ItemType::Energizer) { + // TODO: Make ghosts frightened + tracing::info!("Energizer collected! Ghosts should become frightened."); + } + } + } + + // Check if this is a ghost collision + if let Some(_ghost_index) = self.find_ghost_by_id(entity_id) { + // TODO: Handle Pac-Man being eaten by ghost + tracing::info!("Pac-Man collided with ghost!"); + } + } + } + } + + fn find_item_by_id(&self, entity_id: EntityId) -> Option { + self.state.item_ids.iter().position(|&id| id == entity_id) + } + + fn find_ghost_by_id(&self, entity_id: EntityId) -> Option { + self.state.ghost_ids.iter().position(|&id| id == entity_id) + } + + pub fn draw(&mut self, canvas: &mut Canvas, backbuffer: &mut Texture) -> GameResult<()> { + canvas + .with_texture_canvas(backbuffer, |canvas| { + canvas.set_draw_color(Color::BLACK); + canvas.clear(); + self.state + .map + .render(canvas, &mut self.state.atlas, &mut self.state.map_texture); + + // Render all items + for item in &self.state.items { + if let Err(e) = item.render(canvas, &mut self.state.atlas, &self.state.map.graph) { + tracing::error!("Failed to render item: {}", e); + } + } + + // Render all ghosts + for ghost in &self.state.ghosts { + if let Err(e) = ghost.render(canvas, &mut self.state.atlas, &self.state.map.graph) { + tracing::error!("Failed to render ghost: {}", e); + } + } + + if let Err(e) = self.state.pacman.render(canvas, &mut self.state.atlas, &self.state.map.graph) { + tracing::error!("Failed to render pacman: {}", e); + } + }) + .map_err(|e| GameError::Sdl(e.to_string()))?; + + Ok(()) + } + + pub fn present_backbuffer( + &mut self, + canvas: &mut Canvas, + backbuffer: &Texture, + cursor_pos: glam::Vec2, + ) -> GameResult<()> { + canvas + .copy(backbuffer, None, None) + .map_err(|e| GameError::Sdl(e.to_string()))?; + if self.state.debug_mode { + if let Err(e) = + self.state + .map + .debug_render_with_cursor(canvas, &mut self.state.text_texture, &mut self.state.atlas, cursor_pos) + { + tracing::error!("Failed to render debug cursor: {}", e); + } + self.render_pathfinding_debug(canvas)?; + } + self.draw_hud(canvas)?; + canvas.present(); + Ok(()) + } + + /// Renders pathfinding debug lines from each ghost to Pac-Man. + /// + /// Each ghost's path is drawn in its respective color with a small offset + /// to prevent overlapping lines. + fn render_pathfinding_debug(&self, canvas: &mut Canvas) -> GameResult<()> { + let pacman_node = self.state.pacman.current_node_id(); + + for ghost in self.state.ghosts.iter() { + if let Ok(path) = ghost.calculate_path_to_target(&self.state.map.graph, pacman_node) { + if path.len() < 2 { + continue; // Skip if path is too short + } + + // Set the ghost's color + canvas.set_draw_color(ghost.debug_color()); + + // Calculate offset based on ghost index to prevent overlapping lines + // let offset = (i as f32) * 2.0 - 3.0; // Offset range: -3.0 to 3.0 + + // Calculate a consistent offset direction for the entire path + // let first_node = self.map.graph.get_node(path[0]).unwrap(); + // let last_node = self.map.graph.get_node(path[path.len() - 1]).unwrap(); + + // Use the overall direction from start to end to determine the perpendicular offset + let offset = match ghost.ghost_type { + GhostType::Blinky => Vec2::new(0.25, 0.5), + GhostType::Pinky => Vec2::new(-0.25, -0.25), + GhostType::Inky => Vec2::new(0.5, -0.5), + GhostType::Clyde => Vec2::new(-0.5, 0.25), + } * 5.0; + + // Calculate offset positions for all nodes using the same perpendicular direction + let mut offset_positions = Vec::new(); + for &node_id in &path { + let node = self + .state + .map + .graph + .get_node(node_id) + .ok_or(GameError::Entity(EntityError::NodeNotFound(node_id)))?; + let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2(); + offset_positions.push(pos + offset); + } + + // Draw lines between the offset positions + for window in offset_positions.windows(2) { + if let (Some(from), Some(to)) = (window.first(), window.get(1)) { + // Skip if the distance is too far (used for preventing lines between tunnel portals) + if from.distance_squared(*to) > (crate::constants::CELL_SIZE * 16).pow(2) as f32 { + continue; + } + + // Draw the line + canvas + .draw_line((from.x as i32, from.y as i32), (to.x as i32, to.y as i32)) + .map_err(|e| GameError::Sdl(e.to_string()))?; + } + } + } + } + + Ok(()) + } + + fn draw_hud(&mut self, canvas: &mut Canvas) -> GameResult<()> { + let lives = 3; + let score_text = format!("{:02}", self.state.score); + let x_offset = 4; + let y_offset = 2; + let lives_offset = 3; + let score_offset = 7 - (score_text.len() as i32); + self.state.text_texture.set_scale(1.0); + if let Err(e) = self.state.text_texture.render( + canvas, + &mut self.state.atlas, + &format!("{lives}UP HIGH SCORE "), + UVec2::new(8 * lives_offset as u32 + x_offset, y_offset), + ) { + tracing::error!("Failed to render HUD text: {}", e); + } + if let Err(e) = self.state.text_texture.render( + canvas, + &mut self.state.atlas, + &score_text, + UVec2::new(8 * score_offset as u32 + x_offset, 8 + y_offset), + ) { + tracing::error!("Failed to render score text: {}", e); + } + + // Display FPS information in top-left corner + // let fps_text = format!("FPS: {:.1} (1s) / {:.1} (10s)", self.fps_1s, self.fps_10s); + // self.render_text_on( + // canvas, + // &*texture_creator, + // &fps_text, + // IVec2::new(10, 10), + // Color::RGB(255, 255, 0), // Yellow color for FPS display + // ); + + Ok(()) + } +} diff --git a/src/game/state.rs b/src/game/state.rs new file mode 100644 index 0000000..4811c9c --- /dev/null +++ b/src/game/state.rs @@ -0,0 +1,135 @@ +use sdl2::{image::LoadTexture, pixels::Color, render::TextureCreator, video::WindowContext}; +use smallvec::SmallVec; + +use crate::{ + asset::{get_asset_bytes, Asset}, + audio::Audio, + constants::RAW_BOARD, + entity::{ + collision::{Collidable, CollisionSystem, EntityId}, + ghost::{Ghost, GhostType}, + item::Item, + pacman::Pacman, + }, + error::{GameError, GameResult, TextureError}, + map::Map, + texture::{ + sprite::{AtlasMapper, AtlasTile, SpriteAtlas}, + text::TextTexture, + }, +}; + +/// The `GameState` struct holds all the essential data for the game. +/// +/// This includes the score, map, entities (Pac-Man, ghosts, items), +/// collision system, and rendering resources. By centralizing the game's state, +/// we can cleanly separate it from the game's logic, making it easier to manage +/// and reason about. +pub struct GameState { + pub score: u32, + pub map: Map, + pub pacman: Pacman, + pub ghosts: SmallVec<[Ghost; 4]>, + pub items: Vec, + pub debug_mode: bool, + + // Collision system + pub(crate) collision_system: CollisionSystem, + pub(crate) pacman_id: EntityId, + pub(crate) ghost_ids: SmallVec<[EntityId; 4]>, + pub(crate) item_ids: Vec, + + // Rendering resources + pub(crate) atlas: SpriteAtlas, + pub(crate) map_texture: AtlasTile, + pub(crate) text_texture: TextTexture, + + // Audio + pub audio: Audio, +} + +impl GameState { + /// Creates a new `GameState` by initializing all the game's data. + /// + /// This function sets up the map, Pac-Man, ghosts, items, collision system, + /// and all rendering resources required to start the game. It returns a `GameResult` + /// to handle any potential errors during initialization. + pub fn new(texture_creator: &'static TextureCreator) -> GameResult { + let map = Map::new(RAW_BOARD)?; + + let pacman_start_node = map.start_positions.pacman; + + let atlas_bytes = get_asset_bytes(Asset::Atlas)?; + let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| { + if e.to_string().contains("format") || e.to_string().contains("unsupported") { + GameError::Texture(TextureError::InvalidFormat(format!("Unsupported texture format: {e}"))) + } else { + GameError::Texture(TextureError::LoadFailed(e.to_string())) + } + })?; + let atlas_json = get_asset_bytes(Asset::AtlasJson)?; + let atlas_mapper: AtlasMapper = serde_json::from_slice(&atlas_json)?; + let atlas = SpriteAtlas::new(atlas_texture, atlas_mapper); + + let mut map_texture = SpriteAtlas::get_tile(&atlas, "maze/full.png") + .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/full.png".to_string())))?; + map_texture.color = Some(Color::RGB(0x20, 0x20, 0xf9)); + + let text_texture = TextTexture::new(1.0); + let audio = Audio::new(); + let pacman = Pacman::new(&map.graph, pacman_start_node, &atlas)?; + + // Generate items (pellets and energizers) + let items = map.generate_items(&atlas)?; + + // Initialize collision system + let mut collision_system = CollisionSystem::default(); + + // Register Pac-Man + let pacman_id = collision_system.register_entity(pacman.position()); + + // Register items + let mut item_ids = Vec::new(); + for item in &items { + let item_id = collision_system.register_entity(item.position()); + item_ids.push(item_id); + } + + // Create and register ghosts + let ghosts = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde] + .iter() + .zip( + [ + map.start_positions.blinky, + map.start_positions.pinky, + map.start_positions.inky, + map.start_positions.clyde, + ] + .iter(), + ) + .map(|(ghost_type, start_node)| Ghost::new(&map.graph, *start_node, *ghost_type, &atlas)) + .collect::>>()?; + + let ghost_ids = ghosts + .iter() + .map(|ghost| collision_system.register_entity(ghost.position())) + .collect::>(); + + Ok(Self { + score: 0, + map, + pacman, + ghosts, + items, + debug_mode: false, + collision_system, + pacman_id, + ghost_ids, + item_ids, + map_texture, + text_texture, + audio, + atlas, + }) + } +} diff --git a/src/map/builder.rs b/src/map/builder.rs index cfc7040..fdfdf18 100644 --- a/src/map/builder.rs +++ b/src/map/builder.rs @@ -7,7 +7,7 @@ use crate::entity::item::{Item, ItemType}; use crate::map::parser::MapTileParser; use crate::map::render::MapRenderer; use crate::texture::sprite::{AtlasTile, Sprite, SpriteAtlas}; -use glam::{IVec2, UVec2, Vec2}; +use glam::{IVec2, Vec2}; use sdl2::render::{Canvas, RenderTarget}; use std::collections::{HashMap, VecDeque}; use tracing::debug; @@ -15,7 +15,6 @@ use tracing::debug; use crate::error::{GameResult, MapError}; /// The starting positions of the entities in the game. -#[allow(dead_code)] pub struct NodePositions { pub pacman: NodeId, pub blinky: NodeId, @@ -26,18 +25,12 @@ pub struct NodePositions { /// The main map structure containing the game board and navigation graph. pub struct Map { - /// The current state of the map. - #[allow(dead_code)] - current: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize], /// The node map for entity movement. pub graph: Graph, /// A mapping from grid positions to node IDs. pub grid_to_node: HashMap, /// A mapping of the starting positions of the entities. - #[allow(dead_code)] pub start_positions: NodePositions, - /// Pac-Man's starting position. - pacman_start: Option, } impl Map { @@ -56,7 +49,6 @@ impl Map { let map = parsed_map.tiles; let house_door = parsed_map.house_door; let tunnel_ends = parsed_map.tunnel_ends; - let pacman_start = parsed_map.pacman_start; let mut graph = Graph::new(); let mut grid_to_node = HashMap::new(); @@ -64,8 +56,9 @@ impl Map { let cell_offset = Vec2::splat(CELL_SIZE as f32 / 2.0); // Find a starting point for the graph generation, preferably Pac-Man's position. - let start_pos = - pacman_start.ok_or_else(|| MapError::InvalidConfig("Pac-Man's starting position not found".to_string()))?; + let start_pos = parsed_map + .pacman_start + .ok_or_else(|| MapError::InvalidConfig("Pac-Man's starting position not found".to_string()))?; // Add the starting position to the graph/queue let mut queue = VecDeque::new(); @@ -155,31 +148,12 @@ impl Map { Self::build_tunnels(&mut graph, &grid_to_node, &tunnel_ends)?; Ok(Map { - current: map, graph, grid_to_node, start_positions, - pacman_start, }) } - /// Finds the starting position for a given entity ID. - /// - /// # Arguments - /// - /// * `entity_id` - The entity ID (0 for Pac-Man, 1-4 for ghosts) - /// - /// # Returns - /// - /// The starting position as a grid coordinate (`UVec2`), or `None` if not found. - pub fn find_starting_position(&self, entity_id: u8) -> Option { - // For now, only Pac-Man (entity_id 0) is supported - if entity_id == 0 { - return self.pacman_start.map(|pos| UVec2::new(pos.x as u32, pos.y as u32)); - } - None - } - /// Renders the map to the given canvas. /// /// This function draws the static map texture to the screen at the correct diff --git a/src/map/parser.rs b/src/map/parser.rs index 9e35f64..2f80e2c 100644 --- a/src/map/parser.rs +++ b/src/map/parser.rs @@ -1,19 +1,8 @@ //! Map parsing functionality for converting raw board layouts into structured data. use crate::constants::{MapTile, BOARD_CELL_SIZE}; +use crate::error::ParseError; use glam::IVec2; -use thiserror::Error; - -/// Error type for map parsing operations. -#[derive(Debug, Error)] -pub enum ParseError { - #[error("Unknown character in board: {0}")] - UnknownCharacter(char), - #[error("House door must have exactly 2 positions, found {0}")] - InvalidHouseDoorCount(usize), - #[error("Map parsing failed: {0}")] - ParseFailed(String), -} /// Represents the parsed data from a raw board layout. #[derive(Debug)] diff --git a/src/platform/desktop.rs b/src/platform/desktop.rs index ae00cc0..9f42c7e 100644 --- a/src/platform/desktop.rs +++ b/src/platform/desktop.rs @@ -3,8 +3,9 @@ use std::borrow::Cow; use std::time::Duration; -use crate::asset::{Asset, AssetError}; -use crate::platform::{Platform, PlatformError}; +use crate::asset::Asset; +use crate::error::{AssetError, PlatformError}; +use crate::platform::Platform; /// Desktop platform implementation. pub struct DesktopPlatform; diff --git a/src/platform/emscripten.rs b/src/platform/emscripten.rs index 692592b..0cdd8ae 100644 --- a/src/platform/emscripten.rs +++ b/src/platform/emscripten.rs @@ -3,8 +3,9 @@ use std::borrow::Cow; use std::time::Duration; -use crate::asset::{Asset, AssetError}; -use crate::platform::{Platform, PlatformError}; +use crate::asset::Asset; +use crate::error::{AssetError, PlatformError}; +use crate::platform::Platform; /// Emscripten platform implementation. pub struct EmscriptenPlatform; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 7f4e437..e168f7b 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -1,10 +1,10 @@ //! Platform abstraction layer for cross-platform functionality. +use crate::asset::Asset; +use crate::error::{AssetError, PlatformError}; use std::borrow::Cow; use std::time::Duration; -use crate::asset::{Asset, AssetError}; - pub mod desktop; pub mod emscripten; @@ -30,16 +30,6 @@ pub trait Platform { fn get_asset_bytes(&self, asset: Asset) -> Result, AssetError>; } -/// Platform-specific errors. -#[derive(Debug, thiserror::Error)] -#[allow(dead_code)] -pub enum PlatformError { - #[error("Console initialization failed: {0}")] - ConsoleInit(String), - #[error("Platform-specific error: {0}")] - Other(String), -} - /// Get the current platform implementation. #[allow(dead_code)] pub fn get_platform() -> &'static dyn Platform { diff --git a/src/texture/animated.rs b/src/texture/animated.rs index fcf50cb..5bf5e7d 100644 --- a/src/texture/animated.rs +++ b/src/texture/animated.rs @@ -1,16 +1,9 @@ -use anyhow::Result; use sdl2::rect::Rect; use sdl2::render::{Canvas, RenderTarget}; -use thiserror::Error; +use crate::error::{AnimatedTextureError, GameError, GameResult, TextureError}; use crate::texture::sprite::{AtlasTile, SpriteAtlas}; -#[derive(Error, Debug)] -pub enum AnimatedTextureError { - #[error("Frame duration must be positive, got {0}")] - InvalidFrameDuration(f32), -} - #[derive(Debug, Clone)] pub struct AnimatedTexture { tiles: Vec, @@ -20,9 +13,11 @@ pub struct AnimatedTexture { } impl AnimatedTexture { - pub fn new(tiles: Vec, frame_duration: f32) -> Result { + pub fn new(tiles: Vec, frame_duration: f32) -> GameResult { if frame_duration <= 0.0 { - return Err(AnimatedTextureError::InvalidFrameDuration(frame_duration)); + return Err(GameError::Texture(TextureError::Animated( + AnimatedTextureError::InvalidFrameDuration(frame_duration), + ))); } Ok(Self { @@ -45,9 +40,10 @@ impl AnimatedTexture { &self.tiles[self.current_frame] } - pub fn render(&self, canvas: &mut Canvas, atlas: &mut SpriteAtlas, dest: Rect) -> Result<()> { + pub fn render(&self, canvas: &mut Canvas, atlas: &mut SpriteAtlas, dest: Rect) -> GameResult<()> { let mut tile = *self.current_tile(); - tile.render(canvas, atlas, dest) + tile.render(canvas, atlas, dest)?; + Ok(()) } /// Returns the current frame index. diff --git a/src/texture/directional.rs b/src/texture/directional.rs index 7449923..ab477c7 100644 --- a/src/texture/directional.rs +++ b/src/texture/directional.rs @@ -1,8 +1,8 @@ -use anyhow::Result; use sdl2::rect::Rect; use sdl2::render::{Canvas, RenderTarget}; use crate::entity::direction::Direction; +use crate::error::GameResult; use crate::texture::animated::AnimatedTexture; use crate::texture::sprite::SpriteAtlas; @@ -32,7 +32,7 @@ impl DirectionalAnimatedTexture { atlas: &mut SpriteAtlas, dest: Rect, direction: Direction, - ) -> Result<()> { + ) -> GameResult<()> { if let Some(texture) = &self.textures[direction.as_usize()] { texture.render(canvas, atlas, dest) } else { @@ -46,7 +46,7 @@ impl DirectionalAnimatedTexture { atlas: &mut SpriteAtlas, dest: Rect, direction: Direction, - ) -> Result<()> { + ) -> GameResult<()> { if let Some(texture) = &self.stopped_textures[direction.as_usize()] { texture.render(canvas, atlas, dest) } else { diff --git a/src/texture/sprite.rs b/src/texture/sprite.rs index 0a61741..b271c9e 100644 --- a/src/texture/sprite.rs +++ b/src/texture/sprite.rs @@ -6,6 +6,8 @@ use sdl2::render::{Canvas, RenderTarget, Texture}; use serde::Deserialize; use std::collections::HashMap; +use crate::error::TextureError; + /// A simple sprite for stationary items like pellets and energizers. #[derive(Clone, Debug)] pub struct Sprite { @@ -17,13 +19,19 @@ impl Sprite { Self { atlas_tile } } - pub fn render(&self, canvas: &mut Canvas, atlas: &mut SpriteAtlas, position: glam::Vec2) -> Result<()> { + pub fn render( + &self, + canvas: &mut Canvas, + atlas: &mut SpriteAtlas, + position: glam::Vec2, + ) -> Result<(), TextureError> { let dest = crate::helpers::centered_with_size( glam::IVec2::new(position.x as i32, position.y as i32), glam::UVec2::new(self.atlas_tile.size.x as u32, self.atlas_tile.size.y as u32), ); let mut tile = self.atlas_tile; - tile.render(canvas, atlas, dest) + tile.render(canvas, atlas, dest)?; + Ok(()) } } @@ -48,9 +56,15 @@ pub struct AtlasTile { } impl AtlasTile { - pub fn render(&mut self, canvas: &mut Canvas, atlas: &mut SpriteAtlas, dest: Rect) -> Result<()> { + pub fn render( + &mut self, + canvas: &mut Canvas, + atlas: &mut SpriteAtlas, + dest: Rect, + ) -> Result<(), TextureError> { let color = self.color.unwrap_or(atlas.default_color.unwrap_or(Color::WHITE)); - self.render_with_color(canvas, atlas, dest, color) + self.render_with_color(canvas, atlas, dest, color)?; + Ok(()) } pub fn render_with_color( @@ -59,7 +73,7 @@ impl AtlasTile { atlas: &mut SpriteAtlas, dest: Rect, color: Color, - ) -> Result<()> { + ) -> Result<(), TextureError> { let src = Rect::new(self.pos.x as i32, self.pos.y as i32, self.size.x as u32, self.size.y as u32); if atlas.last_modulation != Some(color) { @@ -67,7 +81,7 @@ impl AtlasTile { atlas.last_modulation = Some(color); } - canvas.copy(&atlas.texture, src, dest).map_err(anyhow::Error::msg)?; + canvas.copy(&atlas.texture, src, dest).map_err(TextureError::RenderFailed)?; Ok(()) } diff --git a/tests/animated.rs b/tests/animated.rs index 7a21823..9657a63 100644 --- a/tests/animated.rs +++ b/tests/animated.rs @@ -1,5 +1,6 @@ use glam::U16Vec2; -use pacman::texture::animated::{AnimatedTexture, AnimatedTextureError}; +use pacman::error::{AnimatedTextureError, GameError, TextureError}; +use pacman::texture::animated::AnimatedTexture; use pacman::texture::sprite::AtlasTile; use sdl2::pixels::Color; @@ -17,12 +18,12 @@ fn test_animated_texture_creation_errors() { assert!(matches!( AnimatedTexture::new(tiles.clone(), 0.0).unwrap_err(), - AnimatedTextureError::InvalidFrameDuration(0.0) + GameError::Texture(TextureError::Animated(AnimatedTextureError::InvalidFrameDuration(0.0))) )); assert!(matches!( AnimatedTexture::new(tiles, -0.1).unwrap_err(), - AnimatedTextureError::InvalidFrameDuration(-0.1) + GameError::Texture(TextureError::Animated(AnimatedTextureError::InvalidFrameDuration(-0.1))) )); } diff --git a/tests/game.rs b/tests/game.rs index c5e41c6..508d522 100644 --- a/tests/game.rs +++ b/tests/game.rs @@ -10,15 +10,4 @@ fn test_game_map_creation() { assert!(map.graph.node_count() > 0); assert!(!map.grid_to_node.is_empty()); - - // Should find Pac-Man's starting position - let pacman_pos = map.find_starting_position(0); - assert!(pacman_pos.is_some()); -} - -#[test] -fn test_game_score_initialization() { - // This would require creating a full Game instance, but we can test the concept - let map = Map::new(RAW_BOARD).unwrap(); - assert!(map.find_starting_position(0).is_some()); } diff --git a/tests/map_builder.rs b/tests/map_builder.rs index fa926c7..bc35af9 100644 --- a/tests/map_builder.rs +++ b/tests/map_builder.rs @@ -1,5 +1,5 @@ use glam::Vec2; -use pacman::constants::{BOARD_CELL_SIZE, CELL_SIZE, RAW_BOARD}; +use pacman::constants::{CELL_SIZE, RAW_BOARD}; use pacman::map::Map; use sdl2::render::Texture; @@ -21,19 +21,6 @@ fn test_map_creation() { assert!(has_connections); } -#[test] -fn test_map_starting_positions() { - let map = Map::new(RAW_BOARD).unwrap(); - - let pacman_pos = map.find_starting_position(0); - assert!(pacman_pos.is_some()); - assert!(pacman_pos.unwrap().x < BOARD_CELL_SIZE.x); - assert!(pacman_pos.unwrap().y < BOARD_CELL_SIZE.y); - - let nonexistent_pos = map.find_starting_position(99); - assert_eq!(nonexistent_pos, None); -} - #[test] fn test_map_node_positions() { let map = Map::new(RAW_BOARD).unwrap(); diff --git a/tests/parser.rs b/tests/parser.rs index e3b82a8..53a1ee7 100644 --- a/tests/parser.rs +++ b/tests/parser.rs @@ -1,5 +1,6 @@ use pacman::constants::{BOARD_CELL_SIZE, RAW_BOARD}; -use pacman::map::parser::{MapTileParser, ParseError}; +use pacman::error::ParseError; +use pacman::map::parser::MapTileParser; #[test] fn test_parse_character() {