mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-12 05:10:44 -06:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dc8aca373 | |||
| 02089a78da |
8
.github/workflows/build.yaml
vendored
8
.github/workflows/build.yaml
vendored
@@ -14,19 +14,19 @@ jobs:
|
|||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
artifact_name: pacman
|
artifact_name: pacman
|
||||||
toolchain: 1.88.0
|
toolchain: 1.86.0
|
||||||
- os: macos-13
|
- os: macos-13
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
artifact_name: pacman
|
artifact_name: pacman
|
||||||
toolchain: 1.88.0
|
toolchain: 1.86.0
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
target: aarch64-apple-darwin
|
target: aarch64-apple-darwin
|
||||||
artifact_name: pacman
|
artifact_name: pacman
|
||||||
toolchain: 1.88.0
|
toolchain: 1.86.0
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
target: x86_64-pc-windows-gnu
|
target: x86_64-pc-windows-gnu
|
||||||
artifact_name: pacman.exe
|
artifact_name: pacman.exe
|
||||||
toolchain: 1.88.0
|
toolchain: 1.86.0
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
2
.github/workflows/tests.yaml
vendored
2
.github/workflows/tests.yaml
vendored
@@ -4,7 +4,7 @@ on: ["push", "pull_request"]
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
RUST_TOOLCHAIN: 1.88.0
|
RUST_TOOLCHAIN: 1.86.0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|||||||
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.86.0"
|
||||||
95
src/entity/item.rs
Normal file
95
src/entity/item.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use crate::{
|
||||||
|
constants,
|
||||||
|
entity::graph::Graph,
|
||||||
|
error::EntityError,
|
||||||
|
texture::sprite::{Sprite, SpriteAtlas},
|
||||||
|
};
|
||||||
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ItemType {
|
||||||
|
Pellet,
|
||||||
|
Energizer,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
Fruit {
|
||||||
|
kind: FruitKind,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemType {
|
||||||
|
pub fn get_score(self) -> u32 {
|
||||||
|
match self {
|
||||||
|
ItemType::Pellet => 10,
|
||||||
|
ItemType::Energizer => 50,
|
||||||
|
ItemType::Fruit { kind } => kind.get_score(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum FruitKind {
|
||||||
|
Apple,
|
||||||
|
Strawberry,
|
||||||
|
Orange,
|
||||||
|
Melon,
|
||||||
|
Bell,
|
||||||
|
Key,
|
||||||
|
Galaxian,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FruitKind {
|
||||||
|
pub fn get_score(self) -> u32 {
|
||||||
|
match self {
|
||||||
|
FruitKind::Apple => 100,
|
||||||
|
FruitKind::Strawberry => 300,
|
||||||
|
FruitKind::Orange => 500,
|
||||||
|
FruitKind::Melon => 700,
|
||||||
|
FruitKind::Bell => 1000,
|
||||||
|
FruitKind::Key => 2000,
|
||||||
|
FruitKind::Galaxian => 3000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Item {
|
||||||
|
pub node_index: usize,
|
||||||
|
pub item_type: ItemType,
|
||||||
|
pub sprite: Sprite,
|
||||||
|
pub collected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item {
|
||||||
|
pub fn new(node_index: usize, item_type: ItemType, sprite: Sprite) -> Self {
|
||||||
|
Self {
|
||||||
|
node_index,
|
||||||
|
item_type,
|
||||||
|
sprite,
|
||||||
|
collected: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_collected(&self) -> bool {
|
||||||
|
self.collected
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect(&mut self) {
|
||||||
|
self.collected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_score(&self) -> u32 {
|
||||||
|
self.item_type.get_score()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, graph: &Graph) -> anyhow::Result<()> {
|
||||||
|
if !self.collected {
|
||||||
|
let node = graph
|
||||||
|
.get_node(self.node_index)
|
||||||
|
.ok_or(EntityError::NodeNotFound(self.node_index))?;
|
||||||
|
let position = node.position + constants::BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
|
self.sprite.render(canvas, atlas, position)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod direction;
|
pub mod direction;
|
||||||
pub mod ghost;
|
pub mod ghost;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
|
pub mod item;
|
||||||
pub mod pacman;
|
pub mod pacman;
|
||||||
pub mod r#trait;
|
pub mod r#trait;
|
||||||
pub mod traversal;
|
pub mod traversal;
|
||||||
|
|||||||
36
src/game.rs
36
src/game.rs
@@ -18,6 +18,7 @@ use crate::{
|
|||||||
constants::{CELL_SIZE, RAW_BOARD},
|
constants::{CELL_SIZE, RAW_BOARD},
|
||||||
entity::{
|
entity::{
|
||||||
ghost::{Ghost, GhostType},
|
ghost::{Ghost, GhostType},
|
||||||
|
item::Item,
|
||||||
pacman::Pacman,
|
pacman::Pacman,
|
||||||
r#trait::Entity,
|
r#trait::Entity,
|
||||||
},
|
},
|
||||||
@@ -37,6 +38,7 @@ pub struct Game {
|
|||||||
pub map: Map,
|
pub map: Map,
|
||||||
pub pacman: Pacman,
|
pub pacman: Pacman,
|
||||||
pub ghosts: Vec<Ghost>,
|
pub ghosts: Vec<Ghost>,
|
||||||
|
pub items: Vec<Item>,
|
||||||
pub debug_mode: bool,
|
pub debug_mode: bool,
|
||||||
|
|
||||||
// Rendering resources
|
// Rendering resources
|
||||||
@@ -87,6 +89,9 @@ impl Game {
|
|||||||
let audio = Audio::new();
|
let audio = Audio::new();
|
||||||
let pacman = Pacman::new(&map.graph, pacman_start_node, &atlas)?;
|
let pacman = Pacman::new(&map.graph, pacman_start_node, &atlas)?;
|
||||||
|
|
||||||
|
// Generate items (pellets and energizers)
|
||||||
|
let items = map.generate_items(&atlas)?;
|
||||||
|
|
||||||
// Create ghosts at random positions
|
// Create ghosts at random positions
|
||||||
let mut ghosts = Vec::new();
|
let mut ghosts = Vec::new();
|
||||||
let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde];
|
let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde];
|
||||||
@@ -109,6 +114,7 @@ impl Game {
|
|||||||
map,
|
map,
|
||||||
pacman,
|
pacman,
|
||||||
ghosts,
|
ghosts,
|
||||||
|
items,
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
map_texture,
|
map_texture,
|
||||||
text_texture,
|
text_texture,
|
||||||
@@ -146,6 +152,9 @@ impl Game {
|
|||||||
|
|
||||||
self.pacman = Pacman::new(&self.map.graph, pacman_start_node, &self.atlas)?;
|
self.pacman = Pacman::new(&self.map.graph, pacman_start_node, &self.atlas)?;
|
||||||
|
|
||||||
|
// Reset items
|
||||||
|
self.items = self.map.generate_items(&self.atlas)?;
|
||||||
|
|
||||||
// Randomize ghost positions
|
// Randomize ghost positions
|
||||||
let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde];
|
let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde];
|
||||||
let mut rng = SmallRng::from_os_rng();
|
let mut rng = SmallRng::from_os_rng();
|
||||||
@@ -165,6 +174,26 @@ impl Game {
|
|||||||
for ghost in &mut self.ghosts {
|
for ghost in &mut self.ghosts {
|
||||||
ghost.tick(dt, &self.map.graph);
|
ghost.tick(dt, &self.map.graph);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for item collisions
|
||||||
|
self.check_item_collisions();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_item_collisions(&mut self) {
|
||||||
|
let pacman_node = self.pacman.current_node_id();
|
||||||
|
|
||||||
|
for item in &mut self.items {
|
||||||
|
if !item.is_collected() && item.node_index == pacman_node {
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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<()> {
|
||||||
@@ -174,6 +203,13 @@ impl Game {
|
|||||||
canvas.clear();
|
canvas.clear();
|
||||||
self.map.render(canvas, &mut self.atlas, &mut self.map_texture);
|
self.map.render(canvas, &mut self.atlas, &mut self.map_texture);
|
||||||
|
|
||||||
|
// Render all items
|
||||||
|
for item in &self.items {
|
||||||
|
if let Err(e) = item.render(canvas, &mut self.atlas, &self.map.graph) {
|
||||||
|
tracing::error!("Failed to render item: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Render all ghosts
|
// Render all ghosts
|
||||||
for ghost in &self.ghosts {
|
for ghost in &self.ghosts {
|
||||||
if let Err(e) = ghost.render(canvas, &mut self.atlas, &self.map.graph) {
|
if let Err(e) = ghost.render(canvas, &mut self.atlas, &self.map.graph) {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
//! Map construction and building functionality.
|
//! Map construction and building functionality.
|
||||||
|
|
||||||
use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE};
|
use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE, RAW_BOARD};
|
||||||
use crate::entity::direction::Direction;
|
use crate::entity::direction::Direction;
|
||||||
use crate::entity::graph::{EdgePermissions, Graph, Node, NodeId};
|
use crate::entity::graph::{EdgePermissions, Graph, Node, NodeId};
|
||||||
|
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, SpriteAtlas};
|
use crate::texture::sprite::{AtlasTile, Sprite, SpriteAtlas};
|
||||||
use glam::{IVec2, UVec2, Vec2};
|
use glam::{IVec2, UVec2, Vec2};
|
||||||
use sdl2::render::{Canvas, RenderTarget};
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
@@ -187,6 +188,44 @@ impl Map {
|
|||||||
MapRenderer::render_map(canvas, atlas, map_texture);
|
MapRenderer::render_map(canvas, atlas, map_texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates Item entities for pellets and energizers from the parsed map.
|
||||||
|
pub fn generate_items(&self, atlas: &SpriteAtlas) -> GameResult<Vec<Item>> {
|
||||||
|
// Pre-load sprites to avoid repeated texture lookups
|
||||||
|
let pellet_sprite = SpriteAtlas::get_tile(atlas, "maze/pellet.png")
|
||||||
|
.ok_or_else(|| MapError::InvalidConfig("Pellet texture not found".to_string()))?;
|
||||||
|
let energizer_sprite = SpriteAtlas::get_tile(atlas, "maze/energizer.png")
|
||||||
|
.ok_or_else(|| MapError::InvalidConfig("Energizer texture not found".to_string()))?;
|
||||||
|
|
||||||
|
// Pre-allocate with estimated capacity (typical Pac-Man maps have ~240 pellets + 4 energizers)
|
||||||
|
let mut items = Vec::with_capacity(250);
|
||||||
|
|
||||||
|
// Parse the raw board once
|
||||||
|
let parsed_map = MapTileParser::parse_board(RAW_BOARD)?;
|
||||||
|
let map = parsed_map.tiles;
|
||||||
|
|
||||||
|
// Iterate through the map and collect items more efficiently
|
||||||
|
for (x, row) in map.iter().enumerate() {
|
||||||
|
for (y, tile) in row.iter().enumerate() {
|
||||||
|
match tile {
|
||||||
|
MapTile::Pellet | MapTile::PowerPellet => {
|
||||||
|
let grid_pos = IVec2::new(x as i32, y as i32);
|
||||||
|
if let Some(&node_id) = self.grid_to_node.get(&grid_pos) {
|
||||||
|
let (item_type, sprite) = match tile {
|
||||||
|
MapTile::Pellet => (ItemType::Pellet, Sprite::new(pellet_sprite)),
|
||||||
|
MapTile::PowerPellet => (ItemType::Energizer, Sprite::new(energizer_sprite)),
|
||||||
|
_ => unreachable!(), // We already filtered for these types
|
||||||
|
};
|
||||||
|
items.push(Item::new(node_id, item_type, sprite));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
/// Renders a debug visualization with cursor-based highlighting.
|
/// Renders a debug visualization with cursor-based highlighting.
|
||||||
///
|
///
|
||||||
/// This function provides interactive debugging by highlighting the nearest node
|
/// This function provides interactive debugging by highlighting the nearest node
|
||||||
|
|||||||
@@ -6,6 +6,27 @@ use sdl2::render::{Canvas, RenderTarget, Texture};
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// 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<()> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct AtlasMapper {
|
pub struct AtlasMapper {
|
||||||
pub frames: HashMap<String, MapperFrame>,
|
pub frames: HashMap<String, MapperFrame>,
|
||||||
|
|||||||
Reference in New Issue
Block a user