Compare commits

...

4 Commits

8 changed files with 320 additions and 35 deletions

22
Cargo.lock generated
View File

@@ -17,6 +17,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "c_vec"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdd7a427adc0135366d99db65b36dae9237130997e560ed61118041fb72be6e8"
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -160,11 +166,12 @@ checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
[[package]]
name = "sdl2"
version = "0.35.2"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a"
checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef"
dependencies = [
"bitflags",
"c_vec",
"lazy_static",
"libc",
"sdl2-sys",
@@ -172,12 +179,13 @@ dependencies = [
[[package]]
name = "sdl2-sys"
version = "0.35.2"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0"
checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0"
dependencies = [
"cfg-if",
"libc",
"vcpkg",
"version-compare",
]
@@ -311,6 +319,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version-compare"
version = "0.1.1"

View File

@@ -7,8 +7,20 @@ edition = "2021"
[dependencies]
lazy_static = "1.4.0"
sdl2 = { version = "0.35", features = ["image", "ttf", "mixer"] }
spin_sleep = "1.1.1"
tracing = { version = "0.1.37", features = ["max_level_debug", "release_max_level_warn"]}
tracing-error = "0.2.0"
tracing-subscriber = {version = "0.3.17", features = ["env-filter"]}
tracing-subscriber = {version = "0.3.17", features = ["env-filter"]}
[dependencies.sdl2]
version = "0.38"
default-features = false
features = ["ttf","image","gfx","mixer","static-link","use-vcpkg"]
[package.metadata.vcpkg]
dependencies = ["sdl2", "sdl2-image[libjpeg-turbo,tiff,libwebp]", "sdl2-ttf", "sdl2-gfx", "sdl2-mixer"]
git = "https://github.com/microsoft/vcpkg"
rev = "2024.05.24" # release 2024.05.24 # to check for a new one, check https://github.com/microsoft/vcpkg/releases
[package.metadata.vcpkg.target]
x86_64-pc-windows-msvc = { triplet = "x64-windows-static-md" }

BIN
assets/font/konami.ttf Normal file
View File

Binary file not shown.

View File

@@ -3,8 +3,10 @@ use std::rc::Rc;
use sdl2::image::LoadTexture;
use sdl2::keyboard::Keycode;
use sdl2::render::{Texture, TextureCreator};
use sdl2::ttf::{Font, FontStyle};
use sdl2::video::WindowContext;
use sdl2::{pixels::Color, render::Canvas, video::Window};
use tracing::event;
use crate::constants::{MapTile, BOARD_HEIGHT, BOARD_WIDTH, RAW_BOARD};
use crate::direction::Direction;
@@ -15,22 +17,38 @@ use crate::pacman::Pacman;
pub struct Game<'a> {
canvas: &'a mut Canvas<Window>,
map_texture: Texture<'a>,
pellet_texture: Texture<'a>,
power_pellet_texture: Texture<'a>,
font: Font<'a, 'static>,
pacman: Pacman<'a>,
map: Rc<Map>,
map: Rc<std::cell::RefCell<Map>>,
debug: bool,
score: u32,
}
impl Game<'_> {
pub fn new<'a>(
canvas: &'a mut Canvas<Window>,
texture_creator: &'a TextureCreator<WindowContext>,
ttf_context: &'a sdl2::ttf::Sdl2TtfContext,
) -> Game<'a> {
let map = Rc::new(Map::new(RAW_BOARD));
let map = Rc::new(std::cell::RefCell::new(Map::new(RAW_BOARD)));
let pacman_atlas = texture_creator
.load_texture("assets/32/pacman.png")
.expect("Could not load pacman texture");
let pacman = Pacman::new((1, 1), pacman_atlas, Rc::clone(&map));
let pellet_texture = texture_creator
.load_texture("assets/24/pellet.png")
.expect("Could not load pellet texture");
let power_pellet_texture = texture_creator
.load_texture("assets/24/energizer.png")
.expect("Could not load power pellet texture");
let font = ttf_context
.load_font("assets/font/konami.ttf", 24)
.expect("Could not load font");
Game {
canvas,
pacman: pacman,
@@ -38,7 +56,11 @@ impl Game<'_> {
map: map,
map_texture: texture_creator
.load_texture("assets/map.png")
.expect("Could not load pacman texture"),
.expect("Could not load map texture"),
pellet_texture,
power_pellet_texture,
font,
score: 0,
}
}
@@ -53,8 +75,57 @@ impl Game<'_> {
}
}
pub fn add_score(&mut self, points: u32) {
self.score += points;
}
pub fn tick(&mut self) {
self.pacman.tick();
self.check_pellet_eating();
}
fn check_pellet_eating(&mut self) {
let cell_pos = self.pacman.cell_position();
// Check if there's a pellet at the current position
let tile = {
let map = self.map.borrow();
map.get_tile((cell_pos.0 as i32, cell_pos.1 as i32))
};
if let Some(tile) = tile {
match tile {
MapTile::Pellet => {
// Eat the pellet and add score
{
let mut map = self.map.borrow_mut();
map.set_tile((cell_pos.0 as i32, cell_pos.1 as i32), MapTile::Empty);
}
self.add_score(10);
event!(
tracing::Level::DEBUG,
"Pellet eaten at ({}, {})",
cell_pos.0,
cell_pos.1
);
}
MapTile::PowerPellet => {
// Eat the power pellet and add score
{
let mut map = self.map.borrow_mut();
map.set_tile((cell_pos.0 as i32, cell_pos.1 as i32), MapTile::Empty);
}
self.add_score(50);
event!(
tracing::Level::DEBUG,
"Power pellet eaten at ({}, {})",
cell_pos.0,
cell_pos.1
);
}
_ => {}
}
}
}
pub fn draw(&mut self) {
@@ -62,21 +133,27 @@ impl Game<'_> {
self.canvas.set_draw_color(Color::RGB(0, 0, 0));
self.canvas.clear();
// Render the map
// Render the map
self.canvas
.copy(&self.map_texture, None, None)
.expect("Could not render texture on canvas");
// Render pellets
self.render_pellets();
// Render the pacman
self.pacman.render(self.canvas);
// Draw a grid
// Render score
self.render_score();
// Draw the debug grid
if self.debug {
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 mut color = None;
@@ -110,6 +187,7 @@ impl Game<'_> {
fn draw_cell(&mut self, cell: (u32, u32), color: Color) {
let position = Map::cell_to_pixel(cell);
self.canvas.set_draw_color(color);
self.canvas
.draw_rect(sdl2::rect::Rect::new(
@@ -120,4 +198,78 @@ 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) {
let lives = 3;
let score_text = format!("{:02}", self.score);
let x_offset = 12;
let y_offset = 2;
let lives_offset = 3;
let score_offset = 7 - (score_text.len() as i32);
let gap_offset = 6;
self.render_text(
&format!("{}UP HIGH SCORE ", lives),
(24 * lives_offset + x_offset, y_offset),
Color::WHITE,
);
self.render_text(
&score_text,
(24 * score_offset + x_offset, 24 + y_offset + gap_offset),
Color::WHITE,
);
}
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 texture_creator = self.canvas.texture_creator();
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);
self.canvas
.copy(&texture, None, Some(dst_rect))
.expect("Could not render text texture");
}
}

View File

@@ -1,15 +1,110 @@
/// Checks if two grid positions are adjacent to each other
///
/// # Arguments
/// * `a` - First position as (x, y) coordinates
/// * `b` - Second position as (x, y) coordinates
/// * `diagonal` - Whether to consider diagonal adjacency (true) or only orthogonal (false)
///
/// # Returns
/// * `true` if positions are adjacent according to the diagonal parameter
/// * `false` otherwise
pub fn is_adjacent(a: (u32, u32), b: (u32, u32), diagonal: bool) -> bool {
let (ax, ay) = a;
let (bx, by) = b;
// Calculate absolute differences between coordinates
let dx = if ax > bx { ax - bx } else { bx - ax };
let dy = if ay > by { ay - by } else { by - ay };
if diagonal {
(ax == bx && (ay == by + 1 || ay == by - 1))
|| (ay == by && (ax == bx + 1 || ax == bx - 1))
|| (ax == bx + 1 && ay == by + 1)
|| (ax == bx + 1 && ay == by - 1)
|| (ax == bx - 1 && ay == by + 1)
|| (ax == bx - 1 && ay == by - 1)
// For diagonal adjacency: both differences must be ≤ 1 and at least one > 0
dx <= 1 && dy <= 1 && (dx + dy) > 0
} else {
(ax == bx && (ay == by + 1 || ay == by - 1))
|| (ay == by && (ax == bx + 1 || ax == bx - 1))
// For orthogonal adjacency: exactly one difference must be 1, the other 0
(dx == 1 && dy == 0) || (dx == 0 && dy == 1)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_orthogonal_adjacency() {
// Test orthogonal adjacency (diagonal = false)
// Same position should not be adjacent
assert!(!is_adjacent((0, 0), (0, 0), false));
// Adjacent positions should be true
assert!(is_adjacent((0, 0), (1, 0), false)); // Right
assert!(is_adjacent((0, 0), (0, 1), false)); // Down
assert!(is_adjacent((1, 1), (0, 1), false)); // Left
assert!(is_adjacent((1, 1), (1, 0), false)); // Up
// Diagonal positions should be false
assert!(!is_adjacent((0, 0), (1, 1), false));
assert!(!is_adjacent((0, 1), (1, 0), false));
// Positions more than 1 step away should be false
assert!(!is_adjacent((0, 0), (2, 0), false));
assert!(!is_adjacent((0, 0), (0, 2), false));
assert!(!is_adjacent((0, 0), (2, 2), false));
}
#[test]
fn test_diagonal_adjacency() {
// Test diagonal adjacency (diagonal = true)
// Same position should not be adjacent
assert!(!is_adjacent((0, 0), (0, 0), true));
// Orthogonal adjacent positions should be true
assert!(is_adjacent((0, 0), (1, 0), true)); // Right
assert!(is_adjacent((0, 0), (0, 1), true)); // Down
assert!(is_adjacent((1, 1), (0, 1), true)); // Left
assert!(is_adjacent((1, 1), (1, 0), true)); // Up
// Diagonal adjacent positions should be true
assert!(is_adjacent((0, 0), (1, 1), true)); // Down-right
assert!(is_adjacent((1, 0), (0, 1), true)); // Down-left
assert!(is_adjacent((0, 1), (1, 0), true)); // Up-right
assert!(is_adjacent((1, 1), (0, 0), true)); // Up-left
// Positions more than 1 step away should be false
assert!(!is_adjacent((0, 0), (2, 0), true));
assert!(!is_adjacent((0, 0), (0, 2), true));
assert!(!is_adjacent((0, 0), (2, 2), true));
assert!(!is_adjacent((0, 0), (1, 2), true));
}
#[test]
fn test_edge_cases() {
// Test with larger coordinates
assert!(is_adjacent((100, 100), (101, 100), false));
assert!(is_adjacent((100, 100), (100, 101), false));
assert!(!is_adjacent((100, 100), (102, 100), false));
assert!(is_adjacent((100, 100), (101, 101), true));
assert!(!is_adjacent((100, 100), (102, 102), true));
// Test with zero coordinates
assert!(is_adjacent((0, 0), (1, 0), false));
assert!(is_adjacent((0, 0), (0, 1), false));
assert!(is_adjacent((0, 0), (1, 1), true));
}
#[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, 3), true),
is_adjacent((2, 3), (1, 2), true)
);
}
}

