mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-06 13:15:47 -06:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d3d3bf49c |
@@ -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",
|
||||
"######.## # # ##.######",
|
||||
" #.## ######## ##.# ",
|
||||
" #.## ##.# ",
|
||||
|
||||
70
src/game.rs
70
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<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();
|
||||
}
|
||||
|
||||
97
src/ghost.rs
97
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<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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
13
src/map.rs
13
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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user