Compare commits

..

22 Commits

Author SHA1 Message Date
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
44 changed files with 410 additions and 525 deletions

View File

@@ -8,5 +8,5 @@ rustflags = [
# "-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']",
# USE_OGG, USE_VORBIS for OGG/VORBIS usage
"-C", "link-args=--preload-file assets/",
]
"-C", "link-args=--preload-file assets/game/",
]

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

@@ -0,0 +1,137 @@
name: Build
on: [push]
permissions:
contents: write
env:
RUST_TOOLCHAIN: 1.86.0
jobs:
build:
name: Build (${{ matrix.target }})
env:
VCPKG_SYSTEM_LIBRARIES: "OFF"
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: ${{ runner.os }}-vcpkg-${{ hashFiles('Cargo.toml', 'Cargo.lock') }}
restore-keys: |
${{ runner.os }}-vcpkg-
- name: Vcpkg Linux Dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y build-essential gettext 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: mymindstorm/setup-emsdk@v14
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,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

4
.gitignore vendored
View File

@@ -3,5 +3,5 @@
.idea
*.dll
rust-sdl2-emscripten/
assets/build.css
emsdk/
assets/site/build.css
emsdk/

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

7
Cargo.lock generated
View File

@@ -77,6 +77,12 @@ dependencies = [
"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"
@@ -171,6 +177,7 @@ name = "pacman"
version = "0.1.0"
dependencies = [
"anyhow",
"glam",
"lazy_static",
"libc",
"once_cell",

View File

@@ -17,6 +17,7 @@ pathfinding = "4.14"
once_cell = "1.21.3"
thiserror = "1.0"
anyhow = "1.0"
glam = "0.30.4"
[target.'cfg(target_os = "windows")'.dependencies.winapi]
version = "0.3"
@@ -34,13 +35,14 @@ default-features = false
features = ["ttf","image","gfx","mixer","static-link","use-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"
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]
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-release" }
[target.'cfg(target_os = "emscripten")'.dependencies]
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

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= */

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

16
build.sh Normal file → Executable file
View File

@@ -52,20 +52,18 @@ else
fi
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"
mkdir -p dist
output_folder="target/wasm32-unknown-emscripten/$build_type"
cp assets/index.html dist
# cp assets/*.woff* dist
cp assets/build.css dist
cp assets/favicon.ico dist
cp $output_folder/pacman.wasm dist
cp $output_folder/pacman.js dist
# only if .data file exists
cp $output_folder/deps/pacman.data dist
cp assets/site/{build.css,favicon.ico,index.html} dist
cp $output_folder/pacman.{wasm,js} dist
if [ -f $output_folder/deps/pacman.data ]; then
cp $output_folder/deps/pacman.data dist
fi
if [ -f $output_folder/pacman.wasm.map ]; then
cp $output_folder/pacman.wasm.map dist
fi

View File

@@ -56,40 +56,47 @@ async function setupEmscripten() {
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 $`cargo build --target=wasm32-unknown-emscripten --release`;
await $`env RUSTFLAGS=${rustcFlags} cargo build --target=wasm32-unknown-emscripten --release`;
} else {
await $`cargo build --target=wasm32-unknown-emscripten`;
await $`env RUSTFLAGS=${rustcFlags} cargo build --target=wasm32-unknown-emscripten`;
}
console.log("Generating CSS...");
await $`pnpx postcss-cli ./assets/styles.scss -o ./assets/build.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/index.html dist`;
await $`cp assets/*.woff* dist`;
await $`cp assets/build.css dist`;
await $`cp assets/favicon.ico dist`;
await $`cp ${outputFolder}/spiritus.wasm dist`;
await $`cp ${outputFolder}/spiritus.js 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}/deps/spiritus.data`);
await $`cp ${outputFolder}/deps/spiritus.data dist`;
await fs.access(`${outputFolder}/pacman.data`);
await $`cp ${outputFolder}/pacman.data dist`;
} catch (e) {
console.log("No spiritus.data file found, skipping copy.");
console.log("No pacman.data file found, skipping copy.");
}
// Check if .map file exists before copying
try {
await fs.access(`${outputFolder}/spiritus.wasm.map`);
await $`cp ${outputFolder}/spiritus.wasm.map dist`;
await fs.access(`${outputFolder}/pacman.wasm.map`);
await $`cp ${outputFolder}/pacman.wasm.map dist`;
} catch (e) {
console.log("No spiritus.wasm.map file found, skipping copy.");
console.log("No pacman.wasm.map file found, skipping copy.");
}
console.log("WASM files copied.");

