diff --git a/src/debug.rs b/src/debug.rs index 117fdfd..5ae2696 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -1,7 +1,7 @@ //! Debug rendering utilities for Pac-Man. use crate::{ constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE}, - entity::blinky::Blinky, + entity::ghost::Ghost, map::Map, }; use glam::{IVec2, UVec2}; @@ -62,9 +62,9 @@ impl DebugRenderer { } } - pub fn draw_pathfinding(canvas: &mut Canvas, blinky: &Blinky, map: &Map) { - let target = blinky.get_target_tile(); - if let Some((path, _)) = blinky.get_path_to_target(target.as_uvec2()) { + pub fn draw_pathfinding(canvas: &mut Canvas, ghost: &Ghost, map: &Map) { + let target = ghost.get_target_tile(); + if let Some((path, _)) = ghost.get_path_to_target(target.unwrap().as_uvec2()) { for pos in &path { Self::draw_cell(canvas, map, *pos, Color::YELLOW); } diff --git a/src/entity/blinky.rs b/src/entity/blinky.rs deleted file mode 100644 index 7f138d9..0000000 --- a/src/entity/blinky.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::cell::RefCell; -use std::rc::Rc; - -use crate::entity::direction::Direction; -use crate::entity::ghost::{Ghost, GhostMode, GhostType}; -use crate::entity::pacman::Pacman; -use crate::entity::{Entity, Moving, Renderable, StaticEntity}; -use crate::map::Map; -use crate::texture::sprite::SpriteAtlas; -use anyhow::Result; -use glam::{IVec2, UVec2}; -use sdl2::render::WindowCanvas; - -pub struct Blinky { - ghost: Ghost, -} - -impl Blinky { - pub fn new( - starting_position: UVec2, - atlas: Rc>, - map: Rc>, - pacman: Rc>, - ) -> Blinky { - Blinky { - ghost: Ghost::new(GhostType::Blinky, starting_position, atlas, map, pacman), - } - } - - /// Gets Blinky's chase target - directly targets Pac-Man's current position - pub fn get_chase_target(&self) -> IVec2 { - let pacman = self.ghost.pacman.borrow(); - let cell = pacman.base().cell_position; - IVec2::new(cell.x as i32, cell.y as i32) - } - - pub fn set_mode(&mut self, mode: GhostMode) { - self.ghost.set_mode(mode); - } - - pub fn tick(&mut self) { - self.ghost.tick(); - } -} - -impl Entity for Blinky { - fn base(&self) -> &StaticEntity { - self.ghost.base.base() - } -} - -impl Renderable for Blinky { - fn render(&mut self, canvas: &mut WindowCanvas) -> Result<()> { - self.ghost.render(canvas) - } -} - -impl Moving for Blinky { - fn tick_movement(&mut self) { - self.ghost.tick_movement(); - } - fn update_cell_position(&mut self) { - self.ghost.update_cell_position(); - } - fn next_cell(&self, direction: Option) -> IVec2 { - self.ghost.next_cell(direction) - } - fn is_wall_ahead(&self, direction: Option) -> bool { - self.ghost.is_wall_ahead(direction) - } - fn handle_tunnel(&mut self) -> bool { - self.ghost.handle_tunnel() - } - fn is_grid_aligned(&self) -> bool { - self.ghost.is_grid_aligned() - } - fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool { - self.ghost.set_direction_if_valid(new_direction) - } -} - -// Allow direct access to ghost fields -impl std::ops::Deref for Blinky { - type Target = Ghost; - - fn deref(&self) -> &Self::Target { - &self.ghost - } -} - -impl std::ops::DerefMut for Blinky { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.ghost - } -} diff --git a/src/entity/ghost.rs b/src/entity/ghost.rs index 823f18c..01dc214 100644 --- a/src/entity/ghost.rs +++ b/src/entity/ghost.rs @@ -32,7 +32,14 @@ pub enum GhostMode { /// Eyes mode - ghost returns to the ghost house after being eaten Eyes, /// House mode - ghost is in the ghost house, waiting to exit - House, + House(HouseMode), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum HouseMode { + Entering, + Exiting, + Waiting, } /// The different ghost personalities @@ -69,6 +76,8 @@ pub struct Ghost { pub texture: DirectionalAnimatedTexture, pub frightened_texture: BlinkingTexture, pub eyes_texture: DirectionalAnimatedTexture, + pub house_offset: i32, + pub current_house_offset: i32, } impl Ghost { @@ -79,6 +88,7 @@ impl Ghost { atlas: Rc>, map: Rc>, pacman: Rc>, + house_offset: i32, ) -> Ghost { let pixel_position = Map::cell_to_pixel(starting_position); let name = match ghost_type { @@ -127,23 +137,25 @@ impl Ghost { SimpleTickModulator::new(0.9375), map, ), - mode: GhostMode::Chase, + mode: GhostMode::House(HouseMode::Waiting), ghost_type, pacman, texture, frightened_texture, eyes_texture, + house_offset, + current_house_offset: house_offset, } } /// Gets the target tile for this ghost based on its current mode - pub fn get_target_tile(&self) -> IVec2 { + pub fn get_target_tile(&self) -> Option { match self.mode { - GhostMode::Scatter => self.get_scatter_target(), - GhostMode::Chase => self.get_chase_target(), - GhostMode::Frightened => self.get_random_target(), - GhostMode::Eyes => self.get_house_target(), - GhostMode::House => self.get_house_exit_target(), + GhostMode::Scatter => Some(self.get_scatter_target()), + GhostMode::Chase => Some(self.get_chase_target()), + GhostMode::Frightened => Some(self.get_random_target()), + GhostMode::Eyes => Some(self.get_house_target()), + GhostMode::House(_) => None, } } @@ -189,16 +201,51 @@ impl Ghost { IVec2::new(13, 14) // Center of ghost house } - /// Gets the exit point target when leaving house - fn get_house_exit_target(&self) -> IVec2 { - IVec2::new(13, 11) // Just above ghost house - } - - /// Gets this ghost's chase mode target (to be implemented by each ghost type) + /// Gets this ghost's chase mode target based on its personality fn get_chase_target(&self) -> IVec2 { let pacman = self.pacman.borrow(); - let cell = pacman.base().cell_position; - IVec2::new(cell.x as i32, cell.y as i32) + let pacman_cell = pacman.base().cell_position; + let pacman_direction = pacman.base.direction; + + match self.ghost_type { + GhostType::Blinky => { + // Blinky (Red) - Directly targets Pac-Man's current position + IVec2::new(pacman_cell.x as i32, pacman_cell.y as i32) + } + GhostType::Pinky => { + // Pinky (Pink) - Targets 4 cells ahead of Pac-Man in his direction + let offset = pacman_direction.offset(); + let target_x = (pacman_cell.x as i32) + (offset.x * 4); + let target_y = (pacman_cell.y as i32) + (offset.y * 4); + IVec2::new(target_x, target_y) + } + GhostType::Inky => { + // Inky (Cyan) - Uses Blinky's position and Pac-Man's position to calculate target + // For now, just target Pac-Man with some randomness + let mut rng = SmallRng::from_os_rng(); + let random_offset_x = rng.random_range(-2..=2); + let random_offset_y = rng.random_range(-2..=2); + IVec2::new( + (pacman_cell.x as i32) + random_offset_x, + (pacman_cell.y as i32) + random_offset_y, + ) + } + GhostType::Clyde => { + // Clyde (Orange) - Targets Pac-Man when far, runs to scatter corner when close + let distance = ((self.base.base.cell_position.x as i32 - pacman_cell.x as i32).pow(2) + + (self.base.base.cell_position.y as i32 - pacman_cell.y as i32).pow(2)) + as f32; + let distance = distance.sqrt(); + + if distance > 8.0 { + // Far from Pac-Man - chase + IVec2::new(pacman_cell.x as i32, pacman_cell.y as i32) + } else { + // Close to Pac-Man - scatter to bottom left + IVec2::new(0, 35) + } + } + } } /// Calculates the path to the target tile using the A* algorithm. @@ -239,8 +286,10 @@ impl Ghost { /// Changes the ghost's mode and handles direction reversal pub fn set_mode(&mut self, new_mode: GhostMode) { // Don't reverse if going to/from frightened or if in house - let should_reverse = - self.mode != GhostMode::House && new_mode != GhostMode::Frightened && self.mode != GhostMode::Frightened; + let should_reverse = !matches!(self.mode, GhostMode::House(_)) + && !matches!(new_mode, GhostMode::House(_)) + && !matches!(self.mode, GhostMode::Frightened) + && !matches!(new_mode, GhostMode::Frightened); self.mode = new_mode; @@ -249,7 +298,7 @@ impl Ghost { GhostMode::Scatter => 0.85, GhostMode::Frightened => 0.7, GhostMode::Eyes => 1.5, - GhostMode::House => 0f32, + GhostMode::House(_) => 0.7, }); if should_reverse { @@ -258,36 +307,144 @@ impl Ghost { } pub fn tick(&mut self) { - if self.mode == GhostMode::House { - // For now, do nothing in the house + if let GhostMode::House(house_mode) = self.mode { + match house_mode { + HouseMode::Waiting => { + // Ghosts in waiting mode move up and down + if self.base.is_grid_aligned() { + self.base.update_cell_position(); + + // Simple up and down movement + let current_pos = self.base.base.cell_position; + let start_pos = UVec2::new(13, 14); // Center of ghost house + + if current_pos.y > start_pos.y + 1 { + // Too far down, move up + self.base.set_direction_if_valid(Direction::Up); + } else if current_pos.y < start_pos.y - 1 { + // Too far up, move down + self.base.set_direction_if_valid(Direction::Down); + } else if self.base.direction == Direction::Up { + // At top, switch to down + self.base.set_direction_if_valid(Direction::Down); + } else if self.base.direction == Direction::Down { + // At bottom, switch to up + self.base.set_direction_if_valid(Direction::Up); + } + } + } + HouseMode::Exiting => { + // Ghosts exiting move towards the exit + if self.base.is_grid_aligned() { + self.base.update_cell_position(); + + let exit_pos = UVec2::new(13, 11); + let current_pos = self.base.base.cell_position; + + // Determine direction to exit + if current_pos.y > exit_pos.y { + // Need to move up + self.base.set_direction_if_valid(Direction::Up); + } else if current_pos.y == exit_pos.y && current_pos.x != exit_pos.x { + // At exit level, move horizontally to center + if current_pos.x < exit_pos.x { + self.base.set_direction_if_valid(Direction::Right); + } else { + self.base.set_direction_if_valid(Direction::Left); + } + } else if current_pos == exit_pos { + // Reached exit, transition to chase mode + self.mode = GhostMode::Chase; + self.current_house_offset = 0; // Reset offset + } + } + } + HouseMode::Entering => { + // Ghosts entering move towards their starting position + if self.base.is_grid_aligned() { + self.base.update_cell_position(); + + let start_pos = UVec2::new(13, 14); // Center of ghost house + let current_pos = self.base.base.cell_position; + + // Determine direction to starting position + if current_pos.y < start_pos.y { + // Need to move down + self.base.set_direction_if_valid(Direction::Down); + } else if current_pos.y == start_pos.y && current_pos.x != start_pos.x { + // At house level, move horizontally to center + if current_pos.x < start_pos.x { + self.base.set_direction_if_valid(Direction::Right); + } else { + self.base.set_direction_if_valid(Direction::Left); + } + } else if current_pos == start_pos { + // Reached starting position, switch to waiting + self.mode = GhostMode::House(HouseMode::Waiting); + } + } + } + } + + // Update house offset for smooth transitions + if self.current_house_offset != 0 { + // Gradually reduce offset when turning + if self.base.direction == Direction::Left || self.base.direction == Direction::Right { + if self.current_house_offset > 0 { + self.current_house_offset -= 1; + } else if self.current_house_offset < 0 { + self.current_house_offset += 1; + } + } + } + + self.base.tick(); + self.texture.tick(); + self.frightened_texture.tick(); + self.eyes_texture.tick(); return; } + + // Normal ghost behavior if self.base.is_grid_aligned() { self.base.update_cell_position(); if !self.base.handle_tunnel() { // 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.as_uvec2()) { - if path.len() > 1 { - let next_move = path[1]; - let x = self.base.base.cell_position.x; - let y = self.base.base.cell_position.y; - let dx = next_move.x as i32 - x as i32; - let dy = next_move.y as i32 - y as i32; - let new_direction = if dx > 0 { - Direction::Right - } else if dx < 0 { - Direction::Left - } else if dy > 0 { - Direction::Down - } else { - Direction::Up - }; - self.base.set_direction_if_valid(new_direction); + if let Some(target_tile) = self.get_target_tile() { + if let Some((path, _)) = self.get_path_to_target(target_tile.as_uvec2()) { + if path.len() > 1 { + let next_move = path[1]; + let x = self.base.base.cell_position.x; + let y = self.base.base.cell_position.y; + let dx = next_move.x as i32 - x as i32; + let dy = next_move.y as i32 - y as i32; + let new_direction = if dx > 0 { + Direction::Right + } else if dx < 0 { + Direction::Left + } else if dy > 0 { + Direction::Down + } else { + Direction::Up + }; + self.base.set_direction_if_valid(new_direction); + } } } } } + + // Handle house offset transition when turning + if self.current_house_offset != 0 { + if self.base.direction == Direction::Left || self.base.direction == Direction::Right { + if self.current_house_offset > 0 { + self.current_house_offset -= 1; + } else if self.current_house_offset < 0 { + self.current_house_offset += 1; + } + } + } + self.base.tick(); // Handles wall collision and movement self.texture.tick(); self.frightened_texture.tick(); @@ -324,9 +481,14 @@ impl Moving for Ghost { impl Renderable for Ghost { fn render(&mut self, canvas: &mut WindowCanvas) -> Result<()> { - let pos = self.base.base.pixel_position; + let mut pos = self.base.base.pixel_position; let dir = self.base.direction; + // Apply house offset if in house mode or transitioning + if matches!(self.mode, GhostMode::House(_)) || self.current_house_offset != 0 { + pos.x += self.current_house_offset; + } + match self.mode { GhostMode::Frightened => { let tile = self.frightened_texture.animation.current_tile(); diff --git a/src/entity/mod.rs b/src/entity/mod.rs index 888042f..0ae1b10 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -1,4 +1,3 @@ -pub mod blinky; pub mod direction; pub mod edible; pub mod ghost; @@ -23,8 +22,8 @@ pub trait Entity { /// Returns true if the entity is colliding with the other entity. fn is_colliding(&self, other: &dyn Entity) -> bool { - let a = self.base().pixel_position; - let b = other.base().pixel_position; + let a = self.base().cell_position; + let b = other.base().cell_position; a == b } } diff --git a/src/game.rs b/src/game.rs index 3f62614..d861eac 100644 --- a/src/game.rs +++ b/src/game.rs @@ -5,9 +5,7 @@ use std::rc::Rc; use anyhow::Result; use glam::UVec2; -use rand::rngs::SmallRng; -use rand::seq::IteratorRandom; -use rand::SeedableRng; + use sdl2::image::LoadTexture; use sdl2::keyboard::Keycode; @@ -19,9 +17,9 @@ use crate::asset::{get_asset_bytes, Asset}; use crate::audio::Audio; use crate::constants::RAW_BOARD; use crate::debug::{DebugMode, DebugRenderer}; -use crate::entity::blinky::Blinky; use crate::entity::direction::Direction; use crate::entity::edible::{reconstruct_edibles, Edible, EdibleKind}; +use crate::entity::ghost::{Ghost, GhostMode, GhostType, HouseMode}; use crate::entity::pacman::Pacman; use crate::entity::Renderable; use crate::map::Map; @@ -38,7 +36,10 @@ use crate::texture::{get_atlas_tile, sprite}; pub struct Game { // Game state pacman: Rc>, - blinky: Blinky, + blinky: Ghost, + pinky: Ghost, + inky: Ghost, + clyde: Ghost, edibles: Vec, map: Rc>, score: u32, @@ -80,7 +81,60 @@ impl Game { Rc::clone(&atlas), Rc::clone(&map), ))); - let blinky = Blinky::new(UVec2::new(13, 11), Rc::clone(&atlas), Rc::clone(&map), Rc::clone(&pacman)); + + // Find starting positions + let pacman_start = map.borrow().find_starting_position(0).unwrap_or(UVec2::new(13, 23)); + let blinky_start = map.borrow().find_starting_position(1).unwrap_or(UVec2::new(13, 11)); + let pinky_start = map.borrow().find_starting_position(2).unwrap_or(UVec2::new(13, 14)); + let inky_start = map.borrow().find_starting_position(3).unwrap_or(UVec2::new(13, 14)); + let clyde_start = map.borrow().find_starting_position(4).unwrap_or(UVec2::new(13, 14)); + + // Update Pac-Man to proper starting position + { + let mut pacman_mut = pacman.borrow_mut(); + pacman_mut.base.base.pixel_position = Map::cell_to_pixel(pacman_start); + pacman_mut.base.base.cell_position = pacman_start; + } + + let mut blinky = Ghost::new( + GhostType::Blinky, + blinky_start, + Rc::clone(&atlas), + Rc::clone(&map), + Rc::clone(&pacman), + -8, + ); + blinky.mode = GhostMode::Chase; + + let mut pinky = Ghost::new( + GhostType::Pinky, + pinky_start, + Rc::clone(&atlas), + Rc::clone(&map), + Rc::clone(&pacman), + 8, + ); + pinky.mode = crate::entity::ghost::GhostMode::House(crate::entity::ghost::HouseMode::Waiting); + + let mut inky = Ghost::new( + GhostType::Inky, + inky_start, + Rc::clone(&atlas), + Rc::clone(&map), + Rc::clone(&pacman), + -8, + ); + inky.mode = crate::entity::ghost::GhostMode::House(crate::entity::ghost::HouseMode::Waiting); + + let mut clyde = Ghost::new( + GhostType::Clyde, + clyde_start, + Rc::clone(&atlas), + Rc::clone(&map), + Rc::clone(&pacman), + 8, + ); + clyde.mode = crate::entity::ghost::GhostMode::House(crate::entity::ghost::HouseMode::Waiting); let mut map_texture = get_atlas_tile(&atlas, "maze/full.png"); map_texture.color = Some(Color::RGB(0x20, 0x20, 0xf9)); @@ -99,6 +153,9 @@ impl Game { Game { pacman, blinky, + pinky, + inky, + clyde, edibles, map, score: 0, @@ -170,30 +227,52 @@ impl Game { // Reset the score self.score = 0; - // Get valid positions from the cached flood fill and randomize positions in a single block + // Reset entities to their proper starting positions { - let mut map = self.map.borrow_mut(); - let valid_positions = map.get_valid_playable_positions(); - let mut rng = SmallRng::from_os_rng(); + let map = self.map.borrow(); - // Randomize Pac-Man position - if let Some(pos) = valid_positions.iter().choose(&mut rng) { + // Reset Pac-Man to proper starting position + if let Some(pacman_start) = map.find_starting_position(0) { let mut pacman = self.pacman.borrow_mut(); - pacman.base.base.pixel_position = Map::cell_to_pixel(*pos); - pacman.base.base.cell_position = *pos; + pacman.base.base.pixel_position = Map::cell_to_pixel(pacman_start); + pacman.base.base.cell_position = pacman_start; pacman.base.in_tunnel = false; pacman.base.direction = Direction::Right; pacman.next_direction = None; pacman.stopped = false; } - // Randomize ghost position - if let Some(pos) = valid_positions.iter().choose(&mut rng) { - self.blinky.base.base.pixel_position = Map::cell_to_pixel(*pos); - self.blinky.base.base.cell_position = *pos; + // Reset ghosts to their starting positions + if let Some(blinky_start) = map.find_starting_position(1) { + self.blinky.base.base.pixel_position = Map::cell_to_pixel(blinky_start); + self.blinky.base.base.cell_position = blinky_start; self.blinky.base.in_tunnel = false; self.blinky.base.direction = Direction::Left; - self.blinky.mode = crate::entity::ghost::GhostMode::Chase; + self.blinky.mode = crate::entity::ghost::GhostMode::House(crate::entity::ghost::HouseMode::Waiting); + } + + if let Some(pinky_start) = map.find_starting_position(2) { + self.pinky.base.base.pixel_position = Map::cell_to_pixel(pinky_start); + self.pinky.base.base.cell_position = pinky_start; + self.pinky.base.in_tunnel = false; + self.pinky.base.direction = Direction::Down; + self.pinky.mode = crate::entity::ghost::GhostMode::House(crate::entity::ghost::HouseMode::Waiting); + } + + if let Some(inky_start) = map.find_starting_position(3) { + self.inky.base.base.pixel_position = Map::cell_to_pixel(inky_start); + self.inky.base.base.cell_position = inky_start; + self.inky.base.in_tunnel = false; + self.inky.base.direction = Direction::Up; + self.inky.mode = crate::entity::ghost::GhostMode::House(crate::entity::ghost::HouseMode::Waiting); + } + + if let Some(clyde_start) = map.find_starting_position(4) { + self.clyde.base.base.pixel_position = Map::cell_to_pixel(clyde_start); + self.clyde.base.base.cell_position = clyde_start; + self.clyde.base.in_tunnel = false; + self.clyde.base.direction = Direction::Down; + self.clyde.mode = crate::entity::ghost::GhostMode::House(crate::entity::ghost::HouseMode::Waiting); } } @@ -218,6 +297,9 @@ impl Game { fn tick_entities(&mut self) { self.pacman.borrow_mut().tick(); self.blinky.tick(); + self.pinky.tick(); + self.inky.tick(); + self.clyde.tick(); for edible in self.edibles.iter_mut() { if let EdibleKind::PowerPellet = edible.kind { if let crate::entity::edible::EdibleSprite::PowerPellet(texture) = &mut edible.sprite { @@ -271,6 +353,9 @@ impl Game { } let _ = this.pacman.borrow_mut().render(texture_canvas); let _ = this.blinky.render(texture_canvas); + let _ = this.pinky.render(texture_canvas); + let _ = this.inky.render(texture_canvas); + let _ = this.clyde.render(texture_canvas); match this.debug_mode { DebugMode::Grid => { DebugRenderer::draw_debug_grid( diff --git a/src/map.rs b/src/map.rs index 5a226f8..0e944ec 100644 --- a/src/map.rs +++ b/src/map.rs @@ -163,6 +163,28 @@ impl Map { CACHE.get_or_init(|| result) } + /// Finds the starting position for a given entity ID. + /// + /// # Arguments + /// + /// * `entity_id` - The entity ID (0 for Pac-Man, 1-4 for ghosts) + /// + /// # Returns + /// + /// The starting position as UVec2, or None if not found + pub fn find_starting_position(&self, entity_id: u8) -> Option { + for (x, col) in self.current.iter().enumerate().take(BOARD_CELL_SIZE.x as usize) { + for (y, &cell) in col.iter().enumerate().take(BOARD_CELL_SIZE.y as usize) { + if let MapTile::StartingPosition(id) = cell { + if id == entity_id { + return Some(UVec2::new(x as u32, y as u32)); + } + } + } + } + None + } + /// Renders the map to the given canvas using the provided map texture. pub fn render(&self, canvas: &mut Canvas, map_texture: &mut AtlasTile) { let dest = Rect::new( diff --git a/src/texture/animated.rs b/src/texture/animated.rs index 2437ede..20d3ed8 100644 --- a/src/texture/animated.rs +++ b/src/texture/animated.rs @@ -10,7 +10,6 @@ pub struct AnimatedTexture { pub frames: Vec, pub ticks_per_frame: u32, pub ticker: u32, - pub reversed: bool, pub paused: bool, } @@ -20,7 +19,6 @@ impl AnimatedTexture { frames, ticks_per_frame, ticker: 0, - reversed: false, paused: false, } }