mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-09 16:07:55 -06:00
feat: pathfinding for ghosts, add debug rendering of paths
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
//! a traverser and display directional animated textures.
|
//! a traverser and display directional animated textures.
|
||||||
|
|
||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
|
use pathfinding::prelude::dijkstra;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
@@ -175,6 +176,40 @@ impl Ghost {
|
|||||||
Vec2::new(pos.x + BOARD_PIXEL_OFFSET.x as f32, pos.y + BOARD_PIXEL_OFFSET.y as f32)
|
Vec2::new(pos.x + BOARD_PIXEL_OFFSET.x as f32, pos.y + BOARD_PIXEL_OFFSET.y as f32)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculates the shortest path from the ghost's current position to a target node using Dijkstra's algorithm.
|
||||||
|
///
|
||||||
|
/// Returns a vector of NodeIds representing the path, or None if no path exists.
|
||||||
|
/// The path includes the current node and the target node.
|
||||||
|
pub fn calculate_path_to_target(&self, graph: &Graph, target: NodeId) -> Option<Vec<NodeId>> {
|
||||||
|
let start_node = self.traverser.position.from_node_id();
|
||||||
|
|
||||||
|
// Use Dijkstra's algorithm to find the shortest path
|
||||||
|
let result = dijkstra(
|
||||||
|
&start_node,
|
||||||
|
|&node_id| {
|
||||||
|
// Get all edges from the current node
|
||||||
|
graph.adjacency_list[node_id]
|
||||||
|
.edges()
|
||||||
|
.filter(|edge| can_ghost_traverse(*edge))
|
||||||
|
.map(|edge| (edge.target, (edge.distance * 100.0) as u32))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
},
|
||||||
|
|&node_id| node_id == target,
|
||||||
|
);
|
||||||
|
|
||||||
|
result.map(|(path, _cost)| path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the ghost's color for debug rendering.
|
||||||
|
pub fn debug_color(&self) -> sdl2::pixels::Color {
|
||||||
|
match self.ghost_type {
|
||||||
|
GhostType::Blinky => sdl2::pixels::Color::RGB(255, 0, 0), // Red
|
||||||
|
GhostType::Pinky => sdl2::pixels::Color::RGB(255, 182, 255), // Pink
|
||||||
|
GhostType::Inky => sdl2::pixels::Color::RGB(0, 255, 255), // Cyan
|
||||||
|
GhostType::Clyde => sdl2::pixels::Color::RGB(255, 182, 85), // Orange
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Renders the ghost at its current position.
|
/// Renders the ghost at its current position.
|
||||||
///
|
///
|
||||||
/// Draws the appropriate directional sprite based on the ghost's
|
/// Draws the appropriate directional sprite based on the ghost's
|
||||||
|
|||||||
@@ -111,6 +111,17 @@ impl Pacman {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the current node ID that Pac-Man is at or moving towards.
|
||||||
|
///
|
||||||
|
/// If Pac-Man is at a node, returns that node ID.
|
||||||
|
/// If Pac-Man is between nodes, returns the node it's moving towards.
|
||||||
|
pub fn current_node_id(&self) -> NodeId {
|
||||||
|
match self.traverser.position {
|
||||||
|
Position::AtNode(node_id) => node_id,
|
||||||
|
Position::BetweenNodes { to, .. } => to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Renders Pac-Man to the canvas.
|
/// Renders Pac-Man to the canvas.
|
||||||
///
|
///
|
||||||
/// Calculates screen position, determines if Pac-Man is stopped,
|
/// Calculates screen position, determines if Pac-Man is stopped,
|
||||||
|
|||||||
53
src/game.rs
53
src/game.rs
@@ -173,12 +173,65 @@ impl Game {
|
|||||||
if self.debug_mode {
|
if self.debug_mode {
|
||||||
self.map
|
self.map
|
||||||
.debug_render_with_cursor(canvas, &mut self.text_texture, &mut self.atlas, cursor_pos);
|
.debug_render_with_cursor(canvas, &mut self.text_texture, &mut self.atlas, cursor_pos);
|
||||||
|
self.render_pathfinding_debug(canvas)?;
|
||||||
}
|
}
|
||||||
self.draw_hud(canvas)?;
|
self.draw_hud(canvas)?;
|
||||||
canvas.present();
|
canvas.present();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Renders pathfinding debug lines from each ghost to Pac-Man.
|
||||||
|
///
|
||||||
|
/// Each ghost's path is drawn in its respective color with a small offset
|
||||||
|
/// to prevent overlapping lines.
|
||||||
|
fn render_pathfinding_debug<T: RenderTarget>(&self, canvas: &mut Canvas<T>) -> Result<()> {
|
||||||
|
let pacman_node = self.pacman.current_node_id();
|
||||||
|
|
||||||
|
for (i, ghost) in self.ghosts.iter().enumerate() {
|
||||||
|
if let Some(path) = ghost.calculate_path_to_target(&self.map.graph, pacman_node) {
|
||||||
|
if path.len() < 2 {
|
||||||
|
continue; // Skip if path is too short
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the ghost's color
|
||||||
|
canvas.set_draw_color(ghost.debug_color());
|
||||||
|
|
||||||
|
// Calculate offset based on ghost index to prevent overlapping lines
|
||||||
|
let offset = (i as f32) * 2.0 - 3.0; // Offset range: -3.0 to 3.0
|
||||||
|
|
||||||
|
// Calculate a consistent offset direction for the entire path
|
||||||
|
let first_node = self.map.graph.get_node(path[0]).unwrap();
|
||||||
|
let last_node = self.map.graph.get_node(path[path.len() - 1]).unwrap();
|
||||||
|
let first_pos = first_node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
|
let last_pos = last_node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
|
|
||||||
|
// Use the overall direction from start to end to determine the perpendicular offset
|
||||||
|
let overall_dir = (last_pos - first_pos).normalize();
|
||||||
|
let perp_dir = glam::Vec2::new(-overall_dir.y, overall_dir.x);
|
||||||
|
|
||||||
|
// Calculate offset positions for all nodes using the same perpendicular direction
|
||||||
|
let mut offset_positions = Vec::new();
|
||||||
|
for &node_id in &path {
|
||||||
|
let node = self.map.graph.get_node(node_id).unwrap();
|
||||||
|
let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
|
offset_positions.push(pos + perp_dir * offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw lines between the offset positions
|
||||||
|
for window in offset_positions.windows(2) {
|
||||||
|
canvas
|
||||||
|
.draw_line(
|
||||||
|
(window[0].x as i32, window[0].y as i32),
|
||||||
|
(window[1].x as i32, window[1].y as i32),
|
||||||
|
)
|
||||||
|
.map_err(anyhow::Error::msg)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_hud<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>) -> Result<()> {
|
fn draw_hud<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>) -> Result<()> {
|
||||||
let lives = 3;
|
let lives = 3;
|
||||||
let score_text = format!("{:02}", self.score);
|
let score_text = format!("{:02}", self.score);
|
||||||
|
|||||||
117
tests/pathfinding.rs
Normal file
117
tests/pathfinding.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use pacman::entity::direction::Direction;
|
||||||
|
use pacman::entity::ghost::{Ghost, GhostType};
|
||||||
|
use pacman::entity::graph::{Graph, Node};
|
||||||
|
use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
fn create_test_atlas() -> SpriteAtlas {
|
||||||
|
let mut frames = HashMap::new();
|
||||||
|
let directions = ["up", "down", "left", "right"];
|
||||||
|
let ghost_types = ["blinky", "pinky", "inky", "clyde"];
|
||||||
|
|
||||||
|
for ghost_type in &ghost_types {
|
||||||
|
for (i, dir) in directions.iter().enumerate() {
|
||||||
|
frames.insert(
|
||||||
|
format!("ghost/{}/{}_{}.png", ghost_type, dir, "a"),
|
||||||
|
MapperFrame {
|
||||||
|
x: i as u16 * 16,
|
||||||
|
y: 0,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
frames.insert(
|
||||||
|
format!("ghost/{}/{}_{}.png", ghost_type, dir, "b"),
|
||||||
|
MapperFrame {
|
||||||
|
x: i as u16 * 16,
|
||||||
|
y: 16,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mapper = AtlasMapper { frames };
|
||||||
|
let dummy_texture = unsafe { std::mem::zeroed() };
|
||||||
|
SpriteAtlas::new(dummy_texture, mapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ghost_pathfinding() {
|
||||||
|
// Create a simple test graph
|
||||||
|
let mut graph = Graph::new();
|
||||||
|
|
||||||
|
// Add nodes in a simple line: 0 -> 1 -> 2
|
||||||
|
let node0 = graph.add_node(Node {
|
||||||
|
position: glam::Vec2::new(0.0, 0.0),
|
||||||
|
});
|
||||||
|
let node1 = graph.add_node(Node {
|
||||||
|
position: glam::Vec2::new(10.0, 0.0),
|
||||||
|
});
|
||||||
|
let node2 = graph.add_node(Node {
|
||||||
|
position: glam::Vec2::new(20.0, 0.0),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect the nodes
|
||||||
|
graph.connect(node0, node1, false, None, Direction::Right).unwrap();
|
||||||
|
graph.connect(node1, node2, false, None, Direction::Right).unwrap();
|
||||||
|
|
||||||
|
// Create a test atlas for the ghost
|
||||||
|
let atlas = create_test_atlas();
|
||||||
|
|
||||||
|
// Create a ghost at node 0
|
||||||
|
let ghost = Ghost::new(&graph, node0, GhostType::Blinky, &atlas);
|
||||||
|
|
||||||
|
// Test pathfinding from node 0 to node 2
|
||||||
|
let path = ghost.calculate_path_to_target(&graph, node2);
|
||||||
|
|
||||||
|
assert!(path.is_some());
|
||||||
|
let path = path.unwrap();
|
||||||
|
assert_eq!(path, vec![node0, node1, node2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ghost_pathfinding_no_path() {
|
||||||
|
// Create a test graph with disconnected components
|
||||||
|
let mut graph = Graph::new();
|
||||||
|
|
||||||
|
let node0 = graph.add_node(Node {
|
||||||
|
position: glam::Vec2::new(0.0, 0.0),
|
||||||
|
});
|
||||||
|
let node1 = graph.add_node(Node {
|
||||||
|
position: glam::Vec2::new(10.0, 0.0),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't connect the nodes
|
||||||
|
let atlas = create_test_atlas();
|
||||||
|
let ghost = Ghost::new(&graph, node0, GhostType::Blinky, &atlas);
|
||||||
|
|
||||||
|
// Test pathfinding when no path exists
|
||||||
|
let path = ghost.calculate_path_to_target(&graph, node1);
|
||||||
|
|
||||||
|
assert!(path.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ghost_debug_colors() {
|
||||||
|
let atlas = create_test_atlas();
|
||||||
|
let mut graph = Graph::new();
|
||||||
|
let node = graph.add_node(Node {
|
||||||
|
position: glam::Vec2::new(0.0, 0.0),
|
||||||
|
});
|
||||||
|
|
||||||
|
let blinky = Ghost::new(&graph, node, GhostType::Blinky, &atlas);
|
||||||
|
let pinky = Ghost::new(&graph, node, GhostType::Pinky, &atlas);
|
||||||
|
let inky = Ghost::new(&graph, node, GhostType::Inky, &atlas);
|
||||||
|
let clyde = Ghost::new(&graph, node, GhostType::Clyde, &atlas);
|
||||||
|
|
||||||
|
// Test that each ghost has a different debug color
|
||||||
|
let colors = std::collections::HashSet::from([
|
||||||
|
blinky.debug_color(),
|
||||||
|
pinky.debug_color(),
|
||||||
|
inky.debug_color(),
|
||||||
|
clyde.debug_color(),
|
||||||
|
]);
|
||||||
|
assert_eq!(colors.len(), 4, "All ghost colors should be unique");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user