Compare commits

..

64 Commits

Author SHA1 Message Date
f41c550bb8 feat: enable basic binary size reduction options 2025-07-24 15:39:16 -05:00
829462d3b6 refactor: move direction & edible into entity submodule 2025-07-24 12:48:39 -05:00
002da46045 refactor: split up and move texture-related code into src/texture submodule 2025-07-24 12:48:39 -05:00
cfa73c58a8 refactor: move entity-related code into src/entity submodule 2025-07-24 12:36:48 -05:00
5728effcc6 chore: bump to v0.2.0 2025-07-24 12:14:26 -05:00
fa1a0175b0 ci: remove save-always, remove old vcpkg cache key, flush vcpkg caches 2025-07-24 03:29:22 -05:00
85edb18380 ci: drop linux dependencies: build-essential gettext zlib1g-dev 2025-07-24 03:26:05 -05:00
3a535ee04f fix: linux build linking arg, working build 2025-07-24 02:59:58 -05:00
9b31b392d2 fix(wasm): increase asyncify stack size, working wasm build 2025-07-24 02:39:57 -05:00
999fa14059 chore: remove unused config.toml comments 2025-07-24 02:37:49 -05:00
e925376b7a feat: setup emscripten module for api layer 2025-07-24 02:37:41 -05:00
2596034365 feat: use smallrng for emscripten compat 2025-07-24 02:37:27 -05:00
163855b6e7 fix(wasm): increase audio chunksize to 256 minimum for emscripten audio ctx 2025-07-24 01:14:03 -05:00
645d48aeae ci: use updated setup-emsdk from 'pyodide' v15, fixes emsdk caching, always save vcpkg cache 2025-07-24 01:12:45 -05:00
ec800a88fc fix: use sdl2 internal methods for loading resources for emscripten asset handling 2025-07-24 01:07:22 -05:00
abc37dee4e ci: fix vcpkg cache keys to use target for platforms with multiple targets, allow restore oldkey 2025-07-24 01:04:21 -05:00
1ae7839275 ci: install zlib for linux builds, correct deps/.data filepath 2025-07-24 01:04:21 -05:00
d976d1bc59 ci: specify vcpkg triplets for macos targets 2025-07-24 00:49:22 -05:00
531a5b5d05 ci: inline build script contents, shorten assembly process, separate build/assemble steps 2025-07-24 00:48:49 -05:00
67713fab06 ci: add aarch64-apple-drawin target 2025-07-24 00:46:06 -05:00
b572729e9d feat: reorganize assets/ folder into web/ and game/ 2025-07-24 00:46:06 -05:00
cdc6979458 ci: add cache vcpkg step, remove ineffective apt pkgs 2025-07-24 00:09:43 -05:00
564f88fee5 ci: use proper vcpkg triplet for linux, macos 2025-07-24 00:09:43 -05:00
00c99dc05f ci: remove 'stable' from vcpkg target triplet key 2025-07-23 23:55:15 -05:00
1e12940445 docs: remove unused markdown files in root 2025-07-23 23:24:30 -05:00
dc3c4a7580 ci: VCPKG_SYSTEM_LIBRARIES off, cache emsdk, raw jq output 2025-07-23 23:23:41 -05:00
434b62b036 ci: use cargo metadata with jq for acquiring workspace package version, drop binstall 2025-07-23 23:12:58 -05:00
2bd523e58a ci: add build-essential dependency, apt-get update before 2025-07-23 23:05:06 -05:00
7cd6e8005e ci: add names to workflow jobs, configure binstall to have lower timeout 2025-07-23 22:58:09 -05:00
a8a3745ca1 ci: disable fail-fast, ensure linux vcpkg dependencies are installed 2025-07-23 22:46:57 -05:00
cfa26bf146 ci: use build matrix for desktop builds 2025-07-23 22:45:56 -05:00
bfbbb71752 ci: verbose vcpkg builds on linux 2025-07-23 22:30:11 -05:00
979f736f54 ci: drop rust toolchain to 1.86.0 2025-07-23 22:26:08 -05:00
5a7f6a4c10 ci: remove archive assembly steps for statically linked builds 2025-07-23 22:22:55 -05:00
b66c9ce135 fix: emscripten assets 2025-07-23 22:18:54 -05:00
f5363516c3 chore: remove unused sdl2-image vcpkg features 2025-07-23 21:53:55 -05:00
320da36b83 feat: use cargo-vcpkg in build workflow 2025-07-23 21:53:03 -05:00
b68813cf5b ci: explicit toolchain version, fix build script path for wasm 2025-07-23 21:53:03 -05:00
0806fc744c chore: remove unused animation pausing methods 2025-07-23 21:34:27 -05:00
eead31d7fc refactor: add 'glam' for better positioning types, drop position types 2025-07-23 21:24:47 -05:00
eaa4ab37f9 feat: add audio muting button, mute by default in debug builds 2025-07-23 21:00:52 -05:00
076275158e chore: lower audio tracing to trace level 2025-07-23 20:36:56 -05:00
9f9ace0b16 chore: configure rustfmt, switch to LF line endings 2025-07-23 20:15:54 -05:00
2cc47d5904 feat: add pre-comit configuration 2025-07-23 20:12:11 -05:00
11e89a63d0 refactor: add thiserror/anyhow for asset error handling 2025-07-23 19:47:44 -05:00
50afd8c09f feat: improved emscripten-compatible asset loading api 2025-07-23 18:02:19 -05:00
06841fd0d7 refactor: resolve clippy warnings, resolve shared reference with once_cell 2025-07-23 17:38:27 -05:00
4365639a1d chore: lifetimes 2025-07-23 17:31:16 -05:00
7744c06046 chore: cargo fix 2025-07-23 17:27:22 -05:00
978752f0f3 chore: add index for FruitType sprite 2025-07-23 17:25:39 -05:00
f024ce7a54 refactor: fix unnecessary qualified imports 2025-07-23 17:25:28 -05:00
0196282a78 fix: reset code borrows 2025-07-23 17:20:12 -05:00
785a760343 feat: new edible type for pellet/powerpellet, fruits, separate static/moving entities 2025-07-23 17:16:15 -05:00
de1a89b9b0 refactor: move debug related code into debug.rs 2025-07-23 16:31:09 -05:00
66b6cdf01b feat: split animated texture away from atlas texture details 2025-07-23 16:25:40 -05:00
5a48e83b1a feat: flood-filled based playable position with cache, debug mode 2025-07-23 15:06:38 -05:00
df8f858651 refactor: continue improving MovableEntity shared implementation 2025-07-23 15:06:09 -05:00
1fa7a0807f refactor: abstract entity details into MovableEntity 2025-07-23 14:08:28 -05:00
6d3d3bf49c feat: tunnel implementation, pathfinding debug mode 2025-07-22 14:37:26 -05:00
0a46f64866 feat: pathfinding, ghost and blinky state, update dependencies 2025-07-22 13:18:09 -05:00
fd7eecf53e feat: allow instant direction reversal, improve cell position state tracking 2025-07-22 12:12:41 -05:00
f540dc5373 docs: minor documentation commentsa cross project 2025-07-22 12:12:41 -05:00
f51a3ddeb0 chore: web frontend files 2025-07-22 10:06:30 -05:00
9f7c460369 chore: ignore ephemeral submodules, build.css 2025-07-22 10:06:13 -05:00
60 changed files with 2624 additions and 1068 deletions

View File

@@ -1,12 +1,16 @@
[target.'cfg(target_os = "emscripten")'] [target.'cfg(target_os = "emscripten")']
# TODO: Document what the fuck this is.
rustflags = [ rustflags = [
# "-O", "-C", "link-args=-O2 --profiling", # Stack size is required for this project, it will crash otherwise.
#"-C", "link-args=-O3 --closure 1", "-C", "link-args=-sASYNCIFY=1 -sASYNCIFY_STACK_SIZE=8192 -sALLOW_MEMORY_GROWTH=1",
# "-C", "link-args=-g -gsource-map",
"-C", "link-args=-sASYNCIFY -sALLOW_MEMORY_GROWTH=1",
# "-C", "link-args=-sALLOW_MEMORY_GROWTH=1",
"-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']",
# USE_OGG, USE_VORBIS for OGG/VORBIS usage "-C", "link-args=--preload-file assets/game/",
"-C", "link-args=--preload-file assets/", ]
[target.'cfg(target_os = "linux")']
rustflags = [
# Manually link zlib.
# The `sdl2` crate's build script uses `libpng`, which requires `zlib`.
# By adding `-lz` here, we ensure it's passed to the linker after `libpng`,
# which is required for the linker to correctly resolve symbols.
"-C", "link-arg=-lz",
] ]

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

135
.github/workflows/build.yaml vendored Normal file
View File

@@ -0,0 +1,135 @@
name: Build
on: [push]
permissions:
contents: write
env:
RUST_TOOLCHAIN: 1.86.0
jobs:
build:
name: Build (${{ matrix.target }})
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact_name: pacman
- os: macos-13
target: x86_64-apple-darwin
artifact_name: pacman
- os: macos-latest
target: aarch64-apple-darwin
artifact_name: pacman
- os: windows-latest
target: x86_64-pc-windows-gnu
artifact_name: pacman.exe
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Rust Toolchain
uses: dtolnay/rust-toolchain@master
with:
target: ${{ matrix.target }}
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 }}-${{ matrix.target }}-${{ hashFiles('Cargo.toml', 'Cargo.lock') }}
restore-keys: |
A-vcpkg-${{ runner.os }}-${{ matrix.target }}-
- name: Vcpkg Linux Dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libltdl-dev
- name: Vcpkg
run: |
cargo install cargo-vcpkg
cargo vcpkg -v build
- name: Build
run: cargo build --release
- name: Acquire Package Version
shell: bash
run: |
PACKAGE_VERSION=$(cargo metadata --format-version 1 --no-deps | jq '.packages[0].version' -r)
echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> $GITHUB_ENV
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: "pacman-${{ env.PACKAGE_VERSION }}-${{ matrix.target }}"
path: ./target/release/${{ matrix.artifact_name }}
retention-days: 7
if-no-files-found: error
wasm:
name: Build (wasm32-unknown-emscripten)
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Emscripten SDK
uses: pyodide/setup-emsdk@v15
with:
version: 3.1.43
actions-cache-folder: "emsdk-cache"
- name: Setup Rust (WASM32 Emscripten)
uses: dtolnay/rust-toolchain@master
with:
target: wasm32-unknown-emscripten
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
run_install: true
- name: Build with Emscripten
run: |
cargo build --target=wasm32-unknown-emscripten --release
- name: Assemble
run: |
echo "Generating CSS"
pnpx postcss-cli ./assets/site/styles.scss -o ./assets/site/build.css
echo "Copying WASM files"
mkdir -p dist
cp assets/site/{build.css,favicon.ico,index.html} dist
output_folder="target/wasm32-unknown-emscripten/release"
cp $output_folder/pacman.{wasm,js} $output_folder/deps/pacman.data dist
- name: Upload Artifact
uses: actions/upload-pages-artifact@v3
with:
path: "./dist/"
retention-days: 7
- name: Deploy
uses: actions/deploy-pages@v4

View File

