diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11a14be..9c0ab23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -146,6 +146,21 @@ jobs: - name: Diff each ROM's framebuffer against its golden working-directory: tests/emulator run: node run_examples.mjs + - name: Verify docs/platformer.gif is up to date + # The README embeds docs/platformer.gif as the project demo. + # gifenc + jsnes are deterministic, so rebuilding the gif from + # the current compiler should byte-match the committed copy. + # If it doesn't, the contributor changed the compiler / source + # / harness in a way that affects the observable gameplay of + # platformer.ne but forgot to regenerate the gif — the fix is + # the exact command printed in the error. + working-directory: tests/emulator + run: | + node record_gif.mjs platformer 360 2 /tmp/platformer.gif + if ! cmp -s ../../docs/platformer.gif /tmp/platformer.gif; then + echo "::error file=docs/platformer.gif::committed docs/platformer.gif is stale; rerun \`node tests/emulator/record_gif.mjs platformer 360 2 docs/platformer.gif\` and commit the new gif" + exit 1 + fi - name: Upload actual + diff PNGs on failure if: failure() uses: actions/upload-artifact@v4 diff --git a/CLAUDE.md b/CLAUDE.md index 4df3038..c36c8d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,6 +22,22 @@ re-derive the project conventions from scratch. tmp path and fails if the committed version differs, pointing at the exact `cargo run -- build examples/.ne` to run. The pre-commit hook under `scripts/pre-commit` catches this locally. +- **`docs/platformer.gif` is committed** and embedded in the + top-level README as the project demo. `gifenc` + `jsnes` are + deterministic, so the gif's bytes are a function of the compiler, + the runtime, the harness, and `examples/platformer.ne`. Any change + to those that affects the first ~6 seconds of observable platformer + gameplay must be followed by regenerating the gif: + + ```bash + node tests/emulator/record_gif.mjs platformer 360 2 docs/platformer.gif + ``` + + and committing it in the same change. CI's `emulator` job renders + a fresh gif and fails if the committed one doesn't byte-match. The + pre-commit hook rebuilds the gif when `platformer.ne`, `platformer.nes`, + `record_gif.mjs`, or `harness.html` is staged (and `tests/emulator/node_modules` + is installed). - `docs/future-work.md` lists the remaining gaps. If you implement something from that file, update the doc in the same PR. diff --git a/docs/platformer.gif b/docs/platformer.gif index fb2bff5..05f58c8 100644 Binary files a/docs/platformer.gif and b/docs/platformer.gif differ diff --git a/scripts/pre-commit b/scripts/pre-commit index e9539d1..8bfb841 100755 --- a/scripts/pre-commit +++ b/scripts/pre-commit @@ -39,4 +39,26 @@ if [ $stale -ne 0 ]; then exit 1 fi +# Only rebuild docs/platformer.gif when platformer.nes or the gif +# recorder itself changed — the gif regeneration takes ~20s due to +# puppeteer's cold start, so we don't want to pay it on every commit. +# Skipped entirely if node_modules isn't installed in the emulator +# harness; the CI `emulator` job is the authoritative check. +changed_files=$(git diff --cached --name-only) +if echo "$changed_files" | grep -qE '^(examples/platformer\.(ne|nes)|tests/emulator/record_gif\.mjs|tests/emulator/harness\.html)$'; then + if [ -d tests/emulator/node_modules ]; then + echo " Rebuilding docs/platformer.gif (platformer.nes or recorder changed)..." + (cd tests/emulator && node record_gif.mjs platformer 360 2 /tmp/platformer-precheck.gif >/dev/null) + if ! cmp -s docs/platformer.gif /tmp/platformer-precheck.gif; then + echo " STALE: docs/platformer.gif" + echo " rerun: node tests/emulator/record_gif.mjs platformer 360 2 docs/platformer.gif" + rm -f /tmp/platformer-precheck.gif + exit 1 + fi + rm -f /tmp/platformer-precheck.gif + else + echo " (skipping gif freshness check — tests/emulator/node_modules not installed)" + fi +fi + echo "All pre-commit checks passed." diff --git a/tests/emulator/record_gif.mjs b/tests/emulator/record_gif.mjs index 59a3161..dea9ca9 100644 --- a/tests/emulator/record_gif.mjs +++ b/tests/emulator/record_gif.mjs @@ -12,6 +12,18 @@ // At stride=2 we end up with a 30 fps GIF that maps 1:1 to every // other NES frame (NES runs at ~60 fps), which is the right // tradeoff between smoothness and file size for a README demo. +// +// IMPORTANT: `docs/platformer.gif` is committed and embedded in the +// README. Any change to the compiler, the runtime, the harness, or +// `examples/platformer.ne` that alters the gameplay you see in the +// first ~6 seconds of the demo must be followed by +// +// node tests/emulator/record_gif.mjs platformer 360 2 docs/platformer.gif +// +// committed alongside the source change. The CI `emulator` job +// regenerates the gif and fails if the committed copy is stale — +// gifenc + jsnes are deterministic, so the freshly-rendered bytes +// byte-match a valid commit. See `.github/workflows/ci.yml`. import { promises as fs } from "node:fs"; import path from "node:path";