feat: tunnel implementation, pathfinding debug mode

This commit is contained in:
2025-07-22 14:37:26 -05:00
parent 0a46f64866
commit 9476367f13
5 changed files with 186 additions and 70 deletions

View File

@@ -32,6 +32,8 @@ pub enum MapTile {
PowerPellet,
/// A starting position for an entity.
StartingPosition(u8),
/// A tunnel tile.
Tunnel,
}
/// The raw layout of the game board, as a 2D array of characters.
@@ -50,7 +52,7 @@ pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [
" #.## 1 ##.# ",
" #.## ###==### ##.# ",
"######.## # # ##.######",
" . #2 3 4 # . ",
"T . #2 3 4 # . T",
"######.## # # ##.######",
" #.## ######## ##.# ",
" #.## ##.# ",

View File

@@ -2,6 +2,7 @@
use std::cell::RefCell;
use std::rc::Rc;
use rand::seq::IteratorRandom;
use sdl2::image::LoadTexture;
use sdl2::keyboard::Keycode;
use sdl2::render::{Texture, TextureCreator};
@@ -34,6 +35,13 @@ static GHOST_EYES_TEXTURE_DATA: &[u8] = include_bytes!("../assets/32/ghost_eyes.
///
/// This struct contains all the information necessary to run the game, including
/// the canvas, textures, fonts, game objects, and the current score.
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum DebugMode {
None,
Grid,
Pathfinding,
}
pub struct Game<'a> {
canvas: &'a mut Canvas<Window>,
map_texture: Texture<'a>,
@@ -42,7 +50,7 @@ pub struct Game<'a> {
font: Font<'a, 'static>,
pacman: Rc<RefCell<Pacman<'a>>>,
map: Rc<std::cell::RefCell<Map>>,
debug: bool,
debug_mode: DebugMode,
score: u32,
audio: Audio,
// Add ghost
@@ -120,7 +128,7 @@ impl Game<'_> {
Game {
canvas,
pacman,
debug: false,
debug_mode: DebugMode::None,
map,
map_texture,
pellet_texture,
@@ -144,7 +152,11 @@ impl Game<'_> {
// Toggle debug mode
if keycode == Keycode::Space {
self.debug = !self.debug;
self.debug_mode = match self.debug_mode {
DebugMode::None => DebugMode::Grid,
DebugMode::Grid => DebugMode::Pathfinding,
DebugMode::Pathfinding => DebugMode::None,
};
}
// Reset game
@@ -173,18 +185,40 @@ impl Game<'_> {
// Reset the score
self.score = 0;
// Reset Pac-Man position
// Reset Pacman position
let mut pacman = self.pacman.borrow_mut();
pacman.pixel_position = Map::cell_to_pixel((1, 1));
pacman.cell_position = (1, 1);
pacman.in_tunnel = false;
pacman.direction = Direction::Right;
pacman.next_direction = None;
pacman.stopped = false;
// Reset ghost positions
self.blinky.set_mode(crate::ghost::GhostMode::House);
self.blinky.pixel_position = Map::cell_to_pixel((13, 11));
self.blinky.cell_position = (13, 11);
self.blinky.direction = Direction::Left;
// Reset ghost positions and mode
let mut rng = rand::rng();
let map = self.map.borrow();
let mut valid_positions = vec![];
for x in 1..(crate::constants::BOARD_WIDTH - 1) {
for y in 1..(crate::constants::BOARD_HEIGHT - 1) {
let tile_option = map.get_tile((x as i32, y as i32));
event!(tracing::Level::INFO, "Game reset - map and score cleared");
if let Some(tile) = tile_option {
match tile {
MapTile::Empty | MapTile::Pellet | MapTile::PowerPellet => {
valid_positions.push((x, y));
}
_ => {}
}
}
}
}
if let Some(&(gx, gy)) = valid_positions.iter().choose(&mut rng) {
self.blinky.pixel_position = Map::cell_to_pixel((gx, gy));
self.blinky.cell_position = (gx, gy);
self.blinky.in_tunnel = false;
self.blinky.direction = Direction::Left;
self.blinky.mode = crate::ghost::GhostMode::Chase;
}
}
/// Advances the game by one tick.
@@ -275,7 +309,7 @@ impl Game<'_> {
self.render_ui();
// Draw the debug grid
if self.debug {
if self.debug_mode == DebugMode::Grid {
for x in 0..BOARD_WIDTH {
for y in 0..BOARD_HEIGHT {
let tile = self
@@ -294,6 +328,7 @@ impl Game<'_> {
MapTile::Pellet => Some(Color::RED),
MapTile::PowerPellet => Some(Color::MAGENTA),
MapTile::StartingPosition(_) => Some(Color::GREEN),
MapTile::Tunnel => Some(Color::CYAN),
};
}
@@ -308,6 +343,19 @@ impl Game<'_> {
self.draw_cell((next_cell.0 as u32, next_cell.1 as u32), Color::YELLOW);
}
// Pathfinding debug mode
if self.debug_mode == DebugMode::Pathfinding {
// Show the current path for Blinky
if let Some((path, _)) = self.blinky.get_path_to_target({
let (tx, ty) = self.blinky.get_target_tile();
(tx as u32, ty as u32)
}) {
for &(x, y) in &path {
self.draw_cell((x, y), Color::YELLOW);
}
}
}
// Present the canvas
self.canvas.present();
}

View File

@@ -1,4 +1,4 @@
use pathfinding::prelude::astar;
use pathfinding::prelude::dijkstra;
use sdl2::{
pixels::Color,
render::{Canvas, Texture},
@@ -11,7 +11,7 @@ use rand::Rng;
use crate::{
animation::AnimatedTexture,
constants::{MapTile, BOARD_OFFSET, CELL_SIZE},
constants::{MapTile, BOARD_OFFSET, BOARD_WIDTH, CELL_SIZE},
direction::Direction,
entity::Entity,
map::Map,
@@ -67,8 +67,6 @@ pub struct Ghost<'a> {
pub mode: GhostMode,
/// The type/personality of this ghost
pub ghost_type: GhostType,
/// Whether the ghost is currently blue (frightened)
pub is_blue: bool,
/// Reference to the game map
pub map: Rc<RefCell<Map>>,
/// Reference to Pac-Man for targeting
@@ -81,6 +79,8 @@ pub struct Ghost<'a> {
body_sprite: AnimatedTexture<'a>,
/// Ghost eyes sprite
eyes_sprite: AnimatedTexture<'a>,
/// Whether the ghost is currently in a tunnel
pub in_tunnel: bool,
}
impl Ghost<'_> {
@@ -103,13 +103,13 @@ impl Ghost<'_> {
direction: Direction::Left,
mode: GhostMode::Chase,
ghost_type,
is_blue: false,
map,
pacman,
speed: 3,
modulation: SimpleTickModulator::new(1.0),
body_sprite,
eyes_sprite: AnimatedTexture::new(eyes_texture, 1, 4, 32, 32, Some((-4, -4))),
in_tunnel: false,
}
}
@@ -150,14 +150,23 @@ impl Ghost<'_> {
}
/// Calculates the path to the target tile using the A* algorithm.
fn get_path_to_target(&self, target: (u32, u32)) -> Option<(Vec<(u32, u32)>, u32)> {
pub fn get_path_to_target(&self, target: (u32, u32)) -> Option<(Vec<(u32, u32)>, u32)> {
let start = self.cell_position;
let map = self.map.borrow();
astar(
dijkstra(
&start,
|&p| {
let mut successors = vec![];
let tile = map.get_tile((p.0 as i32, p.1 as i32));
// Tunnel wrap: if currently in a tunnel, add the opposite exit as a neighbor
if let Some(MapTile::Tunnel) = tile {
if p.0 == 0 {
successors.push(((BOARD_WIDTH - 2, p.1), 1));
} else if p.0 == BOARD_WIDTH - 1 {
successors.push(((1, p.1), 1));
}
}
for dir in &[
Direction::Up,
Direction::Down,
@@ -167,16 +176,14 @@ impl Ghost<'_> {
let (dx, dy) = dir.offset();
let next_p = (p.0 as i32 + dx, p.1 as i32 + dy);
if let Some(tile) = map.get_tile(next_p) {
if tile != MapTile::Wall {
successors.push(((next_p.0 as u32, next_p.1 as u32), 1));
if tile == MapTile::Wall {
continue;
}
successors.push(((next_p.0 as u32, next_p.1 as u32), 1));
}
}
successors
},
|&p| {
((p.0 as i32 - target.0 as i32).abs() + (p.1 as i32 - target.1 as i32).abs()) as u32
},
|&p| p == target,
)
}
@@ -304,25 +311,53 @@ impl Entity for Ghost<'_> {
(self.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1,
);
// Pathfinding logic
let target_tile = self.get_target_tile();
if let Some((path, _)) =
self.get_path_to_target((target_tile.0 as u32, target_tile.1 as u32))
{
if path.len() > 1 {
let next_move = path[1];
let (x, y) = self.cell_position;
let dx = next_move.0 as i32 - x as i32;
let dy = next_move.1 as i32 - y as i32;
self.direction = if dx > 0 {
Direction::Right
} else if dx < 0 {
Direction::Left
} else if dy > 0 {
Direction::Down
} else {
Direction::Up
};
let current_tile = self
.map
.borrow()
.get_tile((self.cell_position.0 as i32, self.cell_position.1 as i32))
.unwrap_or(MapTile::Empty);
if current_tile == MapTile::Tunnel {
self.in_tunnel = true;
}
// Tunnel logic: if in tunnel, force movement and prevent direction change
if self.in_tunnel {
// If out of bounds, teleport to the opposite side and exit tunnel
if self.cell_position.0 == 0 {
self.cell_position.0 = BOARD_WIDTH - 2;
self.pixel_position =
Map::cell_to_pixel((self.cell_position.0, self.cell_position.1));
self.in_tunnel = false;
} else if self.cell_position.0 == BOARD_WIDTH - 1 {
self.cell_position.0 = 1;
self.pixel_position =
Map::cell_to_pixel((self.cell_position.0, self.cell_position.1));
self.in_tunnel = false;
} else {
// While in tunnel, do not allow direction change
// and always move in the current direction
}
} else {
// Pathfinding logic (only if not in tunnel)
let target_tile = self.get_target_tile();
if let Some((path, _)) =
self.get_path_to_target((target_tile.0 as u32, target_tile.1 as u32))
{
if path.len() > 1 {
let next_move = path[1];
let (x, y) = self.cell_position;
let dx = next_move.0 as i32 - x as i32;
let dy = next_move.1 as i32 - y as i32;
self.direction = if dx > 0 {
Direction::Right
} else if dx < 0 {
Direction::Left
} else if dy > 0 {
Direction::Down
} else {
Direction::Up
};
}
}
}

View File

@@ -2,6 +2,18 @@
use crate::constants::{MapTile, BOARD_OFFSET, CELL_SIZE};
use crate::constants::{BOARD_HEIGHT, BOARD_WIDTH};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Position(pub u32, pub u32);
impl Position {
pub fn as_i32(&self) -> (i32, i32) {
(self.0 as i32, self.1 as i32)
}
pub fn wrapping_add(&self, dx: i32, dy: i32) -> Position {
Position((self.0 as i32 + dx) as u32, (self.1 as i32 + dy) as u32)
}
}
/// The game map.
///
/// The map is represented as a 2D array of `MapTile`s. It also stores a copy of
@@ -41,6 +53,7 @@ impl Map {
'.' => MapTile::Pellet,
'o' => MapTile::PowerPellet,
' ' => MapTile::Empty,
'T' => MapTile::Tunnel,
c @ '0' | c @ '1' | c @ '2' | c @ '3' | c @ '4' => {
MapTile::StartingPosition(c.to_digit(10).unwrap() as u8)
}

View File

@@ -11,7 +11,7 @@ use tracing::event;
use crate::{
animation::AnimatedTexture,
constants::MapTile,
constants::{BOARD_OFFSET, CELL_SIZE},
constants::{BOARD_OFFSET, BOARD_WIDTH, CELL_SIZE},
direction::Direction,
entity::Entity,
map::Map,
@@ -35,6 +35,7 @@ pub struct Pacman<'a> {
speed: u32,
modulation: SimpleTickModulator,
sprite: AnimatedTexture<'a>,
pub in_tunnel: bool,
}
impl Pacman<'_> {
@@ -60,6 +61,7 @@ impl Pacman<'_> {
stopped: false,
modulation: SimpleTickModulator::new(1.0),
sprite: AnimatedTexture::new(atlas, 2, 3, 32, 32, Some((-4, -4))),
in_tunnel: false,
}
}
@@ -171,42 +173,58 @@ impl Entity for Pacman<'_> {
// Pac-Man can only change direction when he is perfectly aligned with the grid.
let can_change = self.internal_position_even() == (0, 0);
if let Some(next_direction) = self.next_direction {
if next_direction == self.direction.opposite() {
let next_tile_position = self.next_cell(Some(next_direction));
if can_change {
self.cell_position = (
(self.pixel_position.0 as u32 / CELL_SIZE) - BOARD_OFFSET.0,
(self.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1,
);
let current_tile = self
.map
.borrow()
.get_tile((self.cell_position.0 as i32, self.cell_position.1 as i32))
.unwrap_or(MapTile::Empty);
if current_tile == MapTile::Tunnel {
self.in_tunnel = true;
}
// Tunnel logic: if in tunnel, force movement and prevent direction change
if self.in_tunnel {
// If out of bounds, teleport to the opposite side and exit tunnel
if self.cell_position.0 == 0 {
self.cell_position.0 = BOARD_WIDTH - 2;
self.pixel_position =
Map::cell_to_pixel((self.cell_position.0 + 1, self.cell_position.1));
self.in_tunnel = false;
} else if self.cell_position.0 == BOARD_WIDTH - 1 {
self.cell_position.0 = 1;
self.pixel_position =
Map::cell_to_pixel((self.cell_position.0 - 1, self.cell_position.1));
self.in_tunnel = false;
} else {
// While in tunnel, do not allow direction change
// and always move in the current direction
}
} else {
// Handle direction change as normal
self.handle_direction_change();
// Check if the next tile in the current direction is a wall.
let next_tile_position = self.next_cell(None);
let next_tile = self
.map
.borrow()
.get_tile(next_tile_position)
.unwrap_or(MapTile::Empty);
if next_tile != MapTile::Wall {
self.direction = next_direction;
self.next_direction = None;
if !self.stopped && next_tile == MapTile::Wall {
self.stopped = true;
} else if self.stopped && next_tile != MapTile::Wall {
self.stopped = false;
}
}
}
if can_change {
self.handle_direction_change();
// Check if the next tile in the current direction is a wall.
let next_tile_position = self.next_cell(None);
let next_tile = self
.map
.borrow()
.get_tile(next_tile_position)
.unwrap_or(MapTile::Empty);
if !self.stopped && next_tile == MapTile::Wall {
event!(tracing::Level::DEBUG, "Wall collision. Stopping.");
self.stopped = true;
} else if self.stopped && next_tile != MapTile::Wall {
event!(tracing::Level::DEBUG, "Wall collision resolved. Moving.");
self.stopped = false;
}
}
if !self.stopped {
if self.modulation.next() {
let speed = self.speed as i32;