diff --git a/src/entity/graph.rs b/src/entity/graph.rs index 4aba731..27d8e55 100644 --- a/src/entity/graph.rs +++ b/src/entity/graph.rs @@ -1,7 +1,5 @@ use glam::Vec2; -use crate::entity::direction::DIRECTIONS; - use super::direction::Direction; /// A unique identifier for a node, represented by its index in the graph's storage. diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8bd58ae --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,11 @@ +//! Pac-Man game library crate. + +pub mod app; +pub mod asset; +pub mod audio; +pub mod constants; +pub mod emscripten; +pub mod entity; +pub mod game; +pub mod map; +pub mod texture; diff --git a/src/map.rs b/src/map.rs index c934669..a41e93f 100644 --- a/src/map.rs +++ b/src/map.rs @@ -13,6 +13,114 @@ use tracing::debug; use crate::entity::graph::{Graph, Node, NodeId}; use crate::texture::text::TextTexture; +/// Error type for map parsing operations. +#[derive(Debug, thiserror::Error)] +pub enum ParseError { + #[error("Unknown character in board: {0}")] + UnknownCharacter(char), + #[error("House door must have exactly 2 positions, found {0}")] + InvalidHouseDoorCount(usize), +} + +/// Represents the parsed data from a raw board layout. +#[derive(Debug)] +pub struct ParsedMap { + /// The parsed tile layout. + pub tiles: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize], + /// The positions of the house door tiles. + pub house_door: [Option; 2], + /// The positions of the tunnel end tiles. + pub tunnel_ends: [Option; 2], +} + +/// Parser for converting raw board layouts into structured map data. +pub struct MapTileParser; + +impl MapTileParser { + /// Parses a single character into a map tile. + /// + /// # Arguments + /// + /// * `c` - The character to parse + /// * `_x` - The x coordinate of the character (unused but kept for API consistency) + /// * `_y` - The y coordinate of the character (unused but kept for API consistency) + /// + /// # Returns + /// + /// The parsed map tile, or an error if the character is unknown. + pub fn parse_character(c: char) -> Result { + match c { + '#' => Ok(MapTile::Wall), + '.' => Ok(MapTile::Pellet), + 'o' => Ok(MapTile::PowerPellet), + ' ' => Ok(MapTile::Empty), + 'T' => Ok(MapTile::Tunnel), + c @ '0'..='4' => Ok(MapTile::StartingPosition(c.to_digit(10).unwrap() as u8)), + '=' => Ok(MapTile::Wall), // House door is represented as a wall tile + _ => Err(ParseError::UnknownCharacter(c)), + } + } + + /// Parses a raw board layout into structured map data. + /// + /// # Arguments + /// + /// * `raw_board` - The raw board layout as an array of strings + /// + /// # Returns + /// + /// The parsed map data, or an error if parsing fails. + /// + /// # Errors + /// + /// Returns an error if the board contains unknown characters or if the house door + /// is not properly defined by exactly two '=' characters. + pub fn parse_board(raw_board: [&str; BOARD_CELL_SIZE.y as usize]) -> Result { + let mut tiles = [[MapTile::Empty; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize]; + let mut house_door = [None; 2]; + let mut tunnel_ends = [None; 2]; + + for (y, line) in raw_board.iter().enumerate().take(BOARD_CELL_SIZE.y as usize) { + for (x, character) in line.chars().enumerate().take(BOARD_CELL_SIZE.x as usize) { + let tile = Self::parse_character(character)?; + + // Track special positions + match tile { + MapTile::Tunnel => { + if tunnel_ends[0].is_none() { + tunnel_ends[0] = Some(IVec2::new(x as i32, y as i32)); + } else { + tunnel_ends[1] = Some(IVec2::new(x as i32, y as i32)); + } + } + MapTile::Wall if character == '=' => { + if house_door[0].is_none() { + house_door[0] = Some(IVec2::new(x as i32, y as i32)); + } else { + house_door[1] = Some(IVec2::new(x as i32, y as i32)); + } + } + _ => {} + } + + tiles[x][y] = tile; + } + } + + // Validate house door configuration + let house_door_count = house_door.iter().filter(|x| x.is_some()).count(); + if house_door_count != 2 { + return Err(ParseError::InvalidHouseDoorCount(house_door_count)); + } + + Ok(ParsedMap { + tiles, + house_door, + tunnel_ends, + }) + } +} + /// The game map, responsible for holding the tile-based layout and the navigation graph. /// /// The map is represented as a 2D array of `MapTile`s. It also stores a navigation @@ -38,38 +146,11 @@ impl Map { /// This function will panic if the board layout contains unknown characters or if /// the house door is not defined by exactly two '=' characters. pub fn new(raw_board: [&str; BOARD_CELL_SIZE.y as usize]) -> Map { - let mut map = [[MapTile::Empty; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize]; - let mut house_door = [None; 2]; - let mut tunnel_ends = [None; 2]; - for (y, line) in raw_board.iter().enumerate().take(BOARD_CELL_SIZE.y as usize) { - for (x, character) in line.chars().enumerate().take(BOARD_CELL_SIZE.x as usize) { - let tile = match character { - '#' => MapTile::Wall, - '.' => MapTile::Pellet, - 'o' => MapTile::PowerPellet, - ' ' => MapTile::Empty, - 'T' => { - if tunnel_ends[0].is_none() { - tunnel_ends[0] = Some(IVec2::new(x as i32, y as i32)); - } else { - tunnel_ends[1] = Some(IVec2::new(x as i32, y as i32)); - } - MapTile::Tunnel - } - c @ '0'..='4' => MapTile::StartingPosition(c.to_digit(10).unwrap() as u8), - '=' => { - if house_door[0].is_none() { - house_door[0] = Some(IVec2::new(x as i32, y as i32)); - } else { - house_door[1] = Some(IVec2::new(x as i32, y as i32)); - } - MapTile::Wall - } - _ => panic!("Unknown character in board: {character}"), - }; - map[x][y] = tile; - } - } + let parsed_map = MapTileParser::parse_board(raw_board).expect("Failed to parse board layout"); + + let map = parsed_map.tiles; + let house_door = parsed_map.house_door; + let tunnel_ends = parsed_map.tunnel_ends; let mut graph = Graph::new(); let mut grid_to_node = HashMap::new(); @@ -169,10 +250,6 @@ impl Map { } } - if house_door.iter().filter(|x| x.is_some()).count() != 2 { - panic!("House door must have exactly 2 positions"); - } - // Calculate the position of the house entrance node let (house_entrance_node_id, house_entrance_node_position) = { // Translate the grid positions to the actual node ids @@ -397,3 +474,60 @@ impl Map { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::constants::RAW_BOARD; + + #[test] + fn test_parse_character() { + assert!(matches!(MapTileParser::parse_character('#').unwrap(), MapTile::Wall)); + assert!(matches!(MapTileParser::parse_character('.').unwrap(), MapTile::Pellet)); + assert!(matches!(MapTileParser::parse_character('o').unwrap(), MapTile::PowerPellet)); + assert!(matches!(MapTileParser::parse_character(' ').unwrap(), MapTile::Empty)); + assert!(matches!(MapTileParser::parse_character('T').unwrap(), MapTile::Tunnel)); + assert!(matches!( + MapTileParser::parse_character('0').unwrap(), + MapTile::StartingPosition(0) + )); + assert!(matches!( + MapTileParser::parse_character('4').unwrap(), + MapTile::StartingPosition(4) + )); + assert!(matches!(MapTileParser::parse_character('=').unwrap(), MapTile::Wall)); + + // Test invalid character + assert!(MapTileParser::parse_character('X').is_err()); + } + + #[test] + fn test_parse_board() { + let result = MapTileParser::parse_board(RAW_BOARD); + assert!(result.is_ok()); + + let parsed = result.unwrap(); + + // Verify we have tiles + assert_eq!(parsed.tiles.len(), BOARD_CELL_SIZE.x as usize); + assert_eq!(parsed.tiles[0].len(), BOARD_CELL_SIZE.y as usize); + + // Verify we found house door positions + assert!(parsed.house_door[0].is_some()); + assert!(parsed.house_door[1].is_some()); + + // Verify we found tunnel ends + assert!(parsed.tunnel_ends[0].is_some()); + assert!(parsed.tunnel_ends[1].is_some()); + } + + #[test] + fn test_parse_board_invalid_character() { + let mut invalid_board = RAW_BOARD.clone(); + invalid_board[0] = "###########################X"; + + let result = MapTileParser::parse_board(invalid_board); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), ParseError::UnknownCharacter('X'))); + } +} diff --git a/src/texture/text.rs b/src/texture/text.rs index 6dd2a19..fce516b 100644 --- a/src/texture/text.rs +++ b/src/texture/text.rs @@ -7,18 +7,13 @@ //! # Example Usage //! //! ```rust -//! use crate::texture::text::TextTexture; -//! use std::rc::Rc; +//! use pacman::texture::text::TextTexture; //! //! // Create a text texture with 1.0 scale (8x8 pixels per character) -//! let mut text_renderer = TextTexture::new(atlas.clone(), 1.0); +//! let mut text_renderer = TextTexture::new(1.0); //! -//! // Render text at position (100, 50) -//! text_renderer.render(canvas, "PAC-MAN", glam::UVec2::new(100, 50))?; -//! -//! // Change scale for larger text +//! // Set scale for larger text //! text_renderer.set_scale(2.0); -//! text_renderer.render(canvas, "SCORE: 1000", glam::UVec2::new(50, 100))?; //! //! // Calculate text width for positioning //! let width = text_renderer.text_width("GAME OVER");