mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-08 14:07:47 -06:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a27f85279e | |||
| bea915b5c7 | |||
| d743aee393 |
@@ -56,8 +56,8 @@ pub const RAW_BOARD: [&str; BOARD_CELL_SIZE.y as usize] = [
|
||||
"#......##....##....##......#",
|
||||
"######.##### ## #####.######",
|
||||
" #.##### ## #####.# ",
|
||||
" #.## 1 ##.# ",
|
||||
" #.## ###==### ##.# ",
|
||||
" #.## == ##.# ",
|
||||
" #.## ######## ##.# ",
|
||||
"######.## ######## ##.######",
|
||||
"T . ######## . T",
|
||||
"######.## ######## ##.######",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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.
|
||||
@@ -89,7 +91,7 @@ impl Intersection {
|
||||
/// in an adjacency list, where each node has a list of outgoing edges.
|
||||
pub struct Graph {
|
||||
nodes: Vec<Node>,
|
||||
adjacency_list: Vec<Intersection>,
|
||||
pub adjacency_list: Vec<Intersection>,
|
||||
}
|
||||
|
||||
impl Graph {
|
||||
@@ -109,6 +111,39 @@ impl Graph {
|
||||
id
|
||||
}
|
||||
|
||||
/// 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<NodeId, &'static str> {
|
||||
let to = self.add_node(new_node);
|
||||
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,
|
||||
replace: bool,
|
||||
distance: Option<f32>,
|
||||
direction: Direction,
|
||||
) -> Result<(), &'static str> {
|
||||
if from >= self.adjacency_list.len() {
|
||||
return Err("From node does not exist.");
|
||||
}
|
||||
if to >= self.adjacency_list.len() {
|
||||
return Err("To node does not exist.");
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a directed edge between two nodes.
|
||||
///
|
||||
/// If `distance` is `None`, it will be calculated automatically based on the
|
||||
@@ -125,6 +160,7 @@ impl Graph {
|
||||
&mut self,
|
||||
from: NodeId,
|
||||
to: NodeId,
|
||||
replace: bool,
|
||||
distance: Option<f32>,
|
||||
direction: Direction,
|
||||
) -> Result<(), &'static str> {
|
||||
@@ -132,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
|
||||
}
|
||||
@@ -155,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."))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
270
src/map.rs
270
src/map.rs
@@ -1,16 +1,16 @@
|
||||
//! 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};
|
||||
use crate::entity::graph::{Graph, Node, NodeId};
|
||||
use crate::texture::text::TextTexture;
|
||||
|
||||
/// The game map, responsible for holding the tile-based layout and the navigation graph.
|
||||
@@ -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<IVec2, NodeId>,
|
||||
}
|
||||
|
||||
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,34 +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}");
|
||||
|
||||
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();
|
||||
|
||||
@@ -115,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,
|
||||
@@ -125,45 +107,216 @@ impl Map {
|
||||
let node_id = graph.add_node(Node { position: pos });
|
||||
grid_to_node.insert(start_pos, node_id);
|
||||
|
||||
while let Some(grid_pos) = queue.pop_front() {
|
||||
// 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 neighbor = grid_pos + dir.to_ivec2();
|
||||
let new_position = source_position + dir.to_ivec2();
|
||||
|
||||
if neighbor.x < 0
|
||||
|| neighbor.x >= BOARD_CELL_SIZE.x as i32
|
||||
|| neighbor.y < 0
|
||||
|| neighbor.y >= BOARD_CELL_SIZE.y as i32
|
||||
// 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
|
||||
|| new_position.y >= BOARD_CELL_SIZE.y as i32
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if grid_to_node.contains_key(&neighbor) {
|
||||
// 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[neighbor.x as usize][neighbor.y as usize],
|
||||
map[new_position.x as usize][new_position.y as usize],
|
||||
MapTile::Pellet | MapTile::PowerPellet | MapTile::Empty | MapTile::Tunnel | MapTile::StartingPosition(_)
|
||||
) {
|
||||
let pos =
|
||||
Vec2::new((neighbor.x * CELL_SIZE as i32) as f32, (neighbor.y * CELL_SIZE as i32) as f32) + cell_offset;
|
||||
let node_id = graph.add_node(Node { position: pos });
|
||||
grid_to_node.insert(neighbor, node_id);
|
||||
queue.push_back(neighbor);
|
||||
// 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,
|
||||
) + cell_offset;
|
||||
let new_node_id = graph.add_node(Node { position: pos });
|
||||
grid_to_node.insert(new_position, new_node_id);
|
||||
queue.push_back(new_position);
|
||||
|
||||
// Connect the new node to the source node
|
||||
let source_node_id = grid_to_node
|
||||
.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, false, None, dir)
|
||||
.expect("Failed to add edge");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.iter() {
|
||||
let neighbor = grid_pos + dir.to_ivec2();
|
||||
|
||||
if let Some(&neighbor_id) = grid_to_node.get(&neighbor) {
|
||||
graph.add_edge(node_id, neighbor_id, None, dir).expect("Failed to add edge");
|
||||
for dir in DIRECTIONS {
|
||||
// If the node doesn't have an edge in this direction, look for a neighbor in that direction
|
||||
if graph.adjacency_list[node_id].get(dir).is_none() {
|
||||
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, false, None, dir)
|
||||
.expect("Failed to add edge");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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");
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the starting position for a given entity ID.
|
||||
@@ -213,15 +366,14 @@ impl Map {
|
||||
let pos = node.position + BOARD_PIXEL_OFFSET.as_vec2();
|
||||
|
||||
// Draw connections
|
||||
// TODO: fix this
|
||||
// canvas.set_draw_color(Color::BLUE);
|
||||
canvas.set_draw_color(Color::BLUE);
|
||||
|
||||
// for neighbor in node.neighbors() {
|
||||
// let end_pos = neighbor.get(&self.node_map).position + BOARD_PIXEL_OFFSET.as_vec2();
|
||||
// canvas
|
||||
// .draw_line((pos.x as i32, pos.y as i32), (end_pos.x as i32, end_pos.y as i32))
|
||||
// .unwrap();
|
||||
// }
|
||||
for edge in self.graph.adjacency_list[i].edges() {
|
||||
let end_pos = self.graph.get_node(edge.target).unwrap().position + BOARD_PIXEL_OFFSET.as_vec2();
|
||||
canvas
|
||||
.draw_line((pos.x as i32, pos.y as i32), (end_pos.x as i32, end_pos.y as i32))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Draw node
|
||||
// let color = if pacman.position.from_node_idx() == i.into() {
|
||||
|
||||
Reference in New Issue
Block a user