Compare commits

..

22 Commits

Author SHA1 Message Date
Ryan Walters
5deccc54a7 ci: setup codecov coverage & badge 2025-09-05 22:41:49 -05:00
Ryan Walters
2455d9724b test: add ttf renderer tests 2025-09-05 21:22:40 -05:00
Ryan Walters
ac7c1b9ce1 test: remove useless/redundant tests 2025-09-05 21:13:53 -05:00
Ryan Walters
d68d76c854 test: improve input & map_builder test coverage 2025-09-05 21:13:48 -05:00
Ryan Walters
f1927cc67e test: general game testing 2025-09-05 20:04:07 -05:00
Ryan Walters
68ab4627d8 test: add asset tests, file exists & has min size 2025-09-05 19:53:56 -05:00
Ryan Walters
0d8d869580 test: blinking system tests 2025-09-05 19:46:52 -05:00
Ryan Walters
a31b85b5df refactor: use speculoos for all test assertions 2025-09-05 19:34:01 -05:00
Ryan Walters
21b08d4866 fix: remove unused BlinkingTexture 2025-09-05 19:32:22 -05:00
Ryan Walters
f075caaa17 refactor: add ticks to DeltaTime, rewrite Blinking system for tick-based calculations with absolute calculations, rewrite Blinking/Direction tests 2025-09-05 19:20:58 -05:00
Ryan Walters
9422168ffc feat: re-implement CustomFormatter to clone Full formatterr 2025-09-05 18:49:38 -05:00
Ryan Walters
35e557e298 feat: enhance profiling with tick-based timing management and zero-padding for skipped frames 2025-09-05 18:49:33 -05:00
Ryan Walters
e810419063 refactor: use welford's algorithm for one-pass avg/std dev. calculations, input logging tweaks 2025-09-05 15:32:06 -05:00
Ryan Walters
f7e7dee28f chore: move ttf context out of game.rs, remove unnecessary window event logging 2025-09-05 15:21:20 -05:00
Ryan Walters
4b0b8f4f2e refactor: reorganize game.rs new() into separate functions 2025-09-05 15:10:15 -05:00
Ryan Walters
03249c88a4 feat: sprite enums for avoiding hardcoded string paths 2025-09-05 15:08:38 -05:00
Ryan Walters
2d4f97e04b fix: use LARGE_SCALE for BatchedLineResource calculations 2025-09-05 14:22:16 -05:00
Ryan Walters
317fce796c feat: measure total system timings using threading indifferent method, padded formatting 2025-09-05 14:22:16 -05:00
Ryan Walters
9832abd131 chore: move BufferedWriter into tracing_buffer.rs 2025-09-05 13:58:59 -05:00
Ryan Walters
c94ebc6b4b feat: special formatting with game tick counter, remove date from tracing formatter 2025-09-05 13:52:19 -05:00
Ryan Walters
8b23c1c7bd fix(ci): allow dead code in buffered_writer & tracing_buffer for desktop non-windows checks 2025-09-04 16:15:11 -05:00
Ryan Walters
5e325a4691 feat: enumerate and display render driver info, increase node id text opacity 2025-09-04 16:12:26 -05:00
376 changed files with 3496 additions and 16563 deletions

View File

@@ -1,23 +1,17 @@
[target.'cfg(target_os = "emscripten")']
rustflags = [
# Stack size is required for this project, it will crash otherwise.
"-C",
"link-args=-sASYNCIFY=1 -sASYNCIFY_STACK_SIZE=8192 -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=--preload-file pacman/assets/game/",
"-C", "link-args=-sASYNCIFY=1 -sASYNCIFY_STACK_SIZE=8192 -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=--preload-file assets/game/",
]
runner = "node"
# despite being semantically identical to `target_os = "linux"`, the `cfg(linux)` syntax is not supported here. Who knows why...
# https://github.com/Xevion/Pac-Man/actions/runs/17596477856
[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",
"-C", "link-arg=-lz",
]

View File

@@ -1,28 +1,5 @@
[profile.default]
fail-fast = false
slow-timeout = { period = "5s", terminate-after = 3 } # max 15 seconds
retries = 1
# CI machines are pretty slow, so we need to increase the timeout
[profile.ci]
slow-timeout = { period = "30s", terminate-after = 4 } # max 2 minutes for slow tests
# Coverage works even slower, so we need to increase the timeout
[profile.coverage]
slow-timeout = { period = "45s", terminate-after = 5 } # max 3.75 minutes for slow tests
status-level = "none"
# Integration tests in SDL2 run serially (may not be required)
[[profile.default.overrides]]
filter = 'test(pacman::game::)'
test-group = 'serial'
# Integration tests run max 4 at a time
[[profile.default.overrides]]
filter = 'test(pacman-server::tests::oauth)'
test-group = 'integration'
[test-groups]
# Ensure serial tests don't run in parallel
serial = { max-threads = 1 }
integration = { max-threads = 4 }

View File

