mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-08 12:07:52 -06:00
Compare commits
7 Commits
v0.36.0
...
2f1ff85d8f
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f1ff85d8f | |||
| b7429cd9ec | |||
| 12a63374a8 | |||
| d80d7061e7 | |||
| abdefe0af0 | |||
| 4f76de7c9f | |||
| db8cd6220a |
@@ -1,6 +1,6 @@
|
|||||||
# Pac-Man
|
# Pac-Man
|
||||||
|
|
||||||
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![Code Coverage][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
|
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![If you're seeing this, Coveralls.io is broken again and it's not my fault.][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
|
||||||
|
|
||||||
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg
|
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg
|
||||||
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg
|
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg
|
||||||
|
|||||||
85
src/app.rs
85
src/app.rs
@@ -2,26 +2,29 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
use sdl2::event::{Event, WindowEvent};
|
use sdl2::event::{Event, WindowEvent};
|
||||||
use sdl2::keyboard::Keycode;
|
|
||||||
use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
|
use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
|
||||||
use sdl2::ttf::Sdl2TtfContext;
|
use sdl2::ttf::Sdl2TtfContext;
|
||||||
use sdl2::video::{Window, WindowContext};
|
use sdl2::video::{Window, WindowContext};
|
||||||
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
|
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
|
||||||
use tracing::{error, event};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use crate::error::{GameError, GameResult};
|
use crate::error::{GameError, GameResult};
|
||||||
|
|
||||||
use crate::constants::{CANVAS_SIZE, LOOP_TIME, SCALE};
|
use crate::constants::{CANVAS_SIZE, LOOP_TIME, SCALE};
|
||||||
use crate::game::Game;
|
use crate::game::Game;
|
||||||
|
use crate::input::commands::GameCommand;
|
||||||
|
use crate::input::InputSystem;
|
||||||
use crate::platform::get_platform;
|
use crate::platform::get_platform;
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
game: Game,
|
game: Game,
|
||||||
|
input_system: InputSystem,
|
||||||
canvas: Canvas<Window>,
|
canvas: Canvas<Window>,
|
||||||
event_pump: &'static mut EventPump,
|
|
||||||
backbuffer: Texture<'static>,
|
backbuffer: Texture<'static>,
|
||||||
paused: bool,
|
event_pump: &'static mut EventPump,
|
||||||
|
|
||||||
last_tick: Instant,
|
last_tick: Instant,
|
||||||
|
focused: bool,
|
||||||
cursor_pos: Vec2,
|
cursor_pos: Vec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +54,13 @@ impl App {
|
|||||||
.build()
|
.build()
|
||||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||||
|
|
||||||
let mut canvas = window.into_canvas().build().map_err(|e| GameError::Sdl(e.to_string()))?;
|
let mut canvas = window
|
||||||
|
.into_canvas()
|
||||||
|
.accelerated()
|
||||||
|
.present_vsync()
|
||||||
|
.build()
|
||||||
|
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||||
|
|
||||||
canvas
|
canvas
|
||||||
.set_logical_size(CANVAS_SIZE.x, CANVAS_SIZE.y)
|
.set_logical_size(CANVAS_SIZE.x, CANVAS_SIZE.y)
|
||||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||||
@@ -72,12 +81,13 @@ impl App {
|
|||||||
game.present_backbuffer(&mut canvas, &backbuffer, glam::Vec2::ZERO)
|
game.present_backbuffer(&mut canvas, &backbuffer, glam::Vec2::ZERO)
|
||||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(App {
|
||||||
game,
|
game,
|
||||||
|
input_system: InputSystem::new(),
|
||||||
canvas,
|
canvas,
|
||||||
event_pump,
|
event_pump,
|
||||||
backbuffer,
|
backbuffer,
|
||||||
paused: false,
|
focused: true,
|
||||||
last_tick: Instant::now(),
|
last_tick: Instant::now(),
|
||||||
cursor_pos: Vec2::ZERO,
|
cursor_pos: Vec2::ZERO,
|
||||||
})
|
})
|
||||||
@@ -90,53 +100,41 @@ impl App {
|
|||||||
for event in self.event_pump.poll_iter() {
|
for event in self.event_pump.poll_iter() {
|
||||||
match event {
|
match event {
|
||||||
Event::Window { win_event, .. } => match win_event {
|
Event::Window { win_event, .. } => match win_event {
|
||||||
WindowEvent::Hidden => {
|
WindowEvent::FocusGained => {
|
||||||
event!(tracing::Level::DEBUG, "Window hidden");
|
self.focused = true;
|
||||||
}
|
}
|
||||||
WindowEvent::Shown => {
|
WindowEvent::FocusLost => {
|
||||||
event!(tracing::Level::DEBUG, "Window shown");
|
self.focused = false;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
// It doesn't really make sense to have this available in the browser
|
|
||||||
#[cfg(not(target_os = "emscripten"))]
|
|
||||||
Event::Quit { .. }
|
|
||||||
| Event::KeyDown {
|
|
||||||
keycode: Some(Keycode::Escape) | Some(Keycode::Q),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
event!(tracing::Level::INFO, "Exit requested. Exiting...");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Event::KeyDown {
|
|
||||||
keycode: Some(Keycode::P),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
self.paused = !self.paused;
|
|
||||||
event!(tracing::Level::INFO, "{}", if self.paused { "Paused" } else { "Unpaused" });
|
|
||||||
}
|
|
||||||
Event::KeyDown {
|
|
||||||
keycode: Some(Keycode::Space),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
self.game.toggle_debug_mode();
|
|
||||||
}
|
|
||||||
Event::KeyDown { keycode: Some(key), .. } => {
|
|
||||||
self.game.keyboard_event(key);
|
|
||||||
}
|
|
||||||
Event::MouseMotion { x, y, .. } => {
|
Event::MouseMotion { x, y, .. } => {
|
||||||
// Convert window coordinates to logical coordinates
|
// Convert window coordinates to logical coordinates
|
||||||
self.cursor_pos = Vec2::new(x as f32, y as f32);
|
self.cursor_pos = Vec2::new(x as f32, y as f32);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(command) = self.input_system.handle_event(&event) {
|
||||||
|
match command {
|
||||||
|
GameCommand::Exit => {
|
||||||
|
info!("Exit requested. Exiting...");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_ => self.game.post_event(command.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let dt = self.last_tick.elapsed().as_secs_f32();
|
let dt = self.last_tick.elapsed().as_secs_f32();
|
||||||
self.last_tick = Instant::now();
|
self.last_tick = Instant::now();
|
||||||
|
|
||||||
if !self.paused {
|
let exit = self.game.tick(dt);
|
||||||
self.game.tick(dt);
|
|
||||||
|
if exit {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(e) = self.game.draw(&mut self.canvas, &mut self.backbuffer) {
|
if let Err(e) = self.game.draw(&mut self.canvas, &mut self.backbuffer) {
|
||||||
error!("Failed to draw game: {}", e);
|
error!("Failed to draw game: {}", e);
|
||||||
}
|
}
|
||||||
@@ -146,19 +144,14 @@ impl App {
|
|||||||
{
|
{
|
||||||
error!("Failed to present backbuffer: {}", e);
|
error!("Failed to present backbuffer: {}", e);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if start.elapsed() < LOOP_TIME {
|
if start.elapsed() < LOOP_TIME {
|
||||||
let time = LOOP_TIME.saturating_sub(start.elapsed());
|
let time = LOOP_TIME.saturating_sub(start.elapsed());
|
||||||
if time != Duration::ZERO {
|
if time != Duration::ZERO {
|
||||||
get_platform().sleep(time);
|
get_platform().sleep(time, self.focused);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
event!(
|
warn!("Game loop behind schedule by: {:?}", start.elapsed() - LOOP_TIME);
|
||||||
tracing::Level::WARN,
|
|
||||||
"Game loop behind schedule by: {:?}",
|
|
||||||
start.elapsed() - LOOP_TIME
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use glam::IVec2;
|
use glam::IVec2;
|
||||||
|
|
||||||
/// The four cardinal directions.
|
/// The four cardinal directions.
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
#[repr(usize)]
|
||||||
pub enum Direction {
|
pub enum Direction {
|
||||||
Up,
|
Up,
|
||||||
Down,
|
Down,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ use crate::entity::{
|
|||||||
use crate::texture::animated::AnimatedTexture;
|
use crate::texture::animated::AnimatedTexture;
|
||||||
use crate::texture::directional::DirectionalAnimatedTexture;
|
use crate::texture::directional::DirectionalAnimatedTexture;
|
||||||
use crate::texture::sprite::SpriteAtlas;
|
use crate::texture::sprite::SpriteAtlas;
|
||||||
use sdl2::keyboard::Keycode;
|
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::error::{GameError, GameResult, TextureError};
|
use crate::error::{GameError, GameResult, TextureError};
|
||||||
@@ -107,24 +106,6 @@ impl Pacman {
|
|||||||
texture: DirectionalAnimatedTexture::new(textures, stopped_textures),
|
texture: DirectionalAnimatedTexture::new(textures, stopped_textures),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles keyboard input to change Pac-Man's direction.
|
|
||||||
///
|
|
||||||
/// Maps arrow keys to directions and queues the direction change
|
|
||||||
/// for the next valid intersection.
|
|
||||||
pub fn handle_key(&mut self, keycode: Keycode) {
|
|
||||||
let direction = match keycode {
|
|
||||||
Keycode::Up => Some(Direction::Up),
|
|
||||||
Keycode::Down => Some(Direction::Down),
|
|
||||||
Keycode::Left => Some(Direction::Left),
|
|
||||||
Keycode::Right => Some(Direction::Right),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(direction) = direction {
|
|
||||||
self.traverser.set_next_direction(direction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collidable for Pacman {
|
impl Collidable for Pacman {
|
||||||
|
|||||||
12
src/game/events.rs
Normal file
12
src/game/events.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use crate::input::commands::GameCommand;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum GameEvent {
|
||||||
|
Command(GameCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GameCommand> for GameEvent {
|
||||||
|
fn from(command: GameCommand) -> Self {
|
||||||
|
GameEvent::Command(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
131
src/game/mod.rs
131
src/game/mod.rs
@@ -1,32 +1,34 @@
|
|||||||
//! This module contains the main game logic and state.
|
//! This module contains the main game logic and state.
|
||||||
|
|
||||||
use glam::{UVec2, Vec2};
|
|
||||||
use rand::{rngs::SmallRng, Rng, SeedableRng};
|
use rand::{rngs::SmallRng, Rng, SeedableRng};
|
||||||
use sdl2::{
|
use sdl2::pixels::Color;
|
||||||
keyboard::Keycode,
|
use sdl2::render::{Canvas, Texture, TextureCreator};
|
||||||
pixels::Color,
|
use sdl2::video::WindowContext;
|
||||||
render::{Canvas, RenderTarget, Texture, TextureCreator},
|
|
||||||
video::WindowContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::error::{EntityError, GameError, GameResult};
|
use crate::entity::r#trait::Entity;
|
||||||
|
use crate::error::GameResult;
|
||||||
|
|
||||||
use crate::entity::{
|
use crate::entity::{
|
||||||
collision::{Collidable, CollisionSystem, EntityId},
|
collision::{Collidable, CollisionSystem, EntityId},
|
||||||
ghost::{Ghost, GhostType},
|
ghost::{Ghost, GhostType},
|
||||||
pacman::Pacman,
|
pacman::Pacman,
|
||||||
r#trait::Entity,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::map::render::MapRenderer;
|
||||||
|
use crate::{constants, texture::sprite::SpriteAtlas};
|
||||||
|
|
||||||
|
use self::events::GameEvent;
|
||||||
|
use self::state::GameState;
|
||||||
|
|
||||||
|
pub mod events;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
use state::GameState;
|
|
||||||
|
|
||||||
/// The `Game` struct is the main entry point for the game.
|
/// The `Game` struct is the main entry point for the game.
|
||||||
///
|
///
|
||||||
/// It contains the game's state and logic, and is responsible for
|
/// It contains the game's state and logic, and is responsible for
|
||||||
/// handling user input, updating the game state, and rendering the game.
|
/// handling user input, updating the game state, and rendering the game.
|
||||||
pub struct Game {
|
pub struct Game {
|
||||||
state: GameState,
|
state: state::GameState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Game {
|
impl Game {
|
||||||
@@ -36,18 +38,41 @@ impl Game {
|
|||||||
Ok(Game { state })
|
Ok(Game { state })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn keyboard_event(&mut self, keycode: Keycode) {
|
pub fn post_event(&mut self, event: GameEvent) {
|
||||||
self.state.pacman.handle_key(keycode);
|
self.state.event_queue.push_back(event);
|
||||||
|
|
||||||
if keycode == Keycode::M {
|
|
||||||
self.state.audio.set_mute(!self.state.audio.is_muted());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if keycode == Keycode::R {
|
fn handle_command(&mut self, command: crate::input::commands::GameCommand) {
|
||||||
|
use crate::input::commands::GameCommand;
|
||||||
|
match command {
|
||||||
|
GameCommand::MovePlayer(direction) => {
|
||||||
|
self.state.pacman.set_next_direction(direction);
|
||||||
|
}
|
||||||
|
GameCommand::ToggleDebug => {
|
||||||
|
self.toggle_debug_mode();
|
||||||
|
}
|
||||||
|
GameCommand::MuteAudio => {
|
||||||
|
let is_muted = self.state.audio.is_muted();
|
||||||
|
self.state.audio.set_mute(!is_muted);
|
||||||
|
}
|
||||||
|
GameCommand::ResetLevel => {
|
||||||
if let Err(e) = self.reset_game_state() {
|
if let Err(e) = self.reset_game_state() {
|
||||||
tracing::error!("Failed to reset game state: {}", e);
|
tracing::error!("Failed to reset game state: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GameCommand::TogglePause => {
|
||||||
|
self.state.paused = !self.state.paused;
|
||||||
|
}
|
||||||
|
GameCommand::Exit => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_events(&mut self) {
|
||||||
|
while let Some(event) = self.state.event_queue.pop_front() {
|
||||||
|
match event {
|
||||||
|
GameEvent::Command(command) => self.handle_command(command),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets the game state, randomizing ghost positions and resetting Pac-Man
|
/// Resets the game state, randomizing ghost positions and resetting Pac-Man
|
||||||
@@ -90,7 +115,18 @@ impl Game {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tick(&mut self, dt: f32) {
|
/// Ticks the game state.
|
||||||
|
///
|
||||||
|
/// Returns true if the game should exit.
|
||||||
|
pub fn tick(&mut self, dt: f32) -> bool {
|
||||||
|
// Process any events that have been posted (such as unpausing)
|
||||||
|
self.process_events();
|
||||||
|
|
||||||
|
// If the game is paused, we don't need to do anything beyond returning
|
||||||
|
if self.state.paused {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
self.state.pacman.tick(dt, &self.state.map.graph);
|
self.state.pacman.tick(dt, &self.state.map.graph);
|
||||||
|
|
||||||
// Update all ghosts
|
// Update all ghosts
|
||||||
@@ -103,6 +139,8 @@ impl Game {
|
|||||||
|
|
||||||
// Check for collisions
|
// Check for collisions
|
||||||
self.check_collisions();
|
self.check_collisions();
|
||||||
|
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggles the debug mode on and off.
|
/// Toggles the debug mode on and off.
|
||||||
@@ -167,14 +205,37 @@ impl Game {
|
|||||||
self.state.ghost_ids.iter().position(|&id| id == entity_id)
|
self.state.ghost_ids.iter().position(|&id| id == entity_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &mut Texture) -> GameResult<()> {
|
pub fn draw<T: sdl2::render::RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &mut Texture) -> GameResult<()> {
|
||||||
|
// Only render the map texture once and cache it
|
||||||
|
if !self.state.map_rendered {
|
||||||
|
let mut map_texture = self
|
||||||
|
.state
|
||||||
|
.texture_creator
|
||||||
|
.create_texture_target(None, constants::CANVAS_SIZE.x, constants::CANVAS_SIZE.y)
|
||||||
|
.map_err(|e| crate::error::GameError::Sdl(e.to_string()))?;
|
||||||
|
|
||||||
|
canvas
|
||||||
|
.with_texture_canvas(&mut map_texture, |map_canvas| {
|
||||||
|
let mut map_tiles = Vec::with_capacity(35);
|
||||||
|
for i in 0..35 {
|
||||||
|
let tile_name = format!("maze/tiles/{}.png", i);
|
||||||
|
let tile = SpriteAtlas::get_tile(&self.state.atlas, &tile_name).unwrap();
|
||||||
|
map_tiles.push(tile);
|
||||||
|
}
|
||||||
|
MapRenderer::render_map(map_canvas, &mut self.state.atlas, &mut map_tiles);
|
||||||
|
})
|
||||||
|
.map_err(|e| crate::error::GameError::Sdl(e.to_string()))?;
|
||||||
|
self.state.map_texture = Some(map_texture);
|
||||||
|
self.state.map_rendered = true;
|
||||||
|
}
|
||||||
|
|
||||||
canvas
|
canvas
|
||||||
.with_texture_canvas(backbuffer, |canvas| {
|
.with_texture_canvas(backbuffer, |canvas| {
|
||||||
canvas.set_draw_color(Color::BLACK);
|
canvas.set_draw_color(Color::BLACK);
|
||||||
canvas.clear();
|
canvas.clear();
|
||||||
self.state
|
if let Some(ref map_texture) = self.state.map_texture {
|
||||||
.map
|
canvas.copy(map_texture, None, None).unwrap();
|
||||||
.render(canvas, &mut self.state.atlas, &mut self.state.map_tiles);
|
}
|
||||||
|
|
||||||
// Render all items
|
// Render all items
|
||||||
for item in &self.state.items {
|
for item in &self.state.items {
|
||||||
@@ -194,12 +255,12 @@ impl Game {
|
|||||||
tracing::error!("Failed to render pacman: {}", e);
|
tracing::error!("Failed to render pacman: {}", e);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
.map_err(|e| crate::error::GameError::Sdl(e.to_string()))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn present_backbuffer<T: RenderTarget>(
|
pub fn present_backbuffer<T: sdl2::render::RenderTarget>(
|
||||||
&mut self,
|
&mut self,
|
||||||
canvas: &mut Canvas<T>,
|
canvas: &mut Canvas<T>,
|
||||||
backbuffer: &Texture,
|
backbuffer: &Texture,
|
||||||
@@ -207,7 +268,7 @@ impl Game {
|
|||||||
) -> GameResult<()> {
|
) -> GameResult<()> {
|
||||||
canvas
|
canvas
|
||||||
.copy(backbuffer, None, None)
|
.copy(backbuffer, None, None)
|
||||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
.map_err(|e| crate::error::GameError::Sdl(e.to_string()))?;
|
||||||
if self.state.debug_mode {
|
if self.state.debug_mode {
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
self.state
|
self.state
|
||||||
@@ -227,7 +288,7 @@ impl Game {
|
|||||||
///
|
///
|
||||||
/// Each ghost's path is drawn in its respective color with a small offset
|
/// Each ghost's path is drawn in its respective color with a small offset
|
||||||
/// to prevent overlapping lines.
|
/// to prevent overlapping lines.
|
||||||
fn render_pathfinding_debug<T: RenderTarget>(&self, canvas: &mut Canvas<T>) -> GameResult<()> {
|
fn render_pathfinding_debug<T: sdl2::render::RenderTarget>(&self, canvas: &mut Canvas<T>) -> GameResult<()> {
|
||||||
let pacman_node = self.state.pacman.current_node_id();
|
let pacman_node = self.state.pacman.current_node_id();
|
||||||
|
|
||||||
for ghost in self.state.ghosts.iter() {
|
for ghost in self.state.ghosts.iter() {
|
||||||
@@ -248,10 +309,10 @@ impl Game {
|
|||||||
|
|
||||||
// Use the overall direction from start to end to determine the perpendicular offset
|
// Use the overall direction from start to end to determine the perpendicular offset
|
||||||
let offset = match ghost.ghost_type {
|
let offset = match ghost.ghost_type {
|
||||||
GhostType::Blinky => Vec2::new(0.25, 0.5),
|
GhostType::Blinky => glam::Vec2::new(0.25, 0.5),
|
||||||
GhostType::Pinky => Vec2::new(-0.25, -0.25),
|
GhostType::Pinky => glam::Vec2::new(-0.25, -0.25),
|
||||||
GhostType::Inky => Vec2::new(0.5, -0.5),
|
GhostType::Inky => glam::Vec2::new(0.5, -0.5),
|
||||||
GhostType::Clyde => Vec2::new(-0.5, 0.25),
|
GhostType::Clyde => glam::Vec2::new(-0.5, 0.25),
|
||||||
} * 5.0;
|
} * 5.0;
|
||||||
|
|
||||||
// Calculate offset positions for all nodes using the same perpendicular direction
|
// Calculate offset positions for all nodes using the same perpendicular direction
|
||||||
@@ -262,7 +323,7 @@ impl Game {
|
|||||||
.map
|
.map
|
||||||
.graph
|
.graph
|
||||||
.get_node(node_id)
|
.get_node(node_id)
|
||||||
.ok_or(GameError::Entity(EntityError::NodeNotFound(node_id)))?;
|
.ok_or(crate::error::EntityError::NodeNotFound(node_id))?;
|
||||||
let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
|
let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
offset_positions.push(pos + offset);
|
offset_positions.push(pos + offset);
|
||||||
}
|
}
|
||||||
@@ -278,7 +339,7 @@ impl Game {
|
|||||||
// Draw the line
|
// Draw the line
|
||||||
canvas
|
canvas
|
||||||
.draw_line((from.x as i32, from.y as i32), (to.x as i32, to.y as i32))
|
.draw_line((from.x as i32, from.y as i32), (to.x as i32, to.y as i32))
|
||||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
.map_err(|e| crate::error::GameError::Sdl(e.to_string()))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,7 +348,7 @@ impl Game {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_hud<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>) -> GameResult<()> {
|
fn draw_hud<T: sdl2::render::RenderTarget>(&mut self, canvas: &mut Canvas<T>) -> GameResult<()> {
|
||||||
let lives = 3;
|
let lives = 3;
|
||||||
let score_text = format!("{:02}", self.state.score);
|
let score_text = format!("{:02}", self.state.score);
|
||||||
let x_offset = 4;
|
let x_offset = 4;
|
||||||
@@ -299,7 +360,7 @@ impl Game {
|
|||||||
canvas,
|
canvas,
|
||||||
&mut self.state.atlas,
|
&mut self.state.atlas,
|
||||||
&format!("{lives}UP HIGH SCORE "),
|
&format!("{lives}UP HIGH SCORE "),
|
||||||
UVec2::new(8 * lives_offset as u32 + x_offset, y_offset),
|
glam::UVec2::new(8 * lives_offset as u32 + x_offset, y_offset),
|
||||||
) {
|
) {
|
||||||
tracing::error!("Failed to render HUD text: {}", e);
|
tracing::error!("Failed to render HUD text: {}", e);
|
||||||
}
|
}
|
||||||
@@ -307,7 +368,7 @@ impl Game {
|
|||||||
canvas,
|
canvas,
|
||||||
&mut self.state.atlas,
|
&mut self.state.atlas,
|
||||||
&score_text,
|
&score_text,
|
||||||
UVec2::new(8 * score_offset as u32 + x_offset, 8 + y_offset),
|
glam::UVec2::new(8 * score_offset as u32 + x_offset, 8 + y_offset),
|
||||||
) {
|
) {
|
||||||
tracing::error!("Failed to render score text: {}", e);
|
tracing::error!("Failed to render score text: {}", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
use sdl2::{image::LoadTexture, render::TextureCreator, video::WindowContext};
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use sdl2::{
|
||||||
|
image::LoadTexture,
|
||||||
|
render::{Texture, TextureCreator},
|
||||||
|
video::WindowContext,
|
||||||
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -6,16 +12,16 @@ use crate::{
|
|||||||
audio::Audio,
|
audio::Audio,
|
||||||
constants::RAW_BOARD,
|
constants::RAW_BOARD,
|
||||||
entity::{
|
entity::{
|
||||||
collision::{Collidable, CollisionSystem},
|
collision::{Collidable, CollisionSystem, EntityId},
|
||||||
ghost::{Ghost, GhostType},
|
ghost::{Ghost, GhostType},
|
||||||
item::Item,
|
item::Item,
|
||||||
pacman::Pacman,
|
pacman::Pacman,
|
||||||
},
|
},
|
||||||
error::{GameError, GameResult, TextureError},
|
error::{GameError, GameResult, TextureError},
|
||||||
game::EntityId,
|
game::events::GameEvent,
|
||||||
map::Map,
|
map::builder::Map,
|
||||||
texture::{
|
texture::{
|
||||||
sprite::{AtlasMapper, AtlasTile, SpriteAtlas},
|
sprite::{AtlasMapper, SpriteAtlas},
|
||||||
text::TextTexture,
|
text::TextTexture,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -29,9 +35,10 @@ include!(concat!(env!("OUT_DIR"), "/atlas_data.rs"));
|
|||||||
/// we can cleanly separate it from the game's logic, making it easier to manage
|
/// we can cleanly separate it from the game's logic, making it easier to manage
|
||||||
/// and reason about.
|
/// and reason about.
|
||||||
pub struct GameState {
|
pub struct GameState {
|
||||||
|
pub paused: bool,
|
||||||
|
|
||||||
pub score: u32,
|
pub score: u32,
|
||||||
pub map: Map,
|
pub map: Map,
|
||||||
pub map_tiles: Vec<AtlasTile>,
|
|
||||||
pub pacman: Pacman,
|
pub pacman: Pacman,
|
||||||
pub pacman_id: EntityId,
|
pub pacman_id: EntityId,
|
||||||
pub ghosts: SmallVec<[Ghost; 4]>,
|
pub ghosts: SmallVec<[Ghost; 4]>,
|
||||||
@@ -39,6 +46,7 @@ pub struct GameState {
|
|||||||
pub items: Vec<Item>,
|
pub items: Vec<Item>,
|
||||||
pub item_ids: Vec<EntityId>,
|
pub item_ids: Vec<EntityId>,
|
||||||
pub debug_mode: bool,
|
pub debug_mode: bool,
|
||||||
|
pub event_queue: VecDeque<GameEvent>,
|
||||||
|
|
||||||
// Collision system
|
// Collision system
|
||||||
pub(crate) collision_system: CollisionSystem,
|
pub(crate) collision_system: CollisionSystem,
|
||||||
@@ -49,6 +57,11 @@ pub struct GameState {
|
|||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
pub audio: Audio,
|
pub audio: Audio,
|
||||||
|
|
||||||
|
// Map texture pre-rendering
|
||||||
|
pub(crate) map_texture: Option<Texture<'static>>,
|
||||||
|
pub(crate) map_rendered: bool,
|
||||||
|
pub(crate) texture_creator: &'static TextureCreator<WindowContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameState {
|
impl GameState {
|
||||||
@@ -60,7 +73,7 @@ impl GameState {
|
|||||||
pub fn new(texture_creator: &'static TextureCreator<WindowContext>) -> GameResult<Self> {
|
pub fn new(texture_creator: &'static TextureCreator<WindowContext>) -> GameResult<Self> {
|
||||||
let map = Map::new(RAW_BOARD)?;
|
let map = Map::new(RAW_BOARD)?;
|
||||||
|
|
||||||
let pacman_start_node = map.start_positions.pacman;
|
let start_node = map.start_positions.pacman;
|
||||||
|
|
||||||
let atlas_bytes = get_asset_bytes(Asset::Atlas)?;
|
let atlas_bytes = get_asset_bytes(Asset::Atlas)?;
|
||||||
let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| {
|
let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| {
|
||||||
@@ -76,17 +89,9 @@ impl GameState {
|
|||||||
};
|
};
|
||||||
let atlas = SpriteAtlas::new(atlas_texture, atlas_mapper);
|
let atlas = SpriteAtlas::new(atlas_texture, atlas_mapper);
|
||||||
|
|
||||||
let mut map_tiles = Vec::with_capacity(35);
|
|
||||||
for i in 0..35 {
|
|
||||||
let tile_name = format!("maze/tiles/{}.png", i);
|
|
||||||
let tile = SpriteAtlas::get_tile(&atlas, &tile_name)
|
|
||||||
.ok_or(GameError::Texture(TextureError::AtlasTileNotFound(tile_name)))?;
|
|
||||||
map_tiles.push(tile);
|
|
||||||
}
|
|
||||||
|
|
||||||
let text_texture = TextTexture::new(1.0);
|
let text_texture = TextTexture::new(1.0);
|
||||||
let audio = Audio::new();
|
let audio = Audio::new();
|
||||||
let pacman = Pacman::new(&map.graph, pacman_start_node, &atlas)?;
|
let pacman = Pacman::new(&map.graph, start_node, &atlas)?;
|
||||||
|
|
||||||
// Generate items (pellets and energizers)
|
// Generate items (pellets and energizers)
|
||||||
let items = map.generate_items(&atlas)?;
|
let items = map.generate_items(&atlas)?;
|
||||||
@@ -125,9 +130,9 @@ impl GameState {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
paused: false,
|
||||||
map,
|
map,
|
||||||
atlas,
|
atlas,
|
||||||
map_tiles,
|
|
||||||
pacman,
|
pacman,
|
||||||
pacman_id,
|
pacman_id,
|
||||||
ghosts,
|
ghosts,
|
||||||
@@ -139,6 +144,10 @@ impl GameState {
|
|||||||
score: 0,
|
score: 0,
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
collision_system,
|
collision_system,
|
||||||
|
map_texture: None,
|
||||||
|
map_rendered: false,
|
||||||
|
texture_creator,
|
||||||
|
event_queue: VecDeque::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/input/commands.rs
Normal file
11
src/input/commands.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
use crate::entity::direction::Direction;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum GameCommand {
|
||||||
|
MovePlayer(Direction),
|
||||||
|
TogglePause,
|
||||||
|
ToggleDebug,
|
||||||
|
MuteAudio,
|
||||||
|
ResetLevel,
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
47
src/input/mod.rs
Normal file
47
src/input/mod.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use sdl2::{event::Event, keyboard::Keycode};
|
||||||
|
|
||||||
|
use crate::{entity::direction::Direction, input::commands::GameCommand};
|
||||||
|
|
||||||
|
pub mod commands;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct InputSystem {
|
||||||
|
key_bindings: HashMap<Keycode, GameCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputSystem {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut key_bindings = HashMap::new();
|
||||||
|
|
||||||
|
// Player movement
|
||||||
|
key_bindings.insert(Keycode::Up, GameCommand::MovePlayer(Direction::Up));
|
||||||
|
key_bindings.insert(Keycode::W, GameCommand::MovePlayer(Direction::Up));
|
||||||
|
key_bindings.insert(Keycode::Down, GameCommand::MovePlayer(Direction::Down));
|
||||||
|
key_bindings.insert(Keycode::S, GameCommand::MovePlayer(Direction::Down));
|
||||||
|
key_bindings.insert(Keycode::Left, GameCommand::MovePlayer(Direction::Left));
|
||||||
|
key_bindings.insert(Keycode::A, GameCommand::MovePlayer(Direction::Left));
|
||||||
|
key_bindings.insert(Keycode::Right, GameCommand::MovePlayer(Direction::Right));
|
||||||
|
key_bindings.insert(Keycode::D, GameCommand::MovePlayer(Direction::Right));
|
||||||
|
|
||||||
|
// Game actions
|
||||||
|
key_bindings.insert(Keycode::P, GameCommand::TogglePause);
|
||||||
|
key_bindings.insert(Keycode::Space, GameCommand::ToggleDebug);
|
||||||
|
key_bindings.insert(Keycode::M, GameCommand::MuteAudio);
|
||||||
|
key_bindings.insert(Keycode::R, GameCommand::ResetLevel);
|
||||||
|
key_bindings.insert(Keycode::Escape, GameCommand::Exit);
|
||||||
|
key_bindings.insert(Keycode::Q, GameCommand::Exit);
|
||||||
|
|
||||||
|
Self { key_bindings }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles an event and returns a command if one is bound to the event.
|
||||||
|
pub fn handle_event(&self, event: &Event) -> Option<GameCommand> {
|
||||||
|
match event {
|
||||||
|
Event::Quit { .. } => Some(GameCommand::Exit),
|
||||||
|
Event::KeyDown { keycode: Some(key), .. } => self.key_bindings.get(key).copied(),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ pub mod entity;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod game;
|
pub mod game;
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
|
pub mod input;
|
||||||
pub mod map;
|
pub mod map;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub mod texture;
|
pub mod texture;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ mod entity;
|
|||||||
mod error;
|
mod error;
|
||||||
mod game;
|
mod game;
|
||||||
mod helpers;
|
mod helpers;
|
||||||
|
mod input;
|
||||||
mod map;
|
mod map;
|
||||||
mod platform;
|
mod platform;
|
||||||
mod texture;
|
mod texture;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::entity::graph::{EdgePermissions, Graph, Node, NodeId};
|
|||||||
use crate::entity::item::{Item, ItemType};
|
use crate::entity::item::{Item, ItemType};
|
||||||
use crate::map::parser::MapTileParser;
|
use crate::map::parser::MapTileParser;
|
||||||
use crate::map::render::MapRenderer;
|
use crate::map::render::MapRenderer;
|
||||||
use crate::texture::sprite::{AtlasTile, Sprite, SpriteAtlas};
|
use crate::texture::sprite::{Sprite, SpriteAtlas};
|
||||||
use glam::{IVec2, Vec2};
|
use glam::{IVec2, Vec2};
|
||||||
use sdl2::render::{Canvas, RenderTarget};
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
@@ -154,14 +154,6 @@ impl Map {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders the map to the given canvas.
|
|
||||||
///
|
|
||||||
/// This function draws the static map texture to the screen at the correct
|
|
||||||
/// position and scale.
|
|
||||||
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, map_tiles: &mut [AtlasTile]) {
|
|
||||||
MapRenderer::render_map(canvas, atlas, map_tiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates Item entities for pellets and energizers from the parsed map.
|
/// Generates Item entities for pellets and energizers from the parsed map.
|
||||||
pub fn generate_items(&self, atlas: &SpriteAtlas) -> GameResult<Vec<Item>> {
|
pub fn generate_items(&self, atlas: &SpriteAtlas) -> GameResult<Vec<Item>> {
|
||||||
// Pre-load sprites to avoid repeated texture lookups
|
// Pre-load sprites to avoid repeated texture lookups
|
||||||
|
|||||||
@@ -4,6 +4,3 @@ pub mod builder;
|
|||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
|
|
||||||
// Re-export main types for convenience
|
|
||||||
pub use builder::Map;
|
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ use crate::platform::Platform;
|
|||||||
pub struct DesktopPlatform;
|
pub struct DesktopPlatform;
|
||||||
|
|
||||||
impl Platform for DesktopPlatform {
|
impl Platform for DesktopPlatform {
|
||||||
fn sleep(&self, duration: Duration) {
|
fn sleep(&self, duration: Duration, focused: bool) {
|
||||||
|
if focused {
|
||||||
spin_sleep::sleep(duration);
|
spin_sleep::sleep(duration);
|
||||||
|
} else {
|
||||||
|
std::thread::sleep(duration);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_time(&self) -> f64 {
|
fn get_time(&self) -> f64 {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::platform::Platform;
|
|||||||
pub struct EmscriptenPlatform;
|
pub struct EmscriptenPlatform;
|
||||||
|
|
||||||
impl Platform for EmscriptenPlatform {
|
impl Platform for EmscriptenPlatform {
|
||||||
fn sleep(&self, duration: Duration) {
|
fn sleep(&self, duration: Duration, _focused: bool) {
|
||||||
unsafe {
|
unsafe {
|
||||||
emscripten_sleep(duration.as_millis() as u32);
|
emscripten_sleep(duration.as_millis() as u32);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub mod emscripten;
|
|||||||
/// Platform abstraction trait that defines cross-platform functionality.
|
/// Platform abstraction trait that defines cross-platform functionality.
|
||||||
pub trait Platform {
|
pub trait Platform {
|
||||||
/// Sleep for the specified duration using platform-appropriate method.
|
/// Sleep for the specified duration using platform-appropriate method.
|
||||||
fn sleep(&self, duration: Duration);
|
fn sleep(&self, duration: Duration, focused: bool);
|
||||||
|
|
||||||
/// Get the current time in seconds since some reference point.
|
/// Get the current time in seconds since some reference point.
|
||||||
/// This is available for future use in timing and performance monitoring.
|
/// This is available for future use in timing and performance monitoring.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use pacman::constants::RAW_BOARD;
|
use pacman::constants::RAW_BOARD;
|
||||||
use pacman::map::Map;
|
use pacman::map::builder::Map;
|
||||||
|
|
||||||
mod collision;
|
mod collision;
|
||||||
mod item;
|
mod item;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
use pacman::constants::{CELL_SIZE, RAW_BOARD};
|
use pacman::constants::{CELL_SIZE, RAW_BOARD};
|
||||||
use pacman::map::Map;
|
use pacman::map::builder::Map;
|
||||||
use sdl2::render::Texture;
|
use sdl2::render::Texture;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use pacman::entity::direction::Direction;
|
|||||||
use pacman::entity::graph::{Graph, Node};
|
use pacman::entity::graph::{Graph, Node};
|
||||||
use pacman::entity::pacman::Pacman;
|
use pacman::entity::pacman::Pacman;
|
||||||
use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
|
use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
|
||||||
use sdl2::keyboard::Keycode;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
fn create_test_graph() -> Graph {
|
fn create_test_graph() -> Graph {
|
||||||
@@ -72,36 +71,3 @@ fn test_pacman_creation() {
|
|||||||
assert!(pacman.traverser.position.is_at_node());
|
assert!(pacman.traverser.position.is_at_node());
|
||||||
assert_eq!(pacman.traverser.direction, Direction::Left);
|
assert_eq!(pacman.traverser.direction, Direction::Left);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pacman_key_handling() {
|
|
||||||
let graph = create_test_graph();
|
|
||||||
let atlas = create_test_atlas();
|
|
||||||
let mut pacman = Pacman::new(&graph, 0, &atlas).unwrap();
|
|
||||||
|
|
||||||
let test_cases = [
|
|
||||||
(Keycode::Up, Direction::Up),
|
|
||||||
(Keycode::Down, Direction::Down),
|
|
||||||
(Keycode::Left, Direction::Left),
|
|
||||||
(Keycode::Right, Direction::Right),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (key, expected_direction) in test_cases {
|
|
||||||
pacman.handle_key(key);
|
|
||||||
assert!(pacman.traverser.next_direction.is_some() || pacman.traverser.direction == expected_direction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pacman_invalid_key() {
|
|
||||||
let graph = create_test_graph();
|
|
||||||
let atlas = create_test_atlas();
|
|
||||||
let mut pacman = Pacman::new(&graph, 0, &atlas).unwrap();
|
|
||||||
|
|
||||||
let original_direction = pacman.traverser.direction;
|
|
||||||
let original_next_direction = pacman.traverser.next_direction;
|
|
||||||
|
|
||||||
pacman.handle_key(Keycode::Space);
|
|
||||||
assert_eq!(pacman.traverser.direction, original_direction);
|
|
||||||
assert_eq!(pacman.traverser.next_direction, original_next_direction);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user