Compare commits

...

8 Commits

36 changed files with 460 additions and 1042 deletions

1
Cargo.lock generated
View File

@@ -571,6 +571,7 @@ version = "0.2.0"
dependencies = [
"anyhow",
"bevy_ecs",
"bitflags 2.9.1",
"glam 0.30.5",
"lazy_static",
"libc",

View File

@@ -25,6 +25,7 @@ strum = "0.27.2"
strum_macros = "0.27.2"
phf = { version = "0.11", features = ["macros"] }
bevy_ecs = "0.16.1"
bitflags = "2.9.1"
[profile.release]
lto = true

View File

@@ -1,11 +1,11 @@
use std::time::{Duration, Instant};
use glam::Vec2;
use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
use sdl2::render::TextureCreator;
use sdl2::ttf::Sdl2TtfContext;
use sdl2::video::{Window, WindowContext};
use sdl2::video::WindowContext;
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
use tracing::{error, warn};
use tracing::warn;
use crate::error::{GameError, GameResult};
@@ -17,7 +17,7 @@ pub struct App {
pub game: Game,
last_tick: Instant,
focused: bool,
cursor_pos: Vec2,
_cursor_pos: Vec2,
}
impl App {
@@ -46,7 +46,7 @@ impl App {
.build()
.map_err(|e| GameError::Sdl(e.to_string()))?;
let mut canvas = Box::leak(Box::new(
let canvas = Box::leak(Box::new(
window
.into_canvas()
.accelerated()
@@ -74,7 +74,7 @@ impl App {
game,
focused: true,
last_tick: Instant::now(),
cursor_pos: Vec2::ZERO,
_cursor_pos: Vec2::ZERO,
})
}

View File

@@ -1,6 +1,6 @@
use glam::Vec2;
use crate::ecs::NodeId;
use crate::systems::components::NodeId;
use super::direction::Direction;

View File

@@ -5,4 +5,3 @@ pub mod graph;
pub mod item;
pub mod pacman;
pub mod r#trait;
pub mod traversal;

View File

@@ -1,161 +0,0 @@
use tracing::error;
use crate::ecs::{NodeId, Position};
use crate::error::GameResult;
use super::direction::Direction;
use super::graph::{Edge, Graph};
/// Manages an entity's movement through the graph.
///
/// A `Traverser` encapsulates the state of an entity's position and direction,
/// providing a way to advance along the graph's paths based on a given distance.
/// It also handles direction changes, buffering the next intended direction.
pub struct Traverser {
/// The current position of the traverser in the graph.
pub position: Position,
/// The current direction of movement.
pub direction: Direction,
/// Buffered direction change with remaining frame count for timing.
///
/// The `u8` value represents the number of frames remaining before
/// the buffered direction expires. This allows for responsive controls
/// by storing direction changes for a limited time.
pub next_direction: Option<(Direction, u8)>,
}
impl Traverser {
/// Sets the next direction for the traverser to take.
///
/// The direction is buffered and will be applied at the next opportunity,
/// typically when the traverser reaches a new node. This allows for responsive
/// controls, as the new direction is stored for a limited time.
pub fn set_next_direction(&mut self, new_direction: Direction) {
if self.direction != new_direction {
self.next_direction = Some((new_direction, 30));
}
}
/// Advances the traverser along the graph by a specified distance.
///
/// This method updates the traverser's position based on its current state
/// and the distance to travel.
///
/// - If at a node, it checks for a buffered direction to start moving.
/// - If between nodes, it moves along the current edge.
/// - If it reaches a node, it attempts to transition to a new edge based on
/// the buffered direction or by continuing straight.
/// - If no valid move is possible, it stops at the node.
///
/// Returns an error if the movement is invalid (e.g., trying to move in an impossible direction).
pub fn advance<F>(&mut self, graph: &Graph, distance: f32, can_traverse: &F) -> GameResult<()>
where
F: Fn(Edge) -> bool,
{
// Decrement the remaining frames for the next direction
if let Some((direction, remaining)) = self.next_direction {
if remaining > 0 {
self.next_direction = Some((direction, remaining - 1));
} else {
self.next_direction = None;
}
}
match self.position {
Position::AtNode(node_id) => {
// We're not moving, but a buffered direction is available.
if let Some((next_direction, _)) = self.next_direction {
if let Some(edge) = graph.find_edge_in_direction(node_id, next_direction) {
if can_traverse(edge) {
// Start moving in that direction
self.position = Position::BetweenNodes {
from: node_id,
to: edge.target,
traversed: distance.max(0.0),
};
self.direction = next_direction;
} else {
return Err(crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement(
format!(
"Cannot traverse edge from {} to {} in direction {:?}",
node_id, edge.target, next_direction
),
)));
}
} else {
return Err(crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement(
format!("No edge found in direction {:?} from node {}", next_direction, node_id),
)));
}
self.next_direction = None; // Consume the buffered direction regardless of whether we started moving with it
}
}
Position::BetweenNodes { from, to, traversed } => {
// There is no point in any of the next logic if we don't travel at all
if distance <= 0.0 {
return Ok(());
}
let edge = graph.find_edge(from, to).ok_or_else(|| {
crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement(format!(
"Inconsistent state: Traverser is on a non-existent edge from {} to {}.",
from, to
)))
})?;
let new_traversed = traversed + distance;
if new_traversed < edge.distance {
// Still on the same edge, just update the distance.
self.position = Position::BetweenNodes {
from,
to,
traversed: new_traversed,
};
} else {
let overflow = new_traversed - edge.distance;
let mut moved = false;
// If we buffered a direction, try to find an edge in that direction
if let Some((next_dir, _)) = self.next_direction {
if let Some(edge) = graph.find_edge_in_direction(to, next_dir) {
if can_traverse(edge) {
self.position = Position::BetweenNodes {
from: to,
to: edge.target,
traversed: overflow,
};
self.direction = next_dir; // Remember our new direction
self.next_direction = None; // Consume the buffered direction
moved = true;
}
}
}
// If we didn't move, try to continue in the current direction
if !moved {
if let Some(edge) = graph.find_edge_in_direction(to, self.direction) {
if can_traverse(edge) {
self.position = Position::BetweenNodes {
from: to,
to: edge.target,
traversed: overflow,
};
} else {
self.position = Position::AtNode(to);
self.next_direction = None;
}
} else {
self.position = Position::AtNode(to);
self.next_direction = None;
}
}
}
}
}
Ok(())
}
}

23
src/events.rs Normal file
View File

@@ -0,0 +1,23 @@
use bevy_ecs::prelude::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GameCommand {
Exit,
MovePlayer(crate::entity::direction::Direction),
ToggleDebug,
MuteAudio,
ResetLevel,
TogglePause,
}
#[derive(Event, Clone, Copy, Debug, PartialEq, Eq)]
pub enum GameEvent {
Command(GameCommand),
Collision(Entity, Entity),
}
impl From<GameCommand> for GameEvent {
fn from(command: GameCommand) -> Self {
GameEvent::Command(command)
}
}

View File

@@ -1,14 +0,0 @@
use bevy_ecs::event::Event;
use crate::input::commands::GameCommand;
#[derive(Debug, Clone, Copy, Event)]
pub enum GameEvent {
Command(GameCommand),
}
impl From<GameCommand> for GameEvent {
fn from(command: GameCommand) -> Self {
GameEvent::Command(command)
}
}

View File

@@ -3,38 +3,40 @@
include!(concat!(env!("OUT_DIR"), "/atlas_data.rs"));
use crate::constants::CANVAS_SIZE;
use crate::ecs::interact::{interact_system, movement_system};
use crate::ecs::render::{directional_render_system, render_system, BackbufferResource, MapTextureResource};
use crate::ecs::{DeltaTime, DirectionalAnimated, GlobalState, PlayerBundle, PlayerControlled, Position, Renderable, Velocity};
use crate::entity::direction::Direction;
use crate::entity::{graph, traversal};
use crate::error::{GameError, GameResult, TextureError};
use crate::input::commands::GameCommand;
use crate::events::GameEvent;
use crate::map::builder::Map;
use crate::systems::blinking::Blinking;
use crate::systems::{
blinking::blinking_system,
collision::collision_system,
components::{
Collider, CollisionLayer, DeltaTime, DirectionalAnimated, EntityType, GlobalState, ItemBundle, ItemCollider,
PacmanCollider, PlayerBundle, PlayerControlled, Position, Renderable, Score, ScoreResource, Velocity,
},
control::player_system,
input::input_system,
movement::movement_system,
render::{directional_render_system, render_system, BackbufferResource, MapTextureResource},
};
use crate::texture::animated::AnimatedTexture;
use crate::texture::directional::DirectionalAnimatedTexture;
use crate::texture::sprite::Sprite;
use bevy_ecs::event::EventRegistry;
use bevy_ecs::observer::Trigger;
use bevy_ecs::schedule::IntoScheduleConfigs;
use bevy_ecs::system::{Commands, ResMut};
use bevy_ecs::{schedule::Schedule, world::World};
use bevy_ecs::{event::EventRegistry, observer::Trigger, schedule::Schedule, system::ResMut, world::World};
use sdl2::image::LoadTexture;
use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
use sdl2::render::{Canvas, ScaleMode, TextureCreator};
use sdl2::video::{Window, WindowContext};
use sdl2::EventPump;
use crate::asset::{get_asset_bytes, Asset};
use crate::input::{handle_input, Bindings};
use crate::map::render::MapRenderer;
use crate::{
asset::{get_asset_bytes, Asset},
constants,
texture::sprite::{AtlasMapper, AtlasTile, SpriteAtlas},
events::GameCommand,
map::render::MapRenderer,
systems::input::Bindings,
texture::sprite::{AtlasMapper, SpriteAtlas},
};
use self::events::GameEvent;
pub mod events;
pub mod state;
/// The `Game` struct is the main entry point for the game.
@@ -133,19 +135,26 @@ impl Game {
player: PlayerControlled,
position: Position::AtNode(pacman_start_node),
velocity: Velocity {
direction: Direction::Up,
next_direction: None,
speed: 1.125,
direction: Direction::Left,
next_direction: Some((Direction::Left, 90)),
speed: 1.15,
},
sprite: Renderable {
sprite: SpriteAtlas::get_tile(&atlas, "pacman/full.png")
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("pacman/full.png".to_string())))?,
layer: 0,
visible: true,
},
directional_animated: DirectionalAnimated {
textures,
stopped_textures,
},
entity_type: EntityType::Player,
collider: Collider {
size: constants::CELL_SIZE as f32 * 1.25,
layer: CollisionLayer::PACMAN,
},
pacman_collider: PacmanCollider,
};
world.insert_non_send_resource(atlas);
@@ -156,23 +165,29 @@ impl Game {
world.insert_resource(map);
world.insert_resource(GlobalState { exit: false });
world.insert_resource(ScoreResource(0));
world.insert_resource(Bindings::default());
world.insert_resource(DeltaTime(0f32));
world.add_observer(|event: Trigger<GameEvent>, mut state: ResMut<GlobalState>| match *event {
GameEvent::Command(command) => match command {
GameCommand::Exit => {
state.exit = true;
}
_ => {}
world.add_observer(
|event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, mut score: ResMut<ScoreResource>| match *event {
GameEvent::Command(command) => match command {
GameCommand::Exit => {
state.exit = true;
}
_ => {}
},
GameEvent::Collision(a, b) => {}
},
});
);
schedule.add_systems(
(
handle_input,
interact_system,
input_system,
player_system,
movement_system,
collision_system,
blinking_system,
directional_render_system,
render_system,
)
@@ -182,6 +197,50 @@ impl Game {
// Spawn player
world.spawn(player);
// Spawn items
let pellet_sprite = SpriteAtlas::get_tile(world.non_send_resource::<SpriteAtlas>(), "maze/pellet.png")
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/pellet.png".to_string())))?;
let energizer_sprite = SpriteAtlas::get_tile(world.non_send_resource::<SpriteAtlas>(), "maze/energizer.png")
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/energizer.png".to_string())))?;
let nodes: Vec<_> = world.resource::<Map>().iter_nodes().map(|(id, tile)| (*id, *tile)).collect();
for (node_id, tile) in nodes {
let (item_type, score, sprite, size) = match tile {
crate::constants::MapTile::Pellet => (EntityType::Pellet, 10, pellet_sprite, constants::CELL_SIZE as f32 * 0.2),
crate::constants::MapTile::PowerPellet => (
EntityType::PowerPellet,
50,
energizer_sprite,
constants::CELL_SIZE as f32 * 0.9,
),
_ => continue,
};
let mut item = world.spawn(ItemBundle {
position: Position::AtNode(node_id),
sprite: Renderable {
sprite,
layer: 1,
visible: true,
},
entity_type: item_type,
score: Score(score),
collider: Collider {
size,
layer: CollisionLayer::ITEM,
},
item_collider: ItemCollider,
});
if item_type == EntityType::PowerPellet {
item.insert(Blinking {
timer: 0.0,
interval: 0.2,
});
}
}
Ok(Game { world, schedule })
}
@@ -215,7 +274,6 @@ impl Game {
// match event {
// GameEvent::Command(command) => self.handle_command(command),
// }
// }
// }
// /// Resets the game state, randomizing ghost positions and resetting Pac-Man

View File

@@ -1,11 +0,0 @@
use crate::entity::direction::Direction;
#[derive(Debug, Clone, Copy)]
pub enum GameCommand {
MovePlayer(Direction),
Exit,
TogglePause,
ToggleDebug,
MuteAudio,
ResetLevel,
}

View File

@@ -4,12 +4,12 @@ pub mod app;
pub mod asset;
pub mod audio;
pub mod constants;
pub mod ecs;
pub mod entity;
pub mod error;
pub mod events;
pub mod game;
pub mod helpers;
pub mod input;
pub mod map;
pub mod platform;
pub mod systems;
pub mod texture;

View File

@@ -10,14 +10,14 @@ mod asset;
mod audio;
mod constants;
mod ecs;
mod entity;
mod error;
mod events;
mod game;
mod helpers;
mod input;
mod map;
mod platform;
mod systems;
mod texture;
/// The main entry point of the application.

View File

@@ -1,12 +1,12 @@
//! Map construction and building functionality.
use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE, RAW_BOARD};
use crate::ecs::NodeId;
use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE};
use crate::entity::direction::Direction;
use crate::entity::graph::{EdgePermissions, Graph, Node};
use crate::map::parser::MapTileParser;
use crate::map::render::MapRenderer;
use crate::texture::sprite::{Sprite, SpriteAtlas};
use crate::systems::components::NodeId;
use crate::texture::sprite::SpriteAtlas;
use bevy_ecs::resource::Resource;
use glam::{IVec2, Vec2};
use sdl2::render::{Canvas, RenderTarget};
@@ -33,6 +33,8 @@ pub struct Map {
pub grid_to_node: HashMap<IVec2, NodeId>,
/// A mapping of the starting positions of the entities.
pub start_positions: NodePositions,
/// The raw tile data for the map.
tiles: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize],
}
impl Map {
@@ -153,6 +155,14 @@ impl Map {
graph,
grid_to_node,
start_positions,
tiles: map,
})
}
pub fn iter_nodes(&self) -> impl Iterator<Item = (&NodeId, &MapTile)> {
self.grid_to_node.iter().map(move |(pos, node_id)| {
let tile = &self.tiles[pos.x as usize][pos.y as usize];
(node_id, tile)
})
}

