mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-06 17:15:47 -06:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 183a432116 | |||
| ead1466b2d | |||
| 8ef09a4e3e | |||
| 33672d8d5a |
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -89,6 +89,12 @@ version = "0.15.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.10.0"
|
version = "2.10.0"
|
||||||
@@ -194,6 +200,8 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"spin_sleep",
|
"spin_sleep",
|
||||||
|
"strum",
|
||||||
|
"strum_macros",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-error",
|
"tracing-error",
|
||||||
@@ -406,6 +414,24 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.27.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.27.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.104"
|
version = "2.0.104"
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ glam = { version = "0.30.4", features = [] }
|
|||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.141"
|
serde_json = "1.0.141"
|
||||||
smallvec = "1.15.1"
|
smallvec = "1.15.1"
|
||||||
|
strum = "0.27.2"
|
||||||
|
strum_macros = "0.27.2"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.86.0"
|
channel = "1.86.0"
|
||||||
|
components = ["rustfmt", "llvm-tools-preview", "clippy"]
|
||||||
|
|||||||
128
src/entity/collision.rs
Normal file
128
src/entity/collision.rs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::entity::traversal::Position;
|
||||||
|
|
||||||
|
/// Trait for entities that can participate in collision detection.
|
||||||
|
pub trait Collidable {
|
||||||
|
/// Returns the current position of this entity.
|
||||||
|
fn position(&self) -> Position;
|
||||||
|
|
||||||
|
/// Checks if this entity is colliding with another entity.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn is_colliding_with(&self, other: &dyn Collidable) -> bool {
|
||||||
|
positions_overlap(&self.position(), &other.position())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// System for tracking entities by their positions for efficient collision detection.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct CollisionSystem {
|
||||||
|
/// Maps node IDs to lists of entity IDs that are at that node
|
||||||
|
node_entities: HashMap<usize, Vec<EntityId>>,
|
||||||
|
/// Maps entity IDs to their current positions
|
||||||
|
entity_positions: HashMap<EntityId, Position>,
|
||||||
|
/// Next available entity ID
|
||||||
|
next_id: EntityId,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unique identifier for an entity in the collision system
|
||||||
|
pub type EntityId = u32;
|
||||||
|
|
||||||
|
impl CollisionSystem {
|
||||||
|
/// Registers an entity with the collision system and returns its ID
|
||||||
|
pub fn register_entity(&mut self, position: Position) -> EntityId {
|
||||||
|
let id = self.next_id;
|
||||||
|
self.next_id += 1;
|
||||||
|
|
||||||
|
self.entity_positions.insert(id, position);
|
||||||
|
self.update_node_entities(id, position);
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates an entity's position
|
||||||
|
pub fn update_position(&mut self, entity_id: EntityId, new_position: Position) {
|
||||||
|
if let Some(old_position) = self.entity_positions.get(&entity_id) {
|
||||||
|
// Remove from old nodes
|
||||||
|
self.remove_from_nodes(entity_id, *old_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update position and add to new nodes
|
||||||
|
self.entity_positions.insert(entity_id, new_position);
|
||||||
|
self.update_node_entities(entity_id, new_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes an entity from the collision system
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn remove_entity(&mut self, entity_id: EntityId) {
|
||||||
|
if let Some(position) = self.entity_positions.remove(&entity_id) {
|
||||||
|
self.remove_from_nodes(entity_id, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets all entity IDs at a specific node
|
||||||
|
pub fn entities_at_node(&self, node: usize) -> &[EntityId] {
|
||||||
|
self.node_entities.get(&node).map(|v| v.as_slice()).unwrap_or(&[])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets all entity IDs that could collide with an entity at the given position
|
||||||
|
pub fn potential_collisions(&self, position: &Position) -> Vec<EntityId> {
|
||||||
|
let mut collisions = Vec::new();
|
||||||
|
let nodes = get_nodes(position);
|
||||||
|
|
||||||
|
for node in nodes {
|
||||||
|
collisions.extend(self.entities_at_node(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicates
|
||||||
|
collisions.sort_unstable();
|
||||||
|
collisions.dedup();
|
||||||
|
collisions
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the node_entities map when an entity's position changes
|
||||||
|
fn update_node_entities(&mut self, entity_id: EntityId, position: Position) {
|
||||||
|
let nodes = get_nodes(&position);
|
||||||
|
for node in nodes {
|
||||||
|
self.node_entities.entry(node).or_default().push(entity_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes an entity from all nodes it was previously at
|
||||||
|
fn remove_from_nodes(&mut self, entity_id: EntityId, position: Position) {
|
||||||
|
let nodes = get_nodes(&position);
|
||||||
|
for node in nodes {
|
||||||
|
if let Some(entities) = self.node_entities.get_mut(&node) {
|
||||||
|
entities.retain(|&id| id != entity_id);
|
||||||
|
if entities.is_empty() {
|
||||||
|
self.node_entities.remove(&node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if two positions overlap (entities are at the same location).
|
||||||
|
fn positions_overlap(a: &Position, b: &Position) -> bool {
|
||||||
|
let a_nodes = get_nodes(a);
|
||||||
|
let b_nodes = get_nodes(b);
|
||||||
|
|
||||||
|
// Check if any nodes overlap
|
||||||
|
a_nodes.iter().any(|a_node| b_nodes.contains(a_node))
|
||||||
|
|
||||||
|
// TODO: More complex overlap detection, the above is a simple check, but it could become an early filter for more precise calculations later
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets all nodes that an entity is currently at or between.
|
||||||
|
fn get_nodes(pos: &Position) -> SmallVec<[usize; 2]> {
|
||||||
|
let mut nodes = SmallVec::new();
|
||||||
|
match pos {
|
||||||
|
Position::AtNode(node) => nodes.push(*node),
|
||||||
|
Position::BetweenNodes { from, to, .. } => {
|
||||||
|
nodes.push(*from);
|
||||||
|
nodes.push(*to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes
|
||||||
|
}
|
||||||
@@ -9,10 +9,13 @@ use rand::prelude::*;
|
|||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::entity::direction::Direction;
|
use crate::entity::{
|
||||||
use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId};
|
collision::Collidable,
|
||||||
use crate::entity::r#trait::Entity;
|
direction::Direction,
|
||||||
use crate::entity::traversal::Traverser;
|
graph::{Edge, EdgePermissions, Graph, NodeId},
|
||||||
|
r#trait::Entity,
|
||||||
|
traversal::Traverser,
|
||||||
|
};
|
||||||
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;
|
||||||
@@ -245,3 +248,9 @@ impl Ghost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Collidable for Ghost {
|
||||||
|
fn position(&self) -> crate::entity::traversal::Position {
|
||||||
|
self.traverser.position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
constants,
|
constants,
|
||||||
entity::graph::Graph,
|
entity::{collision::Collidable, graph::Graph},
|
||||||
error::EntityError,
|
error::EntityError,
|
||||||
texture::sprite::{Sprite, SpriteAtlas},
|
texture::sprite::{Sprite, SpriteAtlas},
|
||||||
};
|
};
|
||||||
use sdl2::render::{Canvas, RenderTarget};
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
|
use strum_macros::{EnumCount, EnumIter};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ItemType {
|
pub enum ItemType {
|
||||||
@@ -26,7 +27,7 @@ impl ItemType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCount)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enum FruitKind {
|
pub enum FruitKind {
|
||||||
Apple,
|
Apple,
|
||||||
@@ -39,6 +40,19 @@ pub enum FruitKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FruitKind {
|
impl FruitKind {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn index(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
FruitKind::Apple => 0,
|
||||||
|
FruitKind::Strawberry => 1,
|
||||||
|
FruitKind::Orange => 2,
|
||||||
|
FruitKind::Melon => 3,
|
||||||
|
FruitKind::Bell => 4,
|
||||||
|
FruitKind::Key => 5,
|
||||||
|
FruitKind::Galaxian => 6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_score(self) -> u32 {
|
pub fn get_score(self) -> u32 {
|
||||||
match self {
|
match self {
|
||||||
FruitKind::Apple => 100,
|
FruitKind::Apple => 100,
|
||||||
@@ -93,3 +107,9 @@ impl Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Collidable for Item {
|
||||||
|
fn position(&self) -> crate::entity::traversal::Position {
|
||||||
|
crate::entity::traversal::Position::AtNode(self.node_index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod collision;
|
||||||
pub mod direction;
|
pub mod direction;
|
||||||
pub mod ghost;
|
pub mod ghost;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
|
|||||||
@@ -4,10 +4,13 @@
|
|||||||
//! animation, and rendering. Pac-Man moves through the game graph using
|
//! animation, and rendering. Pac-Man moves through the game graph using
|
||||||
//! a traverser and displays directional animated textures.
|
//! a traverser and displays directional animated textures.
|
||||||
|
|
||||||
use crate::entity::direction::Direction;
|
use crate::entity::{
|
||||||
use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId};
|
collision::Collidable,
|
||||||
use crate::entity::r#trait::Entity;
|
direction::Direction,
|
||||||
use crate::entity::traversal::Traverser;
|
graph::{Edge, EdgePermissions, Graph, NodeId},
|
||||||
|
r#trait::Entity,
|
||||||
|
traversal::Traverser,
|
||||||
|
};
|
||||||
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;
|
||||||
@@ -125,3 +128,9 @@ impl Pacman {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Collidable for Pacman {
|
||||||
|
fn position(&self) -> crate::entity::traversal::Position {
|
||||||
|
self.traverser.position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
109
src/game.rs
109
src/game.rs
@@ -17,6 +17,7 @@ use crate::{
|
|||||||
audio::Audio,
|
audio::Audio,
|
||||||
constants::{CELL_SIZE, RAW_BOARD},
|
constants::{CELL_SIZE, RAW_BOARD},
|
||||||
entity::{
|
entity::{
|
||||||
|
collision::{Collidable, CollisionSystem, EntityId},
|
||||||
ghost::{Ghost, GhostType},
|
ghost::{Ghost, GhostType},
|
||||||
item::Item,
|
item::Item,
|
||||||
pacman::Pacman,
|
pacman::Pacman,
|
||||||
@@ -41,6 +42,12 @@ pub struct Game {
|
|||||||
pub items: Vec<Item>,
|
pub items: Vec<Item>,
|
||||||
pub debug_mode: bool,
|
pub debug_mode: bool,
|
||||||
|
|
||||||
|
// Collision system
|
||||||
|
collision_system: CollisionSystem,
|
||||||
|
pacman_id: EntityId,
|
||||||
|
ghost_ids: Vec<EntityId>,
|
||||||
|
item_ids: Vec<EntityId>,
|
||||||
|
|
||||||
// Rendering resources
|
// Rendering resources
|
||||||
atlas: SpriteAtlas,
|
atlas: SpriteAtlas,
|
||||||
map_texture: AtlasTile,
|
map_texture: AtlasTile,
|
||||||
@@ -109,6 +116,26 @@ impl Game {
|
|||||||
ghosts.push(ghost);
|
ghosts.push(ghost);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize collision system
|
||||||
|
let mut collision_system = CollisionSystem::default();
|
||||||
|
|
||||||
|
// Register Pac-Man
|
||||||
|
let pacman_id = collision_system.register_entity(pacman.position());
|
||||||
|
|
||||||
|
// Register items
|
||||||
|
let mut item_ids = Vec::new();
|
||||||
|
for item in &items {
|
||||||
|
let item_id = collision_system.register_entity(item.position());
|
||||||
|
item_ids.push(item_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register ghosts
|
||||||
|
let mut ghost_ids = Vec::new();
|
||||||
|
for ghost in &ghosts {
|
||||||
|
let ghost_id = collision_system.register_entity(ghost.position());
|
||||||
|
ghost_ids.push(ghost_id);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Game {
|
Ok(Game {
|
||||||
score: 0,
|
score: 0,
|
||||||
map,
|
map,
|
||||||
@@ -116,6 +143,10 @@ impl Game {
|
|||||||
ghosts,
|
ghosts,
|
||||||
items,
|
items,
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
|
collision_system,
|
||||||
|
pacman_id,
|
||||||
|
ghost_ids,
|
||||||
|
item_ids,
|
||||||
map_texture,
|
map_texture,
|
||||||
text_texture,
|
text_texture,
|
||||||
audio,
|
audio,
|
||||||
@@ -164,6 +195,26 @@ impl Game {
|
|||||||
*ghost = Ghost::new(&self.map.graph, random_node, ghost_types[i], &self.atlas)?;
|
*ghost = Ghost::new(&self.map.graph, random_node, ghost_types[i], &self.atlas)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset collision system
|
||||||
|
self.collision_system = CollisionSystem::default();
|
||||||
|
|
||||||
|
// Re-register Pac-Man
|
||||||
|
self.pacman_id = self.collision_system.register_entity(self.pacman.position());
|
||||||
|
|
||||||
|
// Re-register items
|
||||||
|
self.item_ids.clear();
|
||||||
|
for item in &self.items {
|
||||||
|
let item_id = self.collision_system.register_entity(item.position());
|
||||||
|
self.item_ids.push(item_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-register ghosts
|
||||||
|
self.ghost_ids.clear();
|
||||||
|
for ghost in &self.ghosts {
|
||||||
|
let ghost_id = self.collision_system.register_entity(ghost.position());
|
||||||
|
self.ghost_ids.push(ghost_id);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,27 +226,61 @@ impl Game {
|
|||||||
ghost.tick(dt, &self.map.graph);
|
ghost.tick(dt, &self.map.graph);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for item collisions
|
// Update collision system positions
|
||||||
self.check_item_collisions();
|
self.update_collision_positions();
|
||||||
|
|
||||||
|
// Check for collisions
|
||||||
|
self.check_collisions();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_item_collisions(&mut self) {
|
fn update_collision_positions(&mut self) {
|
||||||
let pacman_node = self.pacman.current_node_id();
|
// Update Pac-Man's position
|
||||||
|
self.collision_system.update_position(self.pacman_id, self.pacman.position());
|
||||||
|
|
||||||
for item in &mut self.items {
|
// Update ghost positions
|
||||||
if !item.is_collected() && item.node_index == pacman_node {
|
for (ghost, &ghost_id) in self.ghosts.iter().zip(&self.ghost_ids) {
|
||||||
item.collect();
|
self.collision_system.update_position(ghost_id, ghost.position());
|
||||||
self.score += item.get_score();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle energizer effects
|
fn check_collisions(&mut self) {
|
||||||
if matches!(item.item_type, crate::entity::item::ItemType::Energizer) {
|
// Check Pac-Man vs Items
|
||||||
// TODO: Make ghosts frightened
|
let potential_collisions = self.collision_system.potential_collisions(&self.pacman.position());
|
||||||
tracing::info!("Energizer collected! Ghosts should become frightened.");
|
|
||||||
|
for entity_id in potential_collisions {
|
||||||
|
if entity_id != self.pacman_id {
|
||||||
|
// Check if this is an item collision
|
||||||
|
if let Some(item_index) = self.find_item_by_id(entity_id) {
|
||||||
|
let item = &mut self.items[item_index];
|
||||||
|
if !item.is_collected() {
|
||||||
|
item.collect();
|
||||||
|
self.score += item.get_score();
|
||||||
|
|
||||||
|
// Handle energizer effects
|
||||||
|
if matches!(item.item_type, crate::entity::item::ItemType::Energizer) {
|
||||||
|
// TODO: Make ghosts frightened
|
||||||
|
tracing::info!("Energizer collected! Ghosts should become frightened.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a ghost collision
|
||||||
|
if let Some(_ghost_index) = self.find_ghost_by_id(entity_id) {
|
||||||
|
// TODO: Handle Pac-Man being eaten by ghost
|
||||||
|
tracing::info!("Pac-Man collided with ghost!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_item_by_id(&self, entity_id: EntityId) -> Option<usize> {
|
||||||
|
self.item_ids.iter().position(|&id| id == entity_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_ghost_by_id(&self, entity_id: EntityId) -> Option<usize> {
|
||||||
|
self.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: RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &mut Texture) -> GameResult<()> {
|
||||||
canvas
|
canvas
|
||||||
.with_texture_canvas(backbuffer, |canvas| {
|
.with_texture_canvas(backbuffer, |canvas| {
|
||||||
|
|||||||
119
tests/collision.rs
Normal file
119
tests/collision.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
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]);
|
||||||
|
}
|
||||||
@@ -52,3 +52,26 @@ fn test_directional_texture_all_directions() {
|
|||||||
assert!(texture.has_direction(*direction));
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use pacman::constants::RAW_BOARD;
|
use pacman::constants::RAW_BOARD;
|
||||||
use pacman::map::Map;
|
use pacman::map::Map;
|
||||||
|
|
||||||
|
mod collision;
|
||||||
|
mod item;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_game_map_creation() {
|
fn test_game_map_creation() {
|
||||||
let map = Map::new(RAW_BOARD).unwrap();
|
let map = Map::new(RAW_BOARD).unwrap();
|
||||||
|
|||||||
53
tests/item.rs
Normal file
53
tests/item.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
use glam::U16Vec2;
|
||||||
|
use pacman::{
|
||||||
|
entity::{
|
||||||
|
collision::Collidable,
|
||||||
|
item::{FruitKind, Item, ItemType},
|
||||||
|
},
|
||||||
|
texture::sprite::{AtlasTile, Sprite},
|
||||||
|
};
|
||||||
|
use strum::{EnumCount, IntoEnumIterator};
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 as usize);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
assert!(!item.is_collected());
|
||||||
|
assert_eq!(item.get_score(), 10);
|
||||||
|
assert_eq!(item.position().from_node_id(), 0);
|
||||||
|
|
||||||
|
item.collect();
|
||||||
|
assert!(item.is_collected());
|
||||||
|
}
|
||||||
@@ -1,47 +1,11 @@
|
|||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
use pacman::constants::{BOARD_CELL_SIZE, CELL_SIZE};
|
use pacman::constants::{BOARD_CELL_SIZE, CELL_SIZE, RAW_BOARD};
|
||||||
use pacman::map::Map;
|
use pacman::map::Map;
|
||||||
|
use sdl2::render::Texture;
|
||||||
fn create_minimal_test_board() -> [&'static str; BOARD_CELL_SIZE.y as usize] {
|
|
||||||
let mut board = [""; BOARD_CELL_SIZE.y as usize];
|
|
||||||
board[0] = "############################";
|
|
||||||
board[1] = "#............##............#";
|
|
||||||
board[2] = "#.####.#####.##.#####.####.#";
|
|
||||||
board[3] = "#o####.#####.##.#####.####o#";
|
|
||||||
board[4] = "#.####.#####.##.#####.####.#";
|
|
||||||
board[5] = "#..........................#";
|
|
||||||
board[6] = "#.####.##.########.##.####.#";
|
|
||||||
board[7] = "#.####.##.########.##.####.#";
|
|
||||||
board[8] = "#......##....##....##......#";
|
|
||||||
board[9] = "######.##### ## #####.######";
|
|
||||||
board[10] = " #.##### ## #####.# ";
|
|
||||||
board[11] = " #.## == ##.# ";
|
|
||||||
board[12] = " #.## ######## ##.# ";
|
|
||||||
board[13] = "######.## ######## ##.######";
|
|
||||||
board[14] = "T . ######## . T";
|
|
||||||
board[15] = "######.## ######## ##.######";
|
|
||||||
board[16] = " #.## ######## ##.# ";
|
|
||||||
board[17] = " #.## ##.# ";
|
|
||||||
board[18] = " #.## ######## ##.# ";
|
|
||||||
board[19] = "######.## ######## ##.######";
|
|
||||||
board[20] = "#............##............#";
|
|
||||||
board[21] = "#.####.#####.##.#####.####.#";
|
|
||||||
board[22] = "#.####.#####.##.#####.####.#";
|
|
||||||
board[23] = "#o..##.......X .......##..o#";
|
|
||||||
board[24] = "###.##.##.########.##.##.###";
|
|
||||||
board[25] = "###.##.##.########.##.##.###";
|
|
||||||
board[26] = "#......##....##....##......#";
|
|
||||||
board[27] = "#.##########.##.##########.#";
|
|
||||||
board[28] = "#.##########.##.##########.#";
|
|
||||||
board[29] = "#..........................#";
|
|
||||||
board[30] = "############################";
|
|
||||||
board
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_map_creation() {
|
fn test_map_creation() {
|
||||||
let board = create_minimal_test_board();
|
let map = Map::new(RAW_BOARD).unwrap();
|
||||||
let map = Map::new(board).unwrap();
|
|
||||||
|
|
||||||
assert!(map.graph.node_count() > 0);
|
assert!(map.graph.node_count() > 0);
|
||||||
assert!(!map.grid_to_node.is_empty());
|
assert!(!map.grid_to_node.is_empty());
|
||||||
@@ -59,8 +23,7 @@ fn test_map_creation() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_map_starting_positions() {
|
fn test_map_starting_positions() {
|
||||||
let board = create_minimal_test_board();
|
let map = Map::new(RAW_BOARD).unwrap();
|
||||||
let map = Map::new(board).unwrap();
|
|
||||||
|
|
||||||
let pacman_pos = map.find_starting_position(0);
|
let pacman_pos = map.find_starting_position(0);
|
||||||
assert!(pacman_pos.is_some());
|
assert!(pacman_pos.is_some());
|
||||||
@@ -73,8 +36,7 @@ fn test_map_starting_positions() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_map_node_positions() {
|
fn test_map_node_positions() {
|
||||||
let board = create_minimal_test_board();
|
let map = Map::new(RAW_BOARD).unwrap();
|
||||||
let map = Map::new(board).unwrap();
|
|
||||||
|
|
||||||
for (grid_pos, &node_id) in &map.grid_to_node {
|
for (grid_pos, &node_id) in &map.grid_to_node {
|
||||||
let node = map.graph.get_node(node_id).unwrap();
|
let node = map.graph.get_node(node_id).unwrap();
|
||||||
@@ -84,3 +46,61 @@ fn test_map_node_positions() {
|
|||||||
assert_eq!(node.position, expected_pos);
|
assert_eq!(node.position, expected_pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generate_items() {
|
||||||
|
use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// 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 have valid node indices
|
||||||
|
assert!(items.iter().all(|item| item.node_index < map.graph.node_count()));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
|
use glam::U16Vec2;
|
||||||
|
use pacman::texture::sprite::{AtlasMapper, AtlasTile, MapperFrame, Sprite, SpriteAtlas};
|
||||||
use sdl2::pixels::Color;
|
use sdl2::pixels::Color;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -76,3 +77,27 @@ fn test_sprite_atlas_color() {
|
|||||||
atlas.set_color(color);
|
atlas.set_color(color);
|
||||||
assert_eq!(atlas.default_color(), Some(color));
|
assert_eq!(atlas.default_color(), Some(color));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_atlas_tile_new_and_with_color() {
|
||||||
|
let pos = U16Vec2::new(10, 20);
|
||||||
|
let size = U16Vec2::new(30, 40);
|
||||||
|
let color = Color::RGB(100, 150, 200);
|
||||||
|
|
||||||
|
let tile = AtlasTile::new(pos, size, None);
|
||||||
|
assert_eq!(tile.pos, pos);
|
||||||
|
assert_eq!(tile.size, size);
|
||||||
|
assert_eq!(tile.color, None);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user