View File

@@ -12,6 +12,7 @@ mod constants;
mod direction;
mod entity;
mod game;
mod helper;
mod map;
mod modulation;
mod pacman;
@@ -19,6 +20,7 @@ mod pacman;
pub fn main() {
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
let ttf_context = sdl2::ttf::init().unwrap();
// Setup tracing
let subscriber = tracing_subscriber::fmt()
@@ -45,7 +47,7 @@ pub fn main() {
.expect("Could not set logical size");
let texture_creator = canvas.texture_creator();
let mut game = Game::new(&mut canvas, &texture_creator);
let mut game = Game::new(&mut canvas, &texture_creator, &ttf_context);
let mut event_pump = sdl_context
.event_pump()
@@ -149,14 +151,6 @@ pub fn main() {
let average_sleep = sleep_time / PERIOD;
let average_process = loop_time - average_sleep;
event!(
tracing::Level::DEBUG,
"Timing Averages [fps={}] [sleep={:?}] [process={:?}]",
average_fps,
average_sleep,
average_process
);
sleep_time = Duration::ZERO;
last_averaging_time = Instant::now();
}

View File

@@ -53,6 +53,18 @@ impl Map {
Some(self.inner[x][y])
}
pub fn set_tile(&mut self, cell: (i32, i32), tile: MapTile) -> bool {
let x = cell.0 as usize;
let y = cell.1 as usize;
if x >= BOARD_WIDTH as usize || y >= BOARD_HEIGHT as usize {
return false;
}
self.inner[x][y] = tile;
true
}
pub fn cell_to_pixel(cell: (u32, u32)) -> (i32, i32) {
((cell.0 as i32) * 24, ((cell.1 + 3) as i32) * 24)
}

View File

@@ -1,3 +1,4 @@
use std::cell::RefCell;
use std::rc::Rc;
use sdl2::{
@@ -22,14 +23,18 @@ pub struct Pacman<'a> {
pub direction: Direction,
pub next_direction: Option<Direction>,
pub stopped: bool,
map: Rc<Map>,
map: Rc<RefCell<Map>>,
speed: u32,
modulation: SimpleTickModulator,
sprite: AnimatedTexture<'a>,
}
impl Pacman<'_> {
pub fn new<'a>(starting_position: (u32, u32), atlas: Texture<'a>, map: Rc<Map>) -> Pacman<'a> {
pub fn new<'a>(
starting_position: (u32, u32),
atlas: Texture<'a>,
map: Rc<RefCell<Map>>,
) -> Pacman<'a> {
Pacman {
position: Map::cell_to_pixel(starting_position),
direction: Direction::Right,
@@ -70,6 +75,7 @@ impl Pacman<'_> {
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 {
@@ -79,7 +85,7 @@ impl Pacman<'_> {
}
fn internal_position_even(&self) -> (u32, u32) {
let (x, y ) = self.internal_position();
let (x, y) = self.internal_position();
((x / 2u32) * 2u32, (y / 2u32) * 2u32)
}
}
@@ -115,7 +121,7 @@ impl Entity for Pacman<'_> {
self.handle_requested_direction();
let next = self.next_cell(None);
let next_tile = self.map.get_tile(next).unwrap_or(MapTile::Empty);
let next_tile = self.map.borrow().get_tile(next).unwrap_or(MapTile::Empty);
if !self.stopped && next_tile == MapTile::Wall {
event!(tracing::Level::DEBUG, "Wall collision. Stopping.");
@@ -125,7 +131,7 @@ impl Entity for Pacman<'_> {
self.stopped = false;
}
}
if !self.stopped && self.modulation.next() {
let speed = self.speed as i32;
match self.direction {