mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-09 10:07:58 -06:00
334 lines
12 KiB
Rust
334 lines
12 KiB
Rust
//! This module contains the main game logic and state.
|
|
use std::cell::RefCell;
|
|
use std::ops::Not;
|
|
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;
|
|
|
|
use sdl2::render::{Texture, TextureCreator};
|
|
use sdl2::video::WindowContext;
|
|
use sdl2::{pixels::Color, render::Canvas, video::Window};
|
|
|
|
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::pacman::Pacman;
|
|
use crate::entity::Renderable;
|
|
use crate::map::Map;
|
|
use crate::texture::animated::AnimatedTexture;
|
|
use crate::texture::blinking::BlinkingTexture;
|
|
use crate::texture::sprite::{AtlasMapper, AtlasTile, SpriteAtlas};
|
|
use crate::texture::text::TextTexture;
|
|
use crate::texture::{get_atlas_tile, sprite};
|
|
|
|
/// The main game state.
|
|
///
|
|
/// Contains all the information necessary to run the game, including
|
|
/// the game state, rendering resources, and audio.
|
|
pub struct Game {
|
|
// Game state
|
|
pacman: Rc<RefCell<Pacman>>,
|
|
blinky: Blinky,
|
|
edibles: Vec<Edible>,
|
|
map: Rc<RefCell<Map>>,
|
|
score: u32,
|
|
debug_mode: DebugMode,
|
|
|
|
// FPS tracking
|
|
fps_1s: f64,
|
|
fps_10s: f64,
|
|
|
|
// Rendering resources
|
|
atlas: Rc<RefCell<SpriteAtlas>>,
|
|
map_texture: AtlasTile,
|
|
text_texture: TextTexture,
|
|
|
|
// Audio
|
|
pub audio: Audio,
|
|
}
|
|
|
|
impl Game {
|
|
/// Creates a new `Game` instance.
|
|
pub fn new(
|
|
texture_creator: &TextureCreator<WindowContext>,
|
|
_ttf_context: &sdl2::ttf::Sdl2TtfContext,
|
|
_audio_subsystem: &sdl2::AudioSubsystem,
|
|
) -> Game {
|
|
let map = Rc::new(RefCell::new(Map::new(RAW_BOARD)));
|
|
let atlas_bytes = get_asset_bytes(Asset::Atlas).expect("Failed to load asset");
|
|
let atlas_texture = unsafe {
|
|
let texture = texture_creator
|
|
.load_texture_bytes(&atlas_bytes)
|
|
.expect("Could not load atlas texture from asset API");
|
|
sprite::texture_to_static(texture)
|
|
};
|
|
let atlas_json = get_asset_bytes(Asset::AtlasJson).expect("Failed to load asset");
|
|
let atlas_mapper: AtlasMapper = serde_json::from_slice(&atlas_json).expect("Could not parse atlas JSON");
|
|
let atlas = Rc::new(RefCell::new(SpriteAtlas::new(atlas_texture, atlas_mapper)));
|
|
let pacman = Rc::new(RefCell::new(Pacman::new(
|
|
UVec2::new(1, 1),
|
|
Rc::clone(&atlas),
|
|
Rc::clone(&map),
|
|
)));
|
|
let blinky = Blinky::new(UVec2::new(13, 11), Rc::clone(&atlas), Rc::clone(&map), Rc::clone(&pacman));
|
|
let mut map_texture = get_atlas_tile(&atlas, "maze/full.png");
|
|
map_texture.color = Some(Color::RGB(0x20, 0x20, 0xf9));
|
|
|
|
let edibles = reconstruct_edibles(
|
|
Rc::clone(&map),
|
|
AnimatedTexture::new(vec![get_atlas_tile(&atlas, "maze/pellet.png")], 0),
|
|
BlinkingTexture::new(
|
|
AnimatedTexture::new(vec![get_atlas_tile(&atlas, "maze/energizer.png")], 0),
|
|
17,
|
|
17,
|
|
),
|
|
AnimatedTexture::new(vec![get_atlas_tile(&atlas, "edible/cherry.png")], 0),
|
|
);
|
|
let text_texture = TextTexture::new(Rc::clone(&atlas), 1.0);
|
|
let audio = Audio::new();
|
|
Game {
|
|
pacman,
|
|
blinky,
|
|
edibles,
|
|
map,
|
|
score: 0,
|
|
debug_mode: DebugMode::None,
|
|
atlas,
|
|
map_texture,
|
|
text_texture,
|
|
audio,
|
|
fps_1s: 0.0,
|
|
fps_10s: 0.0,
|
|
}
|
|
}
|
|
|
|
/// Handles a keyboard event.
|
|
pub fn keyboard_event(&mut self, keycode: Keycode) {
|
|
// Change direction
|
|
let direction = Direction::from_keycode(keycode);
|
|
if direction.is_some() {
|
|
self.pacman.borrow_mut().next_direction = direction;
|
|
return;
|
|
}
|
|
|
|
// Toggle debug mode
|
|
if keycode == Keycode::Space {
|
|
self.debug_mode = match self.debug_mode {
|
|
DebugMode::None => DebugMode::Grid,
|
|
DebugMode::Grid => DebugMode::Pathfinding,
|
|
DebugMode::Pathfinding => DebugMode::ValidPositions,
|
|
DebugMode::ValidPositions => DebugMode::None,
|
|
};
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// Adds points to the score.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `points` - The number of points to add.
|
|
pub fn add_score(&mut self, points: u32) {
|
|
self.score += points;
|
|
}
|
|
|
|
/// Updates the FPS tracking values.
|
|
pub fn update_fps(&mut self, fps_1s: f64, fps_10s: f64) {
|
|
self.fps_1s = fps_1s;
|
|
self.fps_10s = fps_10s;
|
|
}
|
|
|
|
/// Resets the game to its initial state.
|
|
pub fn reset(&mut self) {
|
|
// Reset the map to restore all pellets
|
|
{
|
|
let mut map = self.map.borrow_mut();
|
|
map.reset();
|
|
}
|
|
|
|
// Reset the score
|
|
self.score = 0;
|
|
|
|
// Get valid positions from the cached flood fill and randomize positions in a single block
|
|
{
|
|
let mut map = self.map.borrow_mut();
|
|
let valid_positions = map.get_valid_playable_positions();
|
|
let mut rng = SmallRng::from_os_rng();
|
|
|
|
// Randomize Pac-Man position
|
|
if let Some(pos) = valid_positions.iter().choose(&mut rng) {
|
|
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.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;
|
|
self.blinky.base.in_tunnel = false;
|
|
self.blinky.base.direction = Direction::Left;
|
|
self.blinky.mode = crate::entity::ghost::GhostMode::Chase;
|
|
}
|
|
}
|
|
|
|
self.edibles = reconstruct_edibles(
|
|
Rc::clone(&self.map),
|
|
AnimatedTexture::new(vec![get_atlas_tile(&self.atlas, "maze/pellet.png")], 0),
|
|
BlinkingTexture::new(
|
|
AnimatedTexture::new(vec![get_atlas_tile(&self.atlas, "maze/energizer.png")], 0),
|
|
12,
|
|
12,
|
|
),
|
|
AnimatedTexture::new(vec![get_atlas_tile(&self.atlas, "edible/cherry.png")], 0),
|
|
);
|
|
}
|
|
|
|
/// Advances the game by one tick.
|
|
pub fn tick(&mut self) {
|
|
self.tick_entities();
|
|
self.handle_edible_collisions();
|
|
self.tick_entities();
|
|
}
|
|
fn tick_entities(&mut self) {
|
|
self.pacman.borrow_mut().tick();
|
|
self.blinky.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 {
|
|
texture.tick();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fn handle_edible_collisions(&mut self) {
|
|
let pacman = self.pacman.borrow();
|
|
let mut eaten_indices = vec![];
|
|
for (i, edible) in self.edibles.iter().enumerate() {
|
|
if edible.collide(&*pacman) {
|
|
eaten_indices.push(i);
|
|
}
|
|
}
|
|
drop(pacman);
|
|
for &i in eaten_indices.iter().rev() {
|
|
let edible = &self.edibles[i];
|
|
match edible.kind {
|
|
EdibleKind::Pellet => {
|
|
self.add_score(10);
|
|
self.audio.eat();
|
|
}
|
|
EdibleKind::PowerPellet => {
|
|
self.add_score(50);
|
|
self.audio.eat();
|
|
}
|
|
EdibleKind::Fruit(_fruit) => {
|
|
self.add_score(100);
|
|
self.audio.eat();
|
|
}
|
|
}
|
|
self.edibles.remove(i);
|
|
// Set Pac-Man to skip the next movement tick
|
|
self.pacman.borrow_mut().skip_move_tick = true;
|
|
}
|
|
}
|
|
|
|
/// Draws the entire game to the canvas using a backbuffer.
|
|
pub fn draw(&mut self, window_canvas: &mut Canvas<Window>, backbuffer: &mut Texture) -> Result<()> {
|
|
window_canvas
|
|
.with_texture_canvas(backbuffer, |texture_canvas| {
|
|
let this = self as *mut Self;
|
|
let this = unsafe { &mut *this };
|
|
texture_canvas.set_draw_color(Color::BLACK);
|
|
texture_canvas.clear();
|
|
this.map.borrow_mut().render(texture_canvas, &mut this.map_texture);
|
|
for edible in this.edibles.iter_mut() {
|
|
let _ = edible.render(texture_canvas);
|
|
}
|
|
let _ = this.pacman.borrow_mut().render(texture_canvas);
|
|
let _ = this.blinky.render(texture_canvas);
|
|
match this.debug_mode {
|
|
DebugMode::Grid => {
|
|
DebugRenderer::draw_debug_grid(
|
|
texture_canvas,
|
|
&this.map.borrow(),
|
|
this.pacman.borrow().base.base.cell_position,
|
|
);
|
|
let next_cell = <Pacman as crate::entity::Moving>::next_cell(&*this.pacman.borrow(), None);
|
|
DebugRenderer::draw_next_cell(texture_canvas, &this.map.borrow(), next_cell.as_uvec2());
|
|
}
|
|
DebugMode::ValidPositions => {
|
|
DebugRenderer::draw_valid_positions(texture_canvas, &mut this.map.borrow_mut());
|
|
}
|
|
DebugMode::Pathfinding => {
|
|
DebugRenderer::draw_pathfinding(texture_canvas, &this.blinky, &this.map.borrow());
|
|
}
|
|
DebugMode::None => {}
|
|
}
|
|
})
|
|
.map_err(|e| anyhow::anyhow!(format!("Failed to render to backbuffer: {e}")))
|
|
}
|
|
pub fn present_backbuffer(&mut self, canvas: &mut Canvas<Window>, backbuffer: &Texture) -> Result<()> {
|
|
canvas.set_draw_color(Color::BLACK);
|
|
canvas.clear();
|
|
canvas.copy(backbuffer, None, None).map_err(anyhow::Error::msg)?;
|
|
self.render_ui_on(canvas);
|
|
canvas.present();
|
|
Ok(())
|
|
}
|
|
|
|
fn render_ui_on<C: sdl2::render::RenderTarget>(&mut self, canvas: &mut sdl2::render::Canvas<C>) {
|
|
let lives = 3;
|
|
let score_text = format!("{:02}", self.score);
|
|
let x_offset = 4;
|
|
let y_offset = 2;
|
|
let lives_offset = 3;
|
|
let score_offset = 7 - (score_text.len() as i32);
|
|
self.text_texture.set_scale(1.0);
|
|
let _ = self.text_texture.render(
|
|
canvas,
|
|
&format!("{lives}UP HIGH SCORE "),
|
|
UVec2::new(8 * lives_offset as u32 + x_offset, y_offset),
|
|
);
|
|
let _ = self.text_texture.render(
|
|
canvas,
|
|
&score_text,
|
|
UVec2::new(8 * score_offset as u32 + x_offset, 8 + y_offset),
|
|
);
|
|
|
|
// Display FPS information in top-left corner
|
|
// let fps_text = format!("FPS: {:.1} (1s) / {:.1} (10s)", self.fps_1s, self.fps_10s);
|
|
// self.render_text_on(
|
|
// canvas,
|
|
// &*texture_creator,
|
|
// &fps_text,
|
|
// IVec2::new(10, 10),
|
|
// Color::RGB(255, 255, 0), // Yellow color for FPS display
|
|
// );
|
|
}
|
|
}
|