feat: working perfect tunnels with offset house positioning nodes

This commit is contained in:
2025-07-28 14:34:24 -05:00
parent bea915b5c7
commit 4a7ff920a6
4 changed files with 194 additions and 110 deletions

View File

@@ -56,8 +56,8 @@ pub const RAW_BOARD: [&str; BOARD_CELL_SIZE.y as usize] = [
"#......##....##....##......#", "#......##....##....##......#",
"######.##### ## #####.######", "######.##### ## #####.######",
" #.##### ## #####.# ", " #.##### ## #####.# ",
" #.## 1 ##.# ", " #.## == ##.# ",
" #.## ###==### ##.# ", " #.## ######## ##.# ",
"######.## ######## ##.######", "######.## ######## ##.######",
"T . ######## . T", "T . ######## . T",
"######.## ######## ##.######", "######.## ######## ##.######",

View File

@@ -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. /// 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<NodeId, &'static str> {
let to = self.add_node(new_node); 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. /// Connects two existing nodes with an edge.
pub fn connect(&mut self, from: NodeId, to: NodeId, distance: Option<f32>, direction: Direction) -> Result<(), &'static str> { pub fn connect(
&mut self,
from: NodeId,
to: NodeId,
replace: bool,
distance: Option<f32>,
direction: Direction,
) -> Result<(), &'static str> {
if from >= self.adjacency_list.len() { if from >= self.adjacency_list.len() {
return Err("From node does not exist."); return Err("From node does not exist.");
} }
@@ -126,8 +134,8 @@ impl Graph {
return Err("To node does not exist."); return Err("To node does not exist.");
} }
let edge_a = self.add_edge(from, to, distance, direction); let edge_a = self.add_edge(from, to, replace, distance, direction);
let edge_b = self.add_edge(to, from, distance, direction.opposite()); let edge_b = self.add_edge(to, from, replace, distance, direction.opposite());
if edge_a.is_err() && edge_b.is_err() { if edge_a.is_err() && edge_b.is_err() {
return Err("Failed to connect nodes in both directions."); return Err("Failed to connect nodes in both directions.");
@@ -152,6 +160,7 @@ impl Graph {
&mut self, &mut self,
from: NodeId, from: NodeId,
to: NodeId, to: NodeId,
replace: bool,
distance: Option<f32>, distance: Option<f32>,
direction: Direction, direction: Direction,
) -> Result<(), &'static str> { ) -> Result<(), &'static str> {
@@ -159,8 +168,8 @@ impl Graph {
target: to, target: to,
distance: match distance { distance: match distance {
Some(distance) => { Some(distance) => {
if distance <= 0.0 { if distance < 0.0 {
return Err("Edge distance must be positive."); return Err("Edge distance must be on-negative.");
} }
distance distance
} }
@@ -182,7 +191,8 @@ impl Graph {
// Check if the edge already exists in this direction or to the same target // 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 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.")) Some(Err("Edge already exists in this direction."))
} else if e.target == to { } else if e.target == to {
Some(Err("Edge already exists.")) Some(Err("Edge already exists."))

View File

@@ -50,8 +50,11 @@ impl Game {
) -> Game { ) -> Game {
let map = Map::new(RAW_BOARD); let map = Map::new(RAW_BOARD);
let _pacman_start_pos = map.find_starting_position(0).unwrap(); let pacman_start_pos = map.find_starting_position(0).unwrap();
let pacman_start_node = 0; // TODO: Find the actual start node 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_bytes = get_asset_bytes(Asset::Atlas).expect("Failed to load asset");
let atlas_texture = unsafe { let atlas_texture = unsafe {

View File

@@ -1,14 +1,14 @@
//! This module defines the game map and provides functions for interacting with it. //! 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::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 crate::texture::sprite::{AtlasTile, SpriteAtlas};
use glam::{IVec2, UVec2, Vec2}; use glam::{IVec2, UVec2, Vec2};
use sdl2::pixels::Color; use sdl2::pixels::Color;
use sdl2::rect::{Point, Rect}; use sdl2::rect::{Point, Rect};
use sdl2::render::{Canvas, RenderTarget}; use sdl2::render::{Canvas, RenderTarget};
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use tracing::info; use tracing::debug;
use crate::entity::graph::{Graph, Node, NodeId}; use crate::entity::graph::{Graph, Node, NodeId};
use crate::texture::text::TextTexture; 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], current: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize],
/// The node map for entity movement. /// The node map for entity movement.
pub graph: Graph, pub graph: Graph,
/// A mapping from grid positions to node IDs.
pub grid_to_node: HashMap<IVec2, NodeId>,
} }
impl Map { impl Map {
@@ -38,6 +40,7 @@ impl Map {
pub fn new(raw_board: [&str; BOARD_CELL_SIZE.y as usize]) -> 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 map = [[MapTile::Empty; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize];
let mut house_door = [None; 2]; 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 (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) { for (x, character) in line.chars().enumerate().take(BOARD_CELL_SIZE.x as usize) {
let tile = match character { let tile = match character {
@@ -45,7 +48,14 @@ impl Map {
'.' => MapTile::Pellet, '.' => MapTile::Pellet,
'o' => MapTile::PowerPellet, 'o' => MapTile::PowerPellet,
' ' => MapTile::Empty, ' ' => 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), c @ '0'..='4' => MapTile::StartingPosition(c.to_digit(10).unwrap() as u8),
'=' => { '=' => {
if house_door[0].is_none() { 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 graph = Graph::new();
let mut grid_to_node = HashMap::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") .expect("No valid starting position found on map for graph generation")
}); });
// Add the starting position to the graph/queue
let mut queue = VecDeque::new(); let mut queue = VecDeque::new();
queue.push_back(start_pos); queue.push_back(start_pos);
let pos = Vec2::new( let pos = Vec2::new(
(start_pos.x * CELL_SIZE as i32) as f32, (start_pos.x * CELL_SIZE as i32) as f32,
(start_pos.y * 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 }); let node_id = graph.add_node(Node { position: pos });
grid_to_node.insert(start_pos, node_id); 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() { while let Some(source_position) = queue.pop_front() {
for &dir in DIRECTIONS.iter() { for &dir in DIRECTIONS.iter() {
let new_position = source_position + dir.to_ivec2(); let new_position = source_position + dir.to_ivec2();
// Skip if the new position is out of bounds
if new_position.x < 0 if new_position.x < 0
|| new_position.x >= BOARD_CELL_SIZE.x as i32 || new_position.x >= BOARD_CELL_SIZE.x as i32
|| new_position.y < 0 || new_position.y < 0
@@ -140,14 +121,17 @@ impl Map {
continue; continue;
} }
// Skip if the new position is already in the graph
if grid_to_node.contains_key(&new_position) { if grid_to_node.contains_key(&new_position) {
continue; continue;
} }
// Skip if the new position is not a walkable tile
if matches!( if matches!(
map[new_position.x as usize][new_position.y as usize], map[new_position.x as usize][new_position.y as usize],
MapTile::Pellet | MapTile::PowerPellet | MapTile::Empty | MapTile::Tunnel | MapTile::StartingPosition(_) MapTile::Pellet | MapTile::PowerPellet | MapTile::Empty | MapTile::Tunnel | MapTile::StartingPosition(_)
) { ) {
// Add the new position to the graph/queue
let pos = Vec2::new( let pos = Vec2::new(
(new_position.x * CELL_SIZE as i32) as f32, (new_position.x * CELL_SIZE as i32) as f32,
(new_position.y * CELL_SIZE as i32) as f32, (new_position.y * CELL_SIZE as i32) as f32,
@@ -161,14 +145,15 @@ impl Map {
.get(&source_position) .get(&source_position)
.expect(&format!("Source node not found for {source_position}")); .expect(&format!("Source node not found for {source_position}"));
// Connect the new node to the source node
graph 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"); .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 (grid_pos, &node_id) in &grid_to_node {
for dir in DIRECTIONS { for dir in DIRECTIONS {
// If the node doesn't have an edge in this direction, look for a neighbor in that direction // 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(); let neighbor = grid_pos + dir.to_ivec2();
// If the neighbor exists, connect the node to it // If the neighbor exists, connect the node to it
if let Some(&neighbor_id) = grid_to_node.get(&neighbor) { 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 if house_door.iter().filter(|x| x.is_some()).count() != 2 {
} panic!("House door must have exactly 2 positions");
/// 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);
}
}
} }
}
/// Determines the primary direction from one position to another. // Calculate the position of the house entrance node
/// let (house_entrance_node_id, house_entrance_node_position) = {
/// This is a simplified direction calculation that prioritizes the axis // Translate the grid positions to the actual node ids
/// with the larger difference. let left_node = grid_to_node
fn direction_from_to(from: Vec2, to: Vec2) -> crate::entity::direction::Direction { .get(&(house_door[0].expect("First house door position not acquired") + Direction::Left.to_ivec2()))
let diff = to - from; .expect("Left house door node not found");
let abs_x = diff.x.abs(); let right_node = grid_to_node
let abs_y = diff.y.abs(); .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 { // Calculate the position of the house node
if diff.x > 0.0 { let (node_id, node_position) = {
crate::entity::direction::Direction::Right let left_pos = graph.get_node(*left_node).unwrap().position;
} else { let right_pos = graph.get_node(*right_node).unwrap().position;
crate::entity::direction::Direction::Left let house_node = graph.add_node(Node {
} position: left_pos.lerp(right_pos, 0.5),
} else { });
if diff.y > 0.0 { (house_node, left_pos.lerp(right_pos, 0.5))
crate::entity::direction::Direction::Down };
} else {
crate::entity::direction::Direction::Up // 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,
} }
} }