diff --git a/src/direction.rs b/src/direction.rs index 82d5234..0127200 100644 --- a/src/direction.rs +++ b/src/direction.rs @@ -1,5 +1,8 @@ +//! This module defines the `Direction` enum, which is used to represent the +//! direction of an entity. use sdl2::keyboard::Keycode; +/// An enum representing the direction of an entity. #[derive(Debug, Copy, Clone, PartialEq)] pub enum Direction { Up, @@ -9,6 +12,7 @@ pub enum Direction { } impl Direction { + /// Returns the angle of the direction in degrees. pub fn angle(&self) -> f64 { match self { Direction::Right => 0f64, @@ -18,6 +22,7 @@ impl Direction { } } + /// Returns the offset of the direction as a tuple of (x, y). pub fn offset(&self) -> (i32, i32) { match self { Direction::Right => (1, 0), @@ -27,16 +32,27 @@ impl Direction { } } + /// Returns the opposite direction. + pub fn opposite(&self) -> Direction { + match self { + Direction::Right => Direction::Left, + Direction::Down => Direction::Up, + Direction::Left => Direction::Right, + Direction::Up => Direction::Down, + } + } + + /// Creates a `Direction` from a `Keycode`. + /// + /// # Arguments + /// + /// * `keycode` - The keycode to convert. pub fn from_keycode(keycode: Keycode) -> Option { match keycode { - Keycode::D => Some(Direction::Right), - Keycode::Right => Some(Direction::Right), - Keycode::A => Some(Direction::Left), - Keycode::Left => Some(Direction::Left), - Keycode::W => Some(Direction::Up), - Keycode::Up => Some(Direction::Up), - Keycode::S => Some(Direction::Down), - Keycode::Down => Some(Direction::Down), + Keycode::D | Keycode::Right => Some(Direction::Right), + Keycode::A | Keycode::Left => Some(Direction::Left), + Keycode::W | Keycode::Up => Some(Direction::Up), + Keycode::S | Keycode::Down => Some(Direction::Down), _ => None, } } diff --git a/src/game.rs b/src/game.rs index 2339088..91d7136 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,3 +1,4 @@ +//! This module contains the main game logic and state. use std::rc::Rc; use sdl2::image::LoadTexture; @@ -23,6 +24,10 @@ static POWER_PELLET_TEXTURE_DATA: &[u8] = include_bytes!("../assets/24/energizer static MAP_TEXTURE_DATA: &[u8] = include_bytes!("../assets/map.png"); static FONT_DATA: &[u8] = include_bytes!("../assets/font/konami.ttf"); +/// The main game state. +/// +/// This struct contains all the information necessary to run the game, including +/// the canvas, textures, fonts, game objects, and the current score. pub struct Game<'a> { canvas: &'a mut Canvas, map_texture: Texture<'a>, @@ -37,6 +42,14 @@ pub struct Game<'a> { } impl Game<'_> { + /// Creates a new `Game` instance. + /// + /// # Arguments + /// + /// * `canvas` - The SDL canvas to render to. + /// * `texture_creator` - The SDL texture creator. + /// * `ttf_context` - The SDL TTF context. + /// * `_audio_subsystem` - The SDL audio subsystem (currently unused). pub fn new<'a>( canvas: &'a mut Canvas, texture_creator: &'a TextureCreator, @@ -89,6 +102,11 @@ impl Game<'_> { } } + /// Handles a keyboard event. + /// + /// # Arguments + /// + /// * `keycode` - The keycode of the key that was pressed. pub fn keyboard_event(&mut self, keycode: Keycode) { // Change direction let direction = Direction::from_keycode(keycode); @@ -105,10 +123,16 @@ impl Game<'_> { } } + /// Adds points to the score. + /// + /// # Arguments + /// + /// * `points` - The number of points to add. pub fn add_score(&mut self, points: u32) { self.score += points; } + /// Resets the game to its initial state. pub fn reset(&mut self) { // Reset the map to restore all pellets { @@ -126,11 +150,14 @@ impl Game<'_> { event!(tracing::Level::INFO, "Game reset - map and score cleared"); } + /// Advances the game by one tick. pub fn tick(&mut self) { - self.pacman.tick(); self.check_pellet_eating(); + self.pacman.tick(); } + /// Checks if Pac-Man is currently eating a pellet and updates the game state + /// accordingly. fn check_pellet_eating(&mut self) { let cell_pos = self.pacman.cell_position(); @@ -164,6 +191,7 @@ impl Game<'_> { } } + /// Draws the entire game to the canvas. pub fn draw(&mut self) { // Clear the screen (black) self.canvas.set_draw_color(Color::RGB(0, 0, 0)); @@ -175,13 +203,35 @@ impl Game<'_> { .expect("Could not render texture on canvas"); // Render pellets - self.render_pellets(); + for x in 0..BOARD_WIDTH { + for y in 0..BOARD_HEIGHT { + let tile = self + .map + .borrow() + .get_tile((x as i32, y as i32)) + .unwrap_or(MapTile::Empty); + + let texture = match tile { + MapTile::Pellet => Some(&self.pellet_texture), + MapTile::PowerPellet => Some(&self.power_pellet_texture), + _ => None, + }; + + if let Some(texture) = texture { + let position = Map::cell_to_pixel((x, y)); + let dst_rect = sdl2::rect::Rect::new(position.0, position.1, 24, 24); + self.canvas + .copy(texture, None, Some(dst_rect)) + .expect("Could not render pellet"); + } + } + } // Render the pacman self.pacman.render(self.canvas); // Render score - self.render_score(); + self.render_ui(); // Draw the debug grid if self.debug { @@ -221,6 +271,12 @@ impl Game<'_> { self.canvas.present(); } + /// Draws a single cell to the canvas with the given color. + /// + /// # Arguments + /// + /// * `cell` - The cell to draw, in grid coordinates. + /// * `color` - The color to draw the cell with. fn draw_cell(&mut self, cell: (u32, u32), color: Color) { let position = Map::cell_to_pixel(cell); @@ -235,37 +291,8 @@ impl Game<'_> { .expect("Could not draw rectangle"); } - fn render_pellets(&mut self) { - for x in 0..BOARD_WIDTH { - for y in 0..BOARD_HEIGHT { - let tile = self - .map - .borrow() - .get_tile((x as i32, y as i32)) - .unwrap_or(MapTile::Empty); - - match tile { - MapTile::Pellet => { - let position = Map::cell_to_pixel((x, y)); - let dst_rect = sdl2::rect::Rect::new(position.0, position.1, 24, 24); - self.canvas - .copy(&self.pellet_texture, None, Some(dst_rect)) - .expect("Could not render pellet"); - } - MapTile::PowerPellet => { - let position = Map::cell_to_pixel((x, y)); - let dst_rect = sdl2::rect::Rect::new(position.0, position.1, 24, 24); - self.canvas - .copy(&self.power_pellet_texture, None, Some(dst_rect)) - .expect("Could not render power pellet"); - } - _ => {} - } - } - } - } - - fn render_score(&mut self) { + /// Renders the user interface, including the score and lives. + fn render_ui(&mut self) { let lives = 3; let score_text = format!("{:02}", self.score); @@ -275,6 +302,7 @@ impl Game<'_> { let score_offset = 7 - (score_text.len() as i32); let gap_offset = 6; + // Render the score and high score self.render_text( &format!("{}UP HIGH SCORE ", lives), (24 * lives_offset + x_offset, y_offset), @@ -287,6 +315,7 @@ impl Game<'_> { ); } + /// Renders text to the screen at the given position. fn render_text(&mut self, text: &str, position: (i32, i32), color: Color) { let surface = self .font @@ -298,11 +327,9 @@ impl Game<'_> { let texture = texture_creator .create_texture_from_surface(&surface) .expect("Could not create texture from surface"); - let query = texture.query(); - let dst_rect = - sdl2::rect::Rect::new(position.0, position.1, query.width + 4, query.height + 4); + let dst_rect = sdl2::rect::Rect::new(position.0, position.1, query.width, query.height); self.canvas .copy(&texture, None, Some(dst_rect)) diff --git a/src/pacman.rs b/src/pacman.rs index 6a26945..7657ace 100644 --- a/src/pacman.rs +++ b/src/pacman.rs @@ -1,3 +1,4 @@ +//! This module defines the Pac-Man entity, including its behavior and rendering. use std::cell::RefCell; use std::rc::Rc; @@ -17,11 +18,18 @@ use crate::{ modulation::{SimpleTickModulator, TickModulator}, }; +/// The Pac-Man entity. pub struct Pacman<'a> { - // Absolute position on the board (precise) - pub position: (i32, i32), + /// The absolute position of Pac-Man on the board, in pixels. + pub pixel_position: (i32, i32), + /// The position of Pac-Man on the board, in grid coordinates. + /// This is only updated at the moment Pac-Man is aligned with the grid. + pub cell_position: (u32, u32), + /// The current direction of Pac-Man. pub direction: Direction, + /// The next direction of Pac-Man, which will be applied when Pac-Man is next aligned with the grid. pub next_direction: Option, + /// Whether Pac-Man is currently stopped. pub stopped: bool, map: Rc>, speed: u32, @@ -30,13 +38,21 @@ pub struct Pacman<'a> { } impl Pacman<'_> { + /// Creates a new `Pacman` instance. + /// + /// # Arguments + /// + /// * `starting_position` - The starting position of Pac-Man, in grid coordinates. + /// * `atlas` - The texture atlas containing the Pac-Man sprites. + /// * `map` - A reference to the game map. pub fn new<'a>( starting_position: (u32, u32), atlas: Texture<'a>, map: Rc>, ) -> Pacman<'a> { Pacman { - position: Map::cell_to_pixel(starting_position), + pixel_position: Map::cell_to_pixel(starting_position), + cell_position: starting_position, direction: Direction::Right, next_direction: None, speed: 3, @@ -47,53 +63,85 @@ impl Pacman<'_> { } } + /// Renders Pac-Man to the canvas. + /// + /// # Arguments + /// + /// * `canvas` - The SDL canvas to render to. pub fn render(&mut self, canvas: &mut Canvas) { // When stopped, render the last frame of the animation if self.stopped { self.sprite - .render_until(canvas, self.position, self.direction, 2); + .render_until(canvas, self.pixel_position, self.direction, 2); } else { - self.sprite.render(canvas, self.position, self.direction); + self.sprite + .render(canvas, self.pixel_position, self.direction); } } + /// Calculates the next cell in the given direction. + /// + /// # Arguments + /// + /// * `direction` - The direction to check. If `None`, the current direction is used. pub fn next_cell(&self, direction: Option) -> (i32, i32) { let (x, y) = direction.unwrap_or(self.direction).offset(); - let cell = self.cell_position(); + let cell = self.cell_position; (cell.0 as i32 + x, cell.1 as i32 + y) } - fn handle_requested_direction(&mut self) { - if self.next_direction.is_none() { - return; - } - if self.next_direction.unwrap() == self.direction { - self.next_direction = None; - return; + /// Handles a requested direction change. + /// + /// The direction change is only applied if the next tile in the requested + /// direction is not a wall. + fn handle_direction_change(&mut self) -> bool { + match self.next_direction { + // If there is no next direction, do nothing. + None => return false, + // If the next direction is the same as the current direction, do nothing. + Some(next_direction) => { + if next_direction == self.direction { + self.next_direction = None; + return false; + } + } } + // Get the next cell in the proposed direction. let proposed_next_cell = self.next_cell(self.next_direction); let proposed_next_tile = self .map .borrow() .get_tile(proposed_next_cell) .unwrap_or(MapTile::Empty); - if proposed_next_tile != MapTile::Wall { - event!( - tracing::Level::DEBUG, - "Direction change: {:?} -> {:?} at position ({}, {}) internal ({}, {})", - self.direction, - self.next_direction.unwrap(), - self.position.0, - self.position.1, - self.internal_position().0, - self.internal_position().1 - ); - self.direction = self.next_direction.unwrap(); - self.next_direction = None; + + // If the next tile is a wall, do nothing. + if proposed_next_tile == MapTile::Wall { + return false; } + + // If the next tile is not a wall, change direction. + event!( + tracing::Level::DEBUG, + "Direction change: {:?} -> {:?} at position ({}, {}) internal ({}, {})", + self.direction, + self.next_direction.unwrap(), + self.pixel_position.0, + self.pixel_position.1, + self.internal_position().0, + self.internal_position().1 + ); + self.direction = self.next_direction.unwrap(); + self.next_direction = None; + + true } + /// Returns the internal position of Pac-Man, rounded down to the nearest + /// even number. + /// + /// This is used to ensure that Pac-Man is aligned with the grid before + /// changing direction. fn internal_position_even(&self) -> (u32, u32) { let (x, y) = self.internal_position(); ((x / 2u32) * 2u32, (y / 2u32) * 2u32) @@ -108,15 +156,11 @@ impl Entity for Pacman<'_> { } fn position(&self) -> (i32, i32) { - self.position + self.pixel_position } fn cell_position(&self) -> (u32, u32) { - let (x, y) = self.position; - ( - (x as u32 / CELL_SIZE) - BOARD_OFFSET.0, - (y as u32 / CELL_SIZE) - BOARD_OFFSET.1, - ) + self.cell_position } fn internal_position(&self) -> (u32, u32) { @@ -125,13 +169,35 @@ impl Entity for Pacman<'_> { } fn tick(&mut self) { + // Pac-Man can only change direction when he is perfectly aligned with the grid. let can_change = self.internal_position_even() == (0, 0); - if can_change { - self.handle_requested_direction(); + if let Some(next_direction) = self.next_direction { + if next_direction == self.direction.opposite() { + let next_tile_position = self.next_cell(Some(next_direction)); + let next_tile = self + .map + .borrow() + .get_tile(next_tile_position) + .unwrap_or(MapTile::Empty); - let next = self.next_cell(None); - let next_tile = self.map.borrow().get_tile(next).unwrap_or(MapTile::Empty); + if next_tile != MapTile::Wall { + self.direction = next_direction; + self.next_direction = None; + } + } + } + + 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."); @@ -146,18 +212,26 @@ impl Entity for Pacman<'_> { let speed = self.speed as i32; match self.direction { Direction::Right => { - self.position.0 += speed; + self.pixel_position.0 += speed; } Direction::Left => { - self.position.0 -= speed; + self.pixel_position.0 -= speed; } Direction::Up => { - self.position.1 -= speed; + self.pixel_position.1 -= speed; } Direction::Down => { - self.position.1 += speed; + self.pixel_position.1 += speed; } } + + // Update the cell position if Pac-Man is aligned with the grid. + if self.internal_position_even() == (0, 0) { + 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, + ); + } } } }