From 6d3d3bf49c3190390f02c8f61196cc68c7716ffc Mon Sep 17 00:00:00 2001 From: Xevion Date: Tue, 22 Jul 2025 14:37:26 -0500 Subject: [PATCH] feat: tunnel implementation, pathfinding debug mode --- src/constants.rs | 4 +- src/game.rs | 70 ++++++++++++++++++++++++++++------ src/ghost.rs | 97 ++++++++++++++++++++++++++++++++---------------- src/map.rs | 13 +++++++ src/pacman.rs | 72 +++++++++++++++++++++-------------- 5 files changed, 186 insertions(+), 70 deletions(-) diff --git a/src/constants.rs b/src/constants.rs index 0778877..4d58236 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -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", "######.## # # ##.######", " #.## ######## ##.# ", " #.## ##.# ", diff --git a/src/game.rs b/src/game.rs index 5ff2744..046efe4 100644 --- a/src/game.rs +++ b/src/game.rs @@ -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, map_texture: Texture<'a>, @@ -42,7 +50,7 @@ pub struct Game<'a> { font: Font<'a, 'static>, pacman: Rc>>, map: Rc>, - 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(); } diff --git a/src/ghost.rs b/src/ghost.rs index 3e8c6fe..719ed7a 100644 --- a/src/ghost.rs +++ b/src/ghost.rs @@ -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>, /// 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 + }; + } } } diff --git a/src/map.rs b/src/map.rs index 816ee72..734ecc7 100644 --- a/src/map.rs +++ b/src/map.rs @@ -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) } diff --git a/src/pacman.rs b/src/pacman.rs index b8b8b07..6bf3c8a 100644 --- a/src/pacman.rs +++ b/src/pacman.rs @@ -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;