@@ -1,15 +0,0 @@
# Build artifacts
/target
/emsdk
*.exe
/pacman/assets
/assets
# Development files
/.git
/*.md
/Justfile
/bacon.toml
/rust-toolchain.toml
/rustfmt.toml

1
.gitattributes vendored
View File

@@ -1,2 +1 @@
* text=auto eol=lf
scripts/* linguist-detectable=false

View File

@@ -1,86 +1,20 @@
# Dependabot Configuration
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
#
# Strategy:
# - Weekly checks for faster vulnerability detection
# - Separate patch/minor/major updates to prevent blocking
# - Auto-merge patches via GitHub branch protection rules
# - Limit concurrent PRs to avoid spam
version: 2
updates:
# Cargo workspace (all Rust crates)
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 5
ignore:
# Bevy ECS 0.17+ requires API migration
- dependency-name: "bevy_ecs"
versions: ["0.17.x", "0.18.x", "0.19.x"]
# jsonwebtoken 10+ requires crypto backend feature flag
- dependency-name: "jsonwebtoken"
versions: ["10.x", "11.x"]
interval: "monthly"
groups:
rust-patches:
applies-to: "version-updates"
update-types: ["patch"]
rust-minor:
applies-to: "version-updates"
update-types: ["minor"]
rust-major:
applies-to: "version-updates"
update-types: ["major"]
labels:
- "dependencies"
- "rust"
# Frontend (web/)
- package-ecosystem: "npm"
directory: "/web"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 5
groups:
frontend-patches:
applies-to: "version-updates"
update-types: ["patch"]
frontend-minor:
applies-to: "version-updates"
update-types: ["minor"]
frontend-major-framework:
applies-to: "version-updates"
update-types: ["major"]
dependencies:
patterns:
- "react"
- "react-dom"
- "vike"
- "vite"
frontend-major-other:
applies-to: "version-updates"
update-types: ["major"]
exclude-patterns:
- "react"
- "react-dom"
- "vike"
- "vite"
labels:
- "dependencies"
- "frontend"
# GitHub Actions
- "*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 5
interval: "monthly"
groups:
github-actions:
patterns: ["*"]
labels:
- "dependencies"
- "github-actions"
dependencies:
patterns:
- "*"

View File

@@ -1,16 +1,8 @@
name: Builds
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
on: ["push", "pull_request"]
permissions:
contents: read
contents: write
jobs:
build:
@@ -23,11 +15,11 @@ jobs:
target: x86_64-unknown-linux-gnu
artifact_name: pacman
toolchain: 1.86.0
- os: macos-15-intel
- os: macos-13
target: x86_64-apple-darwin
artifact_name: pacman
toolchain: 1.86.0
- os: macos-15
- os: macos-latest
target: aarch64-apple-darwin
artifact_name: pacman
toolchain: 1.86.0
@@ -38,7 +30,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Setup Rust Toolchain
uses: dtolnay/rust-toolchain@master
@@ -49,23 +41,13 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2
- name: Get vcpkg baseline
id: vcpkg_version
shell: bash
run: |
VCPKG_REV=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "pacman") | .metadata.vcpkg.rev // "unknown"')
echo "version=$VCPKG_REV" >> $GITHUB_OUTPUT
working-directory: pacman
- name: Cache vcpkg
uses: actions/cache@v4
with:
path: |
target/vcpkg
~/.cargo/bin/cargo-vcpkg
key: vcpkg-${{ steps.vcpkg_version.outputs.version }}-${{ runner.os }}-${{ matrix.target }}
path: target/vcpkg
key: A-vcpkg-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('Cargo.toml', 'Cargo.lock') }}
restore-keys: |
vcpkg-${{ steps.vcpkg_version.outputs.version }}-${{ runner.os }}-
A-vcpkg-${{ runner.os }}-${{ matrix.target }}-
- name: Vcpkg Linux Dependencies
if: runner.os == 'Linux'
@@ -73,17 +55,13 @@ jobs:
sudo apt-get update
sudo apt-get install -y libltdl-dev
- name: Setup vcpkg
shell: bash
- name: Vcpkg
run: |
cargo install cargo-vcpkg
cargo vcpkg -v build
echo "VCPKG_ROOT=${{ github.workspace }}/target/vcpkg" >> $GITHUB_ENV
working-directory: pacman
- name: Build
run: cargo build --release
working-directory: pacman
- name: Acquire Package Version
id: get_version
@@ -91,12 +69,94 @@ jobs:
run: |
set -euo pipefail # exit on error
echo "version=$(cargo metadata --format-version 1 --no-deps | jq '.packages[0].version' -r)" >> $GITHUB_OUTPUT
working-directory: pacman
- name: Upload Artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: "pacman-${{ steps.get_version.outputs.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
# 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@v5
- name: Setup Emscripten SDK
uses: pyodide/setup-emsdk@v15
with:
version: 3.1.43
actions-cache-folder: "emsdk-cache-b"
- name: Setup Rust (WASM32 Emscripten)
uses: dtolnay/rust-toolchain@master
with:
target: wasm32-unknown-emscripten
toolchain: 1.86.0
- name: Rust Cache
uses: Swatinem/rust-cache@v2
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build with Emscripten
shell: bash
run: |
# Retry mechanism for Emscripten build - only retry on specific hash errors
MAX_RETRIES=3
RETRY_DELAY=30
for attempt in $(seq 1 $MAX_RETRIES); do
echo "Build attempt $attempt of $MAX_RETRIES"
# Capture output and check for specific error while preserving real-time output
if bun run -i web.build.ts 2>&1 | tee /tmp/build_output.log; then
echo "Build successful on attempt $attempt"
break
else
echo "Build failed on attempt $attempt"
# Check if the failure was due to the specific hash error
if grep -q "emcc: error: Unexpected hash:" /tmp/build_output.log; then
echo "::warning::Detected 'emcc: error: Unexpected hash:' error - will retry (attempt $attempt of $MAX_RETRIES)"
if [ $attempt -eq $MAX_RETRIES ]; then
echo "::error::All retry attempts failed. Exiting with error."
exit 1
fi
echo "Waiting $RETRY_DELAY seconds before retry..."
sleep $RETRY_DELAY
# Exponential backoff: double the delay for next attempt
RETRY_DELAY=$((RETRY_DELAY * 2))
else
echo "Build failed but not due to hash error - not retrying"
exit 1
fi
fi
done
- name: Upload Artifact
uses: actions/upload-pages-artifact@v4
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
with:
path: "./dist/"
retention-days: 7
- name: Deploy
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
uses: actions/deploy-pages@v4

View File

@@ -1,64 +0,0 @@
name: Checks
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
RUST_TOOLCHAIN: 1.86.0
jobs:
checks:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: clippy, rustfmt
- name: Rust Cache
uses: Swatinem/rust-cache@v2
- name: Cache vcpkg
uses: actions/cache@v4
with:
path: target/vcpkg
key: A-vcpkg-${{ runner.os }}-${{ hashFiles('Cargo.toml', 'Cargo.lock') }}
restore-keys: |
A-vcpkg-${{ runner.os }}-
- name: Vcpkg Linux Dependencies
run: |
sudo apt-get update
sudo apt-get install -y libltdl-dev
- name: Vcpkg
run: |
cargo install cargo-vcpkg
cargo vcpkg -v build
working-directory: pacman
- name: Run clippy
run: cargo clippy -- -D warnings
working-directory: pacman
- name: Check formatting
run: cargo fmt -- --check
working-directory: pacman
- uses: taiki-e/install-action@cargo-audit
- name: Run security audit
run: cargo audit
working-directory: pacman

View File

@@ -1,23 +1,19 @@
name: Code Coverage
on:
push:
branches:
- master
pull_request:
branches:
- master
on: ["push", "pull_request"]
env:
CARGO_TERM_COLOR: always
RUST_TOOLCHAIN: nightly
RUST_TOOLCHAIN: 1.86.0
jobs:
coverage:
runs-on: ubuntu-latest
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
@@ -45,7 +41,6 @@ jobs:
run: |
cargo install cargo-vcpkg
cargo vcpkg -v build
working-directory: pacman
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: taiki-e/install-action@nextest
@@ -54,11 +49,40 @@ jobs:
- name: Generate coverage report
run: |
just coverage
working-directory: pacman
- name: Coveralls upload
uses: coverallsapp/github-action@v2
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
github-token: ${{ secrets.COVERALLS_REPO_TOKEN }}
path-to-lcov: lcov.info
debug: true
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
- name: Download Coveralls CLI
if: ${{ env.COVERALLS_REPO_TOKEN != '' }}
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
if: ${{ env.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

@@ -1,139 +0,0 @@
name: Deploy to Railway
on:
push:
branches:
- master
workflow_dispatch:
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-deploy:
name: Build and Deploy
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.docker_build.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Emscripten SDK
uses: pyodide/setup-emsdk@v15
with:
version: 3.1.43
actions-cache-folder: "emsdk-cache-b"
- name: Setup Rust (WASM32 Emscripten)
uses: dtolnay/rust-toolchain@master
with:
target: wasm32-unknown-emscripten
toolchain: 1.86.0
- name: Rust Cache
uses: Swatinem/rust-cache@v2
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
# ========== WASM Build ==========
- name: Build WASM with Emscripten
shell: bash
run: |
# Retry mechanism for Emscripten build - only retry on specific hash errors
MAX_RETRIES=3
RETRY_DELAY=30
for attempt in $(seq 1 $MAX_RETRIES); do
echo "Build attempt $attempt of $MAX_RETRIES"
# Capture output and check for specific error while preserving real-time output
if bun run -i pacman/web.build.ts 2>&1 | tee /tmp/build_output.log; then
echo "Build successful on attempt $attempt"
break
else
echo "Build failed on attempt $attempt"
# Check if the failure was due to the specific hash error
if grep -q "emcc: error: Unexpected hash:" /tmp/build_output.log; then
echo "::warning::Detected 'emcc: error: Unexpected hash:' error - will retry (attempt $attempt of $MAX_RETRIES)"
if [ $attempt -eq $MAX_RETRIES ]; then
echo "::error::All retry attempts failed. Exiting with error."
exit 1
fi
echo "Waiting $RETRY_DELAY seconds before retry..."
sleep $RETRY_DELAY
# Exponential backoff: double the delay for next attempt
RETRY_DELAY=$((RETRY_DELAY * 2))
else
echo "Build failed but not due to hash error - not retrying"
exit 1
fi
fi
done
# ========== Docker Build and Push ==========
# Note: Frontend is now built inside Docker using multi-stage build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
file: ./pacman-server/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
GIT_COMMIT_SHA=${{ github.sha }}
# Wait for ghcr.io propagation (paranoid safety)
- name: Wait for registry propagation
run: sleep 5
# Deploy to Railway - separate job to use container properly
deploy:
name: Deploy to Railway
runs-on: ubuntu-latest
needs: build-and-deploy
container: ghcr.io/railwayapp/cli:latest
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
steps:
- name: Generate proxy Dockerfile
run: echo "FROM ghcr.io/xevion/pac-man@${{ needs.build-and-deploy.outputs.digest }}" > Dockerfile
- name: Deploy to Railway
run: railway up --service pac-man

View File

@@ -1,13 +1,6 @@
name: Tests
name: Tests & Checks
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
on: ["push", "pull_request"]
env:
CARGO_TERM_COLOR: always
@@ -19,12 +12,13 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: clippy, rustfmt
- name: Rust Cache
uses: Swatinem/rust-cache@v2
@@ -46,10 +40,19 @@ jobs:
run: |
cargo install cargo-vcpkg
cargo vcpkg -v build
working-directory: pacman
- uses: taiki-e/install-action@nextest
- name: Run nextest
run: cargo nextest run --workspace --profile ci
working-directory: pacman
run: cargo nextest run --workspace
- name: Run clippy
run: cargo clippy -- -D warnings
- name: Check formatting
run: cargo fmt -- --check
- uses: taiki-e/install-action@cargo-audit
- name: Run security audit
run: cargo audit

17
.gitignore vendored
View File

@@ -1,36 +1,21 @@
# IDE, Other files
.vscode
.idea
.claude/
rust-sdl2-emscripten/
# Build files
target/
dist/
emsdk/
node_modules/
# Emscripten build outputs (generated by cargo build)
web/public/pacman.data
web/public/pacman.js
web/public/pacman.wasm
web/public/pacman.wasm.map
# Site build f iles
tailwindcss-*
pacman/assets/site/build.css
assets/site/build.css
# Coverage reports
lcov.info
codecov.json
coverage.html
# Profiling output
flamegraph.svg
/profile.*
# Logs
*.log
# Sensitive
*.env

View File

@@ -12,13 +12,6 @@ repos:
- id: forbid-submodules
- id: mixed-line-ending
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v4.2.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
args: []
- repo: local
hooks:
- id: cargo-fmt
@@ -27,17 +20,15 @@ repos:
language: system
types: [rust]
pass_filenames: false
- id: cargo-check
name: cargo check
entry: cargo check --workspace --all-targets
entry: cargo check --all-targets
language: system
types_or: [rust, cargo, cargo-lock]
pass_filenames: false
- id: cargo-check-wasm
name: cargo check for wasm32-unknown-emscripten
entry: cargo check -p pacman --all-targets --target=wasm32-unknown-emscripten
entry: cargo check --all-targets --target=wasm32-unknown-emscripten
language: system
types_or: [rust, cargo, cargo-lock]
pass_filenames: false

View File

@@ -1,196 +0,0 @@
import { $ } from "bun";
import { readFileSync, writeFileSync, existsSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { createInterface } from "readline";
// Constants for container and volume names
const CONTAINER_NAME = "pacman-server-postgres";
const VOLUME_NAME = "pacman-postgres-data";
// Helper function to get user input
async function getUserChoice(
prompt: string,
choices: string[],
defaultIndex: number = 1
): Promise<string> {
// Check if we're in an interactive TTY
if (!process.stdin.isTTY) {
console.log(
"Non-interactive environment detected; selecting default option " +
defaultIndex
);
return String(defaultIndex);
}
console.log(prompt);
choices.forEach((choice, index) => {
console.log(`${index + 1}. ${choice}`);
});
// Use readline for interactive input
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
const askForChoice = () => {
rl.question("Enter your choice (1-3): ", (answer) => {
const choice = answer.trim();
if (["1", "2", "3"].includes(choice)) {
rl.close();
resolve(choice);
} else {
console.log("Invalid choice. Please enter 1, 2, or 3.");
askForChoice();
}
});
};
askForChoice();
});
}
// Get repository root path from script location
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const repoRoot = join(__dirname, "..");
const envPath = join(repoRoot, "pacman-server", ".env");
console.log("Checking for .env file...");
// Check if .env file exists and read it
let envContent = "";
let envLines: string[] = [];
let databaseUrlLine = -1;
let databaseUrlValue = "";
if (existsSync(envPath)) {
console.log("Found .env file, reading...");
envContent = readFileSync(envPath, "utf-8");
envLines = envContent.split("\n");
// Parse .env file for DATABASE_URL
for (let i = 0; i < envLines.length; i++) {
const line = envLines[i].trim();
if (line.match(/^[A-Z_][A-Z0-9_]*=.*$/)) {
if (line.startsWith("DATABASE_URL=")) {
databaseUrlLine = i;
databaseUrlValue = line.substring(13); // Remove "DATABASE_URL="
break;
}
}
}
} else {
console.log("No .env file found, will create one");
}
// Determine user's choice
let userChoice = "2"; // Default to print
if (databaseUrlLine !== -1) {
console.log(`Found existing DATABASE_URL: ${databaseUrlValue}`);
userChoice = await getUserChoice("\nChoose an action:", [
"Quit",
"Print (create container, print DATABASE_URL)",
"Replace (update DATABASE_URL in .env)",
]);
if (userChoice === "1") {
console.log("Exiting...");
process.exit(0);
}
} else {
console.log("No existing DATABASE_URL found");
// Ask what to do when no .env file or DATABASE_URL exists
if (!existsSync(envPath)) {
userChoice = await getUserChoice(
"\nNo .env file found. What would you like to do?",
[
"Print (create container, print DATABASE_URL)",
"Create .env file and add DATABASE_URL",
"Quit",
]
);
if (userChoice === "3") {
console.log("Exiting...");
process.exit(0);
}
} else {
console.log("Will add DATABASE_URL to existing .env file");
}
}
// Check if container exists
console.log("Checking for existing container...");
const containerExists =
await $`docker ps -a --filter name=${CONTAINER_NAME} --format "{{.Names}}"`
.text()
.then((names) => names.trim() === CONTAINER_NAME)
.catch(() => false);
let shouldReplaceContainer = false;
if (containerExists) {
console.log("Container already exists");
// Always ask what to do if container exists
const replaceChoice = await getUserChoice(
"\nContainer exists. What would you like to do?",
["Use existing container", "Replace container (remove and create new)"],
1
);
shouldReplaceContainer = replaceChoice === "2";
if (shouldReplaceContainer) {
console.log("Removing existing container...");
await $`docker rm --force --volumes ${CONTAINER_NAME}`;
// Explicitly remove the named volume to ensure clean state
console.log("Removing volume...");
await $`docker volume rm ${VOLUME_NAME}`.catch(() => {
console.log("Volume doesn't exist or already removed");
});
} else {
console.log("Using existing container");
}
}
// Create container if needed
if (!containerExists || shouldReplaceContainer) {
console.log("Creating PostgreSQL container...");
await $`docker run --detach --name ${CONTAINER_NAME} --publish 5432:5432 --volume ${VOLUME_NAME}:/var/lib/postgresql/data --env POSTGRES_USER=postgres --env POSTGRES_PASSWORD=postgres --env POSTGRES_DB=pacman-server postgres:17`;
}
// Format DATABASE_URL
const databaseUrl =
"postgresql://postgres:postgres@127.0.0.1:5432/pacman-server";
// Handle the final action based on user choice
if (userChoice === "2") {
// Print option
console.log(`\nDATABASE_URL=${databaseUrl}`);
} else if (
userChoice === "3" ||
(databaseUrlLine === -1 && userChoice === "2")
) {
// Replace or add to .env file
if (databaseUrlLine !== -1) {
// Replace existing line
console.log("Updating DATABASE_URL in .env file...");
envLines[databaseUrlLine] = `DATABASE_URL=${databaseUrl}`;
writeFileSync(envPath, envLines.join("\n"));
console.log("Updated .env file");
} else {
// Add new line
console.log("Adding DATABASE_URL to .env file...");
const newContent =
envContent +
(envContent.endsWith("\n") ? "" : "\n") +
`DATABASE_URL=${databaseUrl}\n`;
writeFileSync(envPath, newContent);
console.log("Added to .env file");
}
}

5172
Cargo.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
[workspace]
members = ["pacman", "pacman-common", "pacman-server"]
resolver = "2"
[workspace.package]
[package]
name = "pacman"
version = "0.2.0"
authors = ["Xevion"]
edition = "2021"
rust-version = "1.86.0"
description = "A cross-platform retro Pac-Man clone, written in Rust and supported by SDL2"
readme = true
homepage = "https://pacman.xevion.dev"
repository = "https://github.com/Xevion/Pac-Man"
@@ -12,13 +12,66 @@ license = "GPL-3.0-or-later"
keywords = ["game", "pacman", "arcade", "sdl2"]
categories = ["games", "emulators"]
publish = false
exclude = ["/assets/unpacked/**", "/assets/site/**", "/bacon.toml", "/Justfile"]
default-run = "pacman"
[profile.dev]
incremental = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
# Improve build times by optimizing sqlx-macros
[profile.dev.package.sqlx-macros]
opt-level = 3
[dependencies]
bevy_ecs = "0.16.1"
glam = "0.30.5"
pathfinding = "4.14"
tracing = { version = "0.1.41", features = ["max_level_debug", "release_max_level_debug"]}
tracing-error = "0.2.0"
tracing-subscriber = {version = "0.3.20", features = ["env-filter"]}
time = { version = "0.3.43", features = ["formatting", "macros"] }
thiserror = "2.0.16"
anyhow = "1.0"
smallvec = "1.15.1"
bitflags = "2.9.4"
micromap = "0.1.0"
circular-buffer = "1.1.0"
parking_lot = "0.12.3"
strum = "0.27.2"
strum_macros = "0.27.2"
thousands = "0.2.0"
num-width = "0.1.0"
# While not actively used in code, `build.rs` generates code that relies on this. Keep the versions synchronized.
phf = { version = "0.13.1", features = ["macros"] }
# Windows-specific dependencies
[target.'cfg(target_os = "windows")'.dependencies]
# Used for customizing console output on Windows; both are required due to the `windows` crate having poor Result handling with `GetStdHandle`.
windows = { version = "0.61.3", features = ["Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console"] }
windows-sys = { version = "0.60.2", features = ["Win32_System_Console"] }
# Desktop-specific dependencies
[target.'cfg(not(target_os = "emscripten"))'.dependencies]
# On desktop platforms, build SDL2 with cargo-vcpkg
sdl2 = { version = "0.38", default-features = false, features = ["image", "ttf", "gfx", "mixer", "unsafe_textures", "static-link", "use-vcpkg"] }
rand = { version = "0.9.2", default-features = false, features = ["thread_rng"] }
spin_sleep = "1.3.2"
# Browser-specific dependencies
[target.'cfg(target_os = "emscripten")'.dependencies]
# On Emscripten, we don't use cargo-vcpkg
sdl2 = { version = "0.38", default-features = false, features = ["image", "ttf", "gfx", "mixer", "unsafe_textures"] }
# TODO: Document why Emscripten cannot use `os_rng`.
rand = { version = "0.9.2", default-features = false, features = ["small_rng", "os_rng"] }
libc = "0.2.175" # TODO: Describe why this is required.
[dev-dependencies]
pretty_assertions = "1.4.1"
speculoos = "0.13.0"
[build-dependencies]
phf = { version = "0.13.1", features = ["macros"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.143"
# phf generates runtime code which machete will not detect
[package.metadata.cargo-machete]
ignored = ["phf"]
# Release profile for profiling (essentially the default 'release' profile with debug enabled)
[profile.profile]
@@ -27,8 +80,7 @@ debug = true
# Undo the customizations for our release profile
opt-level = 3
lto = false
panic = "abort"
strip = "symbols"
panic = 'unwind'
# Optimized release profile for size
[profile.release]
@@ -36,8 +88,13 @@ opt-level = "z"
lto = true
panic = "abort"
# This profile is intended to appear as a 'release' profile to the build system due to`debug_assertions = false`,
# but it will compile faster without optimizations. Useful for rapid testing of release-mode logic.
[profile.dev-release]
inherits = "dev"
debug-assertions = false
[package.metadata.vcpkg]
dependencies = ["sdl2", "sdl2-image", "sdl2-ttf", "sdl2-gfx", "sdl2-mixer"]
git = "https://github.com/microsoft/vcpkg"
rev = "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" }
x86_64-unknown-linux-gnu = { triplet = "x64-linux" }
x86_64-apple-darwin = { triplet = "x64-osx" }
aarch64-apple-darwin = { triplet = "arm64-osx" }

View File

@@ -1,79 +1,45 @@
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|bin\\\\.+\\.rs|main\\.rs|platform\\\\desktop\\.rs|platform\\\\tracing_buffer\\.rs|platform\\\\buffered_writer\\.rs|systems\\\\debug\\.rs|systems\\\\profiling\\.rs"
binary_extension := if os() == "windows" { ".exe" } else { "" }
# Display available recipes
default:
just --list
# !!! --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
# Open HTML coverage report
# Generate HTML report (for humans, source line inspection)
html: coverage
cargo llvm-cov report \
# prevents the absolute path from being used in the generated report
--remap-path-prefix \
--html \
--open
cargo llvm-cov report \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
--html \
--open
# Display coverage report
# Display report (for humans)
report-coverage: coverage
cargo llvm-cov report --remap-path-prefix
cargo llvm-cov report \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}"
# Generate baseline LCOV report
# Run & generate report (for CI)
coverage:
cargo +nightly llvm-cov \
--lcov \
--remap-path-prefix \
--workspace \
--output-path lcov.info \
--profile coverage \
--no-fail-fast nextest
cargo llvm-cov \
--lcov \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
--output-path lcov.info \
--profile coverage \
--no-fail-fast nextest
# Profile the project using samply
# Profile the project using 'samply'
samply:
cargo build --profile profile
samply record ./target/profile/pacman{{ binary_extension }}
cargo build --profile profile
samply record ./target/profile/pacman{{ binary_extension }}
# Build the project for Emscripten
web *args:
bun run pacman/web.build.ts {{args}}
bun run --cwd web build
caddy file-server --root web/dist/client
# Fix linting errors & formatting
fix:
cargo fix --workspace --lib --allow-dirty
cargo fmt --all
# Push commits & tags
push:
git push origin --tags;
git push
# Create a postgres container for the server
server-postgres:
bun run .scripts/postgres.ts
# Build the server image
server-image:
# build the server image
docker build \
--platform linux/amd64 \
--file ./pacman-server/Dockerfile \
--tag pacman-server \
.
# Build and run the server in a Docker container
run-server: server-image
# remove the server container if it exists
docker rm --force --volumes pacman-server
# run the server container
docker run \
--rm \
--stop-timeout 2 \
--name pacman-server \
--publish 3000:3000 \
--env PORT=3000 \
--env-file pacman-server/.env \
pacman-server
bun run web.build.ts {{args}};
caddy file-server --root dist

120
README.md
View File

@@ -1,95 +1,80 @@
<!-- markdownlint-disable MD033 -->
<!-- markdownlint-disable MD041 -->
<div align="center">
<img src="assets/banner.png" alt="Pac-Man Banner Screenshot">
</div>
# Pac-Man
[![A project just for fun, no really!][badge-justforfunnoreally]][justforfunnoreally] ![Built with Rust][badge-built-with-rust] [![Build Status][badge-build]][build] [![Tests Status][badge-test]][test] [![Checks Status][badge-checks]][checks] [![If you're seeing this, Coveralls.io is broken again and it's not my fault.][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo]
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![Code Coverage][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
[badge-built-with-rust]: https://img.shields.io/badge/Built_with-Rust-blue?logo=rust
[badge-justforfunnoreally]: https://img.shields.io/badge/justforfunnoreally-dev-9ff
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg
[badge-checks]: https://github.com/Xevion/Pac-Man/actions/workflows/checks.yaml/badge.svg
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg
[badge-coverage]: https://coveralls.io/repos/github/Xevion/Pac-Man/badge.svg?branch=master
[badge-online-demo]: https://img.shields.io/badge/Online%20Demo-Click%20Me!-brightgreen
[justforfunnoreally]: https://justforfunnoreally.dev
[badge-coverage]: https://codecov.io/github/Xevion/Pac-Man/branch/master/graph/badge.svg?token=R2RBYUQK3I
[badge-demo]: https://img.shields.io/github/deployments/Xevion/Pac-Man/github-pages?label=GitHub%20Pages
[badge-online-demo]: https://img.shields.io/badge/GitHub%20Pages-Demo-brightgreen
[badge-last-commit]: https://img.shields.io/github/last-commit/Xevion/Pac-Man
[build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml
[test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml
[checks]: https://github.com/Xevion/Pac-Man/actions/workflows/checks.yaml
[coverage]: https://coveralls.io/github/Xevion/Pac-Man?branch=master
[coverage]: https://codecov.io/github/Xevion/Pac-Man
[demo]: https://xevion.github.io/Pac-Man/
[commits]: https://github.com/Xevion/Pac-Man/commits/master
A faithful recreation of the classic Pac-Man arcade game, written in Rust.
This project aims to replicate the original game's mechanics, graphics, sound, and behavior as accurately as possible while providing modern development features like cross-platform compatibility and WebAssembly support.
A faithful recreation of the classic Pac-Man arcade game written in Rust. This project aims to replicate the original game's mechanics, graphics, sound, and behavior as accurately as possible while providing modern development features like cross-platform compatibility and WebAssembly support.
The game includes all the original features you'd expect from Pac-Man:
- [x] Classic maze navigation with tunnels and dot collection
- [x] Classic maze navigation and dot collection
- [ ] Four ghosts with their unique AI behaviors (Blinky, Pinky, Inky, and Clyde)
- [x] Power pellets that allow Pac-Man to eat ghosts
- [x] Fruit bonuses that appear periodically
- [ ] Power pellets that allow Pac-Man to eat ghosts
- [ ] Fruit bonuses that appear periodically
- [ ] Progressive difficulty with faster ghosts and shorter power pellet duration
- [x] Authentic sound effects and sprites
This cross-platform implementation is built with SDL2 for graphics, audio, and input handling. It can run on Windows, Linux, macOS, even web browsers via WebAssembly.
## Quick Start
The easiest way to play is to visit the [online demo][demo]. It is more or less identical to the desktop experience at this time.
While I do plan to have desktop builds released automatically, the game is still a work in progress, and I'm not quite ready to start uploading releases.
However, every commit has build artifacts, so you can grab the [latest build artifacts][build-workflow] if available.
## Screenshots
<div align="center">
<img src="assets/screenshots/0.png" alt="Screenshot 0 - Starting Game">
<p><em>Starting a new game</em></p>
<img src="assets/screenshots/1.png" alt="Screenshot 1 - Eating Dots">
<p><em>Pac-Man collecting dots and avoiding ghosts</em></p>
<img src="assets/screenshots/2.png" alt="Screenshot 2 - Game Over">
<p><em>Game over screen after losing all lives</em></p>
<img src="assets/screenshots/3.png" alt="Screenshot 3 - Debug Mode">
<p><em>Debug mode showing hitboxes, node graph, and performance details.</em></p>
</div>
This cross-platform implementation is built with SDL2 for graphics, audio, and input handling. It can run on Windows, Linux, macOS, and in web browsers via WebAssembly.
## Why?
[Just for fun.][justforfunnoreally] And because I wanted to learn more about Rust, inter-operability with C, and compiling to WebAssembly.
Just because. And because I wanted to learn more about Rust, inter-operability with C, and compiling to WebAssembly.
Originally, I was inspired by a certain code review video on YouTube; [SOME UNIQUE C++ CODE // Pacman Clone Code Review](https://www.youtube.com/watch?v=OKs_JewEeOo). For some reason, I was inspired to try and replicate it in Rust, and it was uniquely challenging. It's not easy to integrate SDL2 with Rust, and even harder to get it working with Emscripten.
I was inspired by a certain code review video on YouTube; [SOME UNIQUE C++ CODE // Pacman Clone Code Review](https://www.youtube.com/watch?v=OKs_JewEeOo) by The Cherno.
I wanted to hit a lot of goals and features, making it a 'perfect' project that I could be proud of.
For some reason, I was inspired to try and replicate it in Rust, and it was uniquely challenging.
- Near-perfect replication of logic, scoring, graphics, sound, and behaviors. No hacks, workarounds, or poor designs. Well documented, well-tested, and maintainable.
- Written in Rust, buildable on Windows, Linux, Mac and WebAssembly. Statically linked, no runtime dependencies, automatically built with GitHub Actions.
I wanted to hit a log of goals and features, making it a 'perfect' project that I could be proud of.
- Near-perfect replication of logic, scoring, graphics, sound, and behaviors. No hacks, workarounds, or poor designs.
- Written in Rust, buildable on Windows, Linux, Mac and WebAssembly. Statically linked, no runtime dependencies.
- Performant, low memory, CPU and GPU usage.
- Online demo, playable in a browser, built automatically with GitHub Actions.
- Online demo, playable in a browser.
- Completely automatic build system with releases for all platforms.
- Well documented, well-tested, and maintainable.
If you're curious about the journey of this project, you can read the [story](STORY.md) file. Eventually, I will be using this as the basis for some sort of blog post or more official page, but for now, I'm keeping it within the repository as a simple file.
## Experimental Ideas
## Roadmap
You can read the [roadmap](ROADMAP.md) file for more details on the project's goals and future plans.
- Debug tooling
- Game state visualization
- Game speed controls + pausing
- Log tracing
- Performance details
- Customized Themes & Colors
- Color-blind friendly
- Perfected Ghost Algorithms
- More than 4 ghosts
- Custom Level Generation
- Multi-map tunnelling
- Online Scoreboard
- An online axum server with a simple database and OAuth2 authentication.
- Integrates with GitHub, Discord, and Google OAuth2 to acquire an email identifier & avatar.
- Avatars are optional for score submission and can be disabled, instead using a blank avatar.
- Avatars are downscaled to a low resolution pixellated image to maintain the 8-bit aesthetic.
- A custom name is used for the score submission, which is checked for potential abusive language.
- A max length of 14 characters, and a min length of 3 characters.
- Names are checked for potential abusive language via an external API.
- The client implementation should require zero configuration, environment variables, or special secrets.
- It simply defaults to the pacman server API, or can be overriden manually.
## Build Notes
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.
- Install `cargo-vcpkg` with `cargo install cargo-vcpkg`, then run `cargo vcpkg build --manifest-path pacman/Cargo.toml` to build the requisite dependencies via vcpkg.
- `--manifest-path` is only required if you run it from the root directory; you can omit it if you `cd` into the `pacman` directory first.
- This is only required for the desktop builds, not the web build.
- 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.
- I'm still not sure _why_ 3.1.43 is required, but it is. Perhaps in the future I will attempt to use a more modern version.
@@ -102,18 +87,3 @@ Since this project is still in progress, I'm only going to cover non-obvious bui
- `caddy file-server --root dist` (install with `[sudo apt|brew|choco] install caddy` or [a dozen other ways](https://caddyserver.com/docs/install))
- `web.build.ts` auto installs dependencies, but you may need to pass `-i` or `--install=fallback|force` to install missing packages. My guess is that if you have some packages installed, it won't install any missing ones. If you have no packages installed, it will install all of them.
- If you want to have TypeScript resolution for development, you can manually install the dependencies with `bun install` in the `assets/site` folder.
## Contributing
Contributions are welcome! Please feel free to submit a pull request or open an issue.
- The code is not exactly stable or bulletproof, but it is functional and has a lot of tests.
- I am not actively looking for contributors, but I will review pull requests and merge them if they are useful.
- If you have any ideas, please feel free to submit an issue.
- If you have any private issues, security concerns, or anything sensitive, you can email me at [xevion@xevion.dev](mailto:xevion@xevion.dev).
## License
This project is licensed under the GPLv3 license. See the [LICENSE](LICENSE) file for details.
[build-workflow]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml

View File

@@ -1,164 +0,0 @@
# Roadmap
A comprehensive list of features needed to complete the Pac-Man emulation, organized by priority and implementation complexity.
## Core Game Features
### Ghost AI & Behavior
- [x] Core Ghost System Architecture
- [x] Ghost entity types (Blinky, Pinky, Inky, Clyde)
- [x] Ghost state management (Normal, Frightened, Eyes)
- [x] Ghost movement and pathfinding systems
- [ ] Authentic Ghost AI Personalities
- [ ] Blinky (Red): Direct chase behavior
- [ ] Pinky (Pink): Target 4 tiles ahead of Pac-Man
- [ ] Inky (Cyan): Complex behavior based on Blinky's position
- [ ] Clyde (Orange): Chase when far, flee when close
- [x] Mode Switching System
- [ ] Scatter/Chase pattern with proper timing
- [x] Frightened mode transitions
- [ ] Ghost house entry/exit mechanics
- [x] Ghost House Behavior
- [x] Proper spawning sequence
- [ ] Exit timing and patterns
- [ ] House-specific movement rules
### Fruit Bonus System
- [x] Fruit Spawning Mechanics
- [x] Spawn at pellet counts 70 and 170
- [x] Fruit display in bottom-right corner
- [x] Fruit collection and scoring
- [x] Bonus point display system
### Level Progression
- [ ] Multiple Levels
- [ ] Level completion detection
- [ ] Progressive difficulty scaling
- [ ] Ghost speed increases per level
- [ ] Power pellet duration decreases
- [ ] Intermission Screens
- [ ] Between-level cutscenes
- [ ] Proper graphics and timing
### Audio System Completion
- [x] Core Audio Infrastructure
- [x] Audio event system
- [x] Sound effect playback
- [x] Audio muting controls
- [ ] Background Music
- [x] Intro jingle
- [ ] Continuous gameplay music
- [ ] Escalating siren based on remaining pellets
- [ ] Power pellet mode music
- [ ] Intermission music
- [x] Sound Effects
- [x] Pellet eating sounds
- [x] Fruit collection sounds
- [x] Ghost eaten sounds
- [x] Pac-Man Death
- [ ] Ghost movement sounds
- [ ] Level completion fanfare
### Game Mechanics
- [ ] Bonus Lives
- [ ] Extra life at 10,000 points
- [x] Life counter display
- [ ] High Score System
- [ ] High score tracking
- [x] High score display
- [ ] Score persistence
## Secondary Features (Medium Priority)
### Game Polish
- [x] Core Input System
- [x] Keyboard controls
- [x] Direction buffering for responsive controls
- [x] Touch controls for mobile
- [x] Pause System
- [x] Pause/unpause functionality
- [ ] Pause menu with options
- [ ] Input System
- [ ] Input remapping
- [ ] Multiple input methods
## Advanced Features (Lower Priority)
### Difficulty Options
- [ ] Easy/Normal/Hard modes
- [ ] Customizable ghost speeds
### Data Persistence
- [ ] High Score Persistence
- [ ] Save high scores to file
- [ ] High score table display
- [ ] Settings Storage
- [ ] Save user preferences
- [ ] Audio/visual settings
- [ ] Statistics Tracking
- [ ] Game statistics
- [ ] Achievement system
### Debug & Development Tools
- [x] Performance details
- [x] Core Debug Infrastructure
- [x] Debug mode toggle
- [x] Comprehensive game event logging
- [x] Performance profiling tools
- [ ] Game State Visualization
- [ ] Ghost AI state display
- [ ] Pathfinding visualization
- [ ] Collision detection display
- [ ] Game Speed Controls
- [ ] Variable game speed for testing
- [ ] Frame-by-frame stepping
## Customization & Extensions
### Visual Customization
- [x] Core Rendering System
- [x] Sprite-based rendering
- [x] Layered rendering system
- [x] Animation system
- [x] HUD rendering
- [ ] Display Options
- [x] Fullscreen support
- [x] Window resizing
- [ ] Pause while resizing (SDL2 limitation mitigation)
- [ ] Multiple resolution support
### Gameplay Extensions
- [ ] Advanced Ghost AI
- [ ] Support for >4 ghosts
- [ ] Custom ghost behaviors
- [ ] Level Generation
- [ ] Custom level creation
- [ ] Multi-map tunneling
- [ ] Level editor
## Online Features (Future)
### Scoreboard System
- [ ] Backend Infrastructure
- [ ] Axum server with database
- [ ] OAuth2 authentication
- [ ] GitHub/Discord/Google auth
- [ ] Profile Features
- [ ] Optional avatars (8-bit aesthetic)
- [ ] Custom names (3-14 chars, filtered)
- [ ] Client Implementation
- [ ] Zero-config client
- [ ] Default API endpoint
- [ ] Manual override available

View File

@@ -31,7 +31,7 @@ WebAssembly.
The problem is that much of this work was done for pure-Rust applications - and SDL is C++.
This requires a C++ WebAssembly compiler such as Emscripten; and it's a pain to get working.
Luckily though, someone else has done this before, and they fully documented it - [RuggRouge][ruggrogue].
Luckily though, someone else has done this before, and they fully documented it - [RuggRouge][ruggrouge].
- Built with Rust
- Uses SDL2
@@ -92,7 +92,7 @@ This was weird, and honestly, I'm confused as to why the 2-year old sample code
After a bit of time, I noted that the `Instant` times were printing with only the whole seconds changing, and the nanoseconds were always 0.
```rust
```
Instant { tv_sec: 0, tv_nsec: 0 }
Instant { tv_sec: 1, tv_nsec: 0 }
Instant { tv_sec: 2, tv_nsec: 0 }
@@ -357,7 +357,7 @@ Doing so required a full re-work of the animation and texture system, and I ende
So, I ended up using `unsafe` to forcibly cast the lifetimes to `'static`, which was a bit of a gamble, but given that they essentially behave as `'static` in practice, there wasn't much risk as I see it. I might re-look into my understanding of lifetimes and this in the future, but for the time being, it's a good solution that makes the codebase far easier to work with.
## Implementing Cross-platform Builds for Pac-Man
## Cross-platform Builds
Since the original `rust-sdl2-emscripten` demo project had cross-platform builds, I was ready to get it working for this project. For the most part, it wasn't hard, things tended to click into place, but unfortunately, the `emscripten` os target and somehow, the `linux` os target were both failing.
@@ -412,8 +412,8 @@ The bigger downside was that I had to toss out almost all the existing code for
This ended up being okay though, as I was able to clean up a lot of gross code, and the system ended up being easier to work with by comparison.
[code-review-thumbnail]: https://img.youtube.com/vi/OKs_JewEeOo/hqdefault.jpg
[code-review-video]: https://www.youtube.com/watch?v=OKs_JewEeOo
[code-review-thumbnail]: https://img.youtube.com/vi/OKs_JewEeOo/hqdefault.jpg
[fighting-lifetimes-1]: https://devcry.heiho.net/html/2022/20220709-rust-and-sdl2-fighting-with-lifetimes.html
[fighting-lifetimes-2]: https://devcry.heiho.net/html/2022/20220716-rust-and-sdl2-fighting-with-lifetimes-2.html
[fighting-lifetimes-3]: https://devcry.heiho.net/html/2022/20220724-rust-and-sdl2-fighting-with-lifetimes-3.html

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 120 B

After

Width:  |  Height:  |  Size: 120 B

View File

Before

Width:  |  Height:  |  Size: 120 B

After

Width:  |  Height:  |  Size: 120 B

View File

Before

Width:  |  Height:  |  Size: 116 B

After

Width:  |  Height:  |  Size: 116 B

View File

Before

Width:  |  Height:  |  Size: 115 B

After

Width:  |  Height:  |  Size: 115 B

View File

Before

Width:  |  Height:  |  Size: 192 B

After

Width:  |  Height:  |  Size: 192 B

View File

Before

Width:  |  Height:  |  Size: 187 B

After

Width:  |  Height:  |  Size: 187 B

View File

Before

Width:  |  Height:  |  Size: 196 B

After

Width:  |  Height:  |  Size: 196 B

View File

Before

Width:  |  Height:  |  Size: 215 B

After

Width:  |  Height:  |  Size: 215 B

View File

Before

Width:  |  Height:  |  Size: 107 B

After

Width:  |  Height:  |  Size: 107 B

View File

Before

Width:  |  Height:  |  Size: 189 B

After

Width:  |  Height:  |  Size: 189 B

View File

Before

Width:  |  Height:  |  Size: 115 B

After

Width:  |  Height:  |  Size: 115 B

View File

Before

Width:  |  Height:  |  Size: 195 B

After

Width:  |  Height:  |  Size: 195 B

View File

Before

Width:  |  Height:  |  Size: 177 B

After

Width:  |  Height:  |  Size: 177 B

View File

Before

Width:  |  Height:  |  Size: 177 B

After

Width:  |  Height:  |  Size: 177 B

View File

Before

Width:  |  Height:  |  Size: 125 B

After

Width:  |  Height:  |  Size: 125 B

View File

Before

Width:  |  Height:  |  Size: 122 B

After

Width:  |  Height:  |  Size: 122 B

View File

Before

Width:  |  Height:  |  Size: 173 B

After

Width:  |  Height:  |  Size: 173 B

View File

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 175 B

View File

Before

Width:  |  Height:  |  Size: 213 B

After

Width:  |  Height:  |  Size: 213 B

View File

Before

Width:  |  Height:  |  Size: 178 B

After

Width:  |  Height:  |  Size: 178 B

View File

Before

Width:  |  Height:  |  Size: 162 B

After

Width:  |  Height:  |  Size: 162 B

View File

Before

Width:  |  Height:  |  Size: 210 B

After

Width:  |  Height:  |  Size: 210 B

View File

Before

Width:  |  Height:  |  Size: 177 B

After

Width:  |  Height:  |  Size: 177 B

View File

Before

Width:  |  Height:  |  Size: 195 B

After

Width:  |  Height:  |  Size: 195 B

View File

Before

Width:  |  Height:  |  Size: 136 B

After

Width:  |  Height:  |  Size: 136 B

View File

Before

Width:  |  Height:  |  Size: 137 B

After

Width:  |  Height:  |  Size: 137 B

View File

Before

Width:  |  Height:  |  Size: 147 B

After

Width:  |  Height:  |  Size: 147 B

View File

Before

Width:  |  Height:  |  Size: 139 B

After

Width:  |  Height:  |  Size: 139 B

View File

Before

Width:  |  Height:  |  Size: 148 B

After

Width:  |  Height:  |  Size: 148 B

View File

Before

Width:  |  Height:  |  Size: 149 B

After

Width:  |  Height:  |  Size: 149 B

View File

Before

Width:  |  Height:  |  Size: 152 B

After

Width:  |  Height:  |  Size: 152 B

View File

Before

Width:  |  Height:  |  Size: 135 B

After

Width:  |  Height:  |  Size: 135 B

View File

Before

Width:  |  Height:  |  Size: 150 B

After

Width:  |  Height:  |  Size: 150 B

View File

Before

Width:  |  Height:  |  Size: 151 B

After

Width:  |  Height:  |  Size: 151 B

View File

Before

Width:  |  Height:  |  Size: 145 B

After

Width:  |  Height:  |  Size: 145 B

View File

Before

Width:  |  Height:  |  Size: 135 B

After

Width:  |  Height:  |  Size: 135 B

View File

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 181 B

View File

Before

Width:  |  Height:  |  Size: 178 B

After

Width:  |  Height:  |  Size: 178 B

View File

Before

Width:  |  Height:  |  Size: 184 B

After

Width:  |  Height:  |  Size: 184 B

View File

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 181 B

View File

Before

Width:  |  Height:  |  Size: 183 B

After

Width:  |  Height:  |  Size: 183 B

View File

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 181 B

View File

Before

Width:  |  Height:  |  Size: 177 B

After

Width:  |  Height:  |  Size: 177 B

View File

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 175 B

View File

Before

Width:  |  Height:  |  Size: 189 B

After

Width:  |  Height:  |  Size: 189 B

View File

Before

Width:  |  Height:  |  Size: 187 B

After

Width:  |  Height:  |  Size: 187 B

View File

Before

Width:  |  Height:  |  Size: 194 B

After

Width:  |  Height:  |  Size: 194 B

View File

Before

Width:  |  Height:  |  Size: 191 B

After

Width:  |  Height:  |  Size: 191 B

View File

Before

Width:  |  Height:  |  Size: 194 B

After

Width:  |  Height:  |  Size: 194 B

View File

Before

Width:  |  Height:  |  Size: 190 B

After

Width:  |  Height:  |  Size: 190 B

View File

Before

Width:  |  Height:  |  Size: 184 B

After

Width:  |  Height:  |  Size: 184 B

View File

Before

Width:  |  Height:  |  Size: 179 B

After

Width:  |  Height:  |  Size: 179 B

View File

Before

Width:  |  Height:  |  Size: 127 B

After

Width:  |  Height:  |  Size: 127 B

View File

Before

Width:  |  Height:  |  Size: 135 B

After

Width:  |  Height:  |  Size: 135 B

View File

Before

Width:  |  Height:  |  Size: 134 B

After

Width:  |  Height:  |  Size: 134 B

View File

Before

Width:  |  Height:  |  Size: 134 B

After

Width:  |  Height:  |  Size: 134 B

View File

Before

Width:  |  Height:  |  Size: 190 B

After

Width:  |  Height:  |  Size: 190 B

View File

Before

Width:  |  Height:  |  Size: 186 B

After

Width:  |  Height:  |  Size: 186 B

View File

Before

Width:  |  Height:  |  Size: 183 B

After

Width:  |  Height:  |  Size: 183 B

View File

Before

Width:  |  Height:  |  Size: 183 B

After

Width:  |  Height:  |  Size: 183 B

View File

Before

Width:  |  Height:  |  Size: 177 B

After

Width:  |  Height:  |  Size: 177 B

View File

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 175 B

View File

Before

Width:  |  Height:  |  Size: 183 B

After

Width:  |  Height:  |  Size: 183 B

View File

Before

Width:  |  Height:  |  Size: 180 B

After

Width:  |  Height:  |  Size: 180 B

View File

Before

Width:  |  Height:  |  Size: 182 B

After

Width:  |  Height:  |  Size: 182 B

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