Compare commits

...

24 Commits

Author SHA1 Message Date
09e3d85821 feat!: dynamic map rendering from tiles 2025-08-13 00:25:34 -05:00
c1e421bbbb test: new graph tests 2025-08-12 19:58:37 -05:00
3a9381a56c chore: use NodeId explicitly in collision.rs types 2025-08-12 19:58:11 -05:00
90bdfbd2ae chore: remove emscripten.rs platform from coverage, add html generation task, hide absolute path with remap-path-prefix, organize gitignore 2025-08-12 19:57:52 -05:00
a230d15ffc test: setup common submodule, add text.rs tests, pattern exclude error.rs 2025-08-12 19:24:06 -05:00
60bbd1f5d6 ci: add retry mechanism for coverage reporting via Coveralls CLI 2025-08-12 18:31:07 -05:00
43ce8a4e01 ci: use justfile for coverage, separate report/generate coverage tasks 2025-08-12 18:00:57 -05:00
1529a64588 test: add asset path validity tests 2025-08-12 17:24:12 -05:00
be5eec64c9 Add justfile for handling multiple coverage steps, prevent early termination of coverage job 2025-08-12 17:24:12 -05:00
780a33f657 test: add coverage job to bacon.toml, coverage profile for nextest 2025-08-12 16:48:01 -05:00
c1c5dae6f2 refactor: restructure game logic and state management into separate modules
- Moved game logic from `game.rs` to `game/mod.rs` and `game/state.rs` for better organization.
- Updated `App` to utilize the new `Game` struct and its state management.
- Refactored error handling
- Removed unused audio subsystem references
2025-08-12 14:40:48 -05:00
c489f32908 fix: audio and other subsystems being dropped in App::new(), use Box::leak to ensure static ownership 2025-08-12 13:08:08 -05:00
b91f70cf2f ci: add concurrency group to 'wasm' job to prevent concurrent page deployments 2025-08-12 11:56:03 -05:00
24a207be01 chore: use steps.$.outputs in build workflow, document 1.86.0 toolchain version 2025-08-12 11:41:29 -05:00
44e31d9b21 chore: sync lockfile, add lcov.info to .gitignore 2025-08-12 10:31:10 -05:00
dependabot[bot]
b67234765a chore(deps): bump actions/checkout from 4 to 5 in the dependencies group (#1)
Bumps the dependencies group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 4 to 5
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 09:27:54 -05:00
dependabot[bot]
d07498c30e chore(deps): bump the dependencies group with 5 updates (#2)
Bumps the dependencies group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [thiserror](https://github.com/dtolnay/thiserror) | `1.0.69` | `2.0.12` |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.98` | `1.0.99` |
| [glam](https://github.com/bitshifter/glam-rs) | `0.30.4` | `0.30.5` |
| [serde_json](https://github.com/serde-rs/json) | `1.0.141` | `1.0.142` |
| [libc](https://github.com/rust-lang/libc) | `0.2.174` | `0.2.175` |


Updates `thiserror` from 1.0.69 to 2.0.12
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.69...2.0.12)

Updates `anyhow` from 1.0.98 to 1.0.99
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.98...1.0.99)

Updates `glam` from 0.30.4 to 0.30.5
- [Changelog](https://github.com/bitshifter/glam-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitshifter/glam-rs/compare/0.30.4...0.30.5)

Updates `serde_json` from 1.0.141 to 1.0.142
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.141...v1.0.142)

Updates `libc` from 0.2.174 to 0.2.175
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.175/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.174...0.2.175)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-version: 2.0.12
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: dependencies
- dependency-name: anyhow
  dependency-version: 1.0.99
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: glam
  dependency-version: 0.30.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: serde_json
  dependency-version: 1.0.142
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: libc
  dependency-version: 0.2.175
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Xevion <xevion@xevion.dev>
2025-08-12 09:26:46 -05:00
183a432116 test: add tests for collision, items, directional, sprite
enum macros for FruitKind
2025-08-12 09:18:53 -05:00
ead1466b2d chore: specify 'llvm-tools-preview' toolchain component for coverage in toolchain file 2025-08-12 00:22:27 -05:00
8ef09a4e3e test: drop minimal_test_board, use RAW_BOARD constant, item generation tests 2025-08-11 23:26:28 -05:00
33672d8d5a feat: implement collision detection system for entities 2025-08-11 23:24:23 -05:00
1dc8aca373 feat: item collection & collisions, pellet & energizer generation 2025-08-11 22:45:36 -05:00
02089a78da chore: downgrade toolchain to 1.86 on all versions
This is just because managing both 1.86 and 1.88 is really annoying, so
it's better to just be unified. There's no real point to using 1.88
besides more clippy warnings, which are already impeding my work right
now. So we're downgrading.
2025-08-11 22:10:41 -05:00
1f8e7c6d71 fix: resolve clippy warnings, inline format vars, use tracing to log warnings 2025-08-11 22:09:08 -05:00
129 changed files with 2547 additions and 1472 deletions

View File

@@ -1,2 +1,5 @@
[profile.default]
fail-fast = false
[profile.coverage]
status-level = "none"

View File

@@ -14,23 +14,23 @@ jobs:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact_name: pacman
toolchain: 1.88.0
toolchain: 1.86.0
- os: macos-13
target: x86_64-apple-darwin
artifact_name: pacman
toolchain: 1.88.0
toolchain: 1.86.0
- os: macos-latest
target: aarch64-apple-darwin
artifact_name: pacman
toolchain: 1.88.0
toolchain: 1.86.0
- os: windows-latest
target: x86_64-pc-windows-gnu
artifact_name: pacman.exe
toolchain: 1.88.0
toolchain: 1.86.0
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Rust Toolchain
uses: dtolnay/rust-toolchain@master
@@ -64,15 +64,16 @@ jobs:
run: cargo build --release
- name: Acquire Package Version
shell: bash
id: get_version
shell: bash # required to prevent Windows runners from failing
run: |
PACKAGE_VERSION=$(cargo metadata --format-version 1 --no-deps | jq '.packages[0].version' -r)
echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> $GITHUB_ENV
set -euo pipefail # exit on error
echo "version=$(cargo metadata --format-version 1 --no-deps | jq '.packages[0].version' -r)" >> $GITHUB_OUTPUT
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: "pacman-${{ env.PACKAGE_VERSION }}-${{ matrix.target }}"
name: "pacman-${{ steps.get_version.outputs.version }}-${{ matrix.target }}"
path: ./target/release/${{ matrix.artifact_name }}
retention-days: 7
if-no-files-found: error
@@ -83,10 +84,13 @@ jobs:
permissions:
pages: write
id-token: write
# concurrency group is used to prevent multiple page deployments from being attempted at the same time
concurrency:
group: ${{ github.workflow }}-wasm
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Emscripten SDK
uses: pyodide/setup-emsdk@v15
@@ -98,7 +102,7 @@ jobs:
uses: dtolnay/rust-toolchain@master
with:
target: wasm32-unknown-emscripten
toolchain: 1.86.0 # we are unfortunately pinned to 1.86.0 for some reason, bulk-memory-opt related issues
toolchain: 1.86.0
- name: Rust Cache
uses: Swatinem/rust-cache@v2

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
@@ -42,15 +42,39 @@ jobs:
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: taiki-e/install-action@nextest
- uses: taiki-e/install-action@just
# Note: We manually link zlib. This should be synchronized with the flags set for Linux in .cargo/config.toml.
- name: Generate coverage report
run: |
cargo llvm-cov --no-fail-fast --lcov --output-path lcov.info nextest
just coverage
- name: Download Coveralls CLI
run: |
# use GitHub Releases URL instead of coveralls.io because they can't maintain their own files; it 404s
curl -L https://github.com/coverallsapp/coverage-reporter/releases/download/v0.6.15/coveralls-linux-x86_64.tar.gz | tar -xz -C /usr/local/bin
- name: Upload coverage to Coveralls
uses: coverallsapp/github-action@v2
with:
files: ./lcov.info
format: lcov
allow-empty: false
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
run: |
if [ ! -f "lcov.info" ]; then
echo "Error: lcov.info file not found. Coverage generation may have failed."
exit 1
fi
for i in {1..10}; do
echo "Attempt $i: Uploading coverage to Coveralls..."
if coveralls -n report lcov.info; then
echo "Successfully uploaded coverage report."
exit 0
fi
if [ $i -lt 10 ]; then
delay=$((2**i))
echo "Attempt $i failed. Retrying in $delay seconds..."
sleep $delay
fi
done
echo "Failed to upload coverage report after 10 attempts."
exit 1

View File

@@ -4,7 +4,7 @@ on: ["push", "pull_request"]
env:
CARGO_TERM_COLOR: always
RUST_TOOLCHAIN: 1.88.0
RUST_TOOLCHAIN: 1.86.0
jobs:
test:
@@ -12,7 +12,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master

16
.gitignore vendored
View File

@@ -1,7 +1,17 @@
# IDE, Other files
.vscode
.idea
rust-sdl2-emscripten/
# Build files
target/
dist/
emsdk/
.idea
rust-sdl2-emscripten/
assets/site/build.css
# Site build f iles
tailwindcss-*
assets/site/build.css
# Coverage reports
lcov.info
coverage.html

68
Cargo.lock generated
View File

@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.98"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
[[package]]
name = "autocfg"
@@ -79,9 +79,9 @@ dependencies = [
[[package]]
name = "glam"
version = "0.30.4"
version = "0.30.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50a99dbe56b72736564cfa4b85bf9a33079f16ae8b74983ab06af3b1a3696b11"
checksum = "f2d1aab06663bdce00d6ca5e5ed586ec8d18033a771906c993a1e3755b368d85"
[[package]]
name = "hashbrown"
@@ -89,6 +89,12 @@ version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.10.0"
@@ -122,9 +128,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.174"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "log"
@@ -194,7 +200,9 @@ dependencies = [
"serde_json",
"smallvec",
"spin_sleep",
"thiserror 1.0.69",
"strum",
"strum_macros",
"thiserror",
"tracing",
"tracing-error",
"tracing-subscriber",
@@ -212,7 +220,7 @@ dependencies = [
"integer-sqrt",
"num-traits",
"rustc-hash",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -372,9 +380,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.141"
version = "1.0.142"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
dependencies = [
"itoa",
"memchr",
@@ -406,6 +414,24 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "strum"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
[[package]]
name = "strum_macros"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.104"
@@ -417,33 +443,13 @@ dependencies = [
"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",
"thiserror-impl",
]
[[package]]

View File

@@ -15,12 +15,14 @@ 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"
thiserror = "2.0"
anyhow = "1.0"
glam = { version = "0.30.4", features = [] }
glam = { version = "0.30.5", features = [] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.141"
serde_json = "1.0.142"
smallvec = "1.15.1"
strum = "0.27.2"
strum_macros = "0.27.2"
[profile.release]
lto = true
@@ -54,4 +56,4 @@ x86_64-apple-darwin = { triplet = "x64-osx" }
aarch64-apple-darwin = { triplet = "arm64-osx" }
[target.'cfg(target_os = "emscripten")'.dependencies]
libc = "0.2.16"
libc = "0.2.175"

33
Justfile Normal file
View File

@@ -0,0 +1,33 @@
set shell := ["bash", "-c"]
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
# Regex to exclude files from coverage report, double escapes for Justfile + CLI
# You can use src\\\\..., but the filename alone is acceptable too
coverage_exclude_pattern := "src\\\\app.rs|audio.rs|src\\\\error.rs|platform\\\\emscripten.rs"
# !!! --ignore-filename-regex should be used on both reports & coverage testing
# !!! --remap-path-prefix prevents the absolute path from being used in the generated report
# Generate HTML report (for humans, source line inspection)
html: coverage
cargo llvm-cov report \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
--html \
--open
# Display report (for humans)
report-coverage: coverage
cargo llvm-cov report \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}"
# Run & generate report (for CI)
coverage:
cargo llvm-cov \
--lcov \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
--output-path lcov.info \
--profile coverage \
--no-fail-fast nextest

View File

@@ -72,6 +72,8 @@ I wanted to hit a log of goals and features, making it a 'perfect' project that
Since this project is still in progress, I'm only going to cover non-obvious build details. By reading the code, build scripts, and copying the online build workflows, you should be able to replicate the build process.
- We use rustc 1.86.0 for the build, due to bulk-memory-opt related issues on wasm32-unknown-emscripten.
- Technically, we could probably use stable or even nightly on desktop targets, but using different versions for different targets is a pain, mainly because of clippy warnings changing between versions.
- Install `cargo-vcpkg` with `cargo install cargo-vcpkg`, then run `cargo vcpkg build` to build the requisite dependencies via vcpkg.
- For the WASM build, you need to have the Emscripten SDK cloned; you can do so with `git clone https://github.com/emscripten-core/emsdk.git`
- The first time you clone, you'll need to install the appropriate SDK version with `./emsdk install 3.1.43` and then activate it with `./emsdk activate 3.1.43`. On Windows, use `./emsdk/emsdk.ps1` instead.

View File

File diff suppressed because it is too large Load Diff

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -34,6 +34,30 @@ command = [
need_stdout = true
analyzer = "nextest"
[jobs.coverage]
command = [
"just", "report-coverage"
]
need_stdout = true
ignored_lines = [
"info:",
"\\s+Compiling",
"test result: ok",
"^\\s*$",
"running \\d+ test",
"Nextest run ID",
"[─]+",
"test.+ok",
"PASS|START",
"Starting \\d+ test",
"\\s*#",
"\\s*Finished.+in \\d+",
"\\s*Summary\\s+\\[",
"\\s*Blocking",
"Finished report saved to"
]
on_change_strategy = "wait_then_restart"
[jobs.doc]
command = ["cargo", "doc", "--no-deps"]
need_stdout = false
@@ -59,3 +83,4 @@ c = "job:clippy"
alt-c = "job:check"
ctrl-alt-c = "job:check-all"
shift-c = "job:clippy-all"
f = "job:coverage"

4
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,4 @@
[toolchain]
# we are unfortunately pinned to 1.86.0 for some reason, bulk-memory-opt related issues on wasm32-unknown-emscripten
channel = "1.86.0"
components = ["rustfmt", "llvm-tools-preview", "clippy"]

View File

@@ -4,8 +4,9 @@ use glam::Vec2;
use sdl2::event::{Event, WindowEvent};
use sdl2::keyboard::Keycode;
use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
use sdl2::ttf::Sdl2TtfContext;
use sdl2::video::{Window, WindowContext};
use sdl2::EventPump;
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
use tracing::{error, event};
use crate::error::{GameError, GameResult};
@@ -14,26 +15,31 @@ use crate::constants::{CANVAS_SIZE, LOOP_TIME, SCALE};
use crate::game::Game;
use crate::platform::get_platform;
pub struct App<'a> {
pub struct App {
game: Game,
canvas: Canvas<Window>,
event_pump: EventPump,
backbuffer: Texture<'a>,
event_pump: &'static mut EventPump,
backbuffer: Texture<'static>,
paused: bool,
last_tick: Instant,
cursor_pos: Vec2,
}
impl App<'_> {
impl App {
pub fn new() -> GameResult<Self> {
let sdl_context: &'static Sdl = Box::leak(Box::new(sdl2::init().map_err(|e| GameError::Sdl(e.to_string()))?));
let video_subsystem: &'static VideoSubsystem =
Box::leak(Box::new(sdl_context.video().map_err(|e| GameError::Sdl(e.to_string()))?));
let _audio_subsystem: &'static AudioSubsystem =
Box::leak(Box::new(sdl_context.audio().map_err(|e| GameError::Sdl(e.to_string()))?));
let _ttf_context: &'static Sdl2TtfContext =
Box::leak(Box::new(sdl2::ttf::init().map_err(|e| GameError::Sdl(e.to_string()))?));
let event_pump: &'static mut EventPump =
Box::leak(Box::new(sdl_context.event_pump().map_err(|e| GameError::Sdl(e.to_string()))?));
// Initialize platform-specific console
get_platform().init_console()?;
let sdl_context = sdl2::init().map_err(|e| GameError::Sdl(e.to_string()))?;
let video_subsystem = sdl_context.video().map_err(|e| GameError::Sdl(e.to_string()))?;
let audio_subsystem = sdl_context.audio().map_err(|e| GameError::Sdl(e.to_string()))?;
let ttf_context = sdl2::ttf::init().map_err(|e| GameError::Sdl(e.to_string()))?;
let window = video_subsystem
.window(
"Pac-Man",
@@ -50,18 +56,16 @@ impl App<'_> {
.set_logical_size(CANVAS_SIZE.x, CANVAS_SIZE.y)
.map_err(|e| GameError::Sdl(e.to_string()))?;
let texture_creator_static: &'static TextureCreator<WindowContext> = Box::leak(Box::new(canvas.texture_creator()));
let texture_creator: &'static TextureCreator<WindowContext> = Box::leak(Box::new(canvas.texture_creator()));
let mut game = Game::new(texture_creator_static, &ttf_context, &audio_subsystem)?;
game.audio.set_mute(cfg!(debug_assertions));
let mut game = Game::new(texture_creator)?;
// game.audio.set_mute(cfg!(debug_assertions));
let mut backbuffer = texture_creator_static
let mut backbuffer = texture_creator
.create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)
.map_err(|e| GameError::Sdl(e.to_string()))?;
backbuffer.set_scale_mode(ScaleMode::Nearest);
let event_pump = sdl_context.event_pump().map_err(|e| GameError::Sdl(e.to_string()))?;
// Initial draw
game.draw(&mut canvas, &mut backbuffer)
.map_err(|e| GameError::Sdl(e.to_string()))?;
@@ -115,7 +119,7 @@ impl App<'_> {
keycode: Some(Keycode::Space),
..
} => {
self.game.debug_mode = !self.game.debug_mode;
self.game.toggle_debug_mode();
}
Event::KeyDown { keycode: Some(key), .. } => {
self.game.keyboard_event(key);

View File

@@ -3,20 +3,9 @@
//! 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;
use strum_macros::EnumIter;
#[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)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub enum Asset {
Wav1,
Wav2,
@@ -44,6 +33,7 @@ impl Asset {
mod imp {
use super::*;
use crate::error::AssetError;
use crate::platform::get_platform;
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {

View File

@@ -18,8 +18,6 @@ pub const SCALE: f32 = 2.6;
pub const BOARD_CELL_OFFSET: UVec2 = UVec2::new(0, 3);
/// The offset of the game board from the top-left corner of the window, in pixels.
pub const BOARD_PIXEL_OFFSET: UVec2 = UVec2::new(BOARD_CELL_OFFSET.x * CELL_SIZE, BOARD_CELL_OFFSET.y * CELL_SIZE);
/// The size of the game board, in pixels.
pub const BOARD_PIXEL_SIZE: UVec2 = UVec2::new(BOARD_CELL_SIZE.x * CELL_SIZE, BOARD_CELL_SIZE.y * CELL_SIZE);
/// The size of the canvas, in pixels.
pub const CANVAS_SIZE: UVec2 = UVec2::new(
(BOARD_CELL_SIZE.x + BOARD_CELL_OFFSET.x) * CELL_SIZE,

128
src/entity/collision.rs Normal file
View File

@@ -0,0 +1,128 @@
use smallvec::SmallVec;
use std::collections::HashMap;
use crate::entity::{graph::NodeId, traversal::Position};
/// Trait for entities that can participate in collision detection.
pub trait Collidable {
/// Returns the current position of this entity.
fn position(&self) -> Position;
/// Checks if this entity is colliding with another entity.
#[allow(dead_code)]
fn is_colliding_with(&self, other: &dyn Collidable) -> bool {
positions_overlap(&self.position(), &other.position())
}
}
/// System for tracking entities by their positions for efficient collision detection.
#[derive(Default)]
pub struct CollisionSystem {
/// Maps node IDs to lists of entity IDs that are at that node
node_entities: HashMap<NodeId, Vec<EntityId>>,
/// Maps entity IDs to their current positions
entity_positions: HashMap<EntityId, Position>,
/// Next available entity ID
next_id: EntityId,
}
/// Unique identifier for an entity in the collision system
pub type EntityId = u32;
impl CollisionSystem {
/// Registers an entity with the collision system and returns its ID
pub fn register_entity(&mut self, position: Position) -> EntityId {
let id = self.next_id;
self.next_id += 1;
self.entity_positions.insert(id, position);
self.update_node_entities(id, position);
id
}
/// Updates an entity's position
pub fn update_position(&mut self, entity_id: EntityId, new_position: Position) {
if let Some(old_position) = self.entity_positions.get(&entity_id) {
// Remove from old nodes
self.remove_from_nodes(entity_id, *old_position);
}
// Update position and add to new nodes
self.entity_positions.insert(entity_id, new_position);
self.update_node_entities(entity_id, new_position);
}
/// Removes an entity from the collision system
#[allow(dead_code)]
pub fn remove_entity(&mut self, entity_id: EntityId) {
if let Some(position) = self.entity_positions.remove(&entity_id) {
self.remove_from_nodes(entity_id, position);
}
}
/// Gets all entity IDs at a specific node
pub fn entities_at_node(&self, node: NodeId) -> &[EntityId] {
self.node_entities.get(&node).map(|v| v.as_slice()).unwrap_or(&[])
}
/// Gets all entity IDs that could collide with an entity at the given position
pub fn potential_collisions(&self, position: &Position) -> Vec<EntityId> {
let mut collisions = Vec::new();
let nodes = get_nodes(position);
for node in nodes {
collisions.extend(self.entities_at_node(node));
}
// Remove duplicates
collisions.sort_unstable();
collisions.dedup();
collisions
}
/// Updates the node_entities map when an entity's position changes
fn update_node_entities(&mut self, entity_id: EntityId, position: Position) {
let nodes = get_nodes(&position);
for node in nodes {
self.node_entities.entry(node).or_default().push(entity_id);
}
}
/// Removes an entity from all nodes it was previously at
fn remove_from_nodes(&mut self, entity_id: EntityId, position: Position) {
let nodes = get_nodes(&position);
for node in nodes {
if let Some(entities) = self.node_entities.get_mut(&node) {
entities.retain(|&id| id != entity_id);
if entities.is_empty() {
self.node_entities.remove(&node);
}
}
}
}
}
/// Checks if two positions overlap (entities are at the same location).
fn positions_overlap(a: &Position, b: &Position) -> bool {
let a_nodes = get_nodes(a);
let b_nodes = get_nodes(b);
// Check if any nodes overlap
a_nodes.iter().any(|a_node| b_nodes.contains(a_node))
// TODO: More complex overlap detection, the above is a simple check, but it could become an early filter for more precise calculations later
}
/// Gets all nodes that an entity is currently at or between.
fn get_nodes(pos: &Position) -> SmallVec<[NodeId; 2]> {
let mut nodes = SmallVec::new();
match pos {
Position::AtNode(node) => nodes.push(*node),
Position::BetweenNodes { from, to, .. } => {
nodes.push(*from);
nodes.push(*to);
}
}
nodes
}

View File

@@ -7,11 +7,15 @@
use pathfinding::prelude::dijkstra;
use rand::prelude::*;
use smallvec::SmallVec;
use tracing::error;
use crate::entity::direction::Direction;
use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId};
use crate::entity::r#trait::Entity;
use crate::entity::traversal::Traverser;
use crate::entity::{
collision::Collidable,
direction::Direction,
graph::{Edge, EdgePermissions, Graph, NodeId},
r#trait::Entity,
traversal::Traverser,
};
use crate::texture::animated::AnimatedTexture;
use crate::texture::directional::DirectionalAnimatedTexture;
use crate::texture::sprite::SpriteAtlas;
@@ -104,7 +108,7 @@ impl Entity for Ghost {
}
if let Err(e) = self.traverser.advance(graph, dt * 60.0 * self.speed, &can_ghost_traverse) {
eprintln!("Ghost movement error: {}", e);
error!("Ghost movement error: {}", e);
}
self.texture.tick(dt);
}
@@ -160,10 +164,8 @@ impl Ghost {
})?,
];
textures[direction.as_usize()] =
Some(AnimatedTexture::new(moving_tiles, 0.2).map_err(|e| GameError::Texture(TextureError::Animated(e)))?);
stopped_textures[direction.as_usize()] =
Some(AnimatedTexture::new(stopped_tiles, 0.1).map_err(|e| GameError::Texture(TextureError::Animated(e)))?);
textures[direction.as_usize()] = Some(AnimatedTexture::new(moving_tiles, 0.2)?);
stopped_textures[direction.as_usize()] = Some(AnimatedTexture::new(stopped_tiles, 0.1)?);
}
Ok(Self {
@@ -244,3 +246,9 @@ impl Ghost {
}
}
}
impl Collidable for Ghost {
fn position(&self) -> crate::entity::traversal::Position {
self.traverser.position
}
}

View File

@@ -192,14 +192,15 @@ impl Graph {
// Check if the edge already exists in this direction or to the same target
if let Some(err) = adjacency_list.edges().find_map(|e| {
// If we're not replacing the edge, we don't want to replace an edge that already exists in this direction
if !replace && e.direction == direction {
Some(Err("Edge already exists in this direction."))
} else if e.target == to {
Some(Err("Edge already exists."))
} else {
None
if !replace {
// If we're not replacing the edge, we don't want to replace an edge that already exists in this direction
if e.direction == direction {
return Some(Err("Edge already exists in this direction."));
} else if e.target == to {
return Some(Err("Edge already exists."));
}
}
None
}) {
return err;
}

117
src/entity/item.rs Normal file
View File

@@ -0,0 +1,117 @@
use crate::{
constants,
entity::{collision::Collidable, graph::Graph},
error::{EntityError, GameResult},
texture::sprite::{Sprite, SpriteAtlas},
};
use sdl2::render::{Canvas, RenderTarget};
use strum_macros::{EnumCount, EnumIter};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ItemType {
Pellet,
Energizer,
#[allow(dead_code)]
Fruit {
kind: FruitKind,
},
}
impl ItemType {
pub fn get_score(self) -> u32 {
match self {
ItemType::Pellet => 10,
ItemType::Energizer => 50,
ItemType::Fruit { kind } => kind.get_score(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCount)]
#[allow(dead_code)]
pub enum FruitKind {
Apple,
Strawberry,
Orange,
Melon,
Bell,
Key,
Galaxian,
}
impl FruitKind {
#[allow(dead_code)]
pub fn index(self) -> u8 {
match self {
FruitKind::Apple => 0,
FruitKind::Strawberry => 1,
FruitKind::Orange => 2,
FruitKind::Melon => 3,
FruitKind::Bell => 4,
FruitKind::Key => 5,
FruitKind::Galaxian => 6,
}
}
pub fn get_score(self) -> u32 {
match self {
FruitKind::Apple => 100,
FruitKind::Strawberry => 300,
FruitKind::Orange => 500,
FruitKind::Melon => 700,
FruitKind::Bell => 1000,
FruitKind::Key => 2000,
FruitKind::Galaxian => 3000,
}
}
}
pub struct Item {
pub node_index: usize,
pub item_type: ItemType,
pub sprite: Sprite,
pub collected: bool,
}
impl Item {
pub fn new(node_index: usize, item_type: ItemType, sprite: Sprite) -> Self {
Self {
node_index,
item_type,
sprite,
collected: false,
}
}
pub fn is_collected(&self) -> bool {
self.collected
}
pub fn collect(&mut self) {
self.collected = true;
}
pub fn get_score(&self) -> u32 {
self.item_type.get_score()
}
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, graph: &Graph) -> GameResult<()> {
if self.collected {
return Ok(());
}
let node = graph
.get_node(self.node_index)
.ok_or(EntityError::NodeNotFound(self.node_index))?;
let position = node.position + constants::BOARD_PIXEL_OFFSET.as_vec2();
self.sprite.render(canvas, atlas, position)?;
Ok(())
}
}
impl Collidable for Item {
fn position(&self) -> crate::entity::traversal::Position {
crate::entity::traversal::Position::AtNode(self.node_index)
}
}

Some files were not shown because too many files have changed in this diff Show More