View File

@@ -123,16 +123,6 @@ impl<'a> AnimatedAtlasTexture<'a> {
}
}
pub fn pause(&mut self) {
self.paused = true;
}
pub fn resume(&mut self) {
self.paused = false;
}
pub fn is_paused(&self) -> bool {
self.paused
}
pub fn set_color_modulation(&mut self, r: u8, g: u8, b: u8) {
self.atlas.set_color_modulation(r, g, b);
}

View File

@@ -31,23 +31,42 @@ pub enum Asset {
// 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/wav/1.ogg")),
Asset::Wav2 => Cow::Borrowed(include_bytes!("../assets/wav/2.ogg")),
Asset::Wav3 => Cow::Borrowed(include_bytes!("../assets/wav/3.ogg")),
Asset::Wav4 => Cow::Borrowed(include_bytes!("../assets/wav/4.ogg")),
Asset::Pacman => Cow::Borrowed(include_bytes!("../assets/32/pacman.png")),
Asset::Pellet => Cow::Borrowed(include_bytes!("../assets/24/pellet.png")),
Asset::Energizer => Cow::Borrowed(include_bytes!("../assets/24/energizer.png")),
Asset::Map => Cow::Borrowed(include_bytes!("../assets/map.png")),
Asset::FontKonami => Cow::Borrowed(include_bytes!("../assets/font/konami.ttf")),
Asset::GhostBody => Cow::Borrowed(include_bytes!("../assets/32/ghost_body.png")),
Asset::GhostEyes => Cow::Borrowed(include_bytes!("../assets/32/ghost_eyes.png")),
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")),
}
};
}
@@ -62,7 +81,7 @@ mod imp {
use std::fs;
use std::path::Path;
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
let path = Path::new("assets").join(asset.path());
let path = Path::new("assets/game").join(asset.path());
if !path.exists() {
return Err(AssetError::NotFound(asset.path().to_string()));
}

View File

@@ -4,6 +4,7 @@ use crate::{
ghosts::blinky::Blinky,
map::Map,
};
use glam::{IVec2, UVec2};
use sdl2::{pixels::Color, render::Canvas, video::Window};
#[derive(PartialEq, Eq, Clone, Copy)]
@@ -17,21 +18,22 @@ pub enum DebugMode {
pub struct DebugRenderer;
impl DebugRenderer {
pub fn draw_cell(canvas: &mut Canvas<Window>, _map: &Map, cell: (u32, u32), color: Color) {
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.0, position.1, 24, 24))
.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: (u32, u32)) {
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((x as i32, y as i32)).unwrap_or(MapTile::Empty);
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 (x, y) == pacman_cell {
Self::draw_cell(canvas, map, (x, y), Color::CYAN);
if cell == pacman_cell {
Self::draw_cell(canvas, map, cell, Color::CYAN);
} else {
color = match tile {
MapTile::Empty => None,
@@ -43,31 +45,28 @@ impl DebugRenderer {
};
}
if let Some(color) = color {
Self::draw_cell(canvas, map, (x, y), color);
Self::draw_cell(canvas, map, cell, color);
}
}
}
}
pub fn draw_next_cell(canvas: &mut Canvas<Window>, map: &Map, next_cell: (u32, u32)) {
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.x, pos.y), Color::RGB(255, 140, 0));
// ORANGE
Self::draw_cell(canvas, map, pos, Color::RGB(255, 140, 0));
}
}
pub fn draw_pathfinding(canvas: &mut Canvas<Window>, blinky: &Blinky, map: &Map) {
if let Some((path, _)) = blinky.get_path_to_target({
let (tx, ty) = blinky.get_target_tile();
(tx as u32, ty as u32)
}) {
for &(x, y) in &path {
Self::draw_cell(canvas, map, (x, y), Color::YELLOW);
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

@@ -4,6 +4,7 @@ use crate::constants::{FruitType, MapTile, BOARD_HEIGHT, BOARD_WIDTH};
use crate::direction::Direction;
use crate::entity::{Entity, Renderable, StaticEntity};
use crate::map::Map;
use glam::{IVec2, UVec2};
use sdl2::{render::Canvas, video::Window};
use std::cell::RefCell;
use std::rc::Rc;
@@ -22,7 +23,7 @@ pub struct Edible<'a> {
}
impl<'a> Edible<'a> {
pub fn new(kind: EdibleKind, cell_position: (u32, u32), sprite: Rc<AtlasTexture<'a>>) -> Self {
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),
@@ -45,8 +46,8 @@ impl<'a> Entity for Edible<'a> {
impl<'a> Renderable for Edible<'a> {
fn render(&self, canvas: &mut Canvas<Window>) {
self.sprite
.render(canvas, self.base.pixel_position, Direction::Right, Some(0));
let pos = self.base.pixel_position;
self.sprite.render(canvas, (pos.x, pos.y), Direction::Right, Some(0));
}
}
@@ -60,14 +61,17 @@ pub fn reconstruct_edibles<'a>(
let mut edibles = Vec::new();
for x in 0..BOARD_WIDTH {
for y in 0..BOARD_HEIGHT {
let tile = map.borrow().get_tile((x as i32, y as i32));
let cell = (x, y);
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, cell, Rc::clone(&pellet_sprite)));
edibles.push(Edible::new(EdibleKind::Pellet, UVec2::new(x, y), Rc::clone(&pellet_sprite)));
}
Some(MapTile::PowerPellet) => {
edibles.push(Edible::new(EdibleKind::PowerPellet, cell, Rc::clone(&power_pellet_sprite)));
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
_ => {}

View File

@@ -4,6 +4,7 @@ use crate::{
map::Map,
modulation::SimpleTickModulator,
};
use glam::{IVec2, UVec2};
use std::cell::RefCell;
use std::rc::Rc;
@@ -14,9 +15,9 @@ pub trait Entity {
/// Returns true if the entity is colliding with the other entity.
fn is_colliding(&self, other: &dyn Entity) -> bool {
let (x, y) = self.base().pixel_position;
let (other_x, other_y) = other.base().pixel_position;
x == other_x && y == other_y
let a = self.base().pixel_position;
let b = other.base().pixel_position;
a == b
}
}
@@ -24,7 +25,7 @@ pub trait Entity {
pub trait Moving {
fn move_forward(&mut self);
fn update_cell_position(&mut self);
fn next_cell(&self, direction: Option<Direction>) -> (i32, i32);
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;
@@ -33,12 +34,12 @@ pub trait Moving {
/// A struct for static (non-moving) entities with position only.
pub struct StaticEntity {
pub pixel_position: (i32, i32),
pub cell_position: (u32, u32),
pub pixel_position: IVec2,
pub cell_position: UVec2,
}
impl StaticEntity {
pub fn new(pixel_position: (i32, i32), cell_position: (u32, u32)) -> Self {
pub fn new(pixel_position: IVec2, cell_position: UVec2) -> Self {
Self {
pixel_position,
cell_position,
@@ -58,8 +59,8 @@ pub struct MovableEntity {
impl MovableEntity {
pub fn new(
pixel_position: (i32, i32),
cell_position: (u32, u32),
pixel_position: IVec2,
cell_position: UVec2,
direction: Direction,
speed: u32,
modulation: SimpleTickModulator,
@@ -76,10 +77,10 @@ impl MovableEntity {
}
/// Returns the position within the current cell, in pixels.
pub fn internal_position(&self) -> (u32, u32) {
(
self.base.pixel_position.0 as u32 % CELL_SIZE,
self.base.pixel_position.1 as u32 % CELL_SIZE,
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,
)
}
}
@@ -94,21 +95,21 @@ impl Moving for MovableEntity {
fn move_forward(&mut self) {
let speed = self.speed as i32;
match self.direction {
Direction::Right => self.base.pixel_position.0 += speed,
Direction::Left => self.base.pixel_position.0 -= speed,
Direction::Up => self.base.pixel_position.1 -= speed,
Direction::Down => self.base.pixel_position.1 += speed,
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 = (
(self.base.pixel_position.0 as u32 / CELL_SIZE) - BOARD_OFFSET.0,
(self.base.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1,
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>) -> (i32, i32) {
fn next_cell(&self, direction: Option<Direction>) -> IVec2 {
let (x, y) = direction.unwrap_or(self.direction).offset();
(self.base.cell_position.0 as i32 + x, self.base.cell_position.1 as i32 + y)
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);
@@ -119,20 +120,20 @@ impl Moving for MovableEntity {
let current_tile = self
.map
.borrow()
.get_tile((self.base.cell_position.0 as i32, self.base.cell_position.1 as i32));
.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.0 == 0 {
self.base.cell_position.0 = BOARD_WIDTH - 2;
self.base.pixel_position = Map::cell_to_pixel((self.base.cell_position.0, self.base.cell_position.1));
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.0 == BOARD_WIDTH - 1 {
self.base.cell_position.0 = 1;
self.base.pixel_position = Map::cell_to_pixel((self.base.cell_position.0, self.base.cell_position.1));
} 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 {
@@ -143,7 +144,7 @@ impl Moving for MovableEntity {
}
}
fn is_grid_aligned(&self) -> bool {
self.internal_position() == (0, 0)
self.internal_position() == UVec2::ZERO
}
fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool {
if new_direction == self.direction {

View File

@@ -3,6 +3,7 @@ use std::cell::RefCell;
use std::ops::Not;
use std::rc::Rc;
use glam::UVec2;
use rand::seq::IteratorRandom;
use sdl2::image::LoadTexture;
use sdl2::keyboard::Keycode;
@@ -65,7 +66,7 @@ impl<'a> Game<'a> {
let pacman_atlas = texture_creator
.load_texture_bytes(&pacman_bytes)
.expect("Could not load pacman texture from asset API");
let pacman = Rc::new(RefCell::new(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 ghost textures
let ghost_body_bytes = get_asset_bytes(Asset::GhostBody).expect("Failed to load asset");
@@ -79,7 +80,7 @@ impl<'a> Game<'a> {
// Create Blinky
let blinky = Blinky::new(
(13, 11), // Starting position just above ghost house
UVec2::new(13, 11), // Starting position just above ghost house
ghost_body,
ghost_eyes,
Rc::clone(&map),
@@ -215,8 +216,8 @@ impl<'a> Game<'a> {
// 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.x, pos.y));
pacman.base.base.cell_position = (pos.x, pos.y);
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;
@@ -225,8 +226,8 @@ impl<'a> Game<'a> {
// Randomize ghost position
if let Some(pos) = valid_positions.iter().choose(&mut rng) {
self.blinky.base.base.pixel_position = Map::cell_to_pixel((pos.x, pos.y));
self.blinky.base.base.cell_position = (pos.x, pos.y);
self.blinky.base.base.pixel_position = Map::cell_to_pixel(*pos);
self.blinky.base.base.cell_position = *pos;
self.blinky.base.in_tunnel = false;
self.blinky.base.direction = Direction::Left;
self.blinky.mode = crate::ghost::GhostMode::Chase;
@@ -308,7 +309,7 @@ impl<'a> Game<'a> {
DebugMode::Grid => {
DebugRenderer::draw_debug_grid(self.canvas, &self.map.borrow(), self.pacman.borrow().base.base.cell_position);
let next_cell = <Pacman as crate::entity::Moving>::next_cell(&*self.pacman.borrow(), None);
DebugRenderer::draw_next_cell(self.canvas, &self.map.borrow(), (next_cell.0 as u32, next_cell.1 as u32));
DebugRenderer::draw_next_cell(self.canvas, &self.map.borrow(), next_cell.as_uvec2());
}
DebugMode::ValidPositions => {
DebugRenderer::draw_valid_positions(self.canvas, &mut self.map.borrow_mut());

View File

@@ -1,4 +1,3 @@
use pathfinding::prelude::dijkstra;
use rand::Rng;
use crate::animation::{AnimatedAtlasTexture, FrameDrawn};
@@ -8,6 +7,7 @@ use crate::entity::{Entity, MovableEntity, Moving, Renderable};
use crate::map::Map;
use crate::modulation::{SimpleTickModulator, TickModulator};
use crate::pacman::Pacman;
use glam::{IVec2, UVec2};
use sdl2::pixels::Color;
use sdl2::render::Texture;
use std::cell::RefCell;
@@ -67,7 +67,7 @@ impl Ghost<'_> {
/// Creates a new ghost instance
pub fn new<'a>(
ghost_type: GhostType,
starting_position: (u32, u32),
starting_position: UVec2,
body_texture: Texture<'a>,
eyes_texture: Texture<'a>,
map: Rc<RefCell<Map>>,
@@ -95,7 +95,7 @@ impl Ghost<'_> {
}
/// Gets the target tile for this ghost based on its current mode
pub fn get_target_tile(&self) -> (i32, i32) {
pub fn get_target_tile(&self) -> IVec2 {
match self.mode {
GhostMode::Scatter => self.get_scatter_target(),
GhostMode::Chase => self.get_chase_target(),
@@ -106,17 +106,17 @@ impl Ghost<'_> {
}
/// Gets this ghost's home corner target for scatter mode
fn get_scatter_target(&self) -> (i32, i32) {
fn get_scatter_target(&self) -> IVec2 {
match self.ghost_type {
GhostType::Blinky => (25, 0), // Top right
GhostType::Pinky => (2, 0), // Top left
GhostType::Inky => (27, 35), // Bottom right
GhostType::Clyde => (0, 35), // Bottom left
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) -> (i32, i32) {
fn get_random_target(&self) -> IVec2 {
let mut rng = rand::rng();
let mut possible_moves = Vec::new();
@@ -143,48 +143,49 @@ impl Ghost<'_> {
}
/// Gets the ghost house target for returning eyes
fn get_house_target(&self) -> (i32, i32) {
(13, 14) // Center of ghost house
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) -> (i32, i32) {
(13, 11) // Just above ghost 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) -> (i32, i32) {
fn get_chase_target(&self) -> IVec2 {
let pacman = self.pacman.borrow();
let cell = pacman.base().cell_position;
(cell.0 as i32, cell.1 as i32)
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: (u32, u32)) -> Option<(Vec<(u32, u32)>, u32)> {
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((p.0 as i32, p.1 as i32));
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.0 == 0 {
successors.push(((BOARD_WIDTH - 2, p.1), 1));
} else if p.0 == BOARD_WIDTH - 1 {
successors.push(((1, p.1), 1));
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 = (p.0 as i32 + dx, p.1 as i32 + dy);
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;
}
successors.push(((next_p.0 as u32, next_p.1 as u32), 1));
let next_u = UVec2::new(next_p.x as u32, next_p.y as u32);
successors.push((next_u, 1));
}
}
successors
@@ -226,12 +227,13 @@ impl Ghost<'_> {
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.0 as u32, target_tile.1 as u32)) {
if let Some((path, _)) = self.get_path_to_target(target_tile.as_uvec2()) {
if path.len() > 1 {
let next_move = path[1];
let (x, y) = self.base.base.cell_position;
let dx = next_move.0 as i32 - x as i32;
let dy = next_move.1 as i32 - y as i32;
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 {
@@ -269,7 +271,7 @@ impl<'a> Moving for Ghost<'a> {
fn update_cell_position(&mut self) {
self.base.update_cell_position();
}
fn next_cell(&self, direction: Option<Direction>) -> (i32, i32) {
fn next_cell(&self, direction: Option<Direction>) -> IVec2 {
self.base.next_cell(direction)
}
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool {
@@ -289,7 +291,7 @@ impl<'a> Moving for Ghost<'a> {
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, Direction::Right, None);
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
@@ -301,6 +303,7 @@ impl<'a> Renderable for Ghost<'a> {
Direction::Down => 3,
}
};
self.eyes_sprite.render(canvas, pos, Direction::Right, Some(eye_frame));
self.eyes_sprite
.render(canvas, (pos.x, pos.y), Direction::Right, Some(eye_frame));
}
}

View File

@@ -9,6 +9,7 @@ use crate::entity::{Entity, Moving, Renderable, StaticEntity};
use crate::ghost::{Ghost, GhostMode, GhostType};
use crate::map::Map;
use crate::pacman::Pacman;
use glam::{IVec2, UVec2};
pub struct Blinky<'a> {
ghost: Ghost<'a>,
@@ -16,7 +17,7 @@ pub struct Blinky<'a> {
impl<'a> Blinky<'a> {
pub fn new(
starting_position: (u32, u32),
starting_position: UVec2,
body_texture: Texture<'a>,
eyes_texture: Texture<'a>,
map: Rc<RefCell<Map>>,
@@ -28,10 +29,10 @@ impl<'a> Blinky<'a> {
}
/// Gets Blinky's chase target - directly targets Pac-Man's current position
fn get_chase_target(&self) -> (i32, i32) {
fn get_chase_target(&self) -> IVec2 {
let pacman = self.ghost.pacman.borrow();
let cell = pacman.base().cell_position;
(cell.0 as i32, cell.1 as i32)
IVec2::new(cell.x as i32, cell.y as i32)
}
pub fn set_mode(&mut self, mode: GhostMode) {
@@ -62,7 +63,7 @@ impl<'a> Moving for Blinky<'a> {
fn update_cell_position(&mut self) {
self.ghost.update_cell_position();
}
fn next_cell(&self, direction: Option<Direction>) -> (i32, i32) {
fn next_cell(&self, direction: Option<Direction>) -> IVec2 {
self.ghost.next_cell(direction)
}
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool {

View File

@@ -1,5 +1,7 @@
//! This module contains helper functions that are used throughout the game.
use glam::UVec2;
/// Checks if two grid positions are adjacent to each other
///
/// # Arguments
@@ -10,11 +12,9 @@
/// # Returns
/// * `true` if positions are adjacent according to the diagonal parameter
/// * `false` otherwise
pub fn is_adjacent(a: (u32, u32), b: (u32, u32), diagonal: bool) -> bool {
let (ax, ay) = a;
let (bx, by) = b;
let dx = ax.abs_diff(bx);
let dy = ay.abs_diff(by);
pub fn is_adjacent(a: UVec2, b: UVec2, diagonal: bool) -> bool {
let dx = a.x.abs_diff(b.x);
let dy = a.y.abs_diff(b.y);
if diagonal {
dx <= 1 && dy <= 1 && (dx != 0 || dy != 0)
} else {
@@ -31,22 +31,22 @@ mod tests {
// Test orthogonal adjacency (diagonal = false)
// 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
assert!(is_adjacent((0, 0), (1, 0), false)); // Right
assert!(is_adjacent((0, 0), (0, 1), false)); // Down
assert!(is_adjacent((1, 1), (0, 1), false)); // Left
assert!(is_adjacent((1, 1), (1, 0), false)); // Up
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(1, 0), false)); // Right
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(0, 1), false)); // Down
assert!(is_adjacent(UVec2::new(1, 1), UVec2::new(0, 1), false)); // Left
assert!(is_adjacent(UVec2::new(1, 1), UVec2::new(1, 0), false)); // Up
// Diagonal positions should be false
assert!(!is_adjacent((0, 0), (1, 1), false));
assert!(!is_adjacent((0, 1), (1, 0), false));
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(1, 1), false));
assert!(!is_adjacent(UVec2::new(0, 1), UVec2::new(1, 0), false));
// Positions more than 1 step away should be false
assert!(!is_adjacent((0, 0), (2, 0), false));
assert!(!is_adjacent((0, 0), (0, 2), false));
assert!(!is_adjacent((0, 0), (2, 2), false));
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(2, 0), false));
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(0, 2), false));
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(2, 2), false));
}
#[test]
@@ -54,48 +54,54 @@ mod tests {
// Test diagonal adjacency (diagonal = true)
// 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
assert!(is_adjacent((0, 0), (1, 0), true)); // Right
assert!(is_adjacent((0, 0), (0, 1), true)); // Down
assert!(is_adjacent((1, 1), (0, 1), true)); // Left
assert!(is_adjacent((1, 1), (1, 0), true)); // Up
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(1, 0), true)); // Right
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(0, 1), true)); // Down
assert!(is_adjacent(UVec2::new(1, 1), UVec2::new(0, 1), true)); // Left
assert!(is_adjacent(UVec2::new(1, 1), UVec2::new(1, 0), true)); // Up
// Diagonal adjacent positions should be true
assert!(is_adjacent((0, 0), (1, 1), true)); // Down-right
assert!(is_adjacent((1, 0), (0, 1), true)); // Down-left
assert!(is_adjacent((0, 1), (1, 0), true)); // Up-right
assert!(is_adjacent((1, 1), (0, 0), true)); // Up-left
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(1, 1), true)); // Down-right
assert!(is_adjacent(UVec2::new(1, 0), UVec2::new(0, 1), true)); // Down-left
assert!(is_adjacent(UVec2::new(0, 1), UVec2::new(1, 0), true)); // Up-right
assert!(is_adjacent(UVec2::new(1, 1), UVec2::new(0, 0), true)); // Up-left
// Positions more than 1 step away should be false
assert!(!is_adjacent((0, 0), (2, 0), true));
assert!(!is_adjacent((0, 0), (0, 2), true));
assert!(!is_adjacent((0, 0), (2, 2), true));
assert!(!is_adjacent((0, 0), (1, 2), true));
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(2, 0), true));
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(0, 2), true));
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(2, 2), true));
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(1, 2), true));
}
#[test]
fn test_edge_cases() {
// Test with larger coordinates
assert!(is_adjacent((100, 100), (101, 100), false));
assert!(is_adjacent((100, 100), (100, 101), false));
assert!(!is_adjacent((100, 100), (102, 100), false));
assert!(is_adjacent(UVec2::new(100, 100), UVec2::new(101, 100), false));
assert!(is_adjacent(UVec2::new(100, 100), UVec2::new(100, 101), false));
assert!(!is_adjacent(UVec2::new(100, 100), UVec2::new(102, 100), false));
assert!(is_adjacent((100, 100), (101, 101), true));
assert!(!is_adjacent((100, 100), (102, 102), true));
assert!(is_adjacent(UVec2::new(100, 100), UVec2::new(101, 101), true));
assert!(!is_adjacent(UVec2::new(100, 100), UVec2::new(102, 102), true));
// Test with zero coordinates
assert!(is_adjacent((0, 0), (1, 0), false));
assert!(is_adjacent((0, 0), (0, 1), false));
assert!(is_adjacent((0, 0), (1, 1), true));
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(1, 0), false));
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(0, 1), false));
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(1, 1), true));
}
#[test]
fn test_commutative_property() {
// The function should work the same regardless of parameter order
assert_eq!(is_adjacent((1, 2), (2, 2), false), is_adjacent((2, 2), (1, 2), false));
assert_eq!(
is_adjacent(UVec2::new(1, 2), UVec2::new(2, 2), false),
is_adjacent(UVec2::new(2, 2), UVec2::new(1, 2), false)
);
assert_eq!(is_adjacent((1, 2), (2, 3), true), is_adjacent((2, 3), (1, 2), true));
assert_eq!(
is_adjacent(UVec2::new(1, 2), UVec2::new(2, 3), true),
is_adjacent(UVec2::new(2, 3), UVec2::new(1, 2), true)
);
}
}

View File

@@ -3,49 +3,9 @@ use rand::seq::IteratorRandom;
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};
use std::ops::Add;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct SignedPosition {
pub x: i32,
pub y: i32,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Position {
pub x: u32,
pub y: u32,
}
impl Add<SignedPosition> for Position {
type Output = Position;
fn add(self, rhs: SignedPosition) -> Self::Output {
Position {
x: (self.x as i32 + rhs.x) as u32,
y: (self.y as i32 + rhs.y) as u32,
}
}
}
impl PartialEq<(u32, u32)> for Position {
fn eq(&self, other: &(u32, u32)) -> bool {
self.x == other.0 && self.y == other.1
}
}
impl Position {
pub fn as_i32(&self) -> (i32, i32) {
(self.x as i32, self.y as i32)
}
pub fn wrapping_add(&self, dx: i32, dy: i32) -> Position {
Position {
x: (self.x as i32 + dx) as u32,
y: (self.y as i32 + dy) as u32,
}
}
}
/// The game map.
///
@@ -104,9 +64,9 @@ impl Map {
/// # Arguments
///
/// * `cell` - The cell coordinates, in grid coordinates.
pub fn get_tile(&self, cell: (i32, i32)) -> Option<MapTile> {
let x = cell.0 as usize;
let y = cell.1 as usize;
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 {
return None;
@@ -121,9 +81,9 @@ impl Map {
///
/// * `cell` - The cell coordinates, in grid coordinates.
/// * `tile` - The tile to set.
pub fn set_tile(&mut self, cell: (i32, i32), tile: MapTile) -> bool {
let x = cell.0 as usize;
let y = cell.1 as usize;
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 {
return false;
@@ -138,15 +98,15 @@ impl Map {
/// # Arguments
///
/// * `cell` - The cell coordinates, in grid coordinates.
pub fn cell_to_pixel(cell: (u32, u32)) -> (i32, i32) {
((cell.0 * CELL_SIZE) as i32, ((cell.1 + BOARD_OFFSET.1) * CELL_SIZE) as i32)
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<Position> {
pub fn get_valid_playable_positions(&mut self) -> &Vec<UVec2> {
use MapTile::*;
static CACHE: OnceCell<Vec<Position>> = OnceCell::new();
static CACHE: OnceCell<Vec<UVec2>> = OnceCell::new();
if let Some(cached) = CACHE.get() {
return cached;
}
@@ -155,10 +115,7 @@ impl Map {
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(Position {
x: x as u32,
y: y as u32,
}),
Pellet | PowerPellet => pellet_positions.push(UVec2::new(x as u32, y as u32)),
_ => {}
}
}
@@ -180,13 +137,8 @@ impl Map {
match self.current[pos.x as usize][pos.y as usize] {
Empty | Pellet | PowerPellet => {
for offset in [
SignedPosition { x: -1, y: 0 },
SignedPosition { x: 1, y: 0 },
SignedPosition { x: 0, y: -1 },
SignedPosition { x: 0, y: 1 },
] {
let neighbor = pos + offset;
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) {
@@ -198,8 +150,8 @@ impl Map {
StartingPosition(_) | Wall | Tunnel => {}
}
}
let mut result: Vec<Position> = visited.into_iter().collect();
result.sort_unstable();
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

@@ -15,6 +15,8 @@ use crate::{
modulation::{SimpleTickModulator, TickModulator},
};
use glam::{IVec2, UVec2};
/// The Pac-Man entity.
pub struct Pacman<'a> {
/// Shared movement and position fields.
@@ -39,7 +41,7 @@ impl<'a> Moving for Pacman<'a> {
fn update_cell_position(&mut self) {
self.base.update_cell_position();
}
fn next_cell(&self, direction: Option<Direction>) -> (i32, i32) {
fn next_cell(&self, direction: Option<Direction>) -> IVec2 {
self.base.next_cell(direction)
}
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool {
@@ -58,7 +60,7 @@ impl<'a> Moving for Pacman<'a> {
impl Pacman<'_> {
/// Creates a new `Pacman` instance.
pub fn new<'a>(starting_position: (u32, u32), atlas: Texture<'a>, map: Rc<RefCell<Map>>) -> Pacman<'a> {
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(
@@ -90,13 +92,13 @@ impl Pacman<'_> {
}
/// Returns the internal position of Pac-Man, rounded down to the nearest even number.
fn internal_position_even(&self) -> (u32, u32) {
let (x, y) = self.base.internal_position();
((x / 2u32) * 2u32, (y / 2u32) * 2u32)
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() == (0, 0);
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) {
@@ -110,7 +112,7 @@ impl Pacman<'_> {
}
if !self.stopped && self.base.modulation.next() {
<Pacman as Moving>::move_forward(self);
if self.internal_position_even() == (0, 0) {
if self.internal_position_even() == UVec2::ZERO {
<Pacman as Moving>::update_cell_position(self);
}
}
@@ -122,9 +124,9 @@ impl Renderable for Pacman<'_> {
let pos = self.base.base.pixel_position;
let dir = self.base.direction;
if self.stopped {
self.sprite.render(canvas, pos, dir, Some(2));
self.sprite.render(canvas, (pos.x, pos.y), dir, Some(2));
} else {
self.sprite.render(canvas, pos, dir, None);
self.sprite.render(canvas, (pos.x, pos.y), dir, None);
}
}
}