diff --git a/Cargo.lock b/Cargo.lock index b2de53f..138c971 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,7 +192,6 @@ dependencies = [ "sdl2", "serde", "serde_json", - "smallvec", "spin_sleep", "thiserror 1.0.69", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 5a393b1..94974e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,6 @@ anyhow = "1.0" glam = { version = "0.30.4", features = [] } serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.141" -smallvec = "1.15.1" [profile.release] lto = true diff --git a/src/entity/graph.rs b/src/entity/graph.rs index 1c1e46b..fbec06f 100644 --- a/src/entity/graph.rs +++ b/src/entity/graph.rs @@ -1,5 +1,4 @@ use glam::Vec2; -use smallvec::SmallVec; use super::direction::Direction; @@ -9,21 +8,88 @@ pub type NodeId = usize; /// Represents a directed edge from one node to another with a given weight (e.g., distance). #[derive(Debug, Clone, Copy)] pub struct Edge { + /// The destination node of this edge. pub target: NodeId, + /// The length of the edge. pub distance: f32, + /// The cardinal direction of this edge. pub direction: Direction, } +/// Represents a node in the graph, defined by its position. #[derive(Debug)] pub struct Node { + /// The 2D coordinates of the node. pub position: Vec2, } -/// A generic, arena-based graph. -/// The graph owns all node data and connection information. +/// Represents the four possible directions from a node in the graph. +/// +/// Each field contains an optional edge leading in that direction. +/// This structure is used to represent the adjacency list for each node, +/// providing O(1) access to edges in any cardinal direction. +#[derive(Debug)] +pub struct Intersection { + /// Edge leading upward from this node, if it exists. + pub up: Option, + /// Edge leading downward from this node, if it exists. + pub down: Option, + /// Edge leading leftward from this node, if it exists. + pub left: Option, + /// Edge leading rightward from this node, if it exists. + pub right: Option, +} + +impl Default for Intersection { + fn default() -> Self { + Self { + up: None, + down: None, + left: None, + right: None, + } + } +} + +impl Intersection { + /// Returns an iterator over all edges from this intersection. + /// + /// This iterator yields only the edges that exist (non-None values). + pub fn edges(&self) -> impl Iterator { + [self.up, self.down, self.left, self.right].into_iter().flatten() + } + + /// Retrieves the edge in the specified direction, if it exists. + pub fn get(&self, direction: Direction) -> Option { + match direction { + Direction::Up => self.up, + Direction::Down => self.down, + Direction::Left => self.left, + Direction::Right => self.right, + } + } + + /// Sets the edge in the specified direction. + /// + /// This will overwrite any existing edge in that direction. + pub fn set(&mut self, direction: Direction, edge: Edge) { + match direction { + Direction::Up => self.up = Some(edge), + Direction::Down => self.down = Some(edge), + Direction::Left => self.left = Some(edge), + Direction::Right => self.right = Some(edge), + } + } +} + +/// A directed graph structure using an adjacency list representation. +/// +/// Nodes are stored in a vector, and their indices serve as their `NodeId`. +/// This design provides fast, O(1) lookups for node data. Edges are stored +/// in an adjacency list, where each node has a list of outgoing edges. pub struct Graph { nodes: Vec, - adjacency_list: Vec>, + adjacency_list: Vec, } impl Graph { @@ -39,11 +105,22 @@ impl Graph { pub fn add_node(&mut self, data: Node) -> NodeId { let id = self.nodes.len(); self.nodes.push(data); - self.adjacency_list.push(SmallVec::new()); + self.adjacency_list.push(Intersection::default()); id } /// Adds a directed edge between two nodes. + /// + /// If `distance` is `None`, it will be calculated automatically based on the + /// Euclidean distance between the two nodes. + /// + /// # Errors + /// + /// Returns an error if: + /// - The `from` node does not exist + /// - An edge already exists in the specified direction + /// - An edge already exists to the target node + /// - The provided distance is not positive pub fn add_edge( &mut self, from: NodeId, @@ -77,7 +154,7 @@ impl Graph { let adjacency_list = &mut self.adjacency_list[from]; // Check if the edge already exists in this direction or to the same target - if let Some(err) = adjacency_list.iter().find_map(|e| { + if let Some(err) = adjacency_list.edges().find_map(|e| { if e.direction == direction { Some(Err("Edge already exists in this direction.")) } else if e.target == to { @@ -89,7 +166,7 @@ impl Graph { return err; } - adjacency_list.push(edge); + adjacency_list.set(direction, edge); Ok(()) } @@ -99,17 +176,19 @@ impl Graph { self.nodes.get(id) } + /// Returns the total number of nodes in the graph. pub fn node_count(&self) -> usize { self.nodes.len() } /// Finds a specific edge from a source node to a target node. - pub fn find_edge(&self, from: NodeId, to: NodeId) -> Option<&Edge> { - self.adjacency_list.get(from)?.iter().find(|edge| edge.target == to) + pub fn find_edge(&self, from: NodeId, to: NodeId) -> Option { + self.adjacency_list.get(from)?.edges().find(|edge| edge.target == to) } - pub fn find_edge_in_direction(&self, from: NodeId, direction: Direction) -> Option<&Edge> { - self.adjacency_list.get(from)?.iter().find(|edge| edge.direction == direction) + /// Finds an edge originating from a given node that follows a specific direction. + pub fn find_edge_in_direction(&self, from: NodeId, direction: Direction) -> Option { + self.adjacency_list.get(from)?.get(direction) } } @@ -122,7 +201,10 @@ impl Default for Graph { // --- Traversal State and Logic --- -/// Represents the traverser's current position within the graph. +/// Represents the current position of an entity traversing the graph. +/// +/// This enum allows for precise tracking of whether an entity is exactly at a node +/// or moving along an edge between two nodes. #[derive(Debug, PartialEq, Clone, Copy)] pub enum Position { /// The traverser is located exactly at a node. @@ -137,10 +219,12 @@ pub enum Position { } impl Position { + /// Returns `true` if the position is exactly at a node. pub fn is_at_node(&self) -> bool { matches!(self, Position::AtNode(_)) } + /// Returns the `NodeId` of the current or most recently departed node. pub fn from_node_id(&self) -> NodeId { match self { Position::AtNode(id) => *id, @@ -148,6 +232,7 @@ impl Position { } } + /// Returns the `NodeId` of the destination node, if currently on an edge. pub fn to_node_id(&self) -> Option { match self { Position::AtNode(_) => None, @@ -155,21 +240,34 @@ impl Position { } } + /// Returns `true` if the traverser is stopped at a node. pub fn is_stopped(&self) -> bool { matches!(self, Position::AtNode(_)) } } -/// Manages a traversal session over a graph. -/// It holds a reference to the graph and the current position state. +/// Manages an entity's movement through the graph. +/// +/// A `Traverser` encapsulates the state of an entity's position and direction, +/// providing a way to advance along the graph's paths based on a given distance. +/// It also handles direction changes, buffering the next intended direction. pub struct Traverser { + /// The current position of the traverser in the graph. pub position: Position, + /// The current direction of movement. pub direction: Direction, + /// Buffered direction change with remaining frame count for timing. + /// + /// The `u8` value represents the number of frames remaining before + /// the buffered direction expires. This allows for responsive controls + /// by storing direction changes for a limited time. pub next_direction: Option<(Direction, u8)>, } impl Traverser { /// Creates a new traverser starting at the given node ID. + /// + /// The traverser will immediately attempt to start moving in the initial direction. pub fn new(graph: &Graph, start_node: NodeId, initial_direction: Direction) -> Self { let mut traverser = Traverser { position: Position::AtNode(start_node), @@ -183,12 +281,27 @@ impl Traverser { traverser } + /// Sets the next direction for the traverser to take. + /// + /// The direction is buffered and will be applied at the next opportunity, + /// typically when the traverser reaches a new node. This allows for responsive + /// controls, as the new direction is stored for a limited time. pub fn set_next_direction(&mut self, new_direction: Direction) { if self.direction != new_direction { self.next_direction = Some((new_direction, 30)); } } + /// Advances the traverser along the graph by a specified distance. + /// + /// This method updates the traverser's position based on its current state + /// and the distance to travel. + /// + /// - If at a node, it checks for a buffered direction to start moving. + /// - If between nodes, it moves along the current edge. + /// - If it reaches a node, it attempts to transition to a new edge based on + /// the buffered direction or by continuing straight. + /// - If no valid move is possible, it stops at the node. pub fn advance(&mut self, graph: &Graph, distance: f32) { // Decrement the remaining frames for the next direction if let Some((direction, remaining)) = self.next_direction { diff --git a/src/map.rs b/src/map.rs index 1f882ce..54cf047 100644 --- a/src/map.rs +++ b/src/map.rs @@ -7,17 +7,17 @@ use glam::{IVec2, UVec2, Vec2}; use sdl2::pixels::Color; use sdl2::rect::{Point, Rect}; use sdl2::render::{Canvas, RenderTarget}; -use smallvec::SmallVec; use std::collections::{HashMap, VecDeque}; use tracing::info; use crate::entity::graph::{Graph, Node}; use crate::texture::text::TextTexture; -/// The game map. +/// 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 copy of -/// the original map, which can be used to reset the map to its initial state. +/// The map is represented as a 2D array of `MapTile`s. It also stores a navigation +/// `Graph` that entities like Pac-Man and ghosts use for movement. The graph is +/// generated from the walkable tiles of the map. pub struct Map { /// The current state of the map. current: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize], @@ -28,12 +28,16 @@ pub struct Map { impl Map { /// Creates a new `Map` instance from a raw board layout. /// - /// # Arguments + /// This constructor initializes the map tiles based on the provided character layout + /// and then generates a navigation graph from the walkable areas. /// - /// * `raw_board` - A 2D array of characters representing the board layout. + /// # Panics + /// + /// 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 = SmallVec::<[IVec2; 2]>::new(); + let mut house_door = [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 { @@ -44,7 +48,11 @@ impl Map { 'T' => MapTile::Tunnel, c @ '0'..='4' => MapTile::StartingPosition(c.to_digit(10).unwrap() as u8), '=' => { - house_door.push(IVec2::new(x as i32, y as i32)); + 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}"), @@ -53,17 +61,17 @@ impl Map { } } - if house_door.len() != 2 { + if house_door.iter().filter(|x| x.is_some()).count() != 2 { panic!("House door must have exactly 2 positions"); } - let mut graph = Self::create_graph(&map); + let mut graph = Self::generate_graph(&map); let house_door_node_id = { let offset = Vec2::splat(CELL_SIZE as f32 / 2.0); - let position_a = house_door[0].as_vec2() * Vec2::splat(CELL_SIZE as f32) + offset; - let position_b = house_door[1].as_vec2() * Vec2::splat(CELL_SIZE as f32) + offset; + let position_a = house_door[0].unwrap().as_vec2() * Vec2::splat(CELL_SIZE as f32) + offset; + let position_b = house_door[1].unwrap().as_vec2() * Vec2::splat(CELL_SIZE as f32) + offset; info!("Position A: {position_a}, Position B: {position_b}"); let position = position_a.lerp(position_b, 0.5); @@ -74,7 +82,13 @@ impl Map { Map { current: map, graph } } - fn create_graph(map: &[[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize]) -> Graph { + /// Generates a navigation graph from the given map layout. + /// + /// This function performs a breadth-first search (BFS) starting from Pac-Man's + /// initial position to identify all walkable tiles and create a connected graph. + /// Nodes are placed at the center of each walkable tile, and edges are created + /// between adjacent walkable tiles. + fn generate_graph(map: &[[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize]) -> Graph { let mut graph = Graph::new(); let mut grid_to_node = HashMap::new(); @@ -160,7 +174,7 @@ impl Map { /// /// # Returns /// - /// The starting position as UVec2, or None if not found + /// The starting position as a grid coordinate (`UVec2`), or `None` if not found. pub fn find_starting_position(&self, entity_id: u8) -> Option { for (x, col) in self.current.iter().enumerate().take(BOARD_CELL_SIZE.x as usize) { for (y, &cell) in col.iter().enumerate().take(BOARD_CELL_SIZE.y as usize) { @@ -174,7 +188,10 @@ impl Map { None } - /// Renders the map to the given canvas using the provided map texture. + /// Renders the map to the given canvas. + /// + /// This function draws the static map texture to the screen at the correct + /// position and scale. pub fn render(&self, canvas: &mut Canvas, atlas: &mut SpriteAtlas, map_texture: &mut AtlasTile) { let dest = Rect::new( BOARD_PIXEL_OFFSET.x as i32, @@ -185,6 +202,11 @@ impl Map { let _ = map_texture.render(canvas, atlas, dest); } + /// Renders a debug visualization of the navigation graph. + /// + /// This function is intended for development and debugging purposes. It draws the + /// nodes and edges of the graph on top of the map, allowing for visual + /// inspection of the navigation paths. pub fn debug_render_nodes(&self, canvas: &mut Canvas, atlas: &mut SpriteAtlas, text: &mut TextTexture) { for i in 0..self.graph.node_count() { let node = self.graph.get_node(i).unwrap();