27
src/systems/blinking.rs Normal file
View File

@@ -0,0 +1,27 @@
use bevy_ecs::{
component::Component,
system::{Query, Res},
};
use crate::systems::components::{DeltaTime, Renderable};
#[derive(Component)]
pub struct Blinking {
pub timer: f32,
pub interval: f32,
}
/// Updates blinking entities by toggling their visibility at regular intervals.
///
/// This system manages entities that have both `Blinking` and `Renderable` components,
/// accumulating time and toggling visibility when the specified interval is reached.
pub fn blinking_system(time: Res<DeltaTime>, mut query: Query<(&mut Blinking, &mut Renderable)>) {
for (mut blinking, mut renderable) in query.iter_mut() {
blinking.timer += time.0;
if blinking.timer >= blinking.interval {
blinking.timer = 0.0;
renderable.visible = !renderable.visible;
}
}
}

47
src/systems/collision.rs Normal file
View File

@@ -0,0 +1,47 @@
use bevy_ecs::entity::Entity;
use bevy_ecs::event::EventWriter;
use bevy_ecs::query::With;
use bevy_ecs::system::{Query, Res};
use crate::error::GameError;
use crate::events::GameEvent;
use crate::map::builder::Map;
use crate::systems::components::{Collider, ItemCollider, PacmanCollider, Position};
pub fn collision_system(
map: Res<Map>,
pacman_query: Query<(Entity, &Position, &Collider), With<PacmanCollider>>,
item_query: Query<(Entity, &Position, &Collider), With<ItemCollider>>,
mut events: EventWriter<GameEvent>,
mut errors: EventWriter<GameError>,
) {
// Check PACMAN × ITEM collisions
for (pacman_entity, pacman_pos, pacman_collider) in pacman_query.iter() {
for (item_entity, item_pos, item_collider) in item_query.iter() {
match (pacman_pos.get_pixel_pos(&map.graph), item_pos.get_pixel_pos(&map.graph)) {
(Ok(pacman_pixel), Ok(item_pixel)) => {
// Calculate the distance between the two entities's precise pixel positions
let distance = pacman_pixel.distance(item_pixel);
// Calculate the distance at which the two entities will collide
let collision_distance = (pacman_collider.size + item_collider.size) / 2.0;
// If the distance between the two entities is less than the collision distance, then the two entities are colliding
if distance < collision_distance {
events.write(GameEvent::Collision(pacman_entity, item_entity));
}
}
// Either or both of the pixel positions failed to get, so we need to report the error
(result_a, result_b) => {
for result in [result_a, result_b] {
if let Err(e) = result {
errors.write(GameError::InvalidState(format!(
"Collision system failed to get pixel positions for entities {:?} and {:?}: {}",
pacman_entity, item_entity, e
)));
}
}
}
}
}
}
}

