diff --git a/src/constants.rs b/src/constants.rs index cc3fe1c..d8de78f 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -56,8 +56,8 @@ pub const RAW_BOARD: [&str; BOARD_CELL_SIZE.y as usize] = [ "#......##....##....##......#", "######.##### ## #####.######", " #.##### ## #####.# ", - " #.## 1 ##.# ", - " #.## ###==### ##.# ", + " #.## == ##.# ", + " #.## ######## ##.# ", "######.## ######## ##.######", "T . ######## . T", "######.## ######## ##.######", diff --git a/src/entity/graph.rs b/src/entity/graph.rs index a721dda..4aba731 100644 --- a/src/entity/graph.rs +++ b/src/entity/graph.rs @@ -112,13 +112,21 @@ impl Graph { } /// Connects a new node to the graph and adds an edge between the existing node and the new node. - pub fn connect_node(&mut self, from: NodeId, direction: Direction, new_node: Node) -> Result<(), &'static str> { + pub fn connect_node(&mut self, from: NodeId, direction: Direction, new_node: Node) -> Result { let to = self.add_node(new_node); - self.connect(from, to, None, direction) + self.connect(from, to, false, None, direction)?; + Ok(to) } /// Connects two existing nodes with an edge. - pub fn connect(&mut self, from: NodeId, to: NodeId, distance: Option, direction: Direction) -> Result<(), &'static str> { + pub fn connect( + &mut self, + from: NodeId, + to: NodeId, + replace: bool, + distance: Option, + direction: Direction, + ) -> Result<(), &'static str> { if from >= self.adjacency_list.len() { return Err("From node does not exist."); } @@ -126,8 +134,8 @@ impl Graph { return Err("To node does not exist."); } - let edge_a = self.add_edge(from, to, distance, direction); - let edge_b = self.add_edge(to, from, distance, direction.opposite()); + let edge_a = self.add_edge(from, to, replace, distance, direction); + let edge_b = self.add_edge(to, from, replace, distance, direction.opposite()); if edge_a.is_err() && edge_b.is_err() { return Err("Failed to connect nodes in both directions."); @@ -152,6 +160,7 @@ impl Graph { &mut self, from: NodeId, to: NodeId, + replace: bool, distance: Option, direction: Direction, ) -> Result<(), &'static str> { @@ -159,8 +168,8 @@ impl Graph { target: to, distance: match distance { Some(distance) => { - if distance <= 0.0 { - return Err("Edge distance must be positive."); + if distance < 0.0 { + return Err("Edge distance must be on-negative."); } distance } @@ -182,7 +191,8 @@ impl Graph { // Check if the edge already exists in this direction or to the same target if let Some(err) = adjacency_list.edges().find_map(|e| { - if e.direction == direction { + // If we're not replacing the edge, we don't want to replace an edge that already exists in this direction + if !replace && e.direction == direction { Some(Err("Edge already exists in this direction.")) } else if e.target == to { Some(Err("Edge already exists.")) diff --git a/src/game.rs b/src/game.rs index fe9dff2..581a3a5 100644 --- a/src/game.rs +++ b/src/game.rs @@ -50,8 +50,11 @@ impl Game { ) -> Game { let map = Map::new(RAW_BOARD); - let _pacman_start_pos = map.find_starting_position(0).unwrap(); - let pacman_start_node = 0; // TODO: Find the actual start node + let pacman_start_pos = map.find_starting_position(0).unwrap(); + let pacman_start_node = *map + .grid_to_node + .get(&glam::IVec2::new(pacman_start_pos.x as i32, pacman_start_pos.y as i32)) + .expect("Pac-Man starting position not found in graph"); let atlas_bytes = get_asset_bytes(Asset::Atlas).expect("Failed to load asset"); let atlas_texture = unsafe { diff --git a/src/map.rs b/src/map.rs index 049b0d3..c934669 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,14 +1,14 @@ //! This module defines the game map and provides functions for interacting with it. use crate::constants::{MapTile, BOARD_CELL_SIZE, BOARD_PIXEL_OFFSET, BOARD_PIXEL_SIZE, CELL_SIZE}; -use crate::entity::direction::DIRECTIONS; +use crate::entity::direction::{Direction, DIRECTIONS}; use crate::texture::sprite::{AtlasTile, SpriteAtlas}; use glam::{IVec2, UVec2, Vec2}; use sdl2::pixels::Color; use sdl2::rect::{Point, Rect}; use sdl2::render::{Canvas, RenderTarget}; use std::collections::{HashMap, VecDeque}; -use tracing::info; +use tracing::debug; use crate::entity::graph::{Graph, Node, NodeId}; use crate::texture::text::TextTexture; @@ -23,6 +23,8 @@ pub struct Map { 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, } impl Map { @@ -38,6 +40,7 @@ impl Map { 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 { @@ -45,7 +48,14 @@ impl Map { '.' => MapTile::Pellet, 'o' => MapTile::PowerPellet, ' ' => MapTile::Empty, - 'T' => MapTile::Tunnel, + '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() { @@ -61,37 +71,6 @@ impl Map { } } - if house_door.iter().filter(|x| x.is_some()).count() != 2 { - panic!("House door must have exactly 2 positions"); - } - - 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].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); - - graph.add_node(Node { position }) - }; - info!("House door node id: {house_door_node_id}"); - - // Connect the house door node to nearby nodes - Self::connect_house_door(&mut graph, house_door_node_id, &map); - - Map { current: map, 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(); @@ -118,9 +97,9 @@ impl Map { .expect("No valid starting position found on map for graph generation") }); + // Add the starting position to the graph/queue let mut queue = VecDeque::new(); queue.push_back(start_pos); - let pos = Vec2::new( (start_pos.x * CELL_SIZE as i32) as f32, (start_pos.y * CELL_SIZE as i32) as f32, @@ -128,10 +107,12 @@ impl Map { let node_id = graph.add_node(Node { position: pos }); grid_to_node.insert(start_pos, node_id); + // Iterate over the queue, adding nodes to the graph and connecting them to their neighbors while let Some(source_position) = queue.pop_front() { for &dir in DIRECTIONS.iter() { let new_position = source_position + dir.to_ivec2(); + // Skip if the new position is out of bounds if new_position.x < 0 || new_position.x >= BOARD_CELL_SIZE.x as i32 || new_position.y < 0 @@ -140,14 +121,17 @@ impl Map { continue; } + // Skip if the new position is already in the graph if grid_to_node.contains_key(&new_position) { continue; } + // Skip if the new position is not a walkable tile if matches!( map[new_position.x as usize][new_position.y as usize], MapTile::Pellet | MapTile::PowerPellet | MapTile::Empty | MapTile::Tunnel | MapTile::StartingPosition(_) ) { + // Add the new position to the graph/queue let pos = Vec2::new( (new_position.x * CELL_SIZE as i32) as f32, (new_position.y * CELL_SIZE as i32) as f32, @@ -161,14 +145,15 @@ impl Map { .get(&source_position) .expect(&format!("Source node not found for {source_position}")); + // Connect the new node to the source node graph - .connect(*source_node_id, new_node_id, None, dir) + .connect(*source_node_id, new_node_id, false, None, dir) .expect("Failed to add edge"); } } } - // While most nodes are already connected to their neighbors, some may not be + // While most nodes are already connected to their neighbors, some may not be, so we need to connect them for (grid_pos, &node_id) in &grid_to_node { for dir in DIRECTIONS { // If the node doesn't have an edge in this direction, look for a neighbor in that direction @@ -176,75 +161,161 @@ impl Map { let neighbor = grid_pos + dir.to_ivec2(); // If the neighbor exists, connect the node to it if let Some(&neighbor_id) = grid_to_node.get(&neighbor) { - graph.connect(node_id, neighbor_id, None, dir).expect("Failed to add edge"); + graph + .connect(node_id, neighbor_id, false, None, dir) + .expect("Failed to add edge"); } } } } - graph - } - - /// Connects the house door node to nearby walkable nodes in the graph. - /// - /// This function finds nodes within a reasonable distance of the house door - /// and creates bidirectional connections to them. - fn connect_house_door( - graph: &mut Graph, - house_door_node_id: NodeId, - _map: &[[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize], - ) { - let house_position = graph.get_node(house_door_node_id).unwrap().position; - let connection_distance = CELL_SIZE as f32 * 1.5; // Connect to nodes within 1.5 cells - - // Find all nodes that should be connected to the house door - for node_id in 0..graph.node_count() { - if node_id == house_door_node_id { - continue; // Skip the house door node itself - } - - let node_position = graph.get_node(node_id).unwrap().position; - let distance = house_position.distance(node_position); - - if distance <= connection_distance { - // Determine the direction from house door to this node - let direction = Self::direction_from_to(house_position, node_position); - - // Add bidirectional connection - if let Err(e) = graph.add_edge(house_door_node_id, node_id, None, direction) { - info!("Failed to connect house door to node {}: {}", node_id, e); - } - - // Add reverse connection - let reverse_direction = direction.opposite(); - if let Err(e) = graph.add_edge(node_id, house_door_node_id, None, reverse_direction) { - info!("Failed to connect node {} to house door: {}", node_id, e); - } - } + if house_door.iter().filter(|x| x.is_some()).count() != 2 { + panic!("House door must have exactly 2 positions"); } - } - /// Determines the primary direction from one position to another. - /// - /// This is a simplified direction calculation that prioritizes the axis - /// with the larger difference. - fn direction_from_to(from: Vec2, to: Vec2) -> crate::entity::direction::Direction { - let diff = to - from; - let abs_x = diff.x.abs(); - let abs_y = diff.y.abs(); + // 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 + let left_node = grid_to_node + .get(&(house_door[0].expect("First house door position not acquired") + Direction::Left.to_ivec2())) + .expect("Left house door node not found"); + let right_node = grid_to_node + .get(&(house_door[1].expect("Second house door position not acquired") + Direction::Right.to_ivec2())) + .expect("Right house door node not found"); - if abs_x > abs_y { - if diff.x > 0.0 { - crate::entity::direction::Direction::Right - } else { - crate::entity::direction::Direction::Left - } - } else { - if diff.y > 0.0 { - crate::entity::direction::Direction::Down - } else { - crate::entity::direction::Direction::Up - } + // Calculate the position of the house node + let (node_id, node_position) = { + let left_pos = graph.get_node(*left_node).unwrap().position; + let right_pos = graph.get_node(*right_node).unwrap().position; + let house_node = graph.add_node(Node { + position: left_pos.lerp(right_pos, 0.5), + }); + (house_node, left_pos.lerp(right_pos, 0.5)) + }; + + // Connect the house door to the left and right nodes + graph + .connect(node_id, *left_node, true, None, Direction::Left) + .expect("Failed to connect house door to left node"); + graph + .connect(node_id, *right_node, true, None, Direction::Right) + .expect("Failed to connect house door to right node"); + + (node_id, node_position) + }; + + // A helper function to help create the various 'lines' of nodes within the house + let create_house_line = |graph: &mut Graph, center_pos: Vec2| -> (NodeId, NodeId) { + // Place the nodes at, above, and below the center position + let center_node_id = graph.add_node(Node { position: center_pos }); + let top_node_id = graph.add_node(Node { + position: center_pos + (Direction::Up.to_ivec2() * (CELL_SIZE as i32 / 2)).as_vec2(), + }); + let bottom_node_id = graph.add_node(Node { + position: center_pos + (Direction::Down.to_ivec2() * (CELL_SIZE as i32 / 2)).as_vec2(), + }); + + // Connect the center node to the top and bottom nodes + graph + .connect(center_node_id, top_node_id, false, None, Direction::Up) + .expect("Failed to connect house line to left node"); + graph + .connect(center_node_id, bottom_node_id, false, None, Direction::Down) + .expect("Failed to connect house line to right node"); + + (center_node_id, top_node_id) + }; + + // Calculate the position of the center line's center node + let center_line_center_position = + house_entrance_node_position + (Direction::Down.to_ivec2() * (3 * CELL_SIZE as i32)).as_vec2(); + + // Create the center line + let (center_center_node_id, center_top_node_id) = create_house_line(&mut graph, center_line_center_position); + + // Connect the house entrance to the top line + graph + .connect(house_entrance_node_id, center_top_node_id, false, None, Direction::Down) + .expect("Failed to connect house entrance to top line"); + + // Create the left line + let (left_center_node_id, _) = create_house_line( + &mut graph, + center_line_center_position + (Direction::Left.to_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(), + ); + + // Create the right line + let (right_center_node_id, _) = create_house_line( + &mut graph, + center_line_center_position + (Direction::Right.to_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(), + ); + + debug!("Left center node id: {left_center_node_id}"); + + // Connect the center line to the left and right lines + graph + .connect(center_center_node_id, left_center_node_id, false, None, Direction::Left) + .expect("Failed to connect house entrance to left top line"); + + graph + .connect(center_center_node_id, right_center_node_id, false, None, Direction::Right) + .expect("Failed to connect house entrance to right top line"); + + debug!("House entrance node id: {house_entrance_node_id}"); + + // Create the hidden tunnel nodes + let left_tunnel_hidden_node_id = { + let left_tunnel_entrance_node_id = grid_to_node[&tunnel_ends[0].expect("Left tunnel end not found")]; + let left_tunnel_entrance_node = graph + .get_node(left_tunnel_entrance_node_id) + .expect("Left tunnel entrance node not found"); + + graph + .connect_node( + left_tunnel_entrance_node_id, + Direction::Left, + Node { + position: left_tunnel_entrance_node.position + + (Direction::Left.to_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(), + }, + ) + .expect("Failed to connect left tunnel entrance to left tunnel hidden node") + }; + + // Create the right tunnel nodes + let right_tunnel_hidden_node_id = { + let right_tunnel_entrance_node_id = grid_to_node[&tunnel_ends[1].expect("Right tunnel end not found")]; + let right_tunnel_entrance_node = graph + .get_node(right_tunnel_entrance_node_id) + .expect("Right tunnel entrance node not found"); + + graph + .connect_node( + right_tunnel_entrance_node_id, + Direction::Right, + Node { + position: right_tunnel_entrance_node.position + + (Direction::Right.to_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(), + }, + ) + .expect("Failed to connect right tunnel entrance to right tunnel hidden node") + }; + + // Connect the left tunnel hidden node to the right tunnel hidden node + graph + .connect( + left_tunnel_hidden_node_id, + right_tunnel_hidden_node_id, + false, + Some(0.0), + Direction::Left, + ) + .expect("Failed to connect left tunnel hidden node to right tunnel hidden node"); + + Map { + current: map, + grid_to_node, + graph, } }