Compare commits

..

3 Commits

13 changed files with 87 additions and 179 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

6
rustfmt.toml Normal file
View File

@@ -0,0 +1,6 @@
# Rustfmt configuration
edition = "2021"
max_width = 130
tab_spaces = 4
newline_style = "Unix"
use_small_heuristics = "Default"

View File

@@ -9,13 +9,7 @@ use crate::direction::Direction;
/// Trait for drawable atlas-based textures
pub trait FrameDrawn {
fn render(
&self,
canvas: &mut Canvas<Window>,
position: (i32, i32),
direction: Direction,
frame: Option<u32>,
);
fn render(&self, canvas: &mut Canvas<Window>, position: (i32, i32), direction: Direction, frame: Option<u32>);
}
/// A texture atlas abstraction for static (non-animated) rendering.
@@ -28,13 +22,7 @@ pub struct AtlasTexture<'a> {
}
impl<'a> AtlasTexture<'a> {
pub fn new(
texture: Texture<'a>,
frame_count: u32,
frame_width: u32,
frame_height: u32,
offset: Option<(i32, i32)>,
) -> Self {
pub fn new(texture: Texture<'a>, frame_count: u32, frame_width: u32, frame_height: u32, offset: Option<(i32, i32)>) -> Self {
AtlasTexture {
raw_texture: texture,
frame_count,
@@ -62,13 +50,7 @@ impl<'a> AtlasTexture<'a> {
}
impl<'a> FrameDrawn for AtlasTexture<'a> {
fn render(
&self,
canvas: &mut Canvas<Window>,
position: (i32, i32),
direction: Direction,
frame: Option<u32>,
) {
fn render(&self, canvas: &mut Canvas<Window>, position: (i32, i32), direction: Direction, frame: Option<u32>) {
let texture_source_frame_rect = self.get_frame_rect(frame.unwrap_or(0));
let canvas_destination_rect = Rect::new(
position.0 + self.offset.0,
@@ -157,13 +139,7 @@ impl<'a> AnimatedAtlasTexture<'a> {
}
impl<'a> FrameDrawn for AnimatedAtlasTexture<'a> {
fn render(
&self,
canvas: &mut Canvas<Window>,
position: (i32, i32),
direction: Direction,
frame: Option<u32>,
) {
fn render(&self, canvas: &mut Canvas<Window>, position: (i32, i32), direction: Direction, frame: Option<u32>) {
let frame = frame.unwrap_or_else(|| self.current_frame());
self.atlas.render(canvas, position, direction, Some(frame));
}

View File

@@ -15,6 +15,7 @@ pub struct Audio {
_mixer_context: mixer::Sdl2MixerContext,
sounds: Vec<Chunk>,
next_sound_index: usize,
muted: bool,
}
impl Audio {
@@ -24,6 +25,7 @@ impl Audio {
let format = DEFAULT_FORMAT;
let channels = 4;
let chunk_size = 128;
mixer::open_audio(frequency, format, 1, chunk_size).expect("Failed to open audio");
mixer::allocate_channels(channels);
@@ -39,8 +41,7 @@ impl Audio {
.enumerate()
.map(|(i, asset)| {
let data = get_asset_bytes(*asset).expect("Failed to load sound asset");
let rwops = RWops::from_bytes(&data)
.unwrap_or_else(|_| panic!("Failed to create RWops for sound {}", i + 1));
let rwops = RWops::from_bytes(&data).unwrap_or_else(|_| panic!("Failed to create RWops for sound {}", i + 1));
rwops
.load_wav()
.unwrap_or_else(|_| panic!("Failed to load sound {} from asset API", i + 1))
@@ -51,21 +52,16 @@ impl Audio {
_mixer_context: mixer_context,
sounds,
next_sound_index: 0,
muted: false,
}
}
/// Plays the "eat" sound effect.
///
/// This function also logs the time since the last sound effect was played.
pub fn eat(&mut self) {
if let Some(chunk) = self.sounds.get(self.next_sound_index) {
match mixer::Channel(0).play(chunk, 0) {
Ok(channel) => {
tracing::info!(
"Playing sound #{} on channel {:?}",
self.next_sound_index + 1,
channel
);
tracing::trace!("Playing sound #{} on channel {:?}", self.next_sound_index + 1, channel);
}
Err(e) => {
tracing::warn!("Could not play sound #{}: {}", self.next_sound_index + 1, e);
@@ -74,4 +70,18 @@ impl Audio {
}
self.next_sound_index = (self.next_sound_index + 1) % self.sounds.len();
}
/// Instantly mute or unmute all channels.
pub fn set_mute(&mut self, mute: bool) {
let channels = 4;
let volume = if mute { 0 } else { 32 };
for i in 0..channels {
mixer::Channel(i).set_volume(volume);
}
self.muted = mute;
}
pub fn is_muted(&self) -> bool {
self.muted
}
}

View File

@@ -64,18 +64,10 @@ pub fn reconstruct_edibles<'a>(
let cell = (x, y);
match tile {
Some(MapTile::Pellet) => {
edibles.push(Edible::new(
EdibleKind::Pellet,
cell,
Rc::clone(&pellet_sprite),
));
edibles.push(Edible::new(EdibleKind::Pellet, cell, Rc::clone(&pellet_sprite)));
}
Some(MapTile::PowerPellet) => {
edibles.push(Edible::new(
EdibleKind::PowerPellet,
cell,
Rc::clone(&power_pellet_sprite),
));
edibles.push(Edible::new(EdibleKind::PowerPellet, cell, Rc::clone(&power_pellet_sprite)));
}
// Fruits can be added here if you have fruit positions
_ => {}

View File

@@ -108,10 +108,7 @@ impl Moving for MovableEntity {
}
fn next_cell(&self, direction: Option<Direction>) -> (i32, i32) {
let (x, y) = direction.unwrap_or(self.direction).offset();
(
self.base.cell_position.0 as i32 + x,
self.base.cell_position.1 as i32 + y,
)
(self.base.cell_position.0 as i32 + x, self.base.cell_position.1 as i32 + y)
}
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool {
let next_cell = self.next_cell(direction);
@@ -119,10 +116,10 @@ impl Moving for MovableEntity {
}
fn handle_tunnel(&mut self) -> bool {
if !self.in_tunnel {
let current_tile = self.map.borrow().get_tile((
self.base.cell_position.0 as i32,
self.base.cell_position.1 as i32,
));
let current_tile = self
.map
.borrow()
.get_tile((self.base.cell_position.0 as i32, self.base.cell_position.1 as i32));
if matches!(current_tile, Some(MapTile::Tunnel)) {
self.in_tunnel = true;
}
@@ -130,14 +127,12 @@ impl Moving for MovableEntity {
if self.in_tunnel {
if self.base.cell_position.0 == 0 {
self.base.cell_position.0 = BOARD_WIDTH - 2;
self.base.pixel_position =
Map::cell_to_pixel((self.base.cell_position.0, self.base.cell_position.1));
self.base.pixel_position = Map::cell_to_pixel((self.base.cell_position.0, self.base.cell_position.1));
self.in_tunnel = false;
true
} else if self.base.cell_position.0 == BOARD_WIDTH - 1 {
self.base.cell_position.0 = 1;
self.base.pixel_position =
Map::cell_to_pixel((self.base.cell_position.0, self.base.cell_position.1));
self.base.pixel_position = Map::cell_to_pixel((self.base.cell_position.0, self.base.cell_position.1));
self.in_tunnel = false;
true
} else {

View File

@@ -1,5 +1,6 @@
//! This module contains the main game logic and state.
use std::cell::RefCell;
use std::ops::Not;
use std::rc::Rc;
use rand::seq::IteratorRandom;
@@ -37,7 +38,7 @@ pub struct Game<'a> {
map: Rc<RefCell<Map>>,
debug_mode: DebugMode,
score: u32,
audio: Audio,
pub audio: Audio,
blinky: Blinky<'a>,
edibles: Vec<Edible<'a>>,
}
@@ -64,11 +65,7 @@ impl<'a> Game<'a> {
let pacman_atlas = texture_creator
.load_texture_bytes(&pacman_bytes)
.expect("Could not load pacman texture from asset API");
let pacman = Rc::new(RefCell::new(Pacman::new(
(1, 1),
pacman_atlas,
Rc::clone(&map),
)));
let pacman = Rc::new(RefCell::new(Pacman::new((1, 1), pacman_atlas, Rc::clone(&map))));
// Load ghost textures
let ghost_body_bytes = get_asset_bytes(Asset::GhostBody).expect("Failed to load asset");
@@ -111,21 +108,6 @@ impl<'a> Game<'a> {
None,
));
// Load font from asset API
let font = {
let font_bytes = get_asset_bytes(Asset::FontKonami)
.expect("Failed to load asset")
.into_owned();
let font_bytes_static: &'static [u8] = Box::leak(font_bytes.into_boxed_slice());
let font_rwops =
RWops::from_bytes(font_bytes_static).expect("Failed to create RWops for font");
ttf_context
.load_font_from_rwops(font_rwops, 24)
.expect("Could not load font from asset API")
};
let audio = Audio::new();
// Load map texture from asset API
let map_bytes = get_asset_bytes(Asset::Map).expect("Failed to load asset");
let mut map_texture = texture_creator
@@ -140,6 +122,18 @@ impl<'a> Game<'a> {
Rc::clone(&pellet_texture), // placeholder for fruit sprite
);
// Load font from asset API
let font = {
let font_bytes = get_asset_bytes(Asset::FontKonami).expect("Failed to load asset").into_owned();
let font_bytes_static: &'static [u8] = Box::leak(font_bytes.into_boxed_slice());
let font_rwops = RWops::from_bytes(font_bytes_static).expect("Failed to create RWops for font");
ttf_context
.load_font_from_rwops(font_rwops, 24)
.expect("Could not load font from asset API")
};
let audio = Audio::new();
Game {
canvas,
pacman,
@@ -180,6 +174,12 @@ impl<'a> Game<'a> {
return;
}
// Toggle mute
if keycode == Keycode::M {
self.audio.set_mute(self.audio.is_muted().not());
return;
}
// Reset game
if keycode == Keycode::R {
self.reset();
@@ -306,18 +306,9 @@ impl<'a> Game<'a> {
// Draw the debug grid
match self.debug_mode {
DebugMode::Grid => {
DebugRenderer::draw_debug_grid(
self.canvas,
&self.map.borrow(),
self.pacman.borrow().base.base.cell_position,
);
let next_cell =
<Pacman as crate::entity::Moving>::next_cell(&*self.pacman.borrow(), None);
DebugRenderer::draw_next_cell(
self.canvas,
&self.map.borrow(),
(next_cell.0 as u32, next_cell.1 as u32),
);
DebugRenderer::draw_debug_grid(self.canvas, &self.map.borrow(), self.pacman.borrow().base.base.cell_position);
let next_cell = <Pacman as crate::entity::Moving>::next_cell(&*self.pacman.borrow(), None);
DebugRenderer::draw_next_cell(self.canvas, &self.map.borrow(), (next_cell.0 as u32, next_cell.1 as u32));
}
DebugMode::ValidPositions => {
DebugRenderer::draw_valid_positions(self.canvas, &mut self.map.borrow_mut());
@@ -358,11 +349,7 @@ impl<'a> Game<'a> {
/// 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
.render(text)
.blended(color)
.expect("Could not render text surface");
let surface = self.font.render(text).blended(color).expect("Could not render text surface");
let texture_creator = self.canvas.texture_creator();
let texture = texture_creator

View File

@@ -121,22 +121,14 @@ impl Ghost<'_> {
let mut possible_moves = Vec::new();
// Check all four directions
for dir in &[
Direction::Up,
Direction::Down,
Direction::Left,
Direction::Right,
] {
for dir in &[Direction::Up, Direction::Down, Direction::Left, Direction::Right] {
// Don't allow reversing direction
if *dir == self.base.direction.opposite() {
continue;
}
let next_cell = self.base.next_cell(Some(*dir));
if !matches!(
self.base.map.borrow().get_tile(next_cell),
Some(MapTile::Wall)
) {
if !matches!(self.base.map.borrow().get_tile(next_cell), Some(MapTile::Wall)) {
possible_moves.push(next_cell);
}
}
@@ -185,12 +177,7 @@ impl Ghost<'_> {
successors.push(((1, p.1), 1));
}
}
for dir in &[
Direction::Up,
Direction::Down,
Direction::Left,
Direction::Right,
] {
for dir in &[Direction::Up, Direction::Down, Direction::Left, Direction::Right] {
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) {
@@ -209,9 +196,8 @@ 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 =
self.mode != GhostMode::House && new_mode != GhostMode::Frightened && self.mode != GhostMode::Frightened;
self.mode = new_mode;
@@ -224,8 +210,7 @@ impl Ghost<'_> {
};
if should_reverse {
self.base
.set_direction_if_valid(self.base.direction.opposite());
self.base.set_direction_if_valid(self.base.direction.opposite());
}
}
@@ -241,9 +226,7 @@ impl Ghost<'_> {
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.0 as u32, target_tile.1 as u32))
{
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.base.base.cell_position;
@@ -318,7 +301,6 @@ impl<'a> Renderable for Ghost<'a> {
Direction::Down => 3,
}
};
self.eyes_sprite
.render(canvas, pos, Direction::Right, Some(eye_frame));
self.eyes_sprite.render(canvas, pos, Direction::Right, Some(eye_frame));
}
}

View File

@@ -23,14 +23,7 @@ impl<'a> Blinky<'a> {
pacman: Rc<RefCell<Pacman<'a>>>,
) -> Blinky<'a> {
Blinky {
ghost: Ghost::new(
GhostType::Blinky,
starting_position,
body_texture,
eyes_texture,
map,
pacman,
),
ghost: Ghost::new(GhostType::Blinky, starting_position, body_texture, eyes_texture, map, pacman),
}
}

View File

@@ -4,7 +4,7 @@
///
/// # Arguments
/// * `a` - First position as (x, y) coordinates
/// * `b` - Second position as (x, y) coordinates
/// * `b` - Second position as (x, y) coordinates
/// * `diagonal` - Whether to consider diagonal adjacency (true) or only orthogonal (false)
///
/// # Returns
@@ -94,14 +94,8 @@ mod tests {
#[test]
fn test_commutative_property() {
// The function should work the same regardless of parameter order
assert_eq!(
is_adjacent((1, 2), (2, 2), false),
is_adjacent((2, 2), (1, 2), false)
);
assert_eq!(is_adjacent((1, 2), (2, 2), false), is_adjacent((2, 2), (1, 2), false));
assert_eq!(
is_adjacent((1, 2), (2, 3), true),
is_adjacent((2, 3), (1, 2), true)
);
assert_eq!(is_adjacent((1, 2), (2, 3), true), is_adjacent((2, 3), (1, 2), true));
}
}

View File

@@ -53,6 +53,7 @@ unsafe fn attach_console() {
}
mod animation;
mod asset;
mod audio;
mod constants;
mod debug;
@@ -66,7 +67,6 @@ mod helper;
mod map;
mod modulation;
mod pacman;
mod asset;
/// The main entry point of the application.
///
@@ -99,26 +99,17 @@ pub fn main() {
.build()
.expect("Could not initialize window");
let mut canvas = window
.into_canvas()
.build()
.expect("Could not build canvas");
let mut canvas = window.into_canvas().build().expect("Could not build canvas");
canvas
.set_logical_size(WINDOW_WIDTH, WINDOW_HEIGHT)
.expect("Could not set logical size");
let texture_creator = canvas.texture_creator();
let mut game = Game::new(
&mut canvas,
&texture_creator,
&ttf_context,
&audio_subsystem,
);
let mut game = Game::new(&mut canvas, &texture_creator, &ttf_context, &audio_subsystem);
game.audio.set_mute(cfg!(debug_assertions));
let mut event_pump = sdl_context
.event_pump()
.expect("Could not get SDL EventPump");
let mut event_pump = sdl_context.event_pump().expect("Could not get SDL EventPump");
// Initial draw and tick
game.draw();
@@ -169,11 +160,7 @@ pub fn main() {
..
} => {
paused = !paused;
event!(
tracing::Level::INFO,
"{}",
if paused { "Paused" } else { "Unpaused" }
);
event!(tracing::Level::INFO, "{}", if paused { "Paused" } else { "Unpaused" });
}
Event::KeyDown { keycode, .. } => {
game.keyboard_event(keycode.unwrap());

View File

@@ -75,9 +75,7 @@ impl Map {
'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)
}
c @ '0' | c @ '1' | c @ '2' | c @ '3' | c @ '4' => MapTile::StartingPosition(c.to_digit(10).unwrap() as u8),
'=' => MapTile::Empty,
_ => panic!("Unknown character in board: {character}"),
};
@@ -94,12 +92,7 @@ impl Map {
/// Resets the map to its original state.
pub fn reset(&mut self) {
// Restore the map to its original state
for (x, col) in self
.current
.iter_mut()
.enumerate()
.take(BOARD_WIDTH as usize)
{
for (x, col) in self.current.iter_mut().enumerate().take(BOARD_WIDTH as usize) {
for (y, cell) in col.iter_mut().enumerate().take(BOARD_HEIGHT as usize) {
*cell = self.default[x][y];
}
@@ -146,10 +139,7 @@ impl Map {
///
/// * `cell` - The cell coordinates, in grid coordinates.
pub fn cell_to_pixel(cell: (u32, u32)) -> (i32, i32) {
(
(cell.0 * CELL_SIZE) as i32,
((cell.1 + BOARD_OFFSET.1) * CELL_SIZE) as i32,
)
((cell.0 * CELL_SIZE) as i32, ((cell.1 + BOARD_OFFSET.1) * CELL_SIZE) as i32)
}
/// Returns a reference to a cached vector of all valid playable positions in the maze.
@@ -198,8 +188,7 @@ impl Map {
] {
let neighbor = pos + offset;
if neighbor.x < BOARD_WIDTH && neighbor.y < BOARD_HEIGHT {
let neighbor_tile =
self.current[neighbor.x as usize][neighbor.y as usize];
let neighbor_tile = self.current[neighbor.x as usize][neighbor.y as usize];
if matches!(neighbor_tile, Empty | Pellet | PowerPellet) {
queue.push_back(neighbor);
}

View File

@@ -58,11 +58,7 @@ impl<'a> Moving for Pacman<'a> {
impl Pacman<'_> {
/// Creates a new `Pacman` instance.
pub fn new<'a>(
starting_position: (u32, u32),
atlas: Texture<'a>,
map: Rc<RefCell<Map>>,
) -> Pacman<'a> {
pub fn new<'a>(starting_position: (u32, u32), atlas: Texture<'a>, map: Rc<RefCell<Map>>) -> Pacman<'a> {
let pixel_position = Map::cell_to_pixel(starting_position);
Pacman {
base: MovableEntity::new(