mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-06 09:15:46 -06:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91095ed2cc | |||
| cbf52bb994 | |||
| d763b9646f | |||
| d7a9e0a304 | |||
| db720edeef | |||
| f241e85d8f | |||
| d18b414536 | |||
| c9bcf32381 | |||
| b45980c172 | |||
| b4e3f383ec | |||
| 532abd1e45 | |||
| 70528b0dcc | |||
| c5ca7302c2 |
@@ -5,6 +5,7 @@ rustflags = [
|
|||||||
"-C", "link-args=-sUSE_SDL=2 -sUSE_SDL_IMAGE=2 -sUSE_SDL_MIXER=2 -sUSE_OGG=1 -sUSE_SDL_GFX=2 -sUSE_SDL_TTF=2 -sSDL2_IMAGE_FORMATS=['png']",
|
"-C", "link-args=-sUSE_SDL=2 -sUSE_SDL_IMAGE=2 -sUSE_SDL_MIXER=2 -sUSE_OGG=1 -sUSE_SDL_GFX=2 -sUSE_SDL_TTF=2 -sSDL2_IMAGE_FORMATS=['png']",
|
||||||
"-C", "link-args=--preload-file assets/game/",
|
"-C", "link-args=--preload-file assets/game/",
|
||||||
]
|
]
|
||||||
|
runner = "node"
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")']
|
[target.'cfg(target_os = "linux")']
|
||||||
rustflags = [
|
rustflags = [
|
||||||
@@ -13,4 +14,4 @@ rustflags = [
|
|||||||
# By adding `-lz` here, we ensure it's passed to the linker after `libpng`,
|
# By adding `-lz` here, we ensure it's passed to the linker after `libpng`,
|
||||||
# which is required for the linker to correctly resolve symbols.
|
# which is required for the linker to correctly resolve symbols.
|
||||||
"-C", "link-arg=-lz",
|
"-C", "link-arg=-lz",
|
||||||
]
|
]
|
||||||
|
|||||||
27
.github/workflows/audit.yaml
vendored
Normal file
27
.github/workflows/audit.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Audit
|
||||||
|
|
||||||
|
on: ["push", "pull_request"]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUST_TOOLCHAIN: 1.88.0
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
name: Audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||||
|
|
||||||
|
- name: Install cargo-audit
|
||||||
|
run: cargo install cargo-audit
|
||||||
|
|
||||||
|
- name: Run security audit
|
||||||
|
run: cargo audit
|
||||||
14
.github/workflows/build.yaml
vendored
14
.github/workflows/build.yaml
vendored
@@ -1,19 +1,17 @@
|
|||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
on: [push]
|
on: ["push", "pull_request"]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
env:
|
|
||||||
RUST_TOOLCHAIN: 1.86.0
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build (${{ matrix.target }})
|
name: Build (${{ matrix.target }})
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
toolchain: [1.88.0]
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
@@ -36,7 +34,7 @@ jobs:
|
|||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
@@ -92,13 +90,13 @@ jobs:
|
|||||||
uses: pyodide/setup-emsdk@v15
|
uses: pyodide/setup-emsdk@v15
|
||||||
with:
|
with:
|
||||||
version: 3.1.43
|
version: 3.1.43
|
||||||
actions-cache-folder: "emsdk-cache"
|
actions-cache-folder: "emsdk-cache-b"
|
||||||
|
|
||||||
- name: Setup Rust (WASM32 Emscripten)
|
- name: Setup Rust (WASM32 Emscripten)
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
target: wasm32-unknown-emscripten
|
target: wasm32-unknown-emscripten
|
||||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
toolchain: 1.86.0 # we are unfortunately pinned to 1.86.0 for some reason, bulk-memory-opt related issues
|
||||||
|
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
@@ -127,9 +125,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||||
with:
|
with:
|
||||||
path: "./dist/"
|
path: "./dist/"
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
|
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||||
uses: actions/deploy-pages@v4
|
uses: actions/deploy-pages@v4
|
||||||
|
|||||||
59
.github/workflows/coverage.yaml
vendored
Normal file
59
.github/workflows/coverage.yaml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: Coverage
|
||||||
|
|
||||||
|
on: ["push", "pull_request"]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUST_TOOLCHAIN: 1.86.0
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
coverage:
|
||||||
|
name: Code Coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Cache vcpkg
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: target/vcpkg
|
||||||
|
key: A-vcpkg-${{ runner.os }}-${{ hashFiles('Cargo.toml', 'Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
A-vcpkg-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Vcpkg Linux Dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libltdl-dev
|
||||||
|
|
||||||
|
- name: Vcpkg
|
||||||
|
run: |
|
||||||
|
cargo install cargo-vcpkg
|
||||||
|
cargo vcpkg -v build
|
||||||
|
|
||||||
|
- name: Install cargo-tarpaulin
|
||||||
|
run: cargo install cargo-tarpaulin
|
||||||
|
|
||||||
|
# Note: We manually link zlib. This should be synchronized with the flags set for Linux in .cargo/config.toml.
|
||||||
|
- name: Generate coverage report
|
||||||
|
run: |
|
||||||
|
cargo tarpaulin \
|
||||||
|
--out Lcov \
|
||||||
|
--output-dir coverage \
|
||||||
|
--rustflags="-C link-arg=-lz"
|
||||||
|
|
||||||
|
- name: Upload coverage to Coveralls
|
||||||
|
uses: coverallsapp/github-action@v2
|
||||||
|
with:
|
||||||
|
files: ./coverage/lcov.info
|
||||||
|
format: lcov
|
||||||
|
allow-empty: false
|
||||||
52
.github/workflows/test.yaml
vendored
Normal file
52
.github/workflows/test.yaml
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
name: Test
|
||||||
|
|
||||||
|
on: ["push", "pull_request"]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUST_TOOLCHAIN: 1.88.0
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||||
|
components: clippy, rustfmt
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Cache vcpkg
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: target/vcpkg
|
||||||
|
key: A-vcpkg-${{ runner.os }}-${{ hashFiles('Cargo.toml', 'Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
A-vcpkg-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Vcpkg Linux Dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libltdl-dev
|
||||||
|
|
||||||
|
- name: Vcpkg
|
||||||
|
run: |
|
||||||
|
cargo install cargo-vcpkg
|
||||||
|
cargo vcpkg -v build
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --workspace --verbose
|
||||||
|
|
||||||
|
- name: Run clippy
|
||||||
|
run: cargo clippy -- -D warnings
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: cargo fmt -- --check
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
//! Cross-platform asset loading abstraction.
|
//! Cross-platform asset loading abstraction.
|
||||||
//! On desktop, assets are embedded using include_bytes!; on Emscripten, assets are loaded from the filesystem.
|
//! On desktop, assets are embedded using include_bytes!; on Emscripten, assets are loaded from the filesystem.
|
||||||
|
|
||||||
@@ -5,7 +6,6 @@ use std::borrow::Cow;
|
|||||||
use std::io;
|
use std::io;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum AssetError {
|
pub enum AssetError {
|
||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const SOUND_ASSETS: [Asset; 4] = [Asset::Wav1, Asset::Wav2, Asset::Wav3, Asset::
|
|||||||
///
|
///
|
||||||
/// This struct is responsible for initializing the audio device, loading sounds,
|
/// This struct is responsible for initializing the audio device, loading sounds,
|
||||||
/// and playing them.
|
/// and playing them.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct Audio {
|
pub struct Audio {
|
||||||
_mixer_context: mixer::Sdl2MixerContext,
|
_mixer_context: mixer::Sdl2MixerContext,
|
||||||
sounds: Vec<Chunk>,
|
sounds: Vec<Chunk>,
|
||||||
@@ -18,6 +19,12 @@ pub struct Audio {
|
|||||||
muted: bool,
|
muted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Audio {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Audio {
|
impl Audio {
|
||||||
/// Creates a new `Audio` instance.
|
/// Creates a new `Audio` instance.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
@@ -57,6 +64,7 @@ impl Audio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Plays the "eat" sound effect.
|
/// Plays the "eat" sound effect.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn eat(&mut self) {
|
pub fn eat(&mut self) {
|
||||||
if let Some(chunk) = self.sounds.get(self.next_sound_index) {
|
if let Some(chunk) = self.sounds.get(self.next_sound_index) {
|
||||||
match mixer::Channel(0).play(chunk, 0) {
|
match mixer::Channel(0).play(chunk, 0) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ impl Direction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_ivec2(&self) -> IVec2 {
|
pub fn as_ivec2(&self) -> IVec2 {
|
||||||
(*self).into()
|
(*self).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
|
|
||||||
use crate::entity::direction::DIRECTIONS;
|
|
||||||
|
|
||||||
use super::direction::Direction;
|
use super::direction::Direction;
|
||||||
|
|
||||||
/// A unique identifier for a node, represented by its index in the graph's storage.
|
/// A unique identifier for a node, represented by its index in the graph's storage.
|
||||||
@@ -30,7 +28,7 @@ pub struct Node {
|
|||||||
/// Each field contains an optional edge leading in that direction.
|
/// Each field contains an optional edge leading in that direction.
|
||||||
/// This structure is used to represent the adjacency list for each node,
|
/// This structure is used to represent the adjacency list for each node,
|
||||||
/// providing O(1) access to edges in any cardinal direction.
|
/// providing O(1) access to edges in any cardinal direction.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Intersection {
|
pub struct Intersection {
|
||||||
/// Edge leading upward from this node, if it exists.
|
/// Edge leading upward from this node, if it exists.
|
||||||
pub up: Option<Edge>,
|
pub up: Option<Edge>,
|
||||||
@@ -42,17 +40,6 @@ pub struct Intersection {
|
|||||||
pub right: Option<Edge>,
|
pub right: Option<Edge>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Intersection {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
up: None,
|
|
||||||
down: None,
|
|
||||||
left: None,
|
|
||||||
right: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Intersection {
|
impl Intersection {
|
||||||
/// Returns an iterator over all edges from this intersection.
|
/// Returns an iterator over all edges from this intersection.
|
||||||
///
|
///
|
||||||
@@ -255,6 +242,7 @@ pub enum Position {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
impl Position {
|
impl Position {
|
||||||
/// Returns `true` if the position is exactly at a node.
|
/// Returns `true` if the position is exactly at a node.
|
||||||
pub fn is_at_node(&self) -> bool {
|
pub fn is_at_node(&self) -> bool {
|
||||||
@@ -262,6 +250,7 @@ impl Position {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `NodeId` of the current or most recently departed node.
|
/// Returns the `NodeId` of the current or most recently departed node.
|
||||||
|
#[allow(clippy::wrong_self_convention)]
|
||||||
pub fn from_node_id(&self) -> NodeId {
|
pub fn from_node_id(&self) -> NodeId {
|
||||||
match self {
|
match self {
|
||||||
Position::AtNode(id) => *id,
|
Position::AtNode(id) => *id,
|
||||||
@@ -270,6 +259,7 @@ impl Position {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `NodeId` of the destination node, if currently on an edge.
|
/// Returns the `NodeId` of the destination node, if currently on an edge.
|
||||||
|
#[allow(clippy::wrong_self_convention)]
|
||||||
pub fn to_node_id(&self) -> Option<NodeId> {
|
pub fn to_node_id(&self) -> Option<NodeId> {
|
||||||
match self {
|
match self {
|
||||||
Position::AtNode(_) => None,
|
Position::AtNode(_) => None,
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ impl Pacman {
|
|||||||
Direction::Right => "pacman/right",
|
Direction::Right => "pacman/right",
|
||||||
};
|
};
|
||||||
let moving_tiles = vec![
|
let moving_tiles = vec![
|
||||||
SpriteAtlas::get_tile(&atlas, &format!("{}_a.png", moving_prefix)).unwrap(),
|
SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_a.png")).unwrap(),
|
||||||
SpriteAtlas::get_tile(&atlas, &format!("{}_b.png", moving_prefix)).unwrap(),
|
SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_b.png")).unwrap(),
|
||||||
SpriteAtlas::get_tile(&atlas, "pacman/full.png").unwrap(),
|
SpriteAtlas::get_tile(atlas, "pacman/full.png").unwrap(),
|
||||||
];
|
];
|
||||||
|
|
||||||
let stopped_tiles = vec![SpriteAtlas::get_tile(&atlas, &format!("{}_b.png", moving_prefix)).unwrap()];
|
let stopped_tiles = vec![SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_b.png")).unwrap()];
|
||||||
|
|
||||||
textures.insert(direction, AnimatedTexture::new(moving_tiles, 0.08));
|
textures.insert(direction, AnimatedTexture::new(moving_tiles, 0.08));
|
||||||
stopped_textures.insert(direction, AnimatedTexture::new(stopped_tiles, 0.1));
|
stopped_textures.insert(direction, AnimatedTexture::new(stopped_tiles, 0.1));
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ pub struct Game {
|
|||||||
atlas: SpriteAtlas,
|
atlas: SpriteAtlas,
|
||||||
map_texture: AtlasTile,
|
map_texture: AtlasTile,
|
||||||
text_texture: TextTexture,
|
text_texture: TextTexture,
|
||||||
debug_text_texture: TextTexture,
|
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
pub audio: Audio,
|
pub audio: Audio,
|
||||||
@@ -71,9 +70,9 @@ impl Game {
|
|||||||
map_texture.color = Some(Color::RGB(0x20, 0x20, 0xf9));
|
map_texture.color = Some(Color::RGB(0x20, 0x20, 0xf9));
|
||||||
|
|
||||||
let text_texture = TextTexture::new(1.0);
|
let text_texture = TextTexture::new(1.0);
|
||||||
let debug_text_texture = TextTexture::new(0.5);
|
|
||||||
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);
|
||||||
|
|
||||||
Game {
|
Game {
|
||||||
score: 0,
|
score: 0,
|
||||||
map,
|
map,
|
||||||
@@ -81,7 +80,6 @@ impl Game {
|
|||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
map_texture,
|
map_texture,
|
||||||
text_texture,
|
text_texture,
|
||||||
debug_text_texture,
|
|
||||||
audio,
|
audio,
|
||||||
atlas,
|
atlas,
|
||||||
}
|
}
|
||||||
@@ -92,7 +90,6 @@ impl Game {
|
|||||||
|
|
||||||
if keycode == Keycode::M {
|
if keycode == Keycode::M {
|
||||||
self.audio.set_mute(!self.audio.is_muted());
|
self.audio.set_mute(!self.audio.is_muted());
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,8 +111,7 @@ impl Game {
|
|||||||
pub fn present_backbuffer<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &Texture) -> Result<()> {
|
pub fn present_backbuffer<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &Texture) -> Result<()> {
|
||||||
canvas.copy(backbuffer, None, None).map_err(anyhow::Error::msg)?;
|
canvas.copy(backbuffer, None, None).map_err(anyhow::Error::msg)?;
|
||||||
if self.debug_mode {
|
if self.debug_mode {
|
||||||
self.map
|
self.map.debug_render_nodes(canvas);
|
||||||
.debug_render_nodes(canvas, &mut self.atlas, &mut self.debug_text_texture);
|
|
||||||
}
|
}
|
||||||
self.draw_hud(canvas)?;
|
self.draw_hud(canvas)?;
|
||||||
canvas.present();
|
canvas.present();
|
||||||
@@ -123,7 +119,6 @@ impl Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_hud<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>) -> Result<()> {
|
fn draw_hud<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>) -> Result<()> {
|
||||||
let score_text = self.score.to_string();
|
|
||||||
let lives = 3;
|
let lives = 3;
|
||||||
let score_text = format!("{:02}", self.score);
|
let score_text = format!("{:02}", self.score);
|
||||||
let x_offset = 4;
|
let x_offset = 4;
|
||||||
|
|||||||
11
src/lib.rs
Normal file
11
src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//! Pac-Man game library crate.
|
||||||
|
|
||||||
|
pub mod app;
|
||||||
|
pub mod asset;
|
||||||
|
pub mod audio;
|
||||||
|
pub mod constants;
|
||||||
|
pub mod emscripten;
|
||||||
|
pub mod entity;
|
||||||
|
pub mod game;
|
||||||
|
pub mod map;
|
||||||
|
pub mod texture;
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
//! This module defines the game map and provides functions for interacting with it.
|
//! Map construction and building functionality.
|
||||||
|
|
||||||
use crate::constants::{MapTile, BOARD_CELL_SIZE, BOARD_PIXEL_OFFSET, BOARD_PIXEL_SIZE, CELL_SIZE};
|
use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE};
|
||||||
use crate::entity::direction::{Direction, DIRECTIONS};
|
use crate::entity::direction::{Direction, DIRECTIONS};
|
||||||
|
use crate::entity::graph::{Graph, Node, NodeId};
|
||||||
|
use crate::map::parser::MapTileParser;
|
||||||
|
use crate::map::render::MapRenderer;
|
||||||
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
|
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
|
||||||
use glam::{IVec2, UVec2, Vec2};
|
use glam::{IVec2, UVec2, Vec2};
|
||||||
use sdl2::pixels::Color;
|
|
||||||
use sdl2::rect::{Point, Rect};
|
|
||||||
use sdl2::render::{Canvas, RenderTarget};
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::entity::graph::{Graph, Node, NodeId};
|
|
||||||
use crate::texture::text::TextTexture;
|
|
||||||
|
|
||||||
/// The game map, responsible for holding the tile-based layout and the navigation graph.
|
/// The game map, responsible for holding the tile-based layout and the navigation graph.
|
||||||
///
|
///
|
||||||
/// The map is represented as a 2D array of `MapTile`s. It also stores a navigation
|
/// The map is represented as a 2D array of `MapTile`s. It also stores a navigation
|
||||||
@@ -38,38 +36,11 @@ impl Map {
|
|||||||
/// This function will panic if the board layout contains unknown characters or if
|
/// This function will panic if the board layout contains unknown characters or if
|
||||||
/// the house door is not defined by exactly two '=' characters.
|
/// the house door is not defined by exactly two '=' characters.
|
||||||
pub fn new(raw_board: [&str; BOARD_CELL_SIZE.y as usize]) -> Map {
|
pub fn new(raw_board: [&str; BOARD_CELL_SIZE.y as usize]) -> Map {
|
||||||
let mut map = [[MapTile::Empty; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize];
|
let parsed_map = MapTileParser::parse_board(raw_board).expect("Failed to parse board layout");
|
||||||
let mut house_door = [None; 2];
|
|
||||||
let mut tunnel_ends = [None; 2];
|
let map = parsed_map.tiles;
|
||||||
for (y, line) in raw_board.iter().enumerate().take(BOARD_CELL_SIZE.y as usize) {
|
let house_door = parsed_map.house_door;
|
||||||
for (x, character) in line.chars().enumerate().take(BOARD_CELL_SIZE.x as usize) {
|
let tunnel_ends = parsed_map.tunnel_ends;
|
||||||
let tile = match character {
|
|
||||||
'#' => MapTile::Wall,
|
|
||||||
'.' => MapTile::Pellet,
|
|
||||||
'o' => MapTile::PowerPellet,
|
|
||||||
' ' => MapTile::Empty,
|
|
||||||
'T' => {
|
|
||||||
if tunnel_ends[0].is_none() {
|
|
||||||
tunnel_ends[0] = Some(IVec2::new(x as i32, y as i32));
|
|
||||||
} else {
|
|
||||||
tunnel_ends[1] = Some(IVec2::new(x as i32, y as i32));
|
|
||||||
}
|
|
||||||
MapTile::Tunnel
|
|
||||||
}
|
|
||||||
c @ '0'..='4' => MapTile::StartingPosition(c.to_digit(10).unwrap() as u8),
|
|
||||||
'=' => {
|
|
||||||
if house_door[0].is_none() {
|
|
||||||
house_door[0] = Some(IVec2::new(x as i32, y as i32));
|
|
||||||
} else {
|
|
||||||
house_door[1] = Some(IVec2::new(x as i32, y as i32));
|
|
||||||
}
|
|
||||||
MapTile::Wall
|
|
||||||
}
|
|
||||||
_ => panic!("Unknown character in board: {character}"),
|
|
||||||
};
|
|
||||||
map[x][y] = tile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut graph = Graph::new();
|
let mut graph = Graph::new();
|
||||||
let mut grid_to_node = HashMap::new();
|
let mut grid_to_node = HashMap::new();
|
||||||
@@ -110,7 +81,7 @@ impl Map {
|
|||||||
// Iterate over the queue, adding nodes to the graph and connecting them to their neighbors
|
// Iterate over the queue, adding nodes to the graph and connecting them to their neighbors
|
||||||
while let Some(source_position) = queue.pop_front() {
|
while let Some(source_position) = queue.pop_front() {
|
||||||
for &dir in DIRECTIONS.iter() {
|
for &dir in DIRECTIONS.iter() {
|
||||||
let new_position = source_position + dir.to_ivec2();
|
let new_position = source_position + dir.as_ivec2();
|
||||||
|
|
||||||
// Skip if the new position is out of bounds
|
// Skip if the new position is out of bounds
|
||||||
if new_position.x < 0
|
if new_position.x < 0
|
||||||
@@ -143,7 +114,7 @@ impl Map {
|
|||||||
// Connect the new node to the source node
|
// Connect the new node to the source node
|
||||||
let source_node_id = grid_to_node
|
let source_node_id = grid_to_node
|
||||||
.get(&source_position)
|
.get(&source_position)
|
||||||
.expect(&format!("Source node not found for {source_position}"));
|
.unwrap_or_else(|| panic!("Source node not found for {source_position}"));
|
||||||
|
|
||||||
// Connect the new node to the source node
|
// Connect the new node to the source node
|
||||||
graph
|
graph
|
||||||
@@ -158,7 +129,7 @@ impl Map {
|
|||||||
for dir in DIRECTIONS {
|
for dir in DIRECTIONS {
|
||||||
// If the node doesn't have an edge in this direction, look for a neighbor in that direction
|
// If the node doesn't have an edge in this direction, look for a neighbor in that direction
|
||||||
if graph.adjacency_list[node_id].get(dir).is_none() {
|
if graph.adjacency_list[node_id].get(dir).is_none() {
|
||||||
let neighbor = grid_pos + dir.to_ivec2();
|
let neighbor = grid_pos + dir.as_ivec2();
|
||||||
// If the neighbor exists, connect the node to it
|
// If the neighbor exists, connect the node to it
|
||||||
if let Some(&neighbor_id) = grid_to_node.get(&neighbor) {
|
if let Some(&neighbor_id) = grid_to_node.get(&neighbor) {
|
||||||
graph
|
graph
|
||||||
@@ -169,148 +140,11 @@ impl Map {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if house_door.iter().filter(|x| x.is_some()).count() != 2 {
|
// Build house structure
|
||||||
panic!("House door must have exactly 2 positions");
|
Self::build_house(&mut graph, &grid_to_node, &house_door);
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the position of the house entrance node
|
// Build tunnel connections
|
||||||
let (house_entrance_node_id, house_entrance_node_position) = {
|
Self::build_tunnels(&mut graph, &grid_to_node, &tunnel_ends);
|
||||||
// Translate the grid positions to the actual node ids
|
|
||||||
let left_node = grid_to_node
|
|
||||||
.get(&(house_door[0].expect("First house door position not acquired") + Direction::Left.to_ivec2()))
|
|
||||||
.expect("Left house door node not found");
|
|
||||||
let right_node = grid_to_node
|
|
||||||
.get(&(house_door[1].expect("Second house door position not acquired") + Direction::Right.to_ivec2()))
|
|
||||||
.expect("Right house door node not found");
|
|
||||||
|
|
||||||
// Calculate the position of the house node
|
|
||||||
let (node_id, node_position) = {
|
|
||||||
let left_pos = graph.get_node(*left_node).unwrap().position;
|
|
||||||
let right_pos = graph.get_node(*right_node).unwrap().position;
|
|
||||||
let house_node = graph.add_node(Node {
|
|
||||||
position: left_pos.lerp(right_pos, 0.5),
|
|
||||||
});
|
|
||||||
(house_node, left_pos.lerp(right_pos, 0.5))
|
|
||||||
};
|
|
||||||
|
|
||||||
// Connect the house door to the left and right nodes
|
|
||||||
graph
|
|
||||||
.connect(node_id, *left_node, true, None, Direction::Left)
|
|
||||||
.expect("Failed to connect house door to left node");
|
|
||||||
graph
|
|
||||||
.connect(node_id, *right_node, true, None, Direction::Right)
|
|
||||||
.expect("Failed to connect house door to right node");
|
|
||||||
|
|
||||||
(node_id, node_position)
|
|
||||||
};
|
|
||||||
|
|
||||||
// A helper function to help create the various 'lines' of nodes within the house
|
|
||||||
let create_house_line = |graph: &mut Graph, center_pos: Vec2| -> (NodeId, NodeId) {
|
|
||||||
// Place the nodes at, above, and below the center position
|
|
||||||
let center_node_id = graph.add_node(Node { position: center_pos });
|
|
||||||
let top_node_id = graph.add_node(Node {
|
|
||||||
position: center_pos + (Direction::Up.to_ivec2() * (CELL_SIZE as i32 / 2)).as_vec2(),
|
|
||||||
});
|
|
||||||
let bottom_node_id = graph.add_node(Node {
|
|
||||||
position: center_pos + (Direction::Down.to_ivec2() * (CELL_SIZE as i32 / 2)).as_vec2(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Connect the center node to the top and bottom nodes
|
|
||||||
graph
|
|
||||||
.connect(center_node_id, top_node_id, false, None, Direction::Up)
|
|
||||||
.expect("Failed to connect house line to left node");
|
|
||||||
graph
|
|
||||||
.connect(center_node_id, bottom_node_id, false, None, Direction::Down)
|
|
||||||
.expect("Failed to connect house line to right node");
|
|
||||||
|
|
||||||
(center_node_id, top_node_id)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate the position of the center line's center node
|
|
||||||
let center_line_center_position =
|
|
||||||
house_entrance_node_position + (Direction::Down.to_ivec2() * (3 * CELL_SIZE as i32)).as_vec2();
|
|
||||||
|
|
||||||
// Create the center line
|
|
||||||
let (center_center_node_id, center_top_node_id) = create_house_line(&mut graph, center_line_center_position);
|
|
||||||
|
|
||||||
// Connect the house entrance to the top line
|
|
||||||
graph
|
|
||||||
.connect(house_entrance_node_id, center_top_node_id, false, None, Direction::Down)
|
|
||||||
.expect("Failed to connect house entrance to top line");
|
|
||||||
|
|
||||||
// Create the left line
|
|
||||||
let (left_center_node_id, _) = create_house_line(
|
|
||||||
&mut graph,
|
|
||||||
center_line_center_position + (Direction::Left.to_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create the right line
|
|
||||||
let (right_center_node_id, _) = create_house_line(
|
|
||||||
&mut graph,
|
|
||||||
center_line_center_position + (Direction::Right.to_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
|
|
||||||
);
|
|
||||||
|
|
||||||
debug!("Left center node id: {left_center_node_id}");
|
|
||||||
|
|
||||||
// Connect the center line to the left and right lines
|
|
||||||
graph
|
|
||||||
.connect(center_center_node_id, left_center_node_id, false, None, Direction::Left)
|
|
||||||
.expect("Failed to connect house entrance to left top line");
|
|
||||||
|
|
||||||
graph
|
|
||||||
.connect(center_center_node_id, right_center_node_id, false, None, Direction::Right)
|
|
||||||
.expect("Failed to connect house entrance to right top line");
|
|
||||||
|
|
||||||
debug!("House entrance node id: {house_entrance_node_id}");
|
|
||||||
|
|
||||||
// Create the hidden tunnel nodes
|
|
||||||
let left_tunnel_hidden_node_id = {
|
|
||||||
let left_tunnel_entrance_node_id = grid_to_node[&tunnel_ends[0].expect("Left tunnel end not found")];
|
|
||||||
let left_tunnel_entrance_node = graph
|
|
||||||
.get_node(left_tunnel_entrance_node_id)
|
|
||||||
.expect("Left tunnel entrance node not found");
|
|
||||||
|
|
||||||
graph
|
|
||||||
.connect_node(
|
|
||||||
left_tunnel_entrance_node_id,
|
|
||||||
Direction::Left,
|
|
||||||
Node {
|
|
||||||
position: left_tunnel_entrance_node.position
|
|
||||||
+ (Direction::Left.to_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("Failed to connect left tunnel entrance to left tunnel hidden node")
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the right tunnel nodes
|
|
||||||
let right_tunnel_hidden_node_id = {
|
|
||||||
let right_tunnel_entrance_node_id = grid_to_node[&tunnel_ends[1].expect("Right tunnel end not found")];
|
|
||||||
let right_tunnel_entrance_node = graph
|
|
||||||
.get_node(right_tunnel_entrance_node_id)
|
|
||||||
.expect("Right tunnel entrance node not found");
|
|
||||||
|
|
||||||
graph
|
|
||||||
.connect_node(
|
|
||||||
right_tunnel_entrance_node_id,
|
|
||||||
Direction::Right,
|
|
||||||
Node {
|
|
||||||
position: right_tunnel_entrance_node.position
|
|
||||||
+ (Direction::Right.to_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("Failed to connect right tunnel entrance to right tunnel hidden node")
|
|
||||||
};
|
|
||||||
|
|
||||||
// Connect the left tunnel hidden node to the right tunnel hidden node
|
|
||||||
graph
|
|
||||||
.connect(
|
|
||||||
left_tunnel_hidden_node_id,
|
|
||||||
right_tunnel_hidden_node_id,
|
|
||||||
false,
|
|
||||||
Some(0.0),
|
|
||||||
Direction::Left,
|
|
||||||
)
|
|
||||||
.expect("Failed to connect left tunnel hidden node to right tunnel hidden node");
|
|
||||||
|
|
||||||
Map {
|
Map {
|
||||||
current: map,
|
current: map,
|
||||||
@@ -346,13 +180,7 @@ impl Map {
|
|||||||
/// This function draws the static map texture to the screen at the correct
|
/// This function draws the static map texture to the screen at the correct
|
||||||
/// position and scale.
|
/// position and scale.
|
||||||
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, map_texture: &mut AtlasTile) {
|
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, map_texture: &mut AtlasTile) {
|
||||||
let dest = Rect::new(
|
MapRenderer::render_map(canvas, atlas, map_texture);
|
||||||
BOARD_PIXEL_OFFSET.x as i32,
|
|
||||||
BOARD_PIXEL_OFFSET.y as i32,
|
|
||||||
BOARD_PIXEL_SIZE.x,
|
|
||||||
BOARD_PIXEL_SIZE.y,
|
|
||||||
);
|
|
||||||
let _ = map_texture.render(canvas, atlas, dest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders a debug visualization of the navigation graph.
|
/// Renders a debug visualization of the navigation graph.
|
||||||
@@ -360,40 +188,152 @@ impl Map {
|
|||||||
/// This function is intended for development and debugging purposes. It draws the
|
/// This function is intended for development and debugging purposes. It draws the
|
||||||
/// nodes and edges of the graph on top of the map, allowing for visual
|
/// nodes and edges of the graph on top of the map, allowing for visual
|
||||||
/// inspection of the navigation paths.
|
/// inspection of the navigation paths.
|
||||||
pub fn debug_render_nodes<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, text: &mut TextTexture) {
|
pub fn debug_render_nodes<T: RenderTarget>(&self, canvas: &mut Canvas<T>) {
|
||||||
for i in 0..self.graph.node_count() {
|
MapRenderer::debug_render_nodes(&self.graph, canvas);
|
||||||
let node = self.graph.get_node(i).unwrap();
|
}
|
||||||
let pos = node.position + BOARD_PIXEL_OFFSET.as_vec2();
|
|
||||||
|
|
||||||
// Draw connections
|
/// Builds the house structure in the graph.
|
||||||
canvas.set_draw_color(Color::BLUE);
|
fn build_house(graph: &mut Graph, grid_to_node: &HashMap<IVec2, NodeId>, house_door: &[Option<IVec2>; 2]) {
|
||||||
|
// Calculate the position of the house entrance node
|
||||||
|
let (house_entrance_node_id, house_entrance_node_position) = {
|
||||||
|
// Translate the grid positions to the actual node ids
|
||||||
|
let left_node = grid_to_node
|
||||||
|
.get(&(house_door[0].expect("First house door position not acquired") + Direction::Left.as_ivec2()))
|
||||||
|
.expect("Left house door node not found");
|
||||||
|
let right_node = grid_to_node
|
||||||
|
.get(&(house_door[1].expect("Second house door position not acquired") + Direction::Right.as_ivec2()))
|
||||||
|
.expect("Right house door node not found");
|
||||||
|
|
||||||
for edge in self.graph.adjacency_list[i].edges() {
|
// Calculate the position of the house node
|
||||||
let end_pos = self.graph.get_node(edge.target).unwrap().position + BOARD_PIXEL_OFFSET.as_vec2();
|
let (node_id, node_position) = {
|
||||||
canvas
|
let left_pos = graph.get_node(*left_node).unwrap().position;
|
||||||
.draw_line((pos.x as i32, pos.y as i32), (end_pos.x as i32, end_pos.y as i32))
|
let right_pos = graph.get_node(*right_node).unwrap().position;
|
||||||
.unwrap();
|
let house_node = graph.add_node(Node {
|
||||||
}
|
position: left_pos.lerp(right_pos, 0.5),
|
||||||
|
});
|
||||||
|
(house_node, left_pos.lerp(right_pos, 0.5))
|
||||||
|
};
|
||||||
|
|
||||||
// Draw node
|
// Connect the house door to the left and right nodes
|
||||||
// let color = if pacman.position.from_node_idx() == i.into() {
|
graph
|
||||||
// Color::GREEN
|
.connect(node_id, *left_node, true, None, Direction::Left)
|
||||||
// } else if let Some(to_idx) = pacman.position.to_node_idx() {
|
.expect("Failed to connect house door to left node");
|
||||||
// if to_idx == i.into() {
|
graph
|
||||||
// Color::CYAN
|
.connect(node_id, *right_node, true, None, Direction::Right)
|
||||||
// } else {
|
.expect("Failed to connect house door to right node");
|
||||||
// Color::RED
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// Color::RED
|
|
||||||
// };
|
|
||||||
canvas.set_draw_color(Color::GREEN);
|
|
||||||
canvas
|
|
||||||
.fill_rect(Rect::new(0, 0, 3, 3).centered_on(Point::new(pos.x as i32, pos.y as i32)))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Draw node index
|
(node_id, node_position)
|
||||||
// text.render(canvas, atlas, &i.to_string(), pos.as_uvec2()).unwrap();
|
};
|
||||||
}
|
|
||||||
|
// A helper function to help create the various 'lines' of nodes within the house
|
||||||
|
let create_house_line = |graph: &mut Graph, center_pos: Vec2| -> (NodeId, NodeId) {
|
||||||
|
// Place the nodes at, above, and below the center position
|
||||||
|
let center_node_id = graph.add_node(Node { position: center_pos });
|
||||||
|
let top_node_id = graph.add_node(Node {
|
||||||
|
position: center_pos + (Direction::Up.as_ivec2() * (CELL_SIZE as i32 / 2)).as_vec2(),
|
||||||
|
});
|
||||||
|
let bottom_node_id = graph.add_node(Node {
|
||||||
|
position: center_pos + (Direction::Down.as_ivec2() * (CELL_SIZE as i32 / 2)).as_vec2(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect the center node to the top and bottom nodes
|
||||||
|
graph
|
||||||
|
.connect(center_node_id, top_node_id, false, None, Direction::Up)
|
||||||
|
.expect("Failed to connect house line to left node");
|
||||||
|
graph
|
||||||
|
.connect(center_node_id, bottom_node_id, false, None, Direction::Down)
|
||||||
|
.expect("Failed to connect house line to right node");
|
||||||
|
|
||||||
|
(center_node_id, top_node_id)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate the position of the center line's center node
|
||||||
|
let center_line_center_position =
|
||||||
|
house_entrance_node_position + (Direction::Down.as_ivec2() * (3 * CELL_SIZE as i32)).as_vec2();
|
||||||
|
|
||||||
|
// Create the center line
|
||||||
|
let (center_center_node_id, center_top_node_id) = create_house_line(graph, center_line_center_position);
|
||||||
|
|
||||||
|
// Connect the house entrance to the top line
|
||||||
|
graph
|
||||||
|
.connect(house_entrance_node_id, center_top_node_id, false, None, Direction::Down)
|
||||||
|
.expect("Failed to connect house entrance to top line");
|
||||||
|
|
||||||
|
// Create the left line
|
||||||
|
let (left_center_node_id, _) = create_house_line(
|
||||||
|
graph,
|
||||||
|
center_line_center_position + (Direction::Left.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the right line
|
||||||
|
let (right_center_node_id, _) = create_house_line(
|
||||||
|
graph,
|
||||||
|
center_line_center_position + (Direction::Right.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
|
||||||
|
);
|
||||||
|
|
||||||
|
debug!("Left center node id: {left_center_node_id}");
|
||||||
|
|
||||||
|
// Connect the center line to the left and right lines
|
||||||
|
graph
|
||||||
|
.connect(center_center_node_id, left_center_node_id, false, None, Direction::Left)
|
||||||
|
.expect("Failed to connect house entrance to left top line");
|
||||||
|
|
||||||
|
graph
|
||||||
|
.connect(center_center_node_id, right_center_node_id, false, None, Direction::Right)
|
||||||
|
.expect("Failed to connect house entrance to right top line");
|
||||||
|
|
||||||
|
debug!("House entrance node id: {house_entrance_node_id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the tunnel connections in the graph.
|
||||||
|
fn build_tunnels(graph: &mut Graph, grid_to_node: &HashMap<IVec2, NodeId>, tunnel_ends: &[Option<IVec2>; 2]) {
|
||||||
|
// Create the hidden tunnel nodes
|
||||||
|
let left_tunnel_hidden_node_id = {
|
||||||
|
let left_tunnel_entrance_node_id = grid_to_node[&tunnel_ends[0].expect("Left tunnel end not found")];
|
||||||
|
let left_tunnel_entrance_node = graph
|
||||||
|
.get_node(left_tunnel_entrance_node_id)
|
||||||
|
.expect("Left tunnel entrance node not found");
|
||||||
|
|
||||||
|
graph
|
||||||
|
.connect_node(
|
||||||
|
left_tunnel_entrance_node_id,
|
||||||
|
Direction::Left,
|
||||||
|
Node {
|
||||||
|
position: left_tunnel_entrance_node.position
|
||||||
|
+ (Direction::Left.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("Failed to connect left tunnel entrance to left tunnel hidden node")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the right tunnel nodes
|
||||||
|
let right_tunnel_hidden_node_id = {
|
||||||
|
let right_tunnel_entrance_node_id = grid_to_node[&tunnel_ends[1].expect("Right tunnel end not found")];
|
||||||
|
let right_tunnel_entrance_node = graph
|
||||||
|
.get_node(right_tunnel_entrance_node_id)
|
||||||
|
.expect("Right tunnel entrance node not found");
|
||||||
|
|
||||||
|
graph
|
||||||
|
.connect_node(
|
||||||
|
right_tunnel_entrance_node_id,
|
||||||
|
Direction::Right,
|
||||||
|
Node {
|
||||||
|
position: right_tunnel_entrance_node.position
|
||||||
|
+ (Direction::Right.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("Failed to connect right tunnel entrance to right tunnel hidden node")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Connect the left tunnel hidden node to the right tunnel hidden node
|
||||||
|
graph
|
||||||
|
.connect(
|
||||||
|
left_tunnel_hidden_node_id,
|
||||||
|
right_tunnel_hidden_node_id,
|
||||||
|
false,
|
||||||
|
Some(0.0),
|
||||||
|
Direction::Left,
|
||||||
|
)
|
||||||
|
.expect("Failed to connect left tunnel hidden node to right tunnel hidden node");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
8
src/map/mod.rs
Normal file
8
src/map/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//! This module defines the game map and provides functions for interacting with it.
|
||||||
|
|
||||||
|
pub mod builder;
|
||||||
|
pub mod parser;
|
||||||
|
pub mod render;
|
||||||
|
|
||||||
|
// Re-export main types for convenience
|
||||||
|
pub use builder::Map;
|
||||||
168
src/map/parser.rs
Normal file
168
src/map/parser.rs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
//! Map parsing functionality for converting raw board layouts into structured data.
|
||||||
|
|
||||||
|
use crate::constants::{MapTile, BOARD_CELL_SIZE};
|
||||||
|
use glam::IVec2;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Error type for map parsing operations.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ParseError {
|
||||||
|
#[error("Unknown character in board: {0}")]
|
||||||
|
UnknownCharacter(char),
|
||||||
|
#[error("House door must have exactly 2 positions, found {0}")]
|
||||||
|
InvalidHouseDoorCount(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the parsed data from a raw board layout.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ParsedMap {
|
||||||
|
/// The parsed tile layout.
|
||||||
|
pub tiles: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize],
|
||||||
|
/// The positions of the house door tiles.
|
||||||
|
pub house_door: [Option<IVec2>; 2],
|
||||||
|
/// The positions of the tunnel end tiles.
|
||||||
|
pub tunnel_ends: [Option<IVec2>; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parser for converting raw board layouts into structured map data.
|
||||||
|
pub struct MapTileParser;
|
||||||
|
|
||||||
|
impl MapTileParser {
|
||||||
|
/// Parses a single character into a map tile.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `c` - The character to parse
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The parsed map tile, or an error if the character is unknown.
|
||||||
|
pub fn parse_character(c: char) -> Result<MapTile, ParseError> {
|
||||||
|
match c {
|
||||||
|
'#' => Ok(MapTile::Wall),
|
||||||
|
'.' => Ok(MapTile::Pellet),
|
||||||
|
'o' => Ok(MapTile::PowerPellet),
|
||||||
|
' ' => Ok(MapTile::Empty),
|
||||||
|
'T' => Ok(MapTile::Tunnel),
|
||||||
|
c @ '0'..='4' => Ok(MapTile::StartingPosition(c.to_digit(10).unwrap() as u8)),
|
||||||
|
'=' => Ok(MapTile::Wall), // House door is represented as a wall tile
|
||||||
|
_ => Err(ParseError::UnknownCharacter(c)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a raw board layout into structured map data.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `raw_board` - The raw board layout as an array of strings
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The parsed map data, or an error if parsing fails.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the board contains unknown characters or if the house door
|
||||||
|
/// is not properly defined by exactly two '=' characters.
|
||||||
|
pub fn parse_board(raw_board: [&str; BOARD_CELL_SIZE.y as usize]) -> Result<ParsedMap, ParseError> {
|
||||||
|
let mut tiles = [[MapTile::Empty; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize];
|
||||||
|
let mut house_door = [None; 2];
|
||||||
|
let mut tunnel_ends = [None; 2];
|
||||||
|
|
||||||
|
for (y, line) in raw_board.iter().enumerate().take(BOARD_CELL_SIZE.y as usize) {
|
||||||
|
for (x, character) in line.chars().enumerate().take(BOARD_CELL_SIZE.x as usize) {
|
||||||
|
let tile = Self::parse_character(character)?;
|
||||||
|
|
||||||
|
// Track special positions
|
||||||
|
match tile {
|
||||||
|
MapTile::Tunnel => {
|
||||||
|
if tunnel_ends[0].is_none() {
|
||||||
|
tunnel_ends[0] = Some(IVec2::new(x as i32, y as i32));
|
||||||
|
} else {
|
||||||
|
tunnel_ends[1] = Some(IVec2::new(x as i32, y as i32));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MapTile::Wall if character == '=' => {
|
||||||
|
if house_door[0].is_none() {
|
||||||
|
house_door[0] = Some(IVec2::new(x as i32, y as i32));
|
||||||
|
} else {
|
||||||
|
house_door[1] = Some(IVec2::new(x as i32, y as i32));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles[x][y] = tile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate house door configuration
|
||||||
|
let house_door_count = house_door.iter().filter(|x| x.is_some()).count();
|
||||||
|
if house_door_count != 2 {
|
||||||
|
return Err(ParseError::InvalidHouseDoorCount(house_door_count));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ParsedMap {
|
||||||
|
tiles,
|
||||||
|
house_door,
|
||||||
|
tunnel_ends,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::constants::RAW_BOARD;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_character() {
|
||||||
|
assert!(matches!(MapTileParser::parse_character('#').unwrap(), MapTile::Wall));
|
||||||
|
assert!(matches!(MapTileParser::parse_character('.').unwrap(), MapTile::Pellet));
|
||||||
|
assert!(matches!(MapTileParser::parse_character('o').unwrap(), MapTile::PowerPellet));
|
||||||
|
assert!(matches!(MapTileParser::parse_character(' ').unwrap(), MapTile::Empty));
|
||||||
|
assert!(matches!(MapTileParser::parse_character('T').unwrap(), MapTile::Tunnel));
|
||||||
|
assert!(matches!(
|
||||||
|
MapTileParser::parse_character('0').unwrap(),
|
||||||
|
MapTile::StartingPosition(0)
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
MapTileParser::parse_character('4').unwrap(),
|
||||||
|
MapTile::StartingPosition(4)
|
||||||
|
));
|
||||||
|
assert!(matches!(MapTileParser::parse_character('=').unwrap(), MapTile::Wall));
|
||||||
|
|
||||||
|
// Test invalid character
|
||||||
|
assert!(MapTileParser::parse_character('X').is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_board() {
|
||||||
|
let result = MapTileParser::parse_board(RAW_BOARD);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let parsed = result.unwrap();
|
||||||
|
|
||||||
|
// Verify we have tiles
|
||||||
|
assert_eq!(parsed.tiles.len(), BOARD_CELL_SIZE.x as usize);
|
||||||
|
assert_eq!(parsed.tiles[0].len(), BOARD_CELL_SIZE.y as usize);
|
||||||
|
|
||||||
|
// Verify we found house door positions
|
||||||
|
assert!(parsed.house_door[0].is_some());
|
||||||
|
assert!(parsed.house_door[1].is_some());
|
||||||
|
|
||||||
|
// Verify we found tunnel ends
|
||||||
|
assert!(parsed.tunnel_ends[0].is_some());
|
||||||
|
assert!(parsed.tunnel_ends[1].is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_board_invalid_character() {
|
||||||
|
let mut invalid_board = RAW_BOARD.clone();
|
||||||
|
invalid_board[0] = "###########################X";
|
||||||
|
|
||||||
|
let result = MapTileParser::parse_board(invalid_board);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.unwrap_err(), ParseError::UnknownCharacter('X')));
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/map/render.rs
Normal file
68
src/map/render.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
//! Map rendering functionality.
|
||||||
|
|
||||||
|
use crate::constants::{BOARD_PIXEL_OFFSET, BOARD_PIXEL_SIZE};
|
||||||
|
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
|
||||||
|
use sdl2::pixels::Color;
|
||||||
|
use sdl2::rect::{Point, Rect};
|
||||||
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
|
|
||||||
|
/// Handles rendering operations for the map.
|
||||||
|
pub struct MapRenderer;
|
||||||
|
|
||||||
|
impl MapRenderer {
|
||||||
|
/// Renders the map to the given canvas.
|
||||||
|
///
|
||||||
|
/// This function draws the static map texture to the screen at the correct
|
||||||
|
/// position and scale.
|
||||||
|
pub fn render_map<T: RenderTarget>(canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, map_texture: &mut AtlasTile) {
|
||||||
|
let dest = Rect::new(
|
||||||
|
BOARD_PIXEL_OFFSET.x as i32,
|
||||||
|
BOARD_PIXEL_OFFSET.y as i32,
|
||||||
|
BOARD_PIXEL_SIZE.x,
|
||||||
|
BOARD_PIXEL_SIZE.y,
|
||||||
|
);
|
||||||
|
let _ = map_texture.render(canvas, atlas, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a debug visualization of the navigation graph.
|
||||||
|
///
|
||||||
|
/// This function is intended for development and debugging purposes. It draws the
|
||||||
|
/// nodes and edges of the graph on top of the map, allowing for visual
|
||||||
|
/// inspection of the navigation paths.
|
||||||
|
pub fn debug_render_nodes<T: RenderTarget>(graph: &crate::entity::graph::Graph, canvas: &mut Canvas<T>) {
|
||||||
|
for i in 0..graph.node_count() {
|
||||||
|
let node = graph.get_node(i).unwrap();
|
||||||
|
let pos = node.position + BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
|
|
||||||
|
// Draw connections
|
||||||
|
canvas.set_draw_color(Color::BLUE);
|
||||||
|
|
||||||
|
for edge in graph.adjacency_list[i].edges() {
|
||||||
|
let end_pos = graph.get_node(edge.target).unwrap().position + BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
|
canvas
|
||||||
|
.draw_line((pos.x as i32, pos.y as i32), (end_pos.x as i32, end_pos.y as i32))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw node
|
||||||
|
// let color = if pacman.position.from_node_idx() == i.into() {
|
||||||
|
// Color::GREEN
|
||||||
|
// } else if let Some(to_idx) = pacman.position.to_node_idx() {
|
||||||
|
// if to_idx == i.into() {
|
||||||
|
// Color::CYAN
|
||||||
|
// } else {
|
||||||
|
// Color::RED
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// Color::RED
|
||||||
|
// };
|
||||||
|
canvas.set_draw_color(Color::GREEN);
|
||||||
|
canvas
|
||||||
|
.fill_rect(Rect::new(0, 0, 3, 3).centered_on(Point::new(pos.x as i32, pos.y as i32)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Draw node index
|
||||||
|
// text.render(canvas, atlas, &i.to_string(), pos.as_uvec2()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ impl AnimatedTexture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, dest: Rect) -> Result<()> {
|
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, dest: Rect) -> Result<()> {
|
||||||
let mut tile = self.current_tile().clone();
|
let mut tile = *self.current_tile();
|
||||||
tile.render(canvas, atlas, dest)
|
tile.render(canvas, atlas, dest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
use crate::texture::sprite::AtlasTile;
|
use crate::texture::sprite::AtlasTile;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|||||||
@@ -76,15 +76,30 @@ impl SpriteAtlas {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn set_color(&mut self, color: Color) {
|
pub fn set_color(&mut self, color: Color) {
|
||||||
self.default_color = Some(color);
|
self.default_color = Some(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn texture(&self) -> &Texture<'static> {
|
pub fn texture(&self) -> &Texture<'static> {
|
||||||
&self.texture
|
&self.texture
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts a `Texture` to a `Texture<'static>` using transmute.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// This function is unsafe because it uses `std::mem::transmute` to change the lifetime
|
||||||
|
/// of the texture from the original lifetime to `'static`. The caller must ensure that:
|
||||||
|
///
|
||||||
|
/// - The original `Texture` will live for the entire duration of the program
|
||||||
|
/// - No references to the original texture exist that could become invalid
|
||||||
|
/// - The texture is not dropped while still being used as a `'static` reference
|
||||||
|
///
|
||||||
|
/// This is typically used when you have a texture that you know will live for the entire
|
||||||
|
/// program duration and need to store it in a structure that requires a `'static` lifetime.
|
||||||
pub unsafe fn texture_to_static(texture: Texture) -> Texture<'static> {
|
pub unsafe fn texture_to_static(texture: Texture) -> Texture<'static> {
|
||||||
std::mem::transmute(texture)
|
std::mem::transmute(texture)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
//! This module provides text rendering using the texture atlas.
|
//! This module provides text rendering using the texture atlas.
|
||||||
//!
|
//!
|
||||||
//! The TextTexture system renders text from the atlas using character mapping.
|
//! The TextTexture system renders text from the atlas using character mapping.
|
||||||
@@ -7,18 +9,13 @@
|
|||||||
//! # Example Usage
|
//! # Example Usage
|
||||||
//!
|
//!
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! use crate::texture::text::TextTexture;
|
//! use pacman::texture::text::TextTexture;
|
||||||
//! use std::rc::Rc;
|
|
||||||
//!
|
//!
|
||||||
//! // Create a text texture with 1.0 scale (8x8 pixels per character)
|
//! // Create a text texture with 1.0 scale (8x8 pixels per character)
|
||||||
//! let mut text_renderer = TextTexture::new(atlas.clone(), 1.0);
|
//! let mut text_renderer = TextTexture::new(1.0);
|
||||||
//!
|
//!
|
||||||
//! // Render text at position (100, 50)
|
//! // Set scale for larger text
|
||||||
//! text_renderer.render(canvas, "PAC-MAN", glam::UVec2::new(100, 50))?;
|
|
||||||
//!
|
|
||||||
//! // Change scale for larger text
|
|
||||||
//! text_renderer.set_scale(2.0);
|
//! text_renderer.set_scale(2.0);
|
||||||
//! text_renderer.render(canvas, "SCORE: 1000", glam::UVec2::new(50, 100))?;
|
|
||||||
//!
|
//!
|
||||||
//! // Calculate text width for positioning
|
//! // Calculate text width for positioning
|
||||||
//! let width = text_renderer.text_width("GAME OVER");
|
//! let width = text_renderer.text_width("GAME OVER");
|
||||||
|
|||||||
Reference in New Issue
Block a user