1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-22 15:42:11 +00:00

docs: regenerate platformer.gif and lock it as a CI invariant

The optimizer fix in the previous commit changes the observable
gameplay of `examples/platformer.ne` — pre-fix the player got
spurious enemy-1 stomp bounces every time coin 2 drifted into its
pickup window, so the README demo gif showed the player bouncing
mid-air around emu frames 85-125 instead of walking through the
coin at ground level. Regenerate `docs/platformer.gif` from the
fixed compiler so the README matches reality.

To stop this from drifting again, treat the gif the same way the
repo already treats `examples/*.nes`:

- `gifenc` + `jsnes` + the harness are deterministic, so a fresh
  recording byte-matches a valid commit. Verified across two
  back-to-back runs (identical md5).

- `.github/workflows/ci.yml`'s `emulator` job now renders the gif
  into `/tmp/platformer.gif` and `cmp`s it against `docs/platformer.gif`,
  emitting a `::error` annotation pointing at the exact rerun
  command if the committed copy is stale. This piggybacks on the
  existing puppeteer + node setup, adding ~20s to the job.

- `scripts/pre-commit` runs the same check locally, but only when
  `examples/platformer.{ne,nes}`, `tests/emulator/record_gif.mjs`,
  or `tests/emulator/harness.html` is staged, and only if
  `tests/emulator/node_modules` is already installed. Cold-start
  puppeteer is ~20s — too slow to pay on every commit, but cheap
  enough to pay when something gif-relevant changed.

- The header of `tests/emulator/record_gif.mjs` and the project
  conventions section of `CLAUDE.md` both spell out the rerun
  command and the invariant, so the next agent doesn't have to
  re-derive any of this.

https://claude.ai/code/session_013Bi4H4YQ5or5HtMB4doUFi
This commit is contained in:
Claude 2026-04-13 18:04:17 +00:00
parent 629fdcfce0
commit 5e3e68ca11
No known key found for this signature in database
5 changed files with 65 additions and 0 deletions

View file

@ -146,6 +146,21 @@ jobs:
- name: Diff each ROM's framebuffer against its golden - name: Diff each ROM's framebuffer against its golden
working-directory: tests/emulator working-directory: tests/emulator
run: node run_examples.mjs 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 - name: Upload actual + diff PNGs on failure
if: failure() if: failure()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View file

@ -22,6 +22,22 @@ re-derive the project conventions from scratch.
tmp path and fails if the committed version differs, pointing at tmp path and fails if the committed version differs, pointing at
the exact `cargo run -- build examples/<name>.ne` to run. The the exact `cargo run -- build examples/<name>.ne` to run. The
pre-commit hook under `scripts/pre-commit` catches this locally. 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 - `docs/future-work.md` lists the remaining gaps. If you implement
something from that file, update the doc in the same PR. something from that file, update the doc in the same PR.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 438 KiB

After

Width:  |  Height:  |  Size: 438 KiB

Before After
Before After

View file

@ -39,4 +39,26 @@ if [ $stale -ne 0 ]; then
exit 1 exit 1
fi 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." echo "All pre-commit checks passed."

View file

@ -12,6 +12,18 @@
// At stride=2 we end up with a 30 fps GIF that maps 1:1 to every // 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 // other NES frame (NES runs at ~60 fps), which is the right
// tradeoff between smoothness and file size for a README demo. // 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 { promises as fs } from "node:fs";
import path from "node:path"; import path from "node:path";