@@ -1,225 +0,0 @@
name: Build
on: [push]
permissions:
contents: write
jobs:
wasm:
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Emscripten SDK
uses: mymindstorm/setup-emsdk@v14
with:
version: 3.1.43
- name: Setup Rust (WASM32 Emscripten)
uses: dtolnay/rust-toolchain@stable
with:
target: wasm32-unknown-emscripten
- name: Rust Cache
uses: Swatinem/rust-cache@v2
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
run_install: true
- name: Build
run: ./scripts/build.sh -er # release mode, skip emsdk
- name: Upload Artifact
uses: actions/upload-pages-artifact@v3
with:
path: "./dist/"
retention-days: 7
- name: Deploy
uses: actions/deploy-pages@v4
linux:
runs-on: ubuntu-latest
env:
TARGET: x86_64-unknown-linux-gnu
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install SDL2 Packages
run: sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev libsdl2-gfx-dev
- name: Setup Rust Toolchain (Linux)
uses: dtolnay/rust-toolchain@stable
with:
target: ${{ env.TARGET }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --release
- name: Assemble Archive
run: |
mkdir /tmp/example/
cp ./target/release/pacman /tmp/example/
chmod a+x /tmp/example/pacman
mkdir /tmp/example/assets
cp ./assets/TerminalVector.ttf ./assets/fruit.png /tmp/example/assets
- name: Install Cargo Binstall
uses: cargo-bins/cargo-binstall@main
- name: Acquire Package Version
run: |
cargo binstall toml-cli -y
PACKAGE_VERSION=$(toml get ./Cargo.toml package.version --raw)
echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> $GITHUB_ENV
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: "pacman-${{ env.PACKAGE_VERSION }}-${{ env.TARGET }}"
path: /tmp/example/
retention-days: 7
if-no-files-found: error
macos:
runs-on: macos-13
env:
TARGET: x86_64-apple-darwin
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install SDL2 Packages
run: brew install sdl2 sdl2_image sdl2_mixer sdl2_ttf sdl2_gfx
- name: Setup Rust Toolchain (MacOS)
uses: dtolnay/rust-toolchain@stable
with:
target: ${{ env.TARGET }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --release
- name: Assemble Archive
run: |
mkdir /tmp/example/
cp ./target/release/pacman /tmp/example/
mkdir /tmp/example/assets
cp ./assets/TerminalVector.ttf ./assets/fruit.png /tmp/example/assets
- name: Install Cargo Binstall
uses: cargo-bins/cargo-binstall@main
- name: Acquire Package Version
run: |
cargo binstall toml-cli -y
PACKAGE_VERSION=$(toml get ./Cargo.toml package.version --raw)
echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> $GITHUB_ENV
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: "pacman-${{ env.PACKAGE_VERSION }}-${{ env.TARGET }}"
path: /tmp/example/
retention-days: 7
if-no-files-found: error
windows:
env:
TARGET: x86_64-pc-windows-gnu
SDL2: 2.30.2
SDL2_TTF: 2.22.0
SDL2_MIXER: 2.8.0
SDL2_IMAGE: 2.8.2
# SDL2_GFX: 1.0.4
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download SDL2 Libraries
run: |
curl -L "https://github.com/libsdl-org/SDL/releases/download/release-${{ env.SDL2 }}/SDL2-devel-${{ env.SDL2 }}-VC.zip" -o "sdl2_devel.zip"
curl -L "https://github.com/libsdl-org/SDL_mixer/releases/download/release-${{ env.SDL2_MIXER }}/SDL2_mixer-devel-${{ env.SDL2_MIXER }}-VC.zip" -o "sdl2_mixer_devel.zip"
curl -L "https://github.com/libsdl-org/SDL_ttf/releases/download/release-${{ env.SDL2_TTF }}/SDL2_ttf-devel-${{ env.SDL2_TTF }}-VC.zip" -o "sdl2_ttf_devel.zip"
curl -L "https://github.com/libsdl-org/SDL_image/releases/download/release-${{ env.SDL2_IMAGE }}/SDL2_image-devel-${{ env.SDL2_IMAGE }}-VC.zip" -o "sdl2_image_devel.zip"
- name: Extract SDL2 DLLs
run: |
7z x ./sdl2_devel.zip -o"./tmp/"
mv ./tmp/SDL2-${{ env.SDL2 }}/lib/x64/SDL2.dll ./
mv ./tmp/SDL2-${{ env.SDL2 }}/lib/x64/SDL2.lib ./
7z x ./sdl2_mixer_devel.zip -o"./tmp/"
mv ./tmp/SDL2_mixer-${{ env.SDL2_MIXER }}/lib/x64/SDL2_mixer.dll ./
mv ./tmp/SDL2_mixer-${{ env.SDL2_MIXER }}/lib/x64/SDL2_mixer.lib ./
7z x ./sdl2_ttf_devel.zip -o"./tmp/"
mv ./tmp/SDL2_ttf-${{ env.SDL2_TTF }}/lib/x64/SDL2_ttf.dll ./
mv ./tmp/SDL2_ttf-${{ env.SDL2_TTF }}/lib/x64/SDL2_ttf.lib ./
7z x ./sdl2_image_devel.zip -o"./tmp/"
mv ./tmp/SDL2_image-${{ env.SDL2_IMAGE }}/lib/x64/SDL2_image.dll ./
mv ./tmp/SDL2_image-${{ env.SDL2_IMAGE }}/lib/x64/SDL2_image.lib ./
- name: Install SDL2_gfx
run: |
C:\vcpkg\vcpkg.exe install sdl2-gfx:x64-windows-release
cp C:\vcpkg\packages\sdl2-gfx_x64-windows-release\bin\SDL2_gfx.dll ./
cp C:\vcpkg\packages\sdl2-gfx_x64-windows-release\lib\SDL2_gfx.lib ./
- name: Setup Rust (Windows)
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ env.TARGET }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --release
- name: Prepare Archive
run: |
New-Item -Type Directory ./release/
Move-Item -Path ./target/release/pacman.exe -Destination ./release/
Move-Item -Path ./SDL2.dll, ./SDL2_image.dll, ./SDL2_ttf.dll, ./SDL2_mixer.dll, ./SDL2_gfx.dll -Destination ./release/
New-Item -Type Directory ./release/assets/
Move-Item -Path ./assets/TerminalVector.ttf, ./assets/fruit.png -Destination ./release/assets/
- name: Install Cargo Binstall
uses: cargo-bins/cargo-binstall@main
- name: Acquire Package Version
run: |
cargo binstall toml-cli -y
PACKAGE_VERSION=$(toml get ./Cargo.toml package.version --raw)
echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> $env:GITHUB_ENV
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: "pacman-${{ env.PACKAGE_VERSION }}-${{ env.TARGET }}"
path: ./release/
retention-days: 7
if-no-files-found: error

5
.gitignore vendored
View File

@@ -1,4 +1,7 @@
/target /target
/dist /dist
.idea .idea
*.dll *.dll
rust-sdl2-emscripten/
assets/site/build.css
emsdk/

22
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,22 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-added-large-files
- id: check-merge-conflict
- id: check-case-conflict
- id: check-toml
- id: check-yaml
- id: forbid-submodules
- id: mixed-line-ending
- repo: local
hooks:
- id: cargo-fmt
name: cargo fmt
entry: cargo fmt --all --
language: system
types: [rust]
pass_filenames: false

View File

@@ -1,7 +0,0 @@
# Building Pac-Man
## GitHub Actions Workflow
1. Build workflow produces executables & WASM files for all platforms
2. Uploaded as artifacts
3. Deployment workflow downloads artifacts and uploads to GitHub Pages

371
Cargo.lock generated
View File

@@ -1,22 +1,40 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.0.2" version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]] [[package]]
name = "c_vec" name = "c_vec"
version = "2.0.0" version = "2.0.0"
@@ -30,16 +48,77 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "lazy_static" name = "deprecate-until"
version = "1.4.0" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "7a3767f826efbbe5a5ae093920b58b43b01734202be697e1354914e862e8e704"
dependencies = [
"proc-macro2",
"quote",
"semver",
"syn",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi",
]
[[package]]
name = "glam"
version = "0.30.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50a99dbe56b72736564cfa4b85bf9a33079f16ae8b74983ab06af3b1a3696b11"
[[package]]
name = "hashbrown"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
[[package]]
name = "indexmap"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "integer-sqrt"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770"
dependencies = [
"num-traits",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.147" version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]] [[package]]
name = "log" name = "log"
@@ -58,9 +137,9 @@ dependencies = [
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.5.0" version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
@@ -73,10 +152,19 @@ dependencies = [
] ]
[[package]] [[package]]
name = "once_cell" name = "num-traits"
version = "1.18.0" version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]] [[package]]
name = "overload" name = "overload"
@@ -86,18 +174,38 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "pacman" name = "pacman"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"anyhow",
"glam",
"lazy_static", "lazy_static",
"libc", "libc",
"once_cell",
"pathfinding",
"rand",
"sdl2", "sdl2",
"spin_sleep", "spin_sleep",
"thiserror 1.0.69",
"tracing", "tracing",
"tracing-error", "tracing-error",
"tracing-subscriber", "tracing-subscriber",
"winapi", "winapi",
] ]
[[package]]
name = "pathfinding"
version = "4.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ac35caa284c08f3721fb33c2741b5f763decaf42d080c8a6a722154347017e"
dependencies = [
"deprecate-until",
"indexmap",
"integer-sqrt",
"num-traits",
"rustc-hash",
"thiserror 2.0.12",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.13" version = "0.2.13"
@@ -106,32 +214,56 @@ checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.66" version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.33" version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]] [[package]]
name = "regex" name = "r-efi"
version = "1.9.1" version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-automata 0.3.3", "regex-automata 0.4.9",
"regex-syntax 0.7.4", "regex-syntax 0.8.5",
] ]
[[package]] [[package]]
@@ -145,13 +277,13 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.3.3" version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-syntax 0.7.4", "regex-syntax 0.8.5",
] ]
[[package]] [[package]]
@@ -162,9 +294,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.7.4" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "sdl2" name = "sdl2"
@@ -172,7 +310,7 @@ version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef" checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"c_vec", "c_vec",
"lazy_static", "lazy_static",
"libc", "libc",
@@ -191,6 +329,12 @@ dependencies = [
"version-compare", "version-compare",
] ]
[[package]]
name = "semver"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.4" version = "0.1.4"
@@ -208,25 +352,64 @@ checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]] [[package]]
name = "spin_sleep" name = "spin_sleep"
version = "1.1.1" version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cafa7900db085f4354dbc7025e25d7a839a14360ea13b5fc4fd717f2d3b23134" checksum = "14ac0e4b54d028c2000a13895bcd84cd02a1d63c4f78e08e4ec5ec8f53efd4b9"
dependencies = [ dependencies = [
"once_cell", "windows-sys",
"winapi",
] ]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.31" version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl 2.0.12",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.1.7" version = "1.1.7"
@@ -239,11 +422,10 @@ dependencies = [
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.37" version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [ dependencies = [
"cfg-if",
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
"tracing-core", "tracing-core",
@@ -251,9 +433,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.26" version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -262,9 +444,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.31" version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable", "valuable",
@@ -272,9 +454,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-error" name = "tracing-error"
version = "0.2.0" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
dependencies = [ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
@@ -282,20 +464,20 @@ dependencies = [
[[package]] [[package]]
name = "tracing-log" name = "tracing-log"
version = "0.1.3" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [ dependencies = [
"lazy_static",
"log", "log",
"once_cell",
"tracing-core", "tracing-core",
] ]
[[package]] [[package]]
name = "tracing-subscriber" name = "tracing-subscriber"
version = "0.3.17" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [ dependencies = [
"matchers", "matchers",
"nu-ansi-term", "nu-ansi-term",
@@ -333,6 +515,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@@ -354,3 +545,85 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.1",
]

View File

@@ -1,17 +1,33 @@
[package] [package]
name = "pacman" name = "pacman"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
lazy_static = "1.4.0" tracing = { version = "0.1.40", features = ["max_level_debug", "release_max_level_debug"]}
spin_sleep = "1.1.1"
tracing = { version = "0.1.37", features = ["max_level_debug", "release_max_level_debug"]}
tracing-error = "0.2.0" tracing-error = "0.2.0"
tracing-subscriber = {version = "0.3.17", features = ["env-filter"]} tracing-subscriber = {version = "0.3.17", features = ["env-filter"]}
winapi = { version = "0.3", features = ["consoleapi", "fileapi", "handleapi", "processenv", "winbase", "wincon", "winnt", "winuser", "windef", "minwindef"] } lazy_static = "1.5.0"
sdl2 = { version = "0.38.0", features = ["image", "ttf"] }
spin_sleep = "1.3.2"
rand = { version = "0.9.2", default-features = false, features = ["small_rng", "os_rng"] }
pathfinding = "4.14"
once_cell = "1.21.3"
thiserror = "1.0"
anyhow = "1.0"
glam = "0.30.4"
[profile.release]
lto = true
panic = "abort"
panic-strategy = "abort"
opt-level = "z"
[target.'cfg(target_os = "windows")'.dependencies.winapi]
version = "0.3"
features = ["consoleapi", "fileapi", "handleapi", "processenv", "winbase", "wincon", "winnt", "winuser", "windef", "minwindef"]
[target.'cfg(target_os = "emscripten")'.dependencies.sdl2] [target.'cfg(target_os = "emscripten")'.dependencies.sdl2]
@@ -25,13 +41,15 @@ default-features = false
features = ["ttf","image","gfx","mixer","static-link","use-vcpkg"] features = ["ttf","image","gfx","mixer","static-link","use-vcpkg"]
[package.metadata.vcpkg] [package.metadata.vcpkg]
dependencies = ["sdl2", "sdl2-image[libjpeg-turbo,tiff,libwebp]", "sdl2-ttf", "sdl2-gfx", "sdl2-mixer"] dependencies = ["sdl2", "sdl2-image", "sdl2-ttf", "sdl2-gfx", "sdl2-mixer"]
git = "https://github.com/microsoft/vcpkg" git = "https://github.com/microsoft/vcpkg"
rev = "2024.05.24" # release 2024.05.24 # to check for a new one, check https://github.com/microsoft/vcpkg/releases rev = "2024.05.24" # release 2024.05.24 # to check for a new one, check https://github.com/microsoft/vcpkg/releases
[package.metadata.vcpkg.target] [package.metadata.vcpkg.target]
x86_64-pc-windows-msvc = { triplet = "x64-windows-static-md" } x86_64-pc-windows-msvc = { triplet = "x64-windows-static-md" }
stable-x86_64-unknown-linux-gnu = { triplet = "x86_64-unknown-linux-gnu" } x86_64-unknown-linux-gnu = { triplet = "x64-linux" }
x86_64-apple-darwin = { triplet = "x64-osx" }
aarch64-apple-darwin = { triplet = "arm64-osx" }
[target.'cfg(target_os = "emscripten")'.dependencies] [target.'cfg(target_os = "emscripten")'.dependencies]
libc = "0.2.16" libc = "0.2.16"

View File

@@ -1,35 +0,0 @@
# Implementation
A document detailing the implementation the project from rendering, to game logic, to build systems.
## Rendering
1. Map
- May require procedural text generation later on (cacheable?)
2. Pacman
3. Ghosts
- Requires colors
4. Items
5. Interface
- Requires fonts
## Grid System
1. How does the grid system work?
The grid is 28 x 36 (although, the map texture is 28 x 37), and each cell is 24x24 (pixels).
Many of the walls in the map texture only occupy a portion of the cell, so some items are able to render across multiple cells.
24x24 assets include pellets, the energizer, and the map itself ()
2. What constraints must be enforced on Ghosts and PacMan?
3. How do movement transitions work?
All entities store a precise position, and a direction. This position is only used for animation, rendering, and collision purposes. Otherwise, a separate 'cell position' (which is 24 times less precise, owing to the fact that it is based on the entity's position within the grid).
When an entity is transitioning between cells, movement directions are acknowledged, but won't take effect until the next cell has been entered completely.
4. Between transitions, how does collision detection work?
It appears the original implementation used cell-level detection.
I worry this may be prone to division errors. Make sure to use rounding (50% >=).

View File

Before

Width:  |  Height:  |  Size: 174 B

After

Width:  |  Height:  |  Size: 174 B

View File

Before

Width:  |  Height:  |  Size: 158 B

After

Width:  |  Height:  |  Size: 158 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 528 B

After

Width:  |  Height:  |  Size: 528 B

View File

Before

Width:  |  Height:  |  Size: 394 B

After

Width:  |  Height:  |  Size: 394 B

View File

Before

Width:  |  Height:  |  Size: 228 B

After

Width:  |  Height:  |  Size: 228 B

View File

Before

Width:  |  Height:  |  Size: 370 B

After

Width:  |  Height:  |  Size: 370 B

View File

Before

Width:  |  Height:  |  Size: 90 B

After

Width:  |  Height:  |  Size: 90 B

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

View File

View File

View File

View File

@@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<style>
body {
margin: 0;
padding: 0;
background: #000;
}
canvas {
display: block;
margin: 0 auto;
background: #000;
}
</style>
<body>
<canvas id="canvas"></canvas>
<script>
var Module = {
'canvas': document.getElementById('canvas'),
};
</script>
<script src="pacman.js"></script>
</body>
</html>

View File

Binary file not shown.

23
assets/site/build.css Normal file
View File

@@ -0,0 +1,23 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: "Liberation Mono";
src:
url("LiberationMono.woff2") format("woff2"),
url("LiberationMono.woff") format("woff");
font-weight: normal;
font-style: normal;
font-display: swap;
}
canvas {
@apply w-full h-[65vh] min-h-[200px] block mx-auto bg-black;
}
.code {
@apply px-1 rounded font-mono bg-zinc-900 border border-zinc-700 lowercase;
}
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInN0eWxlcy5zY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBIiwiZmlsZSI6ImJ1aWxkLmNzcyIsInNvdXJjZXNDb250ZW50IjpbIkB0YWlsd2luZCBiYXNlO1xuQHRhaWx3aW5kIGNvbXBvbmVudHM7XG5AdGFpbHdpbmQgdXRpbGl0aWVzO1xuXG5AZm9udC1mYWNlIHtcbiAgICBmb250LWZhbWlseTogXCJMaWJlcmF0aW9uIE1vbm9cIjtcbiAgICBzcmM6XG4gICAgICAgIHVybChcIkxpYmVyYXRpb25Nb25vLndvZmYyXCIpIGZvcm1hdChcIndvZmYyXCIpLFxuICAgICAgICB1cmwoXCJMaWJlcmF0aW9uTW9uby53b2ZmXCIpIGZvcm1hdChcIndvZmZcIik7XG4gICAgZm9udC13ZWlnaHQ6IG5vcm1hbDtcbiAgICBmb250LXN0eWxlOiBub3JtYWw7XG4gICAgZm9udC1kaXNwbGF5OiBzd2FwO1xufVxuXG5jYW52YXMge1xuICAgIEBhcHBseSB3LWZ1bGwgaC1bNjV2aF0gbWluLWgtWzIwMHB4XSBibG9jayBteC1hdXRvIGJnLWJsYWNrO1xufVxuXG4uY29kZSB7XG4gICAgQGFwcGx5IHB4LTEgcm91bmRlZCBmb250LW1vbm8gYmctemluYy05MDAgYm9yZGVyIGJvcmRlci16aW5jLTcwMCBsb3dlcmNhc2U7XG59XG4iXX0= */

BIN
assets/site/favicon.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

65
assets/site/index.html Normal file
View File

@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pac-Man Arcade</title>
<link rel="stylesheet" href="build.css" />
<style>
</style>
</head>
<body class="bg-black text-yellow-400 text-center">
<a
href="https://github.com/Xevion/Pac-Man"
class="absolute top-0 right-0"
aria-label="View source on GitHub"
>
<svg
width="80"
height="80"
viewBox="0 0 250 250"
class="fill-yellow-400 text-black"
aria-hidden="true"
>
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
class="octo-arm"
></path>
<path
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
class="octo-body"
></path>
</svg>
</a>
<h1 class="text-4xl mt-10 scaled-text">Pac-Man Arcade</h1>
<p class="text-lg mt-5 scaled-text">
Welcome to the Pac-Man Arcade! Use the controls below to play.
</p>
<canvas
id="canvas"
class="block mx-auto mt-5"
width="800"
height="600"
></canvas>
<div class="mt-10">
<span
class="inline-block mx-2 px-4 py-2 bg-yellow-400 text-black rounded scaled-text"
>&larr; &uarr; &rarr; &darr; Move</span
>
<span
class="inline-block mx-2 px-4 py-2 bg-yellow-400 text-black rounded scaled-text"
>Space Change Sprite</span
>
<span
class="inline-block mx-2 px-4 py-2 bg-yellow-400 text-black rounded scaled-text"
>Shift + &uarr;&darr; Change Volume</span
>
</div>
<script type="text/javascript">
var Module = {
canvas: document.getElementById("canvas"),
};
</script>
<script type="text/javascript" src="pacman.js"></script>
</body>
</html>

21
assets/site/styles.scss Normal file
View File

@@ -0,0 +1,21 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: "Liberation Mono";
src:
url("LiberationMono.woff2") format("woff2"),
url("LiberationMono.woff") format("woff");
font-weight: normal;
font-style: normal;
font-display: swap;
}
canvas {
@apply w-full h-[65vh] min-h-[200px] block mx-auto bg-black;
}
.code {
@apply px-1 rounded font-mono bg-zinc-900 border border-zinc-700 lowercase;
}

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

22
scripts/build.sh → build.sh Normal file → Executable file
View File

@@ -38,7 +38,7 @@ fi
if [ "$skip_emsdk" = 'false' ]; then if [ "$skip_emsdk" = 'false' ]; then
echo "Activating Emscripten" echo "Activating Emscripten"
# SDL2-TTF requires 3.1.43, fails to build on latest # SDL2-TTF requires 3.1.43, fails to build on latest
./../emsdk/emsdk activate 3.1.43 ../emsdk/emsdk activate 3.1.43
source ../emsdk/emsdk_env.sh source ../emsdk/emsdk_env.sh
fi fi
@@ -52,22 +52,20 @@ else
fi fi
echo "Generating CSS" echo "Generating CSS"
pnpx postcss-cli ./assets/styles.scss -o ./assets/build.css pnpx postcss-cli ./assets/site/styles.scss -o ./assets/site/build.css
echo "Copying WASM files" echo "Copying WASM files"
mkdir -p dist mkdir -p dist
output_folder="target/wasm32-unknown-emscripten/$build_type" output_folder="target/wasm32-unknown-emscripten/$build_type"
cp assets/index.html dist
cp assets/*.woff* dist cp assets/site/{build.css,favicon.ico,index.html} dist
cp assets/build.css dist cp $output_folder/pacman.{wasm,js} dist
cp assets/favicon.ico dist if [ -f $output_folder/deps/pacman.data ]; then
cp $output_folder/spiritus.wasm dist cp $output_folder/deps/pacman.data dist
cp $output_folder/spiritus.js dist fi
# only if .data file exists
cp $output_folder/deps/spiritus.data dist if [ -f $output_folder/pacman.wasm.map ]; then
if [ -f $output_folder/spiritus.wasm.map ]; then cp $output_folder/pacman.wasm.map dist
cp $output_folder/spiritus.wasm.map dist
fi fi
if [ "$serve" = 'true' ]; then if [ "$serve" = 'true' ]; then

201
build.ts Normal file
View File

@@ -0,0 +1,201 @@
import { $ } from "bun";
// This is a bun script, run with `bun run build.ts`
import * as path from "path";
import * as fs from "fs/promises";
async function clean() {
console.log("Cleaning...");
await $`cargo clean`;
await $`rm -rf ./dist/`;
console.log("Cleaned...");
}
async function setupEmscripten() {
const emsdkDir = "./emsdk";
const emsdkExists = await fs
.access(emsdkDir)
.then(() => true)
.catch(() => false);
if (!emsdkExists) {
console.log("Cloning Emscripten SDK...");
await $`git clone https://github.com/emscripten-core/emsdk.git`;
} else {
console.log("Emscripten SDK already exists, skipping clone.");
}
const emscriptenToolchainPath = path.join(emsdkDir, "upstream", "emscripten");
const toolchainInstalled = await fs
.access(emscriptenToolchainPath)
.then(() => true)
.catch(() => false);
if (!toolchainInstalled) {
console.log("Installing Emscripten toolchain...");
await $`./emsdk/emsdk install 3.1.43`;
} else {
console.log(
"Emscripten toolchain 3.1.43 already installed, skipping install."
);
}
console.log("Activating Emscripten...");
await $`./emsdk/emsdk activate 3.1.43`;
console.log("Emscripten activated.");
// Set EMSDK environment variable for subsequent commands
process.env.EMSDK = path.resolve(emsdkDir);
const emsdkPython = path.join(path.resolve(emsdkDir), "python");
const emsdkNode = path.join(path.resolve(emsdkDir), "node", "16.20.0_64bit"); // Adjust node version if needed
const emsdkBin = path.join(path.resolve(emsdkDir), "upstream", "emscripten");
process.env.PATH = `${emsdkPython}:${emsdkNode}:${emsdkBin}:${process.env.PATH}`;
}
async function buildWeb(release: boolean) {
console.log("Building WASM with Emscripten...");
const rustcFlags = [
"-C",
"link-arg=--preload-file",
"-C",
"link-arg=assets",
].join(" ");
if (release) {
await $`env RUSTFLAGS=${rustcFlags} cargo build --target=wasm32-unknown-emscripten --release`;
} else {
await $`env RUSTFLAGS=${rustcFlags} cargo build --target=wasm32-unknown-emscripten`;
}
console.log("Generating CSS...");
await $`pnpx postcss-cli ./assets/site/styles.scss -o ./assets/site/build.css`;
console.log("Copying WASM files...");
const buildType = release ? "release" : "debug";
const outputFolder = `target/wasm32-unknown-emscripten/${buildType}`;
await $`mkdir -p dist`;
await $`cp assets/site/index.html dist`;
await $`cp assets/site/*.woff* dist`;
await $`cp assets/site/build.css dist`;
await $`cp assets/site/favicon.ico dist`;
await $`cp ${outputFolder}/pacman.wasm dist`;
await $`cp ${outputFolder}/pacman.js dist`;
// Check if .data file exists before copying
try {
await fs.access(`${outputFolder}/pacman.data`);
await $`cp ${outputFolder}/pacman.data dist`;
} catch (e) {
console.log("No pacman.data file found, skipping copy.");
}
// Check if .map file exists before copying
try {
await fs.access(`${outputFolder}/pacman.wasm.map`);
await $`cp ${outputFolder}/pacman.wasm.map dist`;
} catch (e) {
console.log("No pacman.wasm.map file found, skipping copy.");
}
console.log("WASM files copied.");
}
async function serve() {
console.log("Serving WASM with Emscripten...");
await $`python3 -m http.server -d ./dist/ 8080`;
}
async function main() {
const args = process.argv.slice(2);
let release = false;
let serveFiles = false;
let skipEmscriptenSetup = false;
let cleanProject = false;
let target = "web"; // Default target
for (const arg of args) {
switch (arg) {
case "-r":
release = true;
break;
case "-s":
serveFiles = true;
break;
case "-e":
skipEmscriptenSetup = true;
break;
case "-c":
cleanProject = true;
break;
case "--target=linux":
target = "linux";
break;
case "--target=windows":
target = "windows";
break;
case "--target=web":
target = "web";
break;
case "-h":
case "--help":
console.log(`
Usage: ts-node build.ts [options]
Options:
-r Build in release mode
-s Serve the WASM files once built (for web target)
-e Skip EMSDK setup (GitHub workflow only)
-c Clean the target/dist directory
--target=[web|linux|windows] Specify target platform (default: web)
-h, --help Show this help message
`);
return;
}
}
if (cleanProject) {
await clean();
}
if (!skipEmscriptenSetup && target === "web") {
await setupEmscripten();
}
switch (target) {
case "web":
await buildWeb(release);
if (serveFiles) {
await serve();
}
break;
case "linux":
console.log("Building for Linux...");
if (release) {
await $`cargo build --release`;
} else {
await $`cargo build`;
}
console.log("Linux build complete.");
break;
case "windows":
console.log("Building for Windows...");
if (release) {
await $`cargo build --release --target=x86_64-pc-windows-msvc`; // Assuming MSVC toolchain
} else {
await $`cargo build --target=x86_64-pc-windows-msvc`;
}
console.log("Windows build complete.");
break;
default:
console.error("Invalid target specified.");
process.exit(1);
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

6
rustfmt.toml Normal file
View File

@@ -0,0 +1,6 @@
# Rustfmt configuration
edition = "2021"
max_width = 130
tab_spaces = 4
newline_style = "Unix"
use_small_heuristics = "Default"

View File

@@ -1,132 +0,0 @@
use sdl2::{
rect::Rect,
render::{Canvas, Texture},
video::Window,
};
use crate::direction::Direction;
pub struct AnimatedTexture<'a> {
raw_texture: Texture<'a>,
ticker: u32,
reversed: bool,
offset: (i32, i32),
ticks_per_frame: u32,
frame_count: u32,
frame_width: u32,
frame_height: u32,
}
impl<'a> AnimatedTexture<'a> {
pub fn new(
texture: Texture<'a>,
ticks_per_frame: u32,
frame_count: u32,
frame_width: u32,
frame_height: u32,
offset: Option<(i32, i32)>,
) -> Self {
AnimatedTexture {
raw_texture: texture,
ticker: 0,
reversed: false,
ticks_per_frame,
frame_count,
frame_width,
frame_height,
offset: offset.unwrap_or((0, 0)),
}
}
// Get the current frame number
fn current_frame(&self) -> u32 {
self.ticker / self.ticks_per_frame
}
// Move to the next frame. If we are at the end of the animation, reverse the direction
pub fn tick(&mut self) {
if self.reversed {
self.ticker -= 1;
if self.ticker == 0 {
self.reversed = !self.reversed;
}
} else {
self.ticker += 1;
if self.ticker + 1 == self.ticks_per_frame * self.frame_count {
self.reversed = !self.reversed;
}
}
}
// Calculate the frame rect (portion of the texture to render) for the given frame.
fn get_frame_rect(&self, frame: u32) -> Rect {
if frame >= self.frame_count {
panic!("Frame {} is out of bounds for this texture", frame);
}
Rect::new(
frame as i32 * self.frame_width as i32,
0,
self.frame_width,
self.frame_height,
)
}
pub fn render(
&mut self,
canvas: &mut Canvas<Window>,
position: (i32, i32),
direction: Direction,
) {
self.render_static(canvas, position, direction, Some(self.current_frame()));
self.tick();
}
// Functions like render, but only ticks the animation until the given frame is reached.
pub fn render_until(
&mut self,
canvas: &mut Canvas<Window>,
position: (i32, i32),
direction: Direction,
frame: u32,
) {
// TODO: If the frame we're targeting is in the opposite direction (due to self.reverse), we should pre-emptively reverse.
let current = self.current_frame();
self.render_static(canvas, position, direction, Some(current));
if frame != current {
self.tick();
}
}
// Renders a specific frame of the animation. Defaults to the current frame.
pub fn render_static(
&mut self,
canvas: &mut Canvas<Window>,
position: (i32, i32),
direction: Direction,
frame: Option<u32>,
) {
let frame_rect = self.get_frame_rect(frame.unwrap_or(self.current_frame()));
let position_rect = Rect::new(
position.0 + self.offset.0,
position.1 + self.offset.1,
self.frame_width,
self.frame_height,
);
canvas
.copy_ex(
&self.raw_texture,
Some(frame_rect),
Some(position_rect),
direction.angle(),
None,
false,
false,
)
.expect("Could not render texture on canvas");
}
}

95
src/asset.rs Normal file
View File

@@ -0,0 +1,95 @@
//! Cross-platform asset loading abstraction.
//! On desktop, assets are embedded using include_bytes!; on Emscripten, assets are loaded from the filesystem.
use std::borrow::Cow;
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AssetError {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Asset not found: {0}")]
NotFound(String),
#[error("Invalid asset format: {0}")]
InvalidFormat(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Asset {
Wav1,
Wav2,
Wav3,
Wav4,
Pacman,
Pellet,
Energizer,
Map,
FontKonami,
GhostBody,
GhostEyes,
// Add more as needed
}
impl Asset {
pub fn path(&self) -> &str {
use Asset::*;
match self {
Wav1 => "wav/1.ogg",
Wav2 => "wav/2.ogg",
Wav3 => "wav/3.ogg",
Wav4 => "wav/4.ogg",
Pacman => "32/pacman.png",
Pellet => "24/pellet.png",
Energizer => "24/energizer.png",
Map => "map.png",
FontKonami => "font/konami.ttf",
GhostBody => "32/ghost_body.png",
GhostEyes => "32/ghost_eyes.png",
}
}
}
#[cfg(not(target_os = "emscripten"))]
mod imp {
use super::*;
macro_rules! asset_bytes_enum {
( $asset:expr ) => {
match $asset {
Asset::Wav1 => Cow::Borrowed(include_bytes!("../assets/game/wav/1.ogg")),
Asset::Wav2 => Cow::Borrowed(include_bytes!("../assets/game/wav/2.ogg")),
Asset::Wav3 => Cow::Borrowed(include_bytes!("../assets/game/wav/3.ogg")),
Asset::Wav4 => Cow::Borrowed(include_bytes!("../assets/game/wav/4.ogg")),
Asset::Pacman => Cow::Borrowed(include_bytes!("../assets/game/32/pacman.png")),
Asset::Pellet => Cow::Borrowed(include_bytes!("../assets/game/24/pellet.png")),
Asset::Energizer => Cow::Borrowed(include_bytes!("../assets/game/24/energizer.png")),
Asset::Map => Cow::Borrowed(include_bytes!("../assets/game/map.png")),
Asset::FontKonami => Cow::Borrowed(include_bytes!("../assets/game/font/konami.ttf")),
Asset::GhostBody => Cow::Borrowed(include_bytes!("../assets/game/32/ghost_body.png")),
Asset::GhostEyes => Cow::Borrowed(include_bytes!("../assets/game/32/ghost_eyes.png")),
}
};
}
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
Ok(asset_bytes_enum!(asset))
}
}
#[cfg(target_os = "emscripten")]
mod imp {
use super::*;
use sdl2::rwops::RWops;
use std::io::Read;
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
let path = format!("assets/game/{}", asset.path());
let mut rwops = RWops::from_file(&path, "rb").map_err(|_| AssetError::NotFound(asset.path().to_string()))?;
let len = rwops.len().ok_or_else(|| AssetError::NotFound(asset.path().to_string()))?;
let mut buf = vec![0u8; len];
rwops
.read_exact(&mut buf)
.map_err(|e| AssetError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
Ok(Cow::Owned(buf))
}
}
pub use imp::get_asset_bytes;

View File

@@ -1,48 +1,50 @@
//! This module handles the audio playback for the game.
use crate::asset::{get_asset_bytes, Asset};
use sdl2::{ use sdl2::{
mixer::{self, Chunk, InitFlag, LoaderRWops, DEFAULT_FORMAT}, mixer::{self, Chunk, InitFlag, LoaderRWops, DEFAULT_FORMAT},
rwops::RWops, rwops::RWops,
}; };
// Embed sound files directly into the executable const SOUND_ASSETS: [Asset; 4] = [Asset::Wav1, Asset::Wav2, Asset::Wav3, Asset::Wav4];
const SOUND_1_DATA: &[u8] = include_bytes!("../assets/wav/1.ogg");
const SOUND_2_DATA: &[u8] = include_bytes!("../assets/wav/2.ogg");
const SOUND_3_DATA: &[u8] = include_bytes!("../assets/wav/3.ogg");
const SOUND_4_DATA: &[u8] = include_bytes!("../assets/wav/4.ogg");
const SOUND_DATA: [&[u8]; 4] = [SOUND_1_DATA, SOUND_2_DATA, SOUND_3_DATA, SOUND_4_DATA];
/// The audio system for the game.
///
/// This struct is responsible for initializing the audio device, loading sounds,
/// and playing them.
pub struct Audio { pub struct Audio {
_mixer_context: mixer::Sdl2MixerContext, _mixer_context: mixer::Sdl2MixerContext,
sounds: Vec<Chunk>, sounds: Vec<Chunk>,
next_sound_index: usize, next_sound_index: usize,
muted: bool,
} }
impl Audio { impl Audio {
/// Creates a new `Audio` instance.
pub fn new() -> Self { pub fn new() -> Self {
let frequency = 44100; let frequency = 44100;
let format = DEFAULT_FORMAT; let format = DEFAULT_FORMAT;
let channels = 4; let channels = 4;
let chunk_size = 128; let chunk_size = 256; // 256 is minimum for emscripten
mixer::open_audio(frequency, format, 1, chunk_size).expect("Failed to open audio"); mixer::open_audio(frequency, format, 1, chunk_size).expect("Failed to open audio");
mixer::allocate_channels(channels); mixer::allocate_channels(channels);
// set channel volume // set channel volume
for i in 0..channels { for i in 0..channels {
mixer::Channel(i as i32).set_volume(32); mixer::Channel(i).set_volume(32);
} }
let mixer_context = mixer::init(InitFlag::OGG).expect("Failed to initialize SDL2_mixer"); let mixer_context = mixer::init(InitFlag::OGG).expect("Failed to initialize SDL2_mixer");
let sounds: Vec<Chunk> = SOUND_DATA let sounds: Vec<Chunk> = SOUND_ASSETS
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, data)| { .map(|(i, asset)| {
let rwops = RWops::from_bytes(data) let data = get_asset_bytes(*asset).expect("Failed to load sound asset");
.expect(&format!("Failed to create RWops for sound {}", i + 1)); let rwops = RWops::from_bytes(&data).unwrap_or_else(|_| panic!("Failed to create RWops for sound {}", i + 1));
rwops.load_wav().expect(&format!( rwops
"Failed to load sound {} from embedded data", .load_wav()
i + 1 .unwrap_or_else(|_| panic!("Failed to load sound {} from asset API", i + 1))
))
}) })
.collect(); .collect();
@@ -50,18 +52,16 @@ impl Audio {
_mixer_context: mixer_context, _mixer_context: mixer_context,
sounds, sounds,
next_sound_index: 0, next_sound_index: 0,
muted: false,
} }
} }
/// Plays the "eat" sound effect.
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) {
Ok(channel) => { Ok(channel) => {
tracing::info!( tracing::trace!("Playing sound #{} on channel {:?}", self.next_sound_index + 1, channel);
"Playing sound #{} on channel {:?}",
self.next_sound_index + 1,
channel
);
} }
Err(e) => { Err(e) => {
tracing::warn!("Could not play sound #{}: {}", self.next_sound_index + 1, e); tracing::warn!("Could not play sound #{}: {}", self.next_sound_index + 1, e);
@@ -70,4 +70,18 @@ impl Audio {
} }
self.next_sound_index = (self.next_sound_index + 1) % self.sounds.len(); self.next_sound_index = (self.next_sound_index + 1) % self.sounds.len();
} }
/// Instantly mute or unmute all channels.
pub fn set_mute(&mut self, mute: bool) {
let channels = 4;
let volume = if mute { 0 } else { 32 };
for i in 0..channels {
mixer::Channel(i).set_volume(volume);
}
self.muted = mute;
}
pub fn is_muted(&self) -> bool {
self.muted
}
} }

View File

@@ -1,21 +1,94 @@
//! This module contains all the constants used in the game.
/// The width of the game board, in cells.
pub const BOARD_WIDTH: u32 = 28; pub const BOARD_WIDTH: u32 = 28;
pub const BOARD_HEIGHT: u32 = 31; // Adjusted to fit map texture? /// The height of the game board, in cells.
pub const BOARD_HEIGHT: u32 = 31;
/// The size of each cell, in pixels.
pub const CELL_SIZE: u32 = 24; pub const CELL_SIZE: u32 = 24;
pub const BOARD_OFFSET: (u32, u32) = (0, 3); // Relative cell offset for where map text / grid starts /// The offset of the game board from the top-left corner of the window, in
/// cells.
pub const BOARD_OFFSET: (u32, u32) = (0, 3);
/// The width of the window, in pixels.
pub const WINDOW_WIDTH: u32 = CELL_SIZE * BOARD_WIDTH; pub const WINDOW_WIDTH: u32 = CELL_SIZE * BOARD_WIDTH;
pub const WINDOW_HEIGHT: u32 = CELL_SIZE * (BOARD_HEIGHT + 6); // Map texture is 6 cells taller (3 above, 3 below) than the grid /// The height of the window, in pixels.
///
/// The map texture is 6 cells taller than the grid (3 above, 3 below), so we
/// add 6 to the board height to get the window height.
pub const WINDOW_HEIGHT: u32 = CELL_SIZE * (BOARD_HEIGHT + 6);
#[derive(Debug, Copy, Clone, PartialEq)] /// An enum representing the different types of tiles on the map.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MapTile { pub enum MapTile {
/// An empty tile.
Empty, Empty,
/// A wall tile.
Wall, Wall,
/// A regular pellet.
Pellet, Pellet,
/// A power pellet.
PowerPellet, PowerPellet,
/// A starting position for an entity.
StartingPosition(u8), StartingPosition(u8),
/// A tunnel tile.
Tunnel,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum FruitType {
Cherry,
Strawberry,
Orange,
Apple,
Melon,
Galaxian,
Bell,
Key,
}
impl FruitType {
pub const ALL: [FruitType; 8] = [
FruitType::Cherry,
FruitType::Strawberry,
FruitType::Orange,
FruitType::Apple,
FruitType::Melon,
FruitType::Galaxian,
FruitType::Bell,
FruitType::Key,
];
pub fn score(self) -> u32 {
match self {
FruitType::Cherry => 100,
FruitType::Strawberry => 300,
FruitType::Orange => 500,
FruitType::Apple => 700,
FruitType::Melon => 1000,
FruitType::Galaxian => 2000,
FruitType::Bell => 3000,
FruitType::Key => 5000,
}
}
pub fn index(self) -> usize {
match self {
FruitType::Cherry => 0,
FruitType::Strawberry => 1,
FruitType::Orange => 2,
FruitType::Apple => 3,
FruitType::Melon => 4,
FruitType::Galaxian => 5,
FruitType::Bell => 6,
FruitType::Key => 7,
}
}
}
/// The raw layout of the game board, as a 2D array of characters.
pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [ pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [
"############################", "############################",
"#............##............#", "#............##............#",
@@ -31,7 +104,7 @@ pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [
" #.## 1 ##.# ", " #.## 1 ##.# ",
" #.## ###==### ##.# ", " #.## ###==### ##.# ",
"######.## # # ##.######", "######.## # # ##.######",
" . #2 3 4 # . ", "T . #2 3 4 # . T",
"######.## # # ##.######", "######.## # # ##.######",
" #.## ######## ##.# ", " #.## ######## ##.# ",
" #.## ##.# ", " #.## ##.# ",

73
src/debug.rs Normal file
View File

@@ -0,0 +1,73 @@
//! Debug rendering utilities for Pac-Man.
use crate::{
constants::{MapTile, BOARD_HEIGHT, BOARD_WIDTH},
entity::blinky::Blinky,
map::Map,
};
use glam::{IVec2, UVec2};
use sdl2::{pixels::Color, render::Canvas, video::Window};
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum DebugMode {
None,
Grid,
Pathfinding,
ValidPositions,
}
pub struct DebugRenderer;
impl DebugRenderer {
pub fn draw_cell(canvas: &mut Canvas<Window>, _map: &Map, cell: UVec2, color: Color) {
let position = Map::cell_to_pixel(cell);
canvas.set_draw_color(color);
canvas
.draw_rect(sdl2::rect::Rect::new(position.x, position.y, 24, 24))
.expect("Could not draw rectangle");
}
pub fn draw_debug_grid(canvas: &mut Canvas<Window>, map: &Map, pacman_cell: UVec2) {
for x in 0..BOARD_WIDTH {
for y in 0..BOARD_HEIGHT {
let tile = map.get_tile(IVec2::new(x as i32, y as i32)).unwrap_or(MapTile::Empty);
let cell = UVec2::new(x, y);
let mut color = None;
if cell == pacman_cell {
Self::draw_cell(canvas, map, cell, Color::CYAN);
} else {
color = match tile {
MapTile::Empty => None,
MapTile::Wall => Some(Color::BLUE),
MapTile::Pellet => Some(Color::RED),
MapTile::PowerPellet => Some(Color::MAGENTA),
MapTile::StartingPosition(_) => Some(Color::GREEN),
MapTile::Tunnel => Some(Color::CYAN),
};
}
if let Some(color) = color {
Self::draw_cell(canvas, map, cell, color);
}
}
}
}
pub fn draw_next_cell(canvas: &mut Canvas<Window>, map: &Map, next_cell: UVec2) {
Self::draw_cell(canvas, map, next_cell, Color::YELLOW);
}
pub fn draw_valid_positions(canvas: &mut Canvas<Window>, map: &mut Map) {
let valid_positions_vec = map.get_valid_playable_positions().clone();
for &pos in &valid_positions_vec {
Self::draw_cell(canvas, map, pos, Color::RGB(255, 140, 0));
}
}
pub fn draw_pathfinding(canvas: &mut Canvas<Window>, blinky: &Blinky, map: &Map) {
let target = blinky.get_target_tile();
if let Some((path, _)) = blinky.get_path_to_target(target.as_uvec2()) {
for pos in &path {
Self::draw_cell(canvas, map, *pos, Color::YELLOW);
}
}
}
}

View File

@@ -1,43 +0,0 @@
use sdl2::keyboard::Keycode;
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
impl Direction {
pub fn angle(&self) -> f64 {
match self {
Direction::Right => 0f64,
Direction::Down => 90f64,
Direction::Left => 180f64,
Direction::Up => 270f64,
}
}
pub fn offset(&self) -> (i32, i32) {
match self {
Direction::Right => (1, 0),
Direction::Down => (0, 1),
Direction::Left => (-1, 0),
Direction::Up => (0, -1),
}
}
pub fn from_keycode(keycode: Keycode) -> Option<Direction> {
match keycode {
Keycode::D => Some(Direction::Right),
Keycode::Right => Some(Direction::Right),
Keycode::A => Some(Direction::Left),
Keycode::Left => Some(Direction::Left),
Keycode::W => Some(Direction::Up),
Keycode::Up => Some(Direction::Up),
Keycode::S => Some(Direction::Down),
Keycode::Down => Some(Direction::Down),
_ => None,
}
}
}

31
src/emscripten.rs Normal file
View File

@@ -0,0 +1,31 @@
#[allow(dead_code)]
#[cfg(target_os = "emscripten")]
pub mod emscripten {
use std::os::raw::c_uint;
extern "C" {
pub fn emscripten_get_now() -> f64;
pub fn emscripten_sleep(ms: c_uint);
pub fn emscripten_get_element_css_size(target: *const u8, width: *mut f64, height: *mut f64) -> i32;
}
// milliseconds since start of program
pub fn now() -> f64 {
unsafe { emscripten_get_now() }
}
pub fn sleep(ms: u32) {
unsafe {
emscripten_sleep(ms);
}
}
pub fn get_canvas_size() -> (u32, u32) {
let mut width = 0.0;
let mut height = 0.0;
unsafe {
emscripten_get_element_css_size("canvas\0".as_ptr(), &mut width, &mut height);
}
(width as u32, height as u32)
}
}

View File

@@ -1,11 +0,0 @@
pub trait Entity {
// Returns true if the entity is colliding with the other entity
fn is_colliding(&self, other: &dyn Entity) -> bool;
// Returns the absolute position of the entity
fn position(&self) -> (i32, i32);
// Returns the cell position of the entity (XY position within the grid)
fn cell_position(&self) -> (u32, u32);
fn internal_position(&self) -> (u32, u32);
// Tick the entity (move it, perform collision checks, etc)
fn tick(&mut self);
}

96
src/entity/blinky.rs Normal file
View File

@@ -0,0 +1,96 @@
use std::cell::RefCell;
use std::rc::Rc;
use sdl2::render::{Canvas, Texture};
use sdl2::video::Window;
use crate::entity::direction::Direction;
use crate::entity::ghost::{Ghost, GhostMode, GhostType};
use crate::entity::pacman::Pacman;
use crate::entity::{Entity, Moving, Renderable, StaticEntity};
use crate::map::Map;
use glam::{IVec2, UVec2};
pub struct Blinky<'a> {
ghost: Ghost<'a>,
}
impl<'a> Blinky<'a> {
pub fn new(
starting_position: UVec2,
body_texture: Texture<'a>,
eyes_texture: Texture<'a>,
map: Rc<RefCell<Map>>,
pacman: Rc<RefCell<Pacman<'a>>>,
) -> Blinky<'a> {
Blinky {
ghost: Ghost::new(GhostType::Blinky, starting_position, body_texture, eyes_texture, map, pacman),
}
}
/// Gets Blinky's chase target - directly targets Pac-Man's current position
pub fn get_chase_target(&self) -> IVec2 {
let pacman = self.ghost.pacman.borrow();
let cell = pacman.base().cell_position;
IVec2::new(cell.x as i32, cell.y as i32)
}
pub fn set_mode(&mut self, mode: GhostMode) {
self.ghost.set_mode(mode);
}
pub fn tick(&mut self) {
self.ghost.tick();
}
}
impl<'a> Entity for Blinky<'a> {
fn base(&self) -> &StaticEntity {
self.ghost.base.base()
}
}
impl<'a> Renderable for Blinky<'a> {
fn render(&self, canvas: &mut Canvas<Window>) {
self.ghost.render(canvas);
}
}
impl<'a> Moving for Blinky<'a> {
fn move_forward(&mut self) {
self.ghost.move_forward();
}
fn update_cell_position(&mut self) {
self.ghost.update_cell_position();
}
fn next_cell(&self, direction: Option<Direction>) -> IVec2 {
self.ghost.next_cell(direction)
}
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool {
self.ghost.is_wall_ahead(direction)
}
fn handle_tunnel(&mut self) -> bool {
self.ghost.handle_tunnel()
}
fn is_grid_aligned(&self) -> bool {
self.ghost.is_grid_aligned()
}
fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool {
self.ghost.set_direction_if_valid(new_direction)
}
}
// Allow direct access to ghost fields
impl<'a> std::ops::Deref for Blinky<'a> {
type Target = Ghost<'a>;
fn deref(&self) -> &Self::Target {
&self.ghost
}
}
impl<'a> std::ops::DerefMut for Blinky<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.ghost
}
}

59
src/entity/direction.rs Normal file
View File

@@ -0,0 +1,59 @@
//! This module defines the `Direction` enum, which is used to represent the
//! direction of an entity.
use sdl2::keyboard::Keycode;
/// An enum representing the direction of an entity.
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
impl Direction {
/// Returns the angle of the direction in degrees.
pub fn angle(&self) -> f64 {
match self {
Direction::Right => 0f64,
Direction::Down => 90f64,
Direction::Left => 180f64,
Direction::Up => 270f64,
}
}
/// Returns the offset of the direction as a tuple of (x, y).
pub fn offset(&self) -> (i32, i32) {
match self {
Direction::Right => (1, 0),
Direction::Down => (0, 1),
Direction::Left => (-1, 0),
Direction::Up => (0, -1),
}
}
/// Returns the opposite direction.
pub fn opposite(&self) -> Direction {
match self {
Direction::Right => Direction::Left,
Direction::Down => Direction::Up,
Direction::Left => Direction::Right,
Direction::Up => Direction::Down,
}
}
/// Creates a `Direction` from a `Keycode`.
///
/// # Arguments
///
/// * `keycode` - The keycode to convert.
pub fn from_keycode(keycode: Keycode) -> Option<Direction> {
match keycode {
Keycode::D | Keycode::Right => Some(Direction::Right),
Keycode::A | Keycode::Left => Some(Direction::Left),
Keycode::W | Keycode::Up => Some(Direction::Up),
Keycode::S | Keycode::Down => Some(Direction::Down),
_ => None,
}
}
}

83
src/entity/edible.rs Normal file
View File

@@ -0,0 +1,83 @@
//! Edible entity for Pac-Man: pellets, power pellets, and fruits.
use crate::constants::{FruitType, MapTile, BOARD_HEIGHT, BOARD_WIDTH};
use crate::entity::direction::Direction;
use crate::entity::{Entity, Renderable, StaticEntity};
use crate::map::Map;
use crate::texture::atlas::AtlasTexture;
use crate::texture::FrameDrawn;
use glam::{IVec2, UVec2};
use sdl2::{render::Canvas, video::Window};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EdibleKind {
Pellet,
PowerPellet,
Fruit(FruitType),
}
pub struct Edible<'a> {
pub base: StaticEntity,
pub kind: EdibleKind,
pub sprite: Rc<AtlasTexture<'a>>,
}
impl<'a> Edible<'a> {
pub fn new(kind: EdibleKind, cell_position: UVec2, sprite: Rc<AtlasTexture<'a>>) -> Self {
let pixel_position = Map::cell_to_pixel(cell_position);
Edible {
base: StaticEntity::new(pixel_position, cell_position),
kind,
sprite,
}
}
/// Checks collision with Pac-Man (or any entity)
pub fn collide(&self, pacman: &dyn Entity) -> bool {
self.base.is_colliding(pacman)
}
}
impl<'a> Entity for Edible<'a> {
fn base(&self) -> &StaticEntity {
&self.base
}
}
impl<'a> Renderable for Edible<'a> {
fn render(&self, canvas: &mut Canvas<Window>) {
let pos = self.base.pixel_position;
self.sprite.render(canvas, (pos.x, pos.y), Direction::Right, Some(0));
}
}
/// Reconstruct all edibles from the original map layout
pub fn reconstruct_edibles<'a>(
map: Rc<RefCell<Map>>,
pellet_sprite: Rc<AtlasTexture<'a>>,
power_pellet_sprite: Rc<AtlasTexture<'a>>,
_fruit_sprite: Rc<AtlasTexture<'a>>,
) -> Vec<Edible<'a>> {
let mut edibles = Vec::new();
for x in 0..BOARD_WIDTH {
for y in 0..BOARD_HEIGHT {
let tile = map.borrow().get_tile(IVec2::new(x as i32, y as i32));
match tile {
Some(MapTile::Pellet) => {
edibles.push(Edible::new(EdibleKind::Pellet, UVec2::new(x, y), Rc::clone(&pellet_sprite)));
}
Some(MapTile::PowerPellet) => {
edibles.push(Edible::new(
EdibleKind::PowerPellet,
UVec2::new(x, y),
Rc::clone(&power_pellet_sprite),
));
}
// Fruits can be added here if you have fruit positions
_ => {}
}
}
}
edibles
}

312
src/entity/ghost.rs Normal file
View File

@@ -0,0 +1,312 @@
use rand::rngs::SmallRng;
use rand::Rng;
use rand::SeedableRng;
use crate::constants::{MapTile, BOARD_WIDTH};
use crate::entity::direction::Direction;
use crate::entity::pacman::Pacman;
use crate::entity::{Entity, MovableEntity, Moving, Renderable};
use crate::map::Map;
use crate::modulation::{SimpleTickModulator, TickModulator};
use crate::texture::animated::AnimatedAtlasTexture;
use crate::texture::FrameDrawn;
use glam::{IVec2, UVec2};
use sdl2::pixels::Color;
use sdl2::render::Texture;
use std::cell::RefCell;
use std::rc::Rc;
/// The different modes a ghost can be in
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GhostMode {
/// Chase mode - ghost actively pursues Pac-Man using its unique strategy
Chase,
/// Scatter mode - ghost heads to its home corner
Scatter,
/// Frightened mode - ghost moves randomly and can be eaten
Frightened,
/// Eyes mode - ghost returns to the ghost house after being eaten
Eyes,
/// House mode - ghost is in the ghost house, waiting to exit
House,
}
/// The different ghost personalities
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GhostType {
Blinky, // Red - Shadow
Pinky, // Pink - Speedy
Inky, // Cyan - Bashful
Clyde, // Orange - Pokey
}
impl GhostType {
/// Returns the color of the ghost.
pub fn color(&self) -> Color {
match self {
GhostType::Blinky => Color::RGB(255, 0, 0),
GhostType::Pinky => Color::RGB(255, 184, 255),
GhostType::Inky => Color::RGB(0, 255, 255),
GhostType::Clyde => Color::RGB(255, 184, 82),
}
}
}
/// Base ghost struct that contains common functionality
pub struct Ghost<'a> {
/// Shared movement and position fields.
pub base: MovableEntity,
/// The current mode of the ghost
pub mode: GhostMode,
/// The type/personality of this ghost
pub ghost_type: GhostType,
/// Reference to Pac-Man for targeting
pub pacman: Rc<RefCell<Pacman<'a>>>,
pub body_sprite: AnimatedAtlasTexture<'a>,
pub eyes_sprite: AnimatedAtlasTexture<'a>,
}
impl Ghost<'_> {
/// Creates a new ghost instance
pub fn new<'a>(
ghost_type: GhostType,
starting_position: UVec2,
body_texture: Texture<'a>,
eyes_texture: Texture<'a>,
map: Rc<RefCell<Map>>,
pacman: Rc<RefCell<Pacman<'a>>>,
) -> Ghost<'a> {
let color = ghost_type.color();
let mut body_sprite = AnimatedAtlasTexture::new(body_texture, 8, 2, 32, 32, Some((-4, -4)));
body_sprite.set_color_modulation(color.r, color.g, color.b);
let pixel_position = Map::cell_to_pixel(starting_position);
Ghost {
base: MovableEntity::new(
pixel_position,
starting_position,
Direction::Left,
3,
SimpleTickModulator::new(1.0),
map,
),
mode: GhostMode::Chase,
ghost_type,
pacman,
body_sprite,
eyes_sprite: AnimatedAtlasTexture::new(eyes_texture, 1, 4, 32, 32, Some((-4, -4))),
}
}
/// Gets the target tile for this ghost based on its current mode
pub fn get_target_tile(&self) -> IVec2 {
match self.mode {
GhostMode::Scatter => self.get_scatter_target(),
GhostMode::Chase => self.get_chase_target(),
GhostMode::Frightened => self.get_random_target(),
GhostMode::Eyes => self.get_house_target(),
GhostMode::House => self.get_house_exit_target(),
}
}
/// Gets this ghost's home corner target for scatter mode
fn get_scatter_target(&self) -> IVec2 {
match self.ghost_type {
GhostType::Blinky => IVec2::new(25, 0), // Top right
GhostType::Pinky => IVec2::new(2, 0), // Top left
GhostType::Inky => IVec2::new(27, 35), // Bottom right
GhostType::Clyde => IVec2::new(0, 35), // Bottom left
}
}
/// Gets a random adjacent tile for frightened mode
fn get_random_target(&self) -> IVec2 {
let mut rng = SmallRng::from_os_rng();
let mut possible_moves = Vec::new();
// Check all four directions
for dir in &[Direction::Up, Direction::Down, Direction::Left, Direction::Right] {
// Don't allow reversing direction
if *dir == self.base.direction.opposite() {
continue;
}
let next_cell = self.base.next_cell(Some(*dir));
if !matches!(self.base.map.borrow().get_tile(next_cell), Some(MapTile::Wall)) {
possible_moves.push(next_cell);
}
}
if possible_moves.is_empty() {
// No valid moves, must reverse
self.base.next_cell(Some(self.base.direction.opposite()))
} else {
// Choose a random valid move
possible_moves[rng.random_range(0..possible_moves.len())]
}
}
/// Gets the ghost house target for returning eyes
fn get_house_target(&self) -> IVec2 {
IVec2::new(13, 14) // Center of ghost house
}
/// Gets the exit point target when leaving house
fn get_house_exit_target(&self) -> IVec2 {
IVec2::new(13, 11) // Just above ghost house
}
/// Gets this ghost's chase mode target (to be implemented by each ghost type)
fn get_chase_target(&self) -> IVec2 {
let pacman = self.pacman.borrow();
let cell = pacman.base().cell_position;
IVec2::new(cell.x as i32, cell.y as i32)
}
/// Calculates the path to the target tile using the A* algorithm.
pub fn get_path_to_target(&self, target: UVec2) -> Option<(Vec<UVec2>, u32)> {
let start = self.base.base.cell_position;
let map = self.base.map.borrow();
use pathfinding::prelude::dijkstra;
dijkstra(
&start,
|&p| {
let mut successors = vec![];
let tile = map.get_tile(IVec2::new(p.x as i32, p.y as i32));
// Tunnel wrap: if currently in a tunnel, add the opposite exit as a neighbor
if let Some(MapTile::Tunnel) = tile {
if p.x == 0 {
successors.push((UVec2::new(BOARD_WIDTH - 2, p.y), 1));
} else if p.x == BOARD_WIDTH - 1 {
successors.push((UVec2::new(1, p.y), 1));
}
}
for dir in &[Direction::Up, Direction::Down, Direction::Left, Direction::Right] {
let (dx, dy) = dir.offset();
let next_p = IVec2::new(p.x as i32 + dx, p.y as i32 + dy);
if let Some(tile) = map.get_tile(next_p) {
if tile == MapTile::Wall {
continue;
}
let next_u = UVec2::new(next_p.x as u32, next_p.y as u32);
successors.push((next_u, 1));
}
}
successors
},
|&p| p == target,
)
}
/// Changes the ghost's mode and handles direction reversal
pub fn set_mode(&mut self, new_mode: GhostMode) {
// Don't reverse if going to/from frightened or if in house
let should_reverse =
self.mode != GhostMode::House && new_mode != GhostMode::Frightened && self.mode != GhostMode::Frightened;
self.mode = new_mode;
self.base.speed = match new_mode {
GhostMode::Chase => 3,
GhostMode::Scatter => 2,
GhostMode::Frightened => 2,
GhostMode::Eyes => 7,
GhostMode::House => 0,
};
if should_reverse {
self.base.set_direction_if_valid(self.base.direction.opposite());
}
}
pub fn tick(&mut self) {
if self.mode == GhostMode::House {
// For now, do nothing in the house
return;
}
if self.base.is_grid_aligned() {
self.base.update_cell_position();
if !self.base.handle_tunnel() {
// Pathfinding logic (only if not in tunnel)
let target_tile = self.get_target_tile();
if let Some((path, _)) = self.get_path_to_target(target_tile.as_uvec2()) {
if path.len() > 1 {
let next_move = path[1];
let x = self.base.base.cell_position.x;
let y = self.base.base.cell_position.y;
let dx = next_move.x as i32 - x as i32;
let dy = next_move.y as i32 - y as i32;
let new_direction = if dx > 0 {
Direction::Right
} else if dx < 0 {
Direction::Left
} else if dy > 0 {
Direction::Down
} else {
Direction::Up
};
self.base.set_direction_if_valid(new_direction);
}
}
}
// Don't move if the next tile is a wall
if self.base.is_wall_ahead(None) {
return;
}
}
if self.base.modulation.next() {
self.base.move_forward();
if self.base.is_grid_aligned() {
self.base.update_cell_position();
}
}
}
}
impl<'a> Moving for Ghost<'a> {
fn move_forward(&mut self) {
self.base.move_forward();
}
fn update_cell_position(&mut self) {
self.base.update_cell_position();
}
fn next_cell(&self, direction: Option<Direction>) -> IVec2 {
self.base.next_cell(direction)
}
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool {
self.base.is_wall_ahead(direction)
}
fn handle_tunnel(&mut self) -> bool {
self.base.handle_tunnel()
}
fn is_grid_aligned(&self) -> bool {
self.base.is_grid_aligned()
}
fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool {
self.base.set_direction_if_valid(new_direction)
}
}
impl<'a> Renderable for Ghost<'a> {
fn render(&self, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>) {
let pos = self.base.base.pixel_position;
self.body_sprite.render(canvas, (pos.x, pos.y), Direction::Right, None);
// Inline the eye_frame logic here
let eye_frame = if self.mode == GhostMode::Frightened {
4 // Frightened frame
} else {
match self.base.direction {
Direction::Right => 0,
Direction::Up => 1,
Direction::Left => 2,
Direction::Down => 3,
}
};
self.eyes_sprite
.render(canvas, (pos.x, pos.y), Direction::Right, Some(eye_frame));
}
}

176
src/entity/mod.rs Normal file
View File

@@ -0,0 +1,176 @@
pub mod blinky;
pub mod direction;
pub mod edible;
pub mod ghost;
pub mod pacman;
use crate::{
constants::{MapTile, BOARD_OFFSET, BOARD_WIDTH, CELL_SIZE},
entity::direction::Direction,
map::Map,
modulation::SimpleTickModulator,
};
use glam::{IVec2, UVec2};
use std::cell::RefCell;
use std::rc::Rc;
/// A trait for game objects that can be moved and rendered.
pub trait Entity {
/// Returns a reference to the base entity (position, etc).
fn base(&self) -> &StaticEntity;
/// Returns true if the entity is colliding with the other entity.
fn is_colliding(&self, other: &dyn Entity) -> bool {
let a = self.base().pixel_position;
let b = other.base().pixel_position;
a == b
}
}
/// A trait for entities that can move and interact with the map.
pub trait Moving {
fn move_forward(&mut self);
fn update_cell_position(&mut self);
fn next_cell(&self, direction: Option<Direction>) -> IVec2;
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool;
fn handle_tunnel(&mut self) -> bool;
fn is_grid_aligned(&self) -> bool;
fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool;
}
/// A struct for static (non-moving) entities with position only.
pub struct StaticEntity {
pub pixel_position: IVec2,
pub cell_position: UVec2,
}
impl StaticEntity {
pub fn new(pixel_position: IVec2, cell_position: UVec2) -> Self {
Self {
pixel_position,
cell_position,
}
}
}
/// A struct for movable game entities with position, direction, speed, and modulation.
pub struct MovableEntity {
pub base: StaticEntity,
pub direction: Direction,
pub speed: u32,
pub modulation: SimpleTickModulator,
pub in_tunnel: bool,
pub map: Rc<RefCell<Map>>,
}
impl MovableEntity {
pub fn new(
pixel_position: IVec2,
cell_position: UVec2,
direction: Direction,
speed: u32,
modulation: SimpleTickModulator,
map: Rc<RefCell<Map>>,
) -> Self {
Self {
base: StaticEntity::new(pixel_position, cell_position),
direction,
speed,
modulation,
in_tunnel: false,
map,
}
}
/// Returns the position within the current cell, in pixels.
pub fn internal_position(&self) -> UVec2 {
UVec2::new(
(self.base.pixel_position.x as u32) % CELL_SIZE,
(self.base.pixel_position.y as u32) % CELL_SIZE,
)
}
}
impl Entity for MovableEntity {
fn base(&self) -> &StaticEntity {
&self.base
}
}
impl Moving for MovableEntity {
fn move_forward(&mut self) {
let speed = self.speed as i32;
match self.direction {
Direction::Right => self.base.pixel_position.x += speed,
Direction::Left => self.base.pixel_position.x -= speed,
Direction::Up => self.base.pixel_position.y -= speed,
Direction::Down => self.base.pixel_position.y += speed,
}
}
fn update_cell_position(&mut self) {
self.base.cell_position = UVec2::new(
(self.base.pixel_position.x as u32 / CELL_SIZE) - BOARD_OFFSET.0,
(self.base.pixel_position.y as u32 / CELL_SIZE) - BOARD_OFFSET.1,
);
}
fn next_cell(&self, direction: Option<Direction>) -> IVec2 {
let (x, y) = direction.unwrap_or(self.direction).offset();
IVec2::new(self.base.cell_position.x as i32 + x, self.base.cell_position.y as i32 + y)
}
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool {
let next_cell = self.next_cell(direction);
matches!(self.map.borrow().get_tile(next_cell), Some(MapTile::Wall))
}
fn handle_tunnel(&mut self) -> bool {
if !self.in_tunnel {
let current_tile = self
.map
.borrow()
.get_tile(IVec2::new(self.base.cell_position.x as i32, self.base.cell_position.y as i32));
if matches!(current_tile, Some(MapTile::Tunnel)) {
self.in_tunnel = true;
}
}
if self.in_tunnel {
if self.base.cell_position.x == 0 {
self.base.cell_position.x = BOARD_WIDTH - 2;
self.base.pixel_position = Map::cell_to_pixel(self.base.cell_position);
self.in_tunnel = false;
true
} else if self.base.cell_position.x == BOARD_WIDTH - 1 {
self.base.cell_position.x = 1;
self.base.pixel_position = Map::cell_to_pixel(self.base.cell_position);
self.in_tunnel = false;
true
} else {
true
}
} else {
false
}
}
fn is_grid_aligned(&self) -> bool {
self.internal_position() == UVec2::ZERO
}
fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool {
if new_direction == self.direction {
return false;
}
if self.is_wall_ahead(Some(new_direction)) {
return false;
}
self.direction = new_direction;
true
}
}
impl Entity for StaticEntity {
fn base(&self) -> &StaticEntity {
self
}
}
/// A trait for entities that can be rendered to the screen.
pub trait Renderable {
fn render(&self, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>);
}

132
src/entity/pacman.rs Normal file
View File

@@ -0,0 +1,132 @@
//! This module defines the Pac-Man entity, including its behavior and rendering.
use std::cell::RefCell;
use std::rc::Rc;
use sdl2::{
render::{Canvas, Texture},
video::Window,
};
use crate::{
entity::{direction::Direction, Entity, MovableEntity, Moving, Renderable, StaticEntity},
map::Map,
modulation::{SimpleTickModulator, TickModulator},
texture::animated::AnimatedAtlasTexture,
texture::FrameDrawn,
};
use glam::{IVec2, UVec2};
/// The Pac-Man entity.
pub struct Pacman<'a> {
/// Shared movement and position fields.
pub base: MovableEntity,
/// The next direction of Pac-Man, which will be applied when Pac-Man is next aligned with the grid.
pub next_direction: Option<Direction>,
/// Whether Pac-Man is currently stopped.
pub stopped: bool,
pub sprite: AnimatedAtlasTexture<'a>,
}
impl<'a> Entity for Pacman<'a> {
fn base(&self) -> &StaticEntity {
&self.base.base
}
}
impl<'a> Moving for Pacman<'a> {
fn move_forward(&mut self) {
self.base.move_forward();
}
fn update_cell_position(&mut self) {
self.base.update_cell_position();
}
fn next_cell(&self, direction: Option<Direction>) -> IVec2 {
self.base.next_cell(direction)
}
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool {
self.base.is_wall_ahead(direction)
}
fn handle_tunnel(&mut self) -> bool {
self.base.handle_tunnel()
}
fn is_grid_aligned(&self) -> bool {
self.base.is_grid_aligned()
}
fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool {
self.base.set_direction_if_valid(new_direction)
}
}
impl Pacman<'_> {
/// Creates a new `Pacman` instance.
pub fn new<'a>(starting_position: UVec2, atlas: Texture<'a>, map: Rc<RefCell<Map>>) -> Pacman<'a> {
let pixel_position = Map::cell_to_pixel(starting_position);
Pacman {
base: MovableEntity::new(
pixel_position,
starting_position,
Direction::Right,
3,
SimpleTickModulator::new(1.0),
map,
),
next_direction: None,
stopped: false,
sprite: AnimatedAtlasTexture::new(atlas, 2, 3, 32, 32, Some((-4, -4))),
}
}
/// Handles a requested direction change.
fn handle_direction_change(&mut self) -> bool {
match self.next_direction {
None => return false,
Some(next_direction) => {
if <Pacman as Moving>::set_direction_if_valid(self, next_direction) {
self.next_direction = None;
return true;
}
}
}
false
}
/// Returns the internal position of Pac-Man, rounded down to the nearest even number.
fn internal_position_even(&self) -> UVec2 {
let pos = self.base.internal_position();
UVec2::new((pos.x / 2) * 2, (pos.y / 2) * 2)
}
pub fn tick(&mut self) {
let can_change = self.internal_position_even() == UVec2::ZERO;
if can_change {
<Pacman as Moving>::update_cell_position(self);
if !<Pacman as Moving>::handle_tunnel(self) {
self.handle_direction_change();
if !self.stopped && <Pacman as Moving>::is_wall_ahead(self, None) {
self.stopped = true;
} else if self.stopped && !<Pacman as Moving>::is_wall_ahead(self, None) {
self.stopped = false;
}
}
}
if !self.stopped && self.base.modulation.next() {
<Pacman as Moving>::move_forward(self);
if self.internal_position_even() == UVec2::ZERO {
<Pacman as Moving>::update_cell_position(self);
}
}
}
}
impl Renderable for Pacman<'_> {
fn render(&self, canvas: &mut Canvas<Window>) {
let pos = self.base.base.pixel_position;
let dir = self.base.direction;
if self.stopped {
self.sprite.render(canvas, (pos.x, pos.y), dir, Some(2));
} else {
self.sprite.render(canvas, (pos.x, pos.y), dir, None);
}
}
}

View File

@@ -1,5 +1,12 @@
//! This module contains the main game logic and state.
use std::cell::RefCell;
use std::ops::Not;
use std::rc::Rc; use std::rc::Rc;
use glam::UVec2;
use rand::rngs::SmallRng;
use rand::seq::IteratorRandom;
use rand::SeedableRng;
use sdl2::image::LoadTexture; use sdl2::image::LoadTexture;
use sdl2::keyboard::Keycode; use sdl2::keyboard::Keycode;
use sdl2::render::{Texture, TextureCreator}; use sdl2::render::{Texture, TextureCreator};
@@ -7,96 +14,173 @@ use sdl2::rwops::RWops;
use sdl2::ttf::Font; use sdl2::ttf::Font;
use sdl2::video::WindowContext; use sdl2::video::WindowContext;
use sdl2::{pixels::Color, render::Canvas, video::Window}; use sdl2::{pixels::Color, render::Canvas, video::Window};
use tracing::event;
use crate::asset::{get_asset_bytes, Asset};
use crate::audio::Audio; use crate::audio::Audio;
use crate::constants::{MapTile, BOARD_HEIGHT, BOARD_WIDTH, RAW_BOARD}; use crate::constants::RAW_BOARD;
use crate::direction::Direction; use crate::debug::{DebugMode, DebugRenderer};
use crate::entity::Entity; use crate::entity::blinky::Blinky;
use crate::entity::direction::Direction;
use crate::entity::edible::{reconstruct_edibles, Edible, EdibleKind};
use crate::entity::pacman::Pacman;
use crate::entity::Renderable;
use crate::map::Map; use crate::map::Map;
use crate::pacman::Pacman; use crate::texture::atlas::AtlasTexture;
// Embed texture data directly into the executable
static PACMAN_TEXTURE_DATA: &[u8] = include_bytes!("../assets/32/pacman.png");
static PELLET_TEXTURE_DATA: &[u8] = include_bytes!("../assets/24/pellet.png");
static POWER_PELLET_TEXTURE_DATA: &[u8] = include_bytes!("../assets/24/energizer.png");
static MAP_TEXTURE_DATA: &[u8] = include_bytes!("../assets/map.png");
static FONT_DATA: &[u8] = include_bytes!("../assets/font/konami.ttf");
/// The main game state.
///
/// This struct contains all the information necessary to run the game, including
/// the canvas, textures, fonts, game objects, and the current score.
pub struct Game<'a> { pub struct Game<'a> {
canvas: &'a mut Canvas<Window>, canvas: &'a mut Canvas<Window>,
map_texture: Texture<'a>, map_texture: Texture<'a>,
pellet_texture: Texture<'a>, pellet_texture: Rc<AtlasTexture<'a>>,
power_pellet_texture: Texture<'a>, power_pellet_texture: Rc<AtlasTexture<'a>>,
font: Font<'a, 'static>, font: Font<'a, 'static>,
pacman: Pacman<'a>, pacman: Rc<RefCell<Pacman<'a>>>,
map: Rc<std::cell::RefCell<Map>>, map: Rc<RefCell<Map>>,
debug: bool, debug_mode: DebugMode,
score: u32, score: u32,
audio: Audio, pub audio: Audio,
blinky: Blinky<'a>,
edibles: Vec<Edible<'a>>,
} }
impl Game<'_> { impl<'a> Game<'a> {
pub fn new<'a>( /// Creates a new `Game` instance.
///
/// # Arguments
///
/// * `canvas` - The SDL canvas to render to.
/// * `texture_creator` - The SDL texture creator.
/// * `ttf_context` - The SDL TTF context.
/// * `_audio_subsystem` - The SDL audio subsystem (currently unused).
pub fn new(
canvas: &'a mut Canvas<Window>, canvas: &'a mut Canvas<Window>,
texture_creator: &'a TextureCreator<WindowContext>, texture_creator: &'a TextureCreator<WindowContext>,
ttf_context: &'a sdl2::ttf::Sdl2TtfContext, ttf_context: &'a sdl2::ttf::Sdl2TtfContext,
_audio_subsystem: &'a sdl2::AudioSubsystem, _audio_subsystem: &'a sdl2::AudioSubsystem,
) -> Game<'a> { ) -> Game<'a> {
let map = Rc::new(std::cell::RefCell::new(Map::new(RAW_BOARD))); let map = Rc::new(RefCell::new(Map::new(RAW_BOARD)));
// Load Pacman texture from embedded data // Load Pacman texture from asset API
let pacman_bytes = get_asset_bytes(Asset::Pacman).expect("Failed to load asset");
let pacman_atlas = texture_creator let pacman_atlas = texture_creator
.load_texture_bytes(PACMAN_TEXTURE_DATA) .load_texture_bytes(&pacman_bytes)
.expect("Could not load pacman texture from embedded data"); .expect("Could not load pacman texture from asset API");
let pacman = Pacman::new((1, 1), pacman_atlas, Rc::clone(&map)); let pacman = Rc::new(RefCell::new(Pacman::new(UVec2::new(1, 1), pacman_atlas, Rc::clone(&map))));
// Load pellet texture from embedded data // Load ghost textures
let pellet_texture = texture_creator let ghost_body_bytes = get_asset_bytes(Asset::GhostBody).expect("Failed to load asset");
.load_texture_bytes(PELLET_TEXTURE_DATA) let ghost_body = texture_creator
.expect("Could not load pellet texture from embedded data"); .load_texture_bytes(&ghost_body_bytes)
.expect("Could not load ghost body texture from asset API");
let ghost_eyes_bytes = get_asset_bytes(Asset::GhostEyes).expect("Failed to load asset");
let ghost_eyes = texture_creator
.load_texture_bytes(&ghost_eyes_bytes)
.expect("Could not load ghost eyes texture from asset API");
// Load power pellet texture from embedded data // Create Blinky
let power_pellet_texture = texture_creator let blinky = Blinky::new(
.load_texture_bytes(POWER_PELLET_TEXTURE_DATA) UVec2::new(13, 11), // Starting position just above ghost house
.expect("Could not load power pellet texture from embedded data"); ghost_body,
ghost_eyes,
Rc::clone(&map),
Rc::clone(&pacman),
);
// Load font from embedded data // Load pellet texture from asset API
let font_rwops = RWops::from_bytes(FONT_DATA).expect("Failed to create RWops for font"); let pellet_bytes = get_asset_bytes(Asset::Pellet).expect("Failed to load asset");
let font = ttf_context let pellet_texture = Rc::new(AtlasTexture::new(
.load_font_from_rwops(font_rwops, 24) texture_creator
.expect("Could not load font from embedded data"); .load_texture_bytes(&pellet_bytes)
.expect("Could not load pellet texture from asset API"),
1,
24,
24,
None,
));
let power_pellet_bytes = get_asset_bytes(Asset::Energizer).expect("Failed to load asset");
let power_pellet_texture = Rc::new(AtlasTexture::new(
texture_creator
.load_texture_bytes(&power_pellet_bytes)
.expect("Could not load power pellet texture from asset API"),
1,
24,
24,
None,
));
// Load map texture from asset API
let map_bytes = get_asset_bytes(Asset::Map).expect("Failed to load asset");
let mut map_texture = texture_creator
.load_texture_bytes(&map_bytes)
.expect("Could not load map texture from asset API");
map_texture.set_color_mod(0, 0, 255);
let edibles = reconstruct_edibles(
Rc::clone(&map),
Rc::clone(&pellet_texture),
Rc::clone(&power_pellet_texture),
Rc::clone(&pellet_texture), // placeholder for fruit sprite
);
// Load font from asset API
let font = {
let font_bytes = get_asset_bytes(Asset::FontKonami).expect("Failed to load asset").into_owned();
let font_bytes_static: &'static [u8] = Box::leak(font_bytes.into_boxed_slice());
let font_rwops = RWops::from_bytes(font_bytes_static).expect("Failed to create RWops for font");
ttf_context
.load_font_from_rwops(font_rwops, 24)
.expect("Could not load font from asset API")
};
let audio = Audio::new(); let audio = Audio::new();
// Load map texture from embedded data
let mut map_texture = texture_creator
.load_texture_bytes(MAP_TEXTURE_DATA)
.expect("Could not load map texture from embedded data");
map_texture.set_color_mod(0, 0, 255);
Game { Game {
canvas, canvas,
pacman: pacman, pacman,
debug: false, debug_mode: DebugMode::None,
map: map, map,
map_texture, map_texture,
pellet_texture, pellet_texture,
power_pellet_texture, power_pellet_texture,
font, font,
score: 0, score: 0,
audio, audio,
blinky,
edibles,
} }
} }
/// Handles a keyboard event.
///
/// # Arguments
///
/// * `keycode` - The keycode of the key that was pressed.
pub fn keyboard_event(&mut self, keycode: Keycode) { pub fn keyboard_event(&mut self, keycode: Keycode) {
// Change direction // Change direction
let direction = Direction::from_keycode(keycode); let direction = Direction::from_keycode(keycode);
self.pacman.next_direction = direction; if direction.is_some() {
self.pacman.borrow_mut().next_direction = direction;
return;
}
// Toggle debug mode // Toggle debug mode
if keycode == Keycode::Space { if keycode == Keycode::Space {
self.debug = !self.debug; self.debug_mode = match self.debug_mode {
DebugMode::None => DebugMode::Grid,
DebugMode::Grid => DebugMode::Pathfinding,
DebugMode::Pathfinding => DebugMode::ValidPositions,
DebugMode::ValidPositions => DebugMode::None,
};
return;
}
// Toggle mute
if keycode == Keycode::M {
self.audio.set_mute(self.audio.is_muted().not());
return;
} }
// Reset game // Reset game
@@ -105,10 +189,16 @@ impl Game<'_> {
} }
} }
/// Adds points to the score.
///
/// # Arguments
///
/// * `points` - The number of points to add.
pub fn add_score(&mut self, points: u32) { pub fn add_score(&mut self, points: u32) {
self.score += points; self.score += points;
} }
/// Resets the game to its initial state.
pub fn reset(&mut self) { pub fn reset(&mut self) {
// Reset the map to restore all pellets // Reset the map to restore all pellets
{ {
@@ -119,51 +209,79 @@ impl Game<'_> {
// Reset the score // Reset the score
self.score = 0; self.score = 0;
// Reset Pacman position (you might want to customize this) // Get valid positions from the cached flood fill and randomize positions in a single block
// For now, we'll keep Pacman where he is, but you could add: {
// self.pacman.position = Map::cell_to_pixel((1, 1)); let mut map = self.map.borrow_mut();
let valid_positions = map.get_valid_playable_positions();
let mut rng = SmallRng::from_os_rng();
event!(tracing::Level::INFO, "Game reset - map and score cleared"); // Randomize Pac-Man position
} if let Some(pos) = valid_positions.iter().choose(&mut rng) {
let mut pacman = self.pacman.borrow_mut();
pacman.base.base.pixel_position = Map::cell_to_pixel(*pos);
pacman.base.base.cell_position = *pos;
pacman.base.in_tunnel = false;
pacman.base.direction = Direction::Right;
pacman.next_direction = None;
pacman.stopped = false;
}
pub fn tick(&mut self) { // Randomize ghost position
self.pacman.tick(); if let Some(pos) = valid_positions.iter().choose(&mut rng) {
self.check_pellet_eating(); self.blinky.base.base.pixel_position = Map::cell_to_pixel(*pos);
} self.blinky.base.base.cell_position = *pos;
self.blinky.base.in_tunnel = false;
fn check_pellet_eating(&mut self) { self.blinky.base.direction = Direction::Left;
let cell_pos = self.pacman.cell_position(); self.blinky.mode = crate::entity::ghost::GhostMode::Chase;
// Check if there's a pellet at the current position
let tile = {
let map = self.map.borrow();
map.get_tile((cell_pos.0 as i32, cell_pos.1 as i32))
};
if let Some(tile) = tile {
let pellet_value = match tile {
MapTile::Pellet => Some(10),
MapTile::PowerPellet => Some(50),
_ => None,
};
if let Some(value) = pellet_value {
{
let mut map = self.map.borrow_mut();
map.set_tile((cell_pos.0 as i32, cell_pos.1 as i32), MapTile::Empty);
}
self.add_score(value);
self.audio.eat();
event!(
tracing::Level::DEBUG,
"Pellet eaten at ({}, {})",
cell_pos.0,
cell_pos.1
);
} }
} }
self.edibles = reconstruct_edibles(
Rc::clone(&self.map),
Rc::clone(&self.pellet_texture),
Rc::clone(&self.power_pellet_texture),
Rc::clone(&self.pellet_texture), // placeholder for fruit sprite
);
} }
/// Advances the game by one tick.
pub fn tick(&mut self) {
// Advance animation frames for Pacman and Blinky
self.pacman.borrow_mut().sprite.tick();
self.blinky.body_sprite.tick();
self.blinky.eyes_sprite.tick();
let pacman = self.pacman.borrow();
let mut eaten_indices = vec![];
for (i, edible) in self.edibles.iter().enumerate() {
if edible.collide(&*pacman) {
eaten_indices.push(i);
}
}
drop(pacman); // Release immutable borrow before mutably borrowing self
for &i in eaten_indices.iter().rev() {
let edible = &self.edibles[i];
match edible.kind {
EdibleKind::Pellet => {
self.add_score(10);
self.audio.eat();
}
EdibleKind::PowerPellet => {
self.add_score(50);
self.audio.eat();
}
EdibleKind::Fruit(_fruit) => {
self.add_score(100);
self.audio.eat();
}
}
self.edibles.remove(i);
}
self.pacman.borrow_mut().tick();
self.blinky.tick();
}
/// Draws the entire game to the canvas.
pub fn draw(&mut self) { pub fn draw(&mut self) {
// Clear the screen (black) // Clear the screen (black)
self.canvas.set_draw_color(Color::RGB(0, 0, 0)); self.canvas.set_draw_color(Color::RGB(0, 0, 0));
@@ -174,98 +292,42 @@ impl Game<'_> {
.copy(&self.map_texture, None, None) .copy(&self.map_texture, None, None)
.expect("Could not render texture on canvas"); .expect("Could not render texture on canvas");
// Render pellets // Render all edibles
self.render_pellets(); for edible in &self.edibles {
edible.render(self.canvas);
}
// Render the pacman // Render Pac-Man
self.pacman.render(self.canvas); self.pacman.borrow().render(self.canvas);
// Render ghost
self.blinky.render(self.canvas);
// Render score // Render score
self.render_score(); self.render_ui();
// Draw the debug grid // Draw the debug grid
if self.debug { match self.debug_mode {
for x in 0..BOARD_WIDTH { DebugMode::Grid => {
for y in 0..BOARD_HEIGHT { DebugRenderer::draw_debug_grid(self.canvas, &self.map.borrow(), self.pacman.borrow().base.base.cell_position);
let tile = self let next_cell = <Pacman as crate::entity::Moving>::next_cell(&*self.pacman.borrow(), None);
.map DebugRenderer::draw_next_cell(self.canvas, &self.map.borrow(), next_cell.as_uvec2());
.borrow()
.get_tile((x as i32, y as i32))
.unwrap_or(MapTile::Empty);
let mut color = None;
if (x, y) == self.pacman.cell_position() {
self.draw_cell((x, y), Color::CYAN);
} else {
color = match tile {
MapTile::Empty => None,
MapTile::Wall => Some(Color::BLUE),
MapTile::Pellet => Some(Color::RED),
MapTile::PowerPellet => Some(Color::MAGENTA),
MapTile::StartingPosition(_) => Some(Color::GREEN),
};
}
if let Some(color) = color {
self.draw_cell((x, y), color);
}
}
} }
DebugMode::ValidPositions => {
// Draw the next cell DebugRenderer::draw_valid_positions(self.canvas, &mut self.map.borrow_mut());
let next_cell = self.pacman.next_cell(None); }
self.draw_cell((next_cell.0 as u32, next_cell.1 as u32), Color::YELLOW); DebugMode::Pathfinding => {
DebugRenderer::draw_pathfinding(self.canvas, &self.blinky, &self.map.borrow());
}
DebugMode::None => {}
} }
// Present the canvas // Present the canvas
self.canvas.present(); self.canvas.present();
} }
fn draw_cell(&mut self, cell: (u32, u32), color: Color) { /// Renders the user interface, including the score and lives.
let position = Map::cell_to_pixel(cell); fn render_ui(&mut self) {
self.canvas.set_draw_color(color);
self.canvas
.draw_rect(sdl2::rect::Rect::new(
position.0 as i32,
position.1 as i32,
24,
24,
))
.expect("Could not draw rectangle");
}
fn render_pellets(&mut self) {
for x in 0..BOARD_WIDTH {
for y in 0..BOARD_HEIGHT {
let tile = self
.map
.borrow()
.get_tile((x as i32, y as i32))
.unwrap_or(MapTile::Empty);
match tile {
MapTile::Pellet => {
let position = Map::cell_to_pixel((x, y));
let dst_rect = sdl2::rect::Rect::new(position.0, position.1, 24, 24);
self.canvas
.copy(&self.pellet_texture, None, Some(dst_rect))
.expect("Could not render pellet");
}
MapTile::PowerPellet => {
let position = Map::cell_to_pixel((x, y));
let dst_rect = sdl2::rect::Rect::new(position.0, position.1, 24, 24);
self.canvas
.copy(&self.power_pellet_texture, None, Some(dst_rect))
.expect("Could not render power pellet");
}
_ => {}
}
}
}
}
fn render_score(&mut self) {
let lives = 3; let lives = 3;
let score_text = format!("{:02}", self.score); let score_text = format!("{:02}", self.score);
@@ -275,8 +337,9 @@ impl Game<'_> {
let score_offset = 7 - (score_text.len() as i32); let score_offset = 7 - (score_text.len() as i32);
let gap_offset = 6; let gap_offset = 6;
// Render the score and high score
self.render_text( self.render_text(
&format!("{}UP HIGH SCORE ", lives), &format!("{lives}UP HIGH SCORE "),
(24 * lives_offset + x_offset, y_offset), (24 * lives_offset + x_offset, y_offset),
Color::WHITE, Color::WHITE,
); );
@@ -287,22 +350,17 @@ impl Game<'_> {
); );
} }
/// Renders text to the screen at the given position.
fn render_text(&mut self, text: &str, position: (i32, i32), color: Color) { fn render_text(&mut self, text: &str, position: (i32, i32), color: Color) {
let surface = self let surface = self.font.render(text).blended(color).expect("Could not render text surface");
.font
.render(text)
.blended(color)
.expect("Could not render text surface");
let texture_creator = self.canvas.texture_creator(); let texture_creator = self.canvas.texture_creator();
let texture = texture_creator let texture = texture_creator
.create_texture_from_surface(&surface) .create_texture_from_surface(&surface)
.expect("Could not create texture from surface"); .expect("Could not create texture from surface");
let query = texture.query(); let query = texture.query();
let dst_rect = let dst_rect = sdl2::rect::Rect::new(position.0, position.1, query.width, query.height);
sdl2::rect::Rect::new(position.0, position.1, query.width + 4, query.height + 4);
self.canvas self.canvas
.copy(&texture, None, Some(dst_rect)) .copy(&texture, None, Some(dst_rect))

View File

@@ -1,26 +1,23 @@
//! This module contains helper functions that are used throughout the game.
use glam::UVec2;
/// Checks if two grid positions are adjacent to each other /// Checks if two grid positions are adjacent to each other
/// ///
/// # Arguments /// # Arguments
/// * `a` - First position as (x, y) coordinates /// * `a` - First position as (x, y) coordinates
/// * `b` - Second position as (x, y) coordinates /// * `b` - Second position as (x, y) coordinates
/// * `diagonal` - Whether to consider diagonal adjacency (true) or only orthogonal (false) /// * `diagonal` - Whether to consider diagonal adjacency (true) or only orthogonal (false)
/// ///
/// # Returns /// # Returns
/// * `true` if positions are adjacent according to the diagonal parameter /// * `true` if positions are adjacent according to the diagonal parameter
/// * `false` otherwise /// * `false` otherwise
pub fn is_adjacent(a: (u32, u32), b: (u32, u32), diagonal: bool) -> bool { pub fn is_adjacent(a: UVec2, b: UVec2, diagonal: bool) -> bool {
let (ax, ay) = a; let dx = a.x.abs_diff(b.x);
let (bx, by) = b; let dy = a.y.abs_diff(b.y);
// Calculate absolute differences between coordinates
let dx = if ax > bx { ax - bx } else { bx - ax };
let dy = if ay > by { ay - by } else { by - ay };
if diagonal { if diagonal {
// For diagonal adjacency: both differences must be ≤ 1 and at least one > 0 dx <= 1 && dy <= 1 && (dx != 0 || dy != 0)
dx <= 1 && dy <= 1 && (dx + dy) > 0
} else { } else {
// For orthogonal adjacency: exactly one difference must be 1, the other 0
(dx == 1 && dy == 0) || (dx == 0 && dy == 1) (dx == 1 && dy == 0) || (dx == 0 && dy == 1)
} }
} }
@@ -34,22 +31,22 @@ mod tests {
// Test orthogonal adjacency (diagonal = false) // Test orthogonal adjacency (diagonal = false)
// Same position should not be adjacent // Same position should not be adjacent
assert!(!is_adjacent((0, 0), (0, 0), false)); assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(0, 0), false));
// Adjacent positions should be true // Adjacent positions should be true
assert!(is_adjacent((0, 0), (1, 0), false)); // Right assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(1, 0), false)); // Right
assert!(is_adjacent((0, 0), (0, 1), false)); // Down assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(0, 1), false)); // Down
assert!(is_adjacent((1, 1), (0, 1), false)); // Left assert!(is_adjacent(UVec2::new(1, 1), UVec2::new(0, 1), false)); // Left
assert!(is_adjacent((1, 1), (1, 0), false)); // Up assert!(is_adjacent(UVec2::new(1, 1), UVec2::new(1, 0), false)); // Up
// Diagonal positions should be false // Diagonal positions should be false
assert!(!is_adjacent((0, 0), (1, 1), false)); assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(1, 1), false));
assert!(!is_adjacent((0, 1), (1, 0), false)); assert!(!is_adjacent(UVec2::new(0, 1), UVec2::new(1, 0), false));
// Positions more than 1 step away should be false // Positions more than 1 step away should be false
assert!(!is_adjacent((0, 0), (2, 0), false)); assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(2, 0), false));
assert!(!is_adjacent((0, 0), (0, 2), false)); assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(0, 2), false));
assert!(!is_adjacent((0, 0), (2, 2), false)); assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(2, 2), false));
} }
#[test] #[test]
@@ -57,54 +54,54 @@ mod tests {
// Test diagonal adjacency (diagonal = true) // Test diagonal adjacency (diagonal = true)
// Same position should not be adjacent // Same position should not be adjacent
assert!(!is_adjacent((0, 0), (0, 0), true)); assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(0, 0), true));
// Orthogonal adjacent positions should be true // Orthogonal adjacent positions should be true
assert!(is_adjacent((0, 0), (1, 0), true)); // Right assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(1, 0), true)); // Right
assert!(is_adjacent((0, 0), (0, 1), true)); // Down assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(0, 1), true)); // Down
assert!(is_adjacent((1, 1), (0, 1), true)); // Left assert!(is_adjacent(UVec2::new(1, 1), UVec2::new(0, 1), true)); // Left
assert!(is_adjacent((1, 1), (1, 0), true)); // Up assert!(is_adjacent(UVec2::new(1, 1), UVec2::new(1, 0), true)); // Up
// Diagonal adjacent positions should be true // Diagonal adjacent positions should be true
assert!(is_adjacent((0, 0), (1, 1), true)); // Down-right assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(1, 1), true)); // Down-right
assert!(is_adjacent((1, 0), (0, 1), true)); // Down-left assert!(is_adjacent(UVec2::new(1, 0), UVec2::new(0, 1), true)); // Down-left
assert!(is_adjacent((0, 1), (1, 0), true)); // Up-right assert!(is_adjacent(UVec2::new(0, 1), UVec2::new(1, 0), true)); // Up-right
assert!(is_adjacent((1, 1), (0, 0), true)); // Up-left assert!(is_adjacent(UVec2::new(1, 1), UVec2::new(0, 0), true)); // Up-left
// Positions more than 1 step away should be false // Positions more than 1 step away should be false
assert!(!is_adjacent((0, 0), (2, 0), true)); assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(2, 0), true));
assert!(!is_adjacent((0, 0), (0, 2), true)); assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(0, 2), true));
assert!(!is_adjacent((0, 0), (2, 2), true)); assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(2, 2), true));
assert!(!is_adjacent((0, 0), (1, 2), true)); assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(1, 2), true));
} }
#[test] #[test]
fn test_edge_cases() { fn test_edge_cases() {
// Test with larger coordinates // Test with larger coordinates
assert!(is_adjacent((100, 100), (101, 100), false)); assert!(is_adjacent(UVec2::new(100, 100), UVec2::new(101, 100), false));
assert!(is_adjacent((100, 100), (100, 101), false)); assert!(is_adjacent(UVec2::new(100, 100), UVec2::new(100, 101), false));
assert!(!is_adjacent((100, 100), (102, 100), false)); assert!(!is_adjacent(UVec2::new(100, 100), UVec2::new(102, 100), false));
assert!(is_adjacent((100, 100), (101, 101), true)); assert!(is_adjacent(UVec2::new(100, 100), UVec2::new(101, 101), true));
assert!(!is_adjacent((100, 100), (102, 102), true)); assert!(!is_adjacent(UVec2::new(100, 100), UVec2::new(102, 102), true));
// Test with zero coordinates // Test with zero coordinates
assert!(is_adjacent((0, 0), (1, 0), false)); assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(1, 0), false));
assert!(is_adjacent((0, 0), (0, 1), false)); assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(0, 1), false));
assert!(is_adjacent((0, 0), (1, 1), true)); assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(1, 1), true));
} }
#[test] #[test]
fn test_commutative_property() { fn test_commutative_property() {
// The function should work the same regardless of parameter order // The function should work the same regardless of parameter order
assert_eq!( assert_eq!(
is_adjacent((1, 2), (2, 2), false), is_adjacent(UVec2::new(1, 2), UVec2::new(2, 2), false),
is_adjacent((2, 2), (1, 2), false) is_adjacent(UVec2::new(2, 2), UVec2::new(1, 2), false)
); );
assert_eq!( assert_eq!(
is_adjacent((1, 2), (2, 3), true), is_adjacent(UVec2::new(1, 2), UVec2::new(2, 3), true),
is_adjacent((2, 3), (1, 2), true) is_adjacent(UVec2::new(2, 3), UVec2::new(1, 2), true)
); );
} }
} }

View File

@@ -11,7 +11,7 @@ use tracing_subscriber::layer::SubscriberExt;
#[cfg(windows)] #[cfg(windows)]
use winapi::{ use winapi::{
shared::{ntdef::NULL, windef::HWND}, shared::ntdef::NULL,
um::{ um::{
fileapi::{CreateFileA, OPEN_EXISTING}, fileapi::{CreateFileA, OPEN_EXISTING},
handleapi::INVALID_HANDLE_VALUE, handleapi::INVALID_HANDLE_VALUE,
@@ -22,15 +22,20 @@ use winapi::{
}, },
}; };
/// Attaches the process to the parent console on Windows.
///
/// This allows the application to print to the console when run from a terminal,
/// which is useful for debugging purposes. If the application is not run from a
/// terminal, this function does nothing.
#[cfg(windows)] #[cfg(windows)]
unsafe fn attach_console() { unsafe fn attach_console() {
if GetConsoleWindow() != std::ptr::null_mut() as HWND { if !std::ptr::eq(GetConsoleWindow(), std::ptr::null_mut()) {
return; return;
} }
if AttachConsole(winapi::um::wincon::ATTACH_PARENT_PROCESS) != 0 { if AttachConsole(winapi::um::wincon::ATTACH_PARENT_PROCESS) != 0 {
let handle = CreateFileA( let handle = CreateFileA(
"CONOUT$\0".as_ptr() as *const i8, c"CONOUT$".as_ptr(),
GENERIC_READ | GENERIC_WRITE, GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null_mut(), std::ptr::null_mut(),
@@ -47,18 +52,45 @@ unsafe fn attach_console() {
// Do NOT call AllocConsole here - we don't want a console when launched from Explorer // Do NOT call AllocConsole here - we don't want a console when launched from Explorer
} }
mod animation; mod asset;
mod audio; mod audio;
mod constants; mod constants;
mod direction; mod debug;
#[cfg(target_os = "emscripten")]
mod emscripten;
mod entity; mod entity;
mod game; mod game;
mod helper; mod helper;
mod map; mod map;
mod modulation; mod modulation;
mod pacman; mod texture;
#[cfg(not(target_os = "emscripten"))]
fn sleep(value: Duration) {
spin_sleep::sleep(value);
}
#[cfg(target_os = "emscripten")]
fn sleep(value: Duration) {
emscripten::emscripten::sleep(value.as_millis() as u32);
}
#[cfg(target_os = "emscripten")]
fn now() -> std::time::Instant {
std::time::Instant::now() + std::time::Duration::from_millis(emscripten::emscripten::now() as u64)
}
#[cfg(not(target_os = "emscripten"))]
fn now() -> std::time::Instant {
std::time::Instant::now()
}
/// The main entry point of the application.
///
/// This function initializes SDL, the window, the game state, and then enters
/// the main game loop.
pub fn main() { pub fn main() {
// Attaches the console on Windows for debugging purposes.
#[cfg(windows)] #[cfg(windows)]
unsafe { unsafe {
attach_console(); attach_console();
@@ -84,38 +116,27 @@ pub fn main() {
.build() .build()
.expect("Could not initialize window"); .expect("Could not initialize window");
let mut canvas = window let mut canvas = window.into_canvas().build().expect("Could not build canvas");
.into_canvas()
.build()
.expect("Could not build canvas");
canvas canvas
.set_logical_size(WINDOW_WIDTH, WINDOW_HEIGHT) .set_logical_size(WINDOW_WIDTH, WINDOW_HEIGHT)
.expect("Could not set logical size"); .expect("Could not set logical size");
let texture_creator = canvas.texture_creator(); let texture_creator = canvas.texture_creator();
let mut game = Game::new( let mut game = Game::new(&mut canvas, &texture_creator, &ttf_context, &audio_subsystem);
&mut canvas, game.audio.set_mute(cfg!(debug_assertions));
&texture_creator,
&ttf_context,
&audio_subsystem,
);
let mut event_pump = sdl_context let mut event_pump = sdl_context.event_pump().expect("Could not get SDL EventPump");
.event_pump()
.expect("Could not get SDL EventPump");
// Initial draw and tick // Initial draw and tick
game.draw(); game.draw();
game.tick(); game.tick();
// The target time for each frame of the game loop (60 FPS).
let loop_time = Duration::from_secs(1) / 60; let loop_time = Duration::from_secs(1) / 60;
let mut tick_no = 0u32;
// The start of a period of time over which we average the frame time.
let mut last_averaging_time = Instant::now();
let mut sleep_time = Duration::ZERO;
let mut paused = false; let mut paused = false;
// Whether the window is currently shown.
let mut shown = false; let mut shown = false;
event!( event!(
@@ -126,7 +147,9 @@ pub fn main() {
let mut main_loop = || { let mut main_loop = || {
let start = Instant::now(); let start = Instant::now();
// TODO: Fix key repeat delay issues by using VecDeque for instant key repeat // TODO: Fix key repeat delay issues by using a queue for keyboard events.
// This would allow for instant key repeat without being affected by the
// main loop's tick rate.
for event in event_pump.poll_iter() { for event in event_pump.poll_iter() {
match event { match event {
Event::Window { win_event, .. } => match win_event { Event::Window { win_event, .. } => match win_event {
@@ -154,11 +177,7 @@ pub fn main() {
.. ..
} => { } => {
paused = !paused; paused = !paused;
event!( event!(tracing::Level::INFO, "{}", if paused { "Paused" } else { "Unpaused" });
tracing::Level::INFO,
"{}",
if paused { "Paused" } else { "Unpaused" }
);
} }
Event::KeyDown { keycode, .. } => { Event::KeyDown { keycode, .. } => {
game.keyboard_event(keycode.unwrap()); game.keyboard_event(keycode.unwrap());
@@ -167,9 +186,9 @@ pub fn main() {
} }
} }
// TODO: Proper pausing implementation that does not interfere with statistic gathering // TODO: Implement a proper pausing mechanism that does not interfere with
// statistic gathering and other background tasks.
if !paused { if !paused {
// game.audio_demo_tick();
game.tick(); game.tick();
game.draw(); game.draw();
} }
@@ -177,16 +196,8 @@ pub fn main() {
if start.elapsed() < loop_time { if start.elapsed() < loop_time {
let time = loop_time.saturating_sub(start.elapsed()); let time = loop_time.saturating_sub(start.elapsed());
if time != Duration::ZERO { if time != Duration::ZERO {
#[cfg(not(target_os = "emscripten"))] sleep(time);
{
spin_sleep::sleep(time);
}
#[cfg(target_os = "emscripten")]
{
std::thread::sleep(time);
}
} }
sleep_time += time;
} else { } else {
event!( event!(
tracing::Level::WARN, tracing::Level::WARN,
@@ -195,19 +206,6 @@ pub fn main() {
); );
} }
tick_no += 1;
const PERIOD: u32 = 60 * 60;
let tick_mod = tick_no % PERIOD;
if tick_mod % PERIOD == 0 {
let average_fps = PERIOD as f32 / last_averaging_time.elapsed().as_secs_f32();
let average_sleep = sleep_time / PERIOD;
let average_process = loop_time - average_sleep;
sleep_time = Duration::ZERO;
last_averaging_time = Instant::now();
}
true true
}; };

View File

@@ -1,63 +1,74 @@
use crate::constants::MapTile; //! This module defines the game map and provides functions for interacting with it.
use crate::constants::{BOARD_HEIGHT, BOARD_WIDTH, RAW_BOARD}; use rand::rngs::SmallRng;
use rand::seq::IteratorRandom;
use rand::SeedableRng;
use crate::constants::{MapTile, BOARD_OFFSET, CELL_SIZE};
use crate::constants::{BOARD_HEIGHT, BOARD_WIDTH};
use glam::{IVec2, UVec2};
use once_cell::sync::OnceCell;
use std::collections::{HashSet, VecDeque};
/// The game map.
///
/// The map is represented as a 2D array of `MapTile`s. It also stores a copy of
/// the original map, which can be used to reset the map to its initial state.
pub struct Map { pub struct Map {
/// The current state of the map.
current: [[MapTile; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize], current: [[MapTile; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize],
/// The default state of the map.
default: [[MapTile; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize], default: [[MapTile; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize],
} }
impl Map { impl Map {
/// Creates a new `Map` instance from a raw board layout.
///
/// # Arguments
///
/// * `raw_board` - A 2D array of characters representing the board layout.
pub fn new(raw_board: [&str; BOARD_HEIGHT as usize]) -> Map { pub fn new(raw_board: [&str; BOARD_HEIGHT as usize]) -> Map {
let mut map = [[MapTile::Empty; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize]; let mut map = [[MapTile::Empty; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize];
for y in 0..BOARD_HEIGHT as usize { for (y, line) in raw_board.iter().enumerate().take(BOARD_HEIGHT as usize) {
let line = raw_board[y]; for (x, character) in line.chars().enumerate().take(BOARD_WIDTH as usize) {
for x in 0..BOARD_WIDTH as usize {
if x >= line.len() {
break;
}
let i = (y * (BOARD_WIDTH as usize) + x) as usize;
let character = line
.chars()
.nth(x as usize)
.unwrap_or_else(|| panic!("Could not get character at {} = ({}, {})", i, x, y));
let tile = match character { let tile = match character {
'#' => MapTile::Wall, '#' => MapTile::Wall,
'.' => MapTile::Pellet, '.' => MapTile::Pellet,
'o' => MapTile::PowerPellet, 'o' => MapTile::PowerPellet,
' ' => MapTile::Empty, ' ' => MapTile::Empty,
c @ '0' | c @ '1' | c @ '2' | c @ '3' | c @ '4' => { 'T' => MapTile::Tunnel,
MapTile::StartingPosition(c.to_digit(10).unwrap() as u8) c @ '0' | c @ '1' | c @ '2' | c @ '3' | c @ '4' => MapTile::StartingPosition(c.to_digit(10).unwrap() as u8),
}
'=' => MapTile::Empty, '=' => MapTile::Empty,
_ => panic!("Unknown character in board: {}", character), _ => panic!("Unknown character in board: {character}"),
}; };
map[x][y] = tile;
map[x as usize][y as usize] = tile;
} }
} }
Map { Map {
current: map, current: map,
default: map.clone(), default: map,
} }
} }
/// Resets the map to its original state.
pub fn reset(&mut self) { pub fn reset(&mut self) {
// Restore the map to its original state // Restore the map to its original state
for x in 0..BOARD_WIDTH as usize { for (x, col) in self.current.iter_mut().enumerate().take(BOARD_WIDTH as usize) {
for y in 0..BOARD_HEIGHT as usize { for (y, cell) in col.iter_mut().enumerate().take(BOARD_HEIGHT as usize) {
self.current[x][y] = self.default[x][y]; *cell = self.default[x][y];
} }
} }
} }
pub fn get_tile(&self, cell: (i32, i32)) -> Option<MapTile> { /// Returns the tile at the given cell coordinates.
let x = cell.0 as usize; ///
let y = cell.1 as usize; /// # Arguments
///
/// * `cell` - The cell coordinates, in grid coordinates.
pub fn get_tile(&self, cell: IVec2) -> Option<MapTile> {
let x = cell.x as usize;
let y = cell.y as usize;
if x >= BOARD_WIDTH as usize || y >= BOARD_HEIGHT as usize { if x >= BOARD_WIDTH as usize || y >= BOARD_HEIGHT as usize {
return None; return None;
@@ -66,9 +77,15 @@ impl Map {
Some(self.current[x][y]) Some(self.current[x][y])
} }
pub fn set_tile(&mut self, cell: (i32, i32), tile: MapTile) -> bool { /// Sets the tile at the given cell coordinates.
let x = cell.0 as usize; ///
let y = cell.1 as usize; /// # Arguments
///
/// * `cell` - The cell coordinates, in grid coordinates.
/// * `tile` - The tile to set.
pub fn set_tile(&mut self, cell: IVec2, tile: MapTile) -> bool {
let x = cell.x as usize;
let y = cell.y as usize;
if x >= BOARD_WIDTH as usize || y >= BOARD_HEIGHT as usize { if x >= BOARD_WIDTH as usize || y >= BOARD_HEIGHT as usize {
return false; return false;
@@ -78,7 +95,65 @@ impl Map {
true true
} }
pub fn cell_to_pixel(cell: (u32, u32)) -> (i32, i32) { /// Converts cell coordinates to pixel coordinates.
((cell.0 as i32) * 24, ((cell.1 + 3) as i32) * 24) ///
/// # Arguments
///
/// * `cell` - The cell coordinates, in grid coordinates.
pub fn cell_to_pixel(cell: UVec2) -> IVec2 {
IVec2::new((cell.x * CELL_SIZE) as i32, ((cell.y + BOARD_OFFSET.1) * CELL_SIZE) as i32)
}
/// Returns a reference to a cached vector of all valid playable positions in the maze.
/// This is computed once using a flood fill from a random pellet, and then cached.
pub fn get_valid_playable_positions(&mut self) -> &Vec<UVec2> {
use MapTile::*;
static CACHE: OnceCell<Vec<UVec2>> = OnceCell::new();
if let Some(cached) = CACHE.get() {
return cached;
}
// Find a random starting pellet
let mut pellet_positions = vec![];
for (x, col) in self.current.iter().enumerate().take(BOARD_WIDTH as usize) {
for (y, &cell) in col.iter().enumerate().take(BOARD_HEIGHT as usize) {
match cell {
Pellet | PowerPellet => pellet_positions.push(UVec2::new(x as u32, y as u32)),
_ => {}
}
}
}
let mut rng = SmallRng::from_os_rng();
let &start = pellet_positions
.iter()
.choose(&mut rng)
.expect("No pellet found for flood fill");
// Flood fill
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(start);
while let Some(pos) = queue.pop_front() {
if !visited.insert(pos) {
continue;
}
match self.current[pos.x as usize][pos.y as usize] {
Empty | Pellet | PowerPellet => {
for offset in [IVec2::new(-1, 0), IVec2::new(1, 0), IVec2::new(0, -1), IVec2::new(0, 1)] {
let neighbor = (pos.as_ivec2() + offset).as_uvec2();
if neighbor.x < BOARD_WIDTH && neighbor.y < BOARD_HEIGHT {
let neighbor_tile = self.current[neighbor.x as usize][neighbor.y as usize];
if matches!(neighbor_tile, Empty | Pellet | PowerPellet) {
queue.push_back(neighbor);
}
}
}
}
StartingPosition(_) | Wall | Tunnel => {}
}
}
let mut result: Vec<UVec2> = visited.into_iter().collect();
result.sort_unstable_by_key(|v| (v.x, v.y));
CACHE.get_or_init(|| result)
} }
} }

View File

@@ -1,3 +1,5 @@
//! This module provides a tick modulator, which can be used to slow down
//! operations by a percentage.
/// A tick modulator allows you to slow down operations by a percentage. /// A tick modulator allows you to slow down operations by a percentage.
/// ///
/// Unfortunately, switching to floating point numbers for entities can induce floating point errors, slow down calculations /// Unfortunately, switching to floating point numbers for entities can induce floating point errors, slow down calculations
@@ -12,17 +14,25 @@
/// ///
/// For example, if we want to slow down the speed by 10%, we would need to skip every 10th tick. /// For example, if we want to slow down the speed by 10%, we would need to skip every 10th tick.
pub trait TickModulator { pub trait TickModulator {
/// Creates a new tick modulator.
///
/// # Arguments
///
/// * `percent` - The percentage to slow down by, from 0.0 to 1.0.
fn new(percent: f32) -> Self; fn new(percent: f32) -> Self;
/// Returns whether or not the operation should be performed on this tick.
fn next(&mut self) -> bool; fn next(&mut self) -> bool;
} }
/// A simple tick modulator that skips every Nth tick.
pub struct SimpleTickModulator { pub struct SimpleTickModulator {
tick_count: u32, tick_count: u32,
ticks_left: u32, ticks_left: u32,
} }
// TODO: Add tests // TODO: Add tests for the tick modulator to ensure that it is working correctly.
// TODO: Look into average precision, binary code modulation strategy // TODO: Look into average precision and binary code modulation strategies to see
// if they would be a better fit for this use case.
impl TickModulator for SimpleTickModulator { impl TickModulator for SimpleTickModulator {
fn new(percent: f32) -> Self { fn new(percent: f32) -> Self {
let ticks_required: u32 = (1f32 / (1f32 - percent)).round() as u32; let ticks_required: u32 = (1f32 / (1f32 - percent)).round() as u32;
@@ -34,15 +44,12 @@ impl TickModulator for SimpleTickModulator {
} }
fn next(&mut self) -> bool { fn next(&mut self) -> bool {
self.ticks_left -= 1;
// Return whether or not we should skip this tick
if self.ticks_left == 0 { if self.ticks_left == 0 {
// We've reached the tick to skip, reset the counter
self.ticks_left = self.tick_count; self.ticks_left = self.tick_count;
false return false;
} else {
true
} }
self.ticks_left -= 1;
true
} }
} }

View File

@@ -1,163 +0,0 @@
use std::cell::RefCell;
use std::rc::Rc;
use sdl2::{
render::{Canvas, Texture},
video::Window,
};
use tracing::event;
use crate::{
animation::AnimatedTexture,
constants::MapTile,
constants::{BOARD_OFFSET, CELL_SIZE},
direction::Direction,
entity::Entity,
map::Map,
modulation::{SimpleTickModulator, TickModulator},
};
pub struct Pacman<'a> {
// Absolute position on the board (precise)
pub position: (i32, i32),
pub direction: Direction,
pub next_direction: Option<Direction>,
pub stopped: bool,
map: Rc<RefCell<Map>>,
speed: u32,
modulation: SimpleTickModulator,
sprite: AnimatedTexture<'a>,
}
impl Pacman<'_> {
pub fn new<'a>(
starting_position: (u32, u32),
atlas: Texture<'a>,
map: Rc<RefCell<Map>>,
) -> Pacman<'a> {
Pacman {
position: Map::cell_to_pixel(starting_position),
direction: Direction::Right,
next_direction: None,
speed: 3,
map,
stopped: false,
modulation: SimpleTickModulator::new(1.0),
sprite: AnimatedTexture::new(atlas, 2, 3, 32, 32, Some((-4, -4))),
}
}
pub fn render(&mut self, canvas: &mut Canvas<Window>) {
// When stopped, render the last frame of the animation
if self.stopped {
self.sprite
.render_until(canvas, self.position, self.direction, 2);
} else {
self.sprite.render(canvas, self.position, self.direction);
}
}
pub fn next_cell(&self, direction: Option<Direction>) -> (i32, i32) {
let (x, y) = direction.unwrap_or(self.direction).offset();
let cell = self.cell_position();
(cell.0 as i32 + x, cell.1 as i32 + y)
}
fn handle_requested_direction(&mut self) {
if self.next_direction.is_none() {
return;
}
if self.next_direction.unwrap() == self.direction {
self.next_direction = None;
return;
}
let proposed_next_cell = self.next_cell(self.next_direction);
let proposed_next_tile = self
.map
.borrow()
.get_tile(proposed_next_cell)
.unwrap_or(MapTile::Empty);
if proposed_next_tile != MapTile::Wall {
event!(
tracing::Level::DEBUG,
"Direction change: {:?} -> {:?} at position ({}, {}) internal ({}, {})",
self.direction,
self.next_direction.unwrap(),
self.position.0,
self.position.1,
self.internal_position().0,
self.internal_position().1
);
self.direction = self.next_direction.unwrap();
self.next_direction = None;
}
}
fn internal_position_even(&self) -> (u32, u32) {
let (x, y) = self.internal_position();
((x / 2u32) * 2u32, (y / 2u32) * 2u32)
}
}
impl Entity for Pacman<'_> {
fn is_colliding(&self, other: &dyn Entity) -> bool {
let (x, y) = self.position();
let (other_x, other_y) = other.position();
x == other_x && y == other_y
}
fn position(&self) -> (i32, i32) {
self.position
}
fn cell_position(&self) -> (u32, u32) {
let (x, y) = self.position;
(
(x as u32 / CELL_SIZE) - BOARD_OFFSET.0,
(y as u32 / CELL_SIZE) - BOARD_OFFSET.1,
)
}
fn internal_position(&self) -> (u32, u32) {
let (x, y) = self.position();
(x as u32 % CELL_SIZE, y as u32 % CELL_SIZE)
}
fn tick(&mut self) {
let can_change = self.internal_position_even() == (0, 0);
if can_change {
self.handle_requested_direction();
let next = self.next_cell(None);
let next_tile = self.map.borrow().get_tile(next).unwrap_or(MapTile::Empty);
if !self.stopped && next_tile == MapTile::Wall {
event!(tracing::Level::DEBUG, "Wall collision. Stopping.");
self.stopped = true;
} else if self.stopped && next_tile != MapTile::Wall {
event!(tracing::Level::DEBUG, "Wall collision resolved. Moving.");
self.stopped = false;
}
}
if !self.stopped && self.modulation.next() {
let speed = self.speed as i32;
match self.direction {
Direction::Right => {
self.position.0 += speed;
}
Direction::Left => {
self.position.0 -= speed;
}
Direction::Up => {
self.position.1 -= speed;
}
Direction::Down => {
self.position.1 += speed;
}
}
}
}
}

72
src/texture/animated.rs Normal file
View File

@@ -0,0 +1,72 @@
//! This module provides a simple animation and atlas system for textures.
use sdl2::{
render::{Canvas, Texture},
video::Window,
};
use crate::entity::direction::Direction;
use crate::texture::atlas::AtlasTexture;
use crate::texture::FrameDrawn;
/// An animated texture using a texture atlas.
pub struct AnimatedAtlasTexture<'a> {
pub atlas: AtlasTexture<'a>,
pub ticks_per_frame: u32,
pub ticker: u32,
pub reversed: bool,
pub paused: bool,
}
impl<'a> AnimatedAtlasTexture<'a> {
pub fn new(
texture: Texture<'a>,
ticks_per_frame: u32,
frame_count: u32,
width: u32,
height: u32,
offset: Option<(i32, i32)>,
) -> Self {
AnimatedAtlasTexture {
atlas: AtlasTexture::new(texture, frame_count, width, height, offset),
ticks_per_frame,
ticker: 0,
reversed: false,
paused: false,
}
}
fn current_frame(&self) -> u32 {
self.ticker / self.ticks_per_frame
}
/// Advances the animation by one tick, unless paused.
pub fn tick(&mut self) {
if self.paused {
return;
}
if self.reversed {
if self.ticker > 0 {
self.ticker -= 1;
}
if self.ticker == 0 {
self.reversed = !self.reversed;
}
} else {
self.ticker += 1;
if self.ticker + 1 == self.ticks_per_frame * self.atlas.frame_count {
self.reversed = !self.reversed;
}
}
}
pub fn set_color_modulation(&mut self, r: u8, g: u8, b: u8) {
self.atlas.set_color_modulation(r, g, b);
}
}
impl<'a> FrameDrawn for AnimatedAtlasTexture<'a> {
fn render(&self, canvas: &mut Canvas<Window>, position: (i32, i32), direction: Direction, frame: Option<u32>) {
let frame = frame.unwrap_or_else(|| self.current_frame());
self.atlas.render(canvas, position, direction, Some(frame));
}
}

67
src/texture/atlas.rs Normal file
View File

@@ -0,0 +1,67 @@
use sdl2::{
rect::Rect,
render::{Canvas, Texture},
video::Window,
};
use crate::{entity::direction::Direction, texture::FrameDrawn};
/// A texture atlas abstraction for static (non-animated) rendering.
pub struct AtlasTexture<'a> {
pub raw_texture: Texture<'a>,
pub offset: (i32, i32),
pub frame_count: u32,
pub frame_width: u32,
pub frame_height: u32,
}
impl<'a> AtlasTexture<'a> {
pub fn new(texture: Texture<'a>, frame_count: u32, frame_width: u32, frame_height: u32, offset: Option<(i32, i32)>) -> Self {
AtlasTexture {
raw_texture: texture,
frame_count,
frame_width,
frame_height,
offset: offset.unwrap_or((0, 0)),
}
}
pub fn get_frame_rect(&self, frame: u32) -> Option<Rect> {
if frame >= self.frame_count {
return None;
}
Some(Rect::new(
frame as i32 * self.frame_width as i32,
0,
self.frame_width,
self.frame_height,
))
}
pub fn set_color_modulation(&mut self, r: u8, g: u8, b: u8) {
self.raw_texture.set_color_mod(r, g, b);
}
}
impl<'a> FrameDrawn for AtlasTexture<'a> {
fn render(&self, canvas: &mut Canvas<Window>, position: (i32, i32), direction: Direction, frame: Option<u32>) {
let texture_source_frame_rect = self.get_frame_rect(frame.unwrap_or(0));
let canvas_destination_rect = Rect::new(
position.0 + self.offset.0,
position.1 + self.offset.1,
self.frame_width,
self.frame_height,
);
canvas
.copy_ex(
&self.raw_texture,
texture_source_frame_rect,
Some(canvas_destination_rect),
direction.angle(),
None,
false,
false,
)
.expect("Could not render texture on canvas");
}
}

11
src/texture/mod.rs Normal file
View File

@@ -0,0 +1,11 @@
use sdl2::{render::Canvas, video::Window};
use crate::entity::direction::Direction;
/// Trait for drawable atlas-based textures
pub trait FrameDrawn {
fn render(&self, canvas: &mut Canvas<Window>, position: (i32, i32), direction: Direction, frame: Option<u32>);
}
pub mod animated;
pub mod atlas;