View File

@@ -1,25 +1,27 @@
//! The Entity-Component-System (ECS) module.
//!
//! This module contains all the ECS-related logic, including components, systems,
//! and resources.
use bevy_ecs::{bundle::Bundle, component::Component, resource::Resource};
use bitflags::bitflags;
use glam::Vec2;
use crate::{
entity::{direction::Direction, graph::Graph, traversal},
entity::{direction::Direction, graph::Graph},
error::{EntityError, GameResult},
texture::{
animated::AnimatedTexture,
directional::DirectionalAnimatedTexture,
sprite::{AtlasTile, Sprite},
},
texture::{animated::AnimatedTexture, sprite::AtlasTile},
};
/// A tag component for entities that are controlled by the player.
#[derive(Default, Component)]
pub struct PlayerControlled;
/// A tag component denoting the type of entity.
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EntityType {
Player,
Ghost,
Pellet,
PowerPellet,
Wall,
}
/// A component for entities that have a sprite, with a layer for ordering.
///
/// This is intended to be modified by other entities allowing animation.
@@ -27,6 +29,7 @@ pub struct PlayerControlled;
pub struct Renderable {
pub sprite: AtlasTile,
pub layer: u8,
pub visible: bool,
}
/// A component for entities that have a directional animated texture.
@@ -61,6 +64,10 @@ impl Position {
///
/// Converts the graph position to screen coordinates, accounting for
/// the board offset and centering the sprite.
///
/// # Errors
///
/// Returns an `EntityError` if the node or edge is not found.
pub fn get_pixel_pos(&self, graph: &Graph) -> GameResult<Vec2> {
let pos = match self {
Position::AtNode(node_id) => {
@@ -129,6 +136,34 @@ pub struct Velocity {
pub speed: f32,
}
bitflags! {
#[derive(Component, Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CollisionLayer: u8 {
const PACMAN = 1 << 0;
const GHOST = 1 << 1;
const ITEM = 1 << 2;
}
}
#[derive(Component)]
pub struct Collider {
pub size: f32,
pub layer: CollisionLayer,
}
/// Marker components for collision filtering optimization
#[derive(Component)]
pub struct PacmanCollider;
#[derive(Component)]
pub struct GhostCollider;
#[derive(Component)]
pub struct ItemCollider;
#[derive(Component)]
pub struct Score(pub u32);
#[derive(Bundle)]
pub struct PlayerBundle {
pub player: PlayerControlled,
@@ -136,6 +171,19 @@ pub struct PlayerBundle {
pub velocity: Velocity,
pub sprite: Renderable,
pub directional_animated: DirectionalAnimated,
pub entity_type: EntityType,
pub collider: Collider,
pub pacman_collider: PacmanCollider,
}
#[derive(Bundle)]
pub struct ItemBundle {
pub position: Position,
pub sprite: Renderable,
pub entity_type: EntityType,
pub score: Score,
pub collider: Collider,
pub item_collider: ItemCollider,
}
#[derive(Resource)]
@@ -144,7 +192,7 @@ pub struct GlobalState {
}
#[derive(Resource)]
pub struct DeltaTime(pub f32);
pub struct ScoreResource(pub u32);
pub mod interact;
pub mod render;
#[derive(Resource)]
pub struct DeltaTime(pub f32);

47
src/systems/control.rs Normal file
View File

@@ -0,0 +1,47 @@
use bevy_ecs::{
event::{EventReader, EventWriter},
prelude::ResMut,
query::With,
system::Query,
};
use crate::{
error::GameError,
events::{GameCommand, GameEvent},
systems::components::{GlobalState, PlayerControlled, Velocity},
};
// Handles
pub fn player_system(
mut events: EventReader<GameEvent>,
mut state: ResMut<GlobalState>,
mut players: Query<&mut Velocity, With<PlayerControlled>>,
mut errors: EventWriter<GameError>,
) {
// Get the player's velocity (handling to ensure there is only one player)
let mut velocity = match players.single_mut() {
Ok(velocity) => velocity,
Err(e) => {
errors.write(GameError::InvalidState(format!("No/multiple entities queried for player system: {}", e)).into());
return;
}
};
// Handle events
for event in events.read() {
match event {
GameEvent::Command(command) => match command {
GameCommand::MovePlayer(direction) => {
velocity.next_direction = Some((*direction, 90));
}
GameCommand::Exit => {
state.exit = true;
}
_ => {}
},
GameEvent::Collision(a, b) => {
tracing::info!("Collision between {:?} and {:?}", a, b);
}
}
}
}

View File

@@ -1,15 +1,12 @@
use std::collections::HashMap;
use bevy_ecs::{
event::EventWriter,
resource::Resource,
system::{Commands, NonSendMut, Res},
};
use bevy_ecs::{event::EventWriter, prelude::Res, resource::Resource, system::NonSendMut};
use sdl2::{event::Event, keyboard::Keycode, EventPump};
use crate::{entity::direction::Direction, game::events::GameEvent, input::commands::GameCommand};
pub mod commands;
use crate::{
entity::direction::Direction,
events::{GameCommand, GameEvent},
};
#[derive(Debug, Clone, Resource)]
pub struct Bindings {
@@ -42,7 +39,7 @@ impl Default for Bindings {
}
}
pub fn handle_input(bindings: Res<Bindings>, mut writer: EventWriter<GameEvent>, mut pump: NonSendMut<&'static mut EventPump>) {
pub fn input_system(bindings: Res<Bindings>, mut writer: EventWriter<GameEvent>, mut pump: NonSendMut<&'static mut EventPump>) {
for event in pump.poll_iter() {
match event {
Event::Quit { .. } => {

12
src/systems/mod.rs Normal file
View File

@@ -0,0 +1,12 @@
//! The Entity-Component-System (ECS) module.
//!
//! This module contains all the ECS-related logic, including components, systems,
//! and resources.
pub mod blinking;
pub mod collision;
pub mod components;
pub mod control;
pub mod input;
pub mod movement;
pub mod render;

View File

@@ -1,25 +1,25 @@
use bevy_ecs::{
event::{EventReader, EventWriter},
system::{Query, Res, ResMut},
};
use tracing::debug;
use crate::entity::graph::{Edge, EdgePermissions};
use crate::error::{EntityError, GameError};
use crate::map::builder::Map;
use crate::systems::components::{DeltaTime, EntityType, Position, Velocity};
use bevy_ecs::event::EventWriter;
use bevy_ecs::system::{Query, Res};
use crate::entity::graph::EdgePermissions;
use crate::{
ecs::{DeltaTime, GlobalState, PlayerControlled, Position, Velocity},
error::{EntityError, GameError},
game::events::GameEvent,
input::commands::GameCommand,
map::builder::Map,
};
fn can_traverse(entity_type: EntityType, edge: Edge) -> bool {
match entity_type {
EntityType::Player => matches!(edge.permissions, EdgePermissions::All),
EntityType::Ghost => matches!(edge.permissions, EdgePermissions::All | EdgePermissions::GhostsOnly),
_ => matches!(edge.permissions, EdgePermissions::All),
}
}
pub fn movement_system(
map: Res<Map>,
delta_time: Res<DeltaTime>,
mut entities: Query<(&mut PlayerControlled, &mut Velocity, &mut Position)>,
mut entities: Query<(&mut Velocity, &mut Position, &EntityType)>,
mut errors: EventWriter<GameError>,
) {
for (mut player, mut velocity, mut position) in entities.iter_mut() {
for (mut velocity, mut position, entity_type) in entities.iter_mut() {
let distance = velocity.speed * 60.0 * delta_time.0;
// Decrement the remaining frames for the next direction
@@ -36,7 +36,7 @@ pub fn movement_system(
// We're not moving, but a buffered direction is available.
if let Some((next_direction, _)) = velocity.next_direction {
if let Some(edge) = map.graph.find_edge_in_direction(node_id, next_direction) {
if can_traverse(&mut player, edge) {
if can_traverse(*entity_type, edge) {
// Start moving in that direction
*position = Position::BetweenNodes {
from: node_id,
@@ -94,7 +94,7 @@ pub fn movement_system(
// If we buffered a direction, try to find an edge in that direction
if let Some((next_dir, _)) = velocity.next_direction {
if let Some(edge) = map.graph.find_edge_in_direction(to, next_dir) {
if can_traverse(&mut player, edge) {
if can_traverse(*entity_type, edge) {
*position = Position::BetweenNodes {
from: to,
to: edge.target,
@@ -111,7 +111,7 @@ pub fn movement_system(
// If we didn't move, try to continue in the current direction
if !moved {
if let Some(edge) = map.graph.find_edge_in_direction(to, velocity.direction) {
if can_traverse(&mut player, edge) {
if can_traverse(*entity_type, edge) {
*position = Position::BetweenNodes {
from: to,
to: edge.target,
@@ -131,39 +131,3 @@ pub fn movement_system(
}
}
}
fn can_traverse(_player: &mut PlayerControlled, edge: crate::entity::graph::Edge) -> bool {
matches!(edge.permissions, EdgePermissions::All)
}
// Handles
pub fn interact_system(
mut events: EventReader<GameEvent>,
mut state: ResMut<GlobalState>,
mut players: Query<(&PlayerControlled, &mut Velocity)>,
mut errors: EventWriter<GameError>,
) {
// Get the player's velocity (handling to ensure there is only one player)
let mut velocity = match players.single_mut() {
Ok((_, velocity)) => velocity,
Err(e) => {
errors.write(GameError::InvalidState(format!("Player not found: {}", e)).into());
return;
}
};
// Handle events
for event in events.read() {
match event {
GameEvent::Command(command) => match command {
GameCommand::MovePlayer(direction) => {
velocity.next_direction = Some((*direction, 90));
}
GameCommand::Exit => {
state.exit = true;
}
_ => {}
},
}
}
}

View File

@@ -1,6 +1,6 @@
use crate::ecs::{DeltaTime, DirectionalAnimated, Position, Renderable, Velocity};
use crate::error::{EntityError, GameError, TextureError};
use crate::error::{GameError, TextureError};
use crate::map::builder::Map;
use crate::systems::components::{DeltaTime, DirectionalAnimated, Position, Renderable, Velocity};
use crate::texture::sprite::SpriteAtlas;
use bevy_ecs::entity::Entity;
use bevy_ecs::event::EventWriter;
@@ -9,6 +9,8 @@ use sdl2::render::{Canvas, Texture};
use sdl2::video::Window;
/// Updates the directional animated texture of an entity.
///
/// This runs before the render system so it can update the sprite based on the current direction of travel, as well as whether the entity is moving.
pub fn directional_render_system(
dt: Res<DeltaTime>,
mut renderables: Query<(&Velocity, &mut DirectionalAnimated, &mut Renderable, &Position)>,
@@ -34,7 +36,10 @@ pub fn directional_render_system(
}
}
/// A non-send resource for the map texture. This just wraps the texture with a type so it can be differentiated when exposed as a resource.
pub struct MapTextureResource(pub Texture<'static>);
/// A non-send resource for the backbuffer texture. This just wraps the texture with a type so it can be differentiated when exposed as a resource.
pub struct BackbufferResource(pub Texture<'static>);
pub fn render_system(
@@ -43,7 +48,7 @@ pub fn render_system(
mut backbuffer: NonSendMut<BackbufferResource>,
mut atlas: NonSendMut<SpriteAtlas>,
map: Res<Map>,
mut renderables: Query<(Entity, &mut Renderable, &Position)>,
renderables: Query<(Entity, &mut Renderable, &Position)>,
mut errors: EventWriter<GameError>,
) {
// Clear the main canvas first
@@ -64,7 +69,11 @@ pub fn render_system(
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
// Render all entities to the backbuffer
for (_, mut renderable, position) in renderables.iter_mut() {
for (_, mut renderable, position) in renderables
// .iter_mut()
// .sort_by_key::<&mut Renderable, _, _>(|(renderable, renderable, renderable)| renderable.layer)
// .collect()
{
let pos = position.get_pixel_pos(&map.graph);
match pos {
Ok(pos) => {

View File

@@ -1,8 +1,5 @@
use sdl2::rect::Rect;
use sdl2::render::{Canvas, RenderTarget};
use crate::error::{AnimatedTextureError, GameError, GameResult, TextureError};
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
use crate::texture::sprite::AtlasTile;
#[derive(Debug, Clone)]
pub struct AnimatedTexture {
@@ -40,12 +37,6 @@ impl AnimatedTexture {
&self.tiles[self.current_frame]
}
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, dest: Rect) -> GameResult<()> {
let mut tile = *self.current_tile();
tile.render(canvas, atlas, dest)?;
Ok(())
}
/// Returns the current frame index.
#[allow(dead_code)]
pub fn current_frame(&self) -> usize {

View File

@@ -1,80 +0,0 @@
use sdl2::rect::Rect;
use sdl2::render::{Canvas, RenderTarget};
use crate::entity::direction::Direction;
use crate::error::GameResult;
use crate::texture::animated::AnimatedTexture;
use crate::texture::sprite::SpriteAtlas;
#[derive(Clone)]
pub struct DirectionalAnimatedTexture {
textures: [Option<AnimatedTexture>; 4],
stopped_textures: [Option<AnimatedTexture>; 4],
}
impl DirectionalAnimatedTexture {
pub fn new(textures: [Option<AnimatedTexture>; 4], stopped_textures: [Option<AnimatedTexture>; 4]) -> Self {
Self {
textures,
stopped_textures,
}
}
pub fn tick(&mut self, dt: f32) {
for texture in self.textures.iter_mut().flatten() {
texture.tick(dt);
}
}
pub fn render<T: RenderTarget>(
&self,
canvas: &mut Canvas<T>,
atlas: &mut SpriteAtlas,
dest: Rect,
direction: Direction,
) -> GameResult<()> {
if let Some(texture) = &self.textures[direction.as_usize()] {
texture.render(canvas, atlas, dest)
} else {
Ok(())
}
}
pub fn render_stopped<T: RenderTarget>(
&self,
canvas: &mut Canvas<T>,
atlas: &mut SpriteAtlas,
dest: Rect,
direction: Direction,
) -> GameResult<()> {
if let Some(texture) = &self.stopped_textures[direction.as_usize()] {
texture.render(canvas, atlas, dest)
} else {
Ok(())
}
}
/// Returns true if the texture has a direction.
#[allow(dead_code)]
pub fn has_direction(&self, direction: Direction) -> bool {
self.textures[direction.as_usize()].is_some()
}
/// Returns true if the texture has a stopped direction.
#[allow(dead_code)]
pub fn has_stopped_direction(&self, direction: Direction) -> bool {
self.stopped_textures[direction.as_usize()].is_some()
}
/// Returns the number of textures.
#[allow(dead_code)]
pub fn texture_count(&self) -> usize {
self.textures.iter().filter(|t| t.is_some()).count()
}
/// Returns the number of stopped textures.
#[allow(dead_code)]
pub fn stopped_texture_count(&self) -> usize {
self.stopped_textures.iter().filter(|t| t.is_some()).count()
}
}

View File

@@ -1,5 +1,4 @@
pub mod animated;
pub mod blinking;
pub mod directional;
pub mod sprite;
pub mod text;

View File

@@ -1,5 +1,4 @@
use anyhow::Result;
use bevy_ecs::resource::Resource;
use glam::U16Vec2;
use sdl2::pixels::Color;
use sdl2::rect::Rect;
@@ -9,33 +8,6 @@ use std::collections::HashMap;
use crate::error::TextureError;
/// A simple sprite for stationary items like pellets and energizers.
#[derive(Clone, Debug)]
pub struct Sprite {
pub atlas_tile: AtlasTile,
}
impl Sprite {
pub fn new(atlas_tile: AtlasTile) -> Self {
Self { atlas_tile }
}
pub fn render<C: RenderTarget>(
&self,
canvas: &mut Canvas<C>,
atlas: &mut SpriteAtlas,
position: glam::Vec2,
) -> Result<(), TextureError> {
let dest = crate::helpers::centered_with_size(
glam::IVec2::new(position.x as i32, position.y as i32),
glam::UVec2::new(self.atlas_tile.size.x as u32, self.atlas_tile.size.y as u32),
);
let mut tile = self.atlas_tile;
tile.render(canvas, atlas, dest)?;
Ok(())
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct AtlasMapper {
pub frames: HashMap<String, MapperFrame>,

View File

@@ -1,119 +0,0 @@
use pacman::entity::collision::{Collidable, CollisionSystem};
use pacman::entity::traversal::Position;
struct MockCollidable {
pos: Position,
}
impl Collidable for MockCollidable {
fn position(&self) -> Position {
self.pos
}
}
#[test]
fn test_is_colliding_with() {
let entity1 = MockCollidable {
pos: Position::AtNode(1),
};
let entity2 = MockCollidable {
pos: Position::AtNode(1),
};
let entity3 = MockCollidable {
pos: Position::AtNode(2),
};
let entity4 = MockCollidable {
pos: Position::BetweenNodes {
from: 1,
to: 2,
traversed: 0.5,
},
};
assert!(entity1.is_colliding_with(&entity2));
assert!(!entity1.is_colliding_with(&entity3));
assert!(entity1.is_colliding_with(&entity4));
assert!(entity3.is_colliding_with(&entity4));
}
#[test]
fn test_collision_system_register_and_query() {
let mut collision_system = CollisionSystem::default();
let pos1 = Position::AtNode(1);
let entity1 = collision_system.register_entity(pos1);
let pos2 = Position::BetweenNodes {
from: 1,
to: 2,
traversed: 0.5,
};
let entity2 = collision_system.register_entity(pos2);
let pos3 = Position::AtNode(3);
let entity3 = collision_system.register_entity(pos3);
// Test entities_at_node
assert_eq!(collision_system.entities_at_node(1), &[entity1, entity2]);
assert_eq!(collision_system.entities_at_node(2), &[entity2]);
assert_eq!(collision_system.entities_at_node(3), &[entity3]);
assert_eq!(collision_system.entities_at_node(4), &[] as &[u32]);
// Test potential_collisions
let mut collisions1 = collision_system.potential_collisions(&pos1);
collisions1.sort_unstable();
assert_eq!(collisions1, vec![entity1, entity2]);
let mut collisions2 = collision_system.potential_collisions(&pos2);
collisions2.sort_unstable();
assert_eq!(collisions2, vec![entity1, entity2]);
let mut collisions3 = collision_system.potential_collisions(&pos3);
collisions3.sort_unstable();
assert_eq!(collisions3, vec![entity3]);
}
#[test]
fn test_collision_system_update() {
let mut collision_system = CollisionSystem::default();
let entity1 = collision_system.register_entity(Position::AtNode(1));
assert_eq!(collision_system.entities_at_node(1), &[entity1]);
assert_eq!(collision_system.entities_at_node(2), &[] as &[u32]);
collision_system.update_position(entity1, Position::AtNode(2));
assert_eq!(collision_system.entities_at_node(1), &[] as &[u32]);
assert_eq!(collision_system.entities_at_node(2), &[entity1]);
collision_system.update_position(
entity1,
Position::BetweenNodes {
from: 2,
to: 3,
traversed: 0.1,
},
);
assert_eq!(collision_system.entities_at_node(1), &[] as &[u32]);
assert_eq!(collision_system.entities_at_node(2), &[entity1]);
assert_eq!(collision_system.entities_at_node(3), &[entity1]);
}
#[test]
fn test_collision_system_remove() {
let mut collision_system = CollisionSystem::default();
let entity1 = collision_system.register_entity(Position::AtNode(1));
let entity2 = collision_system.register_entity(Position::AtNode(1));
assert_eq!(collision_system.entities_at_node(1), &[entity1, entity2]);
collision_system.remove_entity(entity1);
assert_eq!(collision_system.entities_at_node(1), &[entity2]);
collision_system.remove_entity(entity2);
assert_eq!(collision_system.entities_at_node(1), &[] as &[u32]);
}

View File

@@ -2,7 +2,7 @@
use pacman::{
asset::{get_asset_bytes, Asset},
game::state::ATLAS_FRAMES,
game::ATLAS_FRAMES,
texture::sprite::{AtlasMapper, SpriteAtlas},
};
use sdl2::{

View File

@@ -1,77 +0,0 @@
use glam::U16Vec2;
use pacman::entity::direction::Direction;
use pacman::texture::animated::AnimatedTexture;
use pacman::texture::directional::DirectionalAnimatedTexture;
use pacman::texture::sprite::AtlasTile;
use sdl2::pixels::Color;
fn mock_atlas_tile(id: u32) -> AtlasTile {
AtlasTile {
pos: U16Vec2::new(0, 0),
size: U16Vec2::new(16, 16),
color: Some(Color::RGB(id as u8, 0, 0)),
}
}
fn mock_animated_texture(id: u32) -> AnimatedTexture {
AnimatedTexture::new(vec![mock_atlas_tile(id)], 0.1).expect("Invalid frame duration")
}
#[test]
fn test_directional_texture_partial_directions() {
let mut textures = [None, None, None, None];
textures[Direction::Up.as_usize()] = Some(mock_animated_texture(1));
let texture = DirectionalAnimatedTexture::new(textures, [None, None, None, None]);
assert_eq!(texture.texture_count(), 1);
assert!(texture.has_direction(Direction::Up));
assert!(!texture.has_direction(Direction::Down));
assert!(!texture.has_direction(Direction::Left));
assert!(!texture.has_direction(Direction::Right));
}
#[test]
fn test_directional_texture_all_directions() {
let mut textures = [None, None, None, None];
let directions = [
(Direction::Up, 1),
(Direction::Down, 2),
(Direction::Left, 3),
(Direction::Right, 4),
];
for (direction, id) in directions {
textures[direction.as_usize()] = Some(mock_animated_texture(id));
}
let texture = DirectionalAnimatedTexture::new(textures, [None, None, None, None]);
assert_eq!(texture.texture_count(), 4);
for direction in &[Direction::Up, Direction::Down, Direction::Left, Direction::Right] {
assert!(texture.has_direction(*direction));
}
}
#[test]
fn test_directional_texture_stopped() {
let mut stopped_textures = [None, None, None, None];
stopped_textures[Direction::Up.as_usize()] = Some(mock_animated_texture(1));
let texture = DirectionalAnimatedTexture::new([None, None, None, None], stopped_textures);
assert_eq!(texture.stopped_texture_count(), 1);
assert!(texture.has_stopped_direction(Direction::Up));
assert!(!texture.has_stopped_direction(Direction::Down));
}
#[test]
fn test_directional_texture_tick() {
let mut textures = [None, None, None, None];
textures[Direction::Up.as_usize()] = Some(mock_animated_texture(1));
let mut texture = DirectionalAnimatedTexture::new(textures, [None, None, None, None]);
// This is a bit of a placeholder, since we can't inspect the inner state easily.
// We're just ensuring the tick method runs without panicking.
texture.tick(0.1);
}

View File

@@ -1,7 +1,6 @@
use pacman::constants::RAW_BOARD;
use pacman::map::builder::Map;
mod collision;
mod item;
#[test]

View File

@@ -1,48 +0,0 @@
use pacman::entity::ghost::{Ghost, GhostType};
use pacman::entity::graph::Graph;
use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
use std::collections::HashMap;
fn create_test_atlas() -> SpriteAtlas {
let mut frames = HashMap::new();
let directions = ["up", "down", "left", "right"];
let ghost_types = ["blinky", "pinky", "inky", "clyde"];
for ghost_type in &ghost_types {
for (i, dir) in directions.iter().enumerate() {
frames.insert(
format!("ghost/{}/{}_{}.png", ghost_type, dir, "a"),
MapperFrame {
x: i as u16 * 16,
y: 0,
width: 16,
height: 16,
},
);
frames.insert(
format!("ghost/{}/{}_{}.png", ghost_type, dir, "b"),
MapperFrame {
x: i as u16 * 16,
y: 16,
width: 16,
height: 16,
},
);
}
}
let mapper = AtlasMapper { frames };
let dummy_texture = unsafe { std::mem::zeroed() };
SpriteAtlas::new(dummy_texture, mapper)
}
#[test]
fn test_ghost_creation() {
let graph = Graph::new();
let atlas = create_test_atlas();
let ghost = Ghost::new(&graph, 0, GhostType::Blinky, &atlas).unwrap();
assert_eq!(ghost.ghost_type, GhostType::Blinky);
assert_eq!(ghost.traverser.position.from_node_id(), 0);
}

View File

@@ -1,6 +1,5 @@
use pacman::entity::direction::Direction;
use pacman::entity::graph::{EdgePermissions, Graph, Node};
use pacman::entity::traversal::{Position, Traverser};
fn create_test_graph() -> Graph {
let mut graph = Graph::new();
@@ -150,68 +149,3 @@ fn should_find_edge_between_nodes() {
let non_existent_edge = graph.find_edge(0, 99);
assert!(non_existent_edge.is_none());
}
#[test]
fn test_traverser_basic() {
let graph = create_test_graph();
let mut traverser = Traverser::new(&graph, 0, Direction::Left, &|_| true);
traverser.set_next_direction(Direction::Up);
assert!(traverser.next_direction.is_some());
assert_eq!(traverser.next_direction.unwrap().0, Direction::Up);
}
#[test]
fn test_traverser_advance() {
let graph = create_test_graph();
let mut traverser = Traverser::new(&graph, 0, Direction::Right, &|_| true);
traverser.advance(&graph, 5.0, &|_| true).unwrap();
match traverser.position {
Position::BetweenNodes { from, to, traversed } => {
assert_eq!(from, 0);
assert_eq!(to, 1);
assert_eq!(traversed, 5.0);
}
_ => panic!("Expected to be between nodes"),
}
traverser.advance(&graph, 3.0, &|_| true).unwrap();
match traverser.position {
Position::BetweenNodes { from, to, traversed } => {
assert_eq!(from, 0);
assert_eq!(to, 1);
assert_eq!(traversed, 8.0);
}
_ => panic!("Expected to be between nodes"),
}
}
#[test]
fn test_traverser_with_permissions() {
let mut graph = Graph::new();
let node1 = graph.add_node(Node {
position: glam::Vec2::new(0.0, 0.0),
});
let node2 = graph.add_node(Node {
position: glam::Vec2::new(16.0, 0.0),
});
graph
.add_edge(node1, node2, false, None, Direction::Right, EdgePermissions::GhostsOnly)
.unwrap();
// Pacman can't traverse ghost-only edges
let mut traverser = Traverser::new(&graph, node1, Direction::Right, &|edge| {
matches!(edge.permissions, EdgePermissions::All)
});
traverser
.advance(&graph, 5.0, &|edge| matches!(edge.permissions, EdgePermissions::All))
.unwrap();
// Should still be at the node since it can't traverse
assert!(traverser.position.is_at_node());
}

View File

@@ -1,53 +1,46 @@
use glam::U16Vec2;
use pacman::{
entity::{
collision::Collidable,
item::{FruitKind, Item, ItemType},
},
texture::sprite::{AtlasTile, Sprite},
};
use strum::{EnumCount, IntoEnumIterator};
// use glam::U16Vec2;
// use pacman::texture::sprite::{AtlasTile, Sprite};
#[test]
fn test_item_type_get_score() {
assert_eq!(ItemType::Pellet.get_score(), 10);
assert_eq!(ItemType::Energizer.get_score(), 50);
// #[test]
// fn test_item_type_get_score() {
// assert_eq!(ItemType::Pellet.get_score(), 10);
// assert_eq!(ItemType::Energizer.get_score(), 50);
let fruit = ItemType::Fruit { kind: FruitKind::Apple };
assert_eq!(fruit.get_score(), 100);
}
// let fruit = ItemType::Fruit { kind: FruitKind::Apple };
// assert_eq!(fruit.get_score(), 100);
// }
#[test]
fn test_fruit_kind_increasing_score() {
// Build a list of fruit kinds, sorted by their index
let mut kinds = FruitKind::iter()
.map(|kind| (kind.index(), kind.get_score()))
.collect::<Vec<_>>();
kinds.sort_unstable_by_key(|(index, _)| *index);
// #[test]
// fn test_fruit_kind_increasing_score() {
// // Build a list of fruit kinds, sorted by their index
// let mut kinds = FruitKind::iter()
// .map(|kind| (kind.index(), kind.get_score()))
// .collect::<Vec<_>>();
// kinds.sort_unstable_by_key(|(index, _)| *index);
assert_eq!(kinds.len(), FruitKind::COUNT);
// assert_eq!(kinds.len(), FruitKind::COUNT);
// Check that the score increases as expected
for window in kinds.windows(2) {
let ((_, prev), (_, next)) = (window[0], window[1]);
assert!(prev < next, "Fruits should have increasing scores, but {prev:?} < {next:?}");
}
}
// // Check that the score increases as expected
// for window in kinds.windows(2) {
// let ((_, prev), (_, next)) = (window[0], window[1]);
// assert!(prev < next, "Fruits should have increasing scores, but {prev:?} < {next:?}");
// }
// }
#[test]
fn test_item_creation_and_collection() {
let atlas_tile = AtlasTile {
pos: U16Vec2::new(0, 0),
size: U16Vec2::new(16, 16),
color: None,
};
let sprite = Sprite::new(atlas_tile);
let mut item = Item::new(0, ItemType::Pellet, sprite);
// #[test]
// fn test_item_creation_and_collection() {
// let atlas_tile = AtlasTile {
// pos: U16Vec2::new(0, 0),
// size: U16Vec2::new(16, 16),
// color: None,
// };
// let sprite = Sprite::new(atlas_tile);
// let mut item = Item::new(0, ItemType::Pellet, sprite);
assert!(!item.is_collected());
assert_eq!(item.get_score(), 10);
assert_eq!(item.position().from_node_id(), 0);
// assert!(!item.is_collected());
// assert_eq!(item.get_score(), 10);
// assert_eq!(item.position().from_node_id(), 0);
item.collect();
assert!(item.is_collected());
}
// item.collect();
// assert!(item.is_collected());
// }

View File

@@ -1,7 +1,6 @@
use glam::Vec2;
use pacman::constants::{CELL_SIZE, RAW_BOARD};
use pacman::map::builder::Map;
use sdl2::render::Texture;
#[test]
fn test_map_creation() {
@@ -34,60 +33,60 @@ fn test_map_node_positions() {
}
}
#[test]
fn test_generate_items() {
use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
use std::collections::HashMap;
// #[test]
// fn test_generate_items() {
// use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
// use std::collections::HashMap;
let map = Map::new(RAW_BOARD).unwrap();
// let map = Map::new(RAW_BOARD).unwrap();
// Create a minimal atlas for testing
let mut frames = HashMap::new();
frames.insert(
"maze/pellet.png".to_string(),
MapperFrame {
x: 0,
y: 0,
width: 8,
height: 8,
},
);
frames.insert(
"maze/energizer.png".to_string(),
MapperFrame {
x: 8,
y: 0,
width: 8,
height: 8,
},
);
// // Create a minimal atlas for testing
// let mut frames = HashMap::new();
// frames.insert(
// "maze/pellet.png".to_string(),
// MapperFrame {
// x: 0,
// y: 0,
// width: 8,
// height: 8,
// },
// );
// frames.insert(
// "maze/energizer.png".to_string(),
// MapperFrame {
// x: 8,
// y: 0,
// width: 8,
// height: 8,
// },
// );
let mapper = AtlasMapper { frames };
let texture = unsafe { std::mem::transmute::<usize, Texture<'static>>(0usize) };
let atlas = SpriteAtlas::new(texture, mapper);
// let mapper = AtlasMapper { frames };
// let texture = unsafe { std::mem::transmute::<usize, Texture<'static>>(0usize) };
// let atlas = SpriteAtlas::new(texture, mapper);
let items = map.generate_items(&atlas).unwrap();
// let items = map.generate_items(&atlas).unwrap();
// Verify we have items
assert!(!items.is_empty());
// // Verify we have items
// assert!(!items.is_empty());
// Count different types
let pellet_count = items
.iter()
.filter(|item| matches!(item.item_type, pacman::entity::item::ItemType::Pellet))
.count();
let energizer_count = items
.iter()
.filter(|item| matches!(item.item_type, pacman::entity::item::ItemType::Energizer))
.count();
// // Count different types
// let pellet_count = items
// .iter()
// .filter(|item| matches!(item.item_type, pacman::entity::item::ItemType::Pellet))
// .count();
// let energizer_count = items
// .iter()
// .filter(|item| matches!(item.item_type, pacman::entity::item::ItemType::Energizer))
// .count();
// Should have both types
assert_eq!(pellet_count, 240);
assert_eq!(energizer_count, 4);
// // Should have both types
// assert_eq!(pellet_count, 240);
// assert_eq!(energizer_count, 4);
// All items should be uncollected initially
assert!(items.iter().all(|item| !item.is_collected()));
// // All items should be uncollected initially
// assert!(items.iter().all(|item| !item.is_collected()));
// All items should have valid node indices
assert!(items.iter().all(|item| item.node_index < map.graph.node_count()));
}
// // All items should have valid node indices
// assert!(items.iter().all(|item| item.node_index < map.graph.node_count()));
// }

View File

@@ -1,73 +0,0 @@
use pacman::entity::direction::Direction;
use pacman::entity::graph::{Graph, Node};
use pacman::entity::pacman::Pacman;
use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
use std::collections::HashMap;
fn create_test_graph() -> Graph {
let mut graph = Graph::new();
let node1 = graph.add_node(Node {
position: glam::Vec2::new(0.0, 0.0),
});
let node2 = graph.add_node(Node {
position: glam::Vec2::new(16.0, 0.0),
});
let node3 = graph.add_node(Node {
position: glam::Vec2::new(0.0, 16.0),
});
graph.connect(node1, node2, false, None, Direction::Right).unwrap();
graph.connect(node1, node3, false, None, Direction::Down).unwrap();
graph
}
fn create_test_atlas() -> SpriteAtlas {
let mut frames = HashMap::new();
let directions = ["up", "down", "left", "right"];
for (i, dir) in directions.iter().enumerate() {
frames.insert(
format!("pacman/{dir}_a.png"),
MapperFrame {
x: i as u16 * 16,
y: 0,
width: 16,
height: 16,
},
);
frames.insert(
format!("pacman/{dir}_b.png"),
MapperFrame {
x: i as u16 * 16,
y: 16,
width: 16,
height: 16,
},
);
}
frames.insert(
"pacman/full.png".to_string(),
MapperFrame {
x: 64,
y: 0,
width: 16,
height: 16,
},
);
let mapper = AtlasMapper { frames };
let dummy_texture = unsafe { std::mem::zeroed() };
SpriteAtlas::new(dummy_texture, mapper)
}
#[test]
fn test_pacman_creation() {
let graph = create_test_graph();
let atlas = create_test_atlas();
let pacman = Pacman::new(&graph, 0, &atlas).unwrap();
assert!(pacman.traverser.position.is_at_node());
assert_eq!(pacman.traverser.direction, Direction::Left);
}

View File

@@ -1,120 +0,0 @@
use pacman::entity::direction::Direction;
use pacman::entity::ghost::{Ghost, GhostType};
use pacman::entity::graph::{Graph, Node};
use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
use std::collections::HashMap;
fn create_test_atlas() -> SpriteAtlas {
let mut frames = HashMap::new();
let directions = ["up", "down", "left", "right"];
let ghost_types = ["blinky", "pinky", "inky", "clyde"];
for ghost_type in &ghost_types {
for (i, dir) in directions.iter().enumerate() {
frames.insert(
format!("ghost/{}/{}_{}.png", ghost_type, dir, "a"),
MapperFrame {
x: i as u16 * 16,
y: 0,
width: 16,
height: 16,
},
);
frames.insert(
format!("ghost/{}/{}_{}.png", ghost_type, dir, "b"),
MapperFrame {
x: i as u16 * 16,
y: 16,
width: 16,
height: 16,
},
);
}
}
let mapper = AtlasMapper { frames };
let dummy_texture = unsafe { std::mem::zeroed() };
SpriteAtlas::new(dummy_texture, mapper)
}
#[test]
fn test_ghost_pathfinding() {
// Create a simple test graph
let mut graph = Graph::new();
// Add nodes in a simple line: 0 -> 1 -> 2
let node0 = graph.add_node(Node {
position: glam::Vec2::new(0.0, 0.0),
});
let node1 = graph.add_node(Node {
position: glam::Vec2::new(10.0, 0.0),
});
let node2 = graph.add_node(Node {
position: glam::Vec2::new(20.0, 0.0),
});
// Connect the nodes
graph.connect(node0, node1, false, None, Direction::Right).unwrap();
graph.connect(node1, node2, false, None, Direction::Right).unwrap();
// Create a test atlas for the ghost
let atlas = create_test_atlas();
// Create a ghost at node 0
let ghost = Ghost::new(&graph, node0, GhostType::Blinky, &atlas).unwrap();
// Test pathfinding from node 0 to node 2
let path = ghost.calculate_path_to_target(&graph, node2);
assert!(path.is_ok());
let path = path.unwrap();
assert!(
path == vec![node0, node1, node2] || path == vec![node2, node1, node0],
"Path was not what was expected"
);
}
#[test]
fn test_ghost_pathfinding_no_path() {
// Create a test graph with disconnected components
let mut graph = Graph::new();
let node0 = graph.add_node(Node {
position: glam::Vec2::new(0.0, 0.0),
});
let node1 = graph.add_node(Node {
position: glam::Vec2::new(10.0, 0.0),
});
// Don't connect the nodes
let atlas = create_test_atlas();
let ghost = Ghost::new(&graph, node0, GhostType::Blinky, &atlas).unwrap();
// Test pathfinding when no path exists
let path = ghost.calculate_path_to_target(&graph, node1);
assert!(path.is_err());
}
#[test]
fn test_ghost_debug_colors() {
let atlas = create_test_atlas();
let mut graph = Graph::new();
let node = graph.add_node(Node {
position: glam::Vec2::new(0.0, 0.0),
});
let blinky = Ghost::new(&graph, node, GhostType::Blinky, &atlas).unwrap();
let pinky = Ghost::new(&graph, node, GhostType::Pinky, &atlas).unwrap();
let inky = Ghost::new(&graph, node, GhostType::Inky, &atlas).unwrap();
let clyde = Ghost::new(&graph, node, GhostType::Clyde, &atlas).unwrap();
// Test that each ghost has a different debug color
let colors = std::collections::HashSet::from([
blinky.debug_color(),
pinky.debug_color(),
inky.debug_color(),
clyde.debug_color(),
]);
assert_eq!(colors.len(), 4, "All ghost colors should be unique");
}

View File

@@ -1,5 +1,5 @@
use glam::U16Vec2;
use pacman::texture::sprite::{AtlasMapper, AtlasTile, MapperFrame, Sprite, SpriteAtlas};
use pacman::texture::sprite::{AtlasMapper, AtlasTile, MapperFrame, SpriteAtlas};
use sdl2::pixels::Color;
use std::collections::HashMap;
@@ -92,12 +92,3 @@ fn test_atlas_tile_new_and_with_color() {
let tile_with_color = tile.with_color(color);
assert_eq!(tile_with_color.color, Some(color));
}
#[test]
fn test_sprite_new() {
let atlas_tile = AtlasTile::new(U16Vec2::new(0, 0), U16Vec2::new(16, 16), None);
let sprite = Sprite::new(atlas_tile);
assert_eq!(sprite.atlas_tile.pos, atlas_tile.pos);
assert_eq!(sprite.atlas_tile.size, atlas_tile.size);
}