1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-19 07:05:58 +00:00

docs: add docs/war.gif demo to README

Captures the first ~6 s of examples/war.ne via the same
puppeteer + jsnes + gifenc pipeline that powers
docs/platformer.gif: title menu thumbnail, 52-card deal
animation, and a few rounds of CPU vs CPU play. Embedded
in the top-level README right under the platformer demo.

record_gif.mjs gains a 6th positional arg for the warmup
override (defaulting to the existing WARMUP env / 30) so
the war command can keep its title menu as the first frame
while platformer keeps skipping past its own title. The
CI emulator job and the pre-commit hook both rebuild the
gif into a tmp path and fail-with-fix-command if the
committed copy is stale; the war trigger covers war.ne,
war.nes, any examples/war/*.ne include, plus the recorder
and harness.
This commit is contained in:
Claude 2026-04-16 00:37:23 +00:00
parent add0df6ff1
commit 318a2f8bef
No known key found for this signature in database
6 changed files with 107 additions and 41 deletions

View file

@ -161,6 +161,21 @@ jobs:
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" 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 exit 1
fi fi
- name: Verify docs/war.gif is up to date
# Same contract as docs/platformer.gif: the README embeds
# docs/war.gif as the second project demo, gifenc + jsnes are
# deterministic, so any change to the compiler / runtime /
# harness / war source that affects the first ~6 seconds of
# the war.ne gameplay must be followed by regenerating the
# gif. The 5th positional arg (`4`) is the warmup; war's
# title menu is the gif thumbnail so we don't skip past it.
working-directory: tests/emulator
run: |
node record_gif.mjs war 360 2 /tmp/war.gif 4
if ! cmp -s ../../docs/war.gif /tmp/war.gif; then
echo "::error file=docs/war.gif::committed docs/war.gif is stale; rerun \`node tests/emulator/record_gif.mjs war 360 2 docs/war.gif 4\` 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,22 +22,28 @@ 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 - **`docs/platformer.gif` and `docs/war.gif` are committed** and
top-level README as the project demo. `gifenc` + `jsnes` are embedded in the top-level README as the project demos. `gifenc` +
deterministic, so the gif's bytes are a function of the compiler, `jsnes` are deterministic, so each gif's bytes are a function of
the runtime, the harness, and `examples/platformer.ne`. Any change the compiler, the runtime, the harness, and the underlying `.ne`
to those that affects the first ~6 seconds of observable platformer source(s). Any change to those that affects the first ~6 seconds
gameplay must be followed by regenerating the gif: of observable gameplay must be followed by regenerating the
affected gif:
```bash ```bash
node tests/emulator/record_gif.mjs platformer 360 2 docs/platformer.gif node tests/emulator/record_gif.mjs platformer 360 2 docs/platformer.gif
node tests/emulator/record_gif.mjs war 360 2 docs/war.gif 4
``` ```
and committing it in the same change. CI's `emulator` job renders (The trailing `4` on the war command is the warmup-frames
a fresh gif and fails if the committed one doesn't byte-match. The override — war's title menu is the gif thumbnail, so we don't
pre-commit hook rebuilds the gif when `platformer.ne`, `platformer.nes`, skip past it the way the platformer recording does.) Commit the
`record_gif.mjs`, or `harness.html` is staged (and `tests/emulator/node_modules` regenerated gif in the same change. CI's `emulator` job renders
is installed). fresh gifs and fails if either committed copy doesn't byte-match.
The pre-commit hook rebuilds whichever gif is affected when
`platformer.ne`, `platformer.nes`, any file under `examples/war/`,
`war.ne`, `war.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.

View file

@ -8,6 +8,10 @@ NEScript compiles `.ne` source files directly into playable iNES ROM files, with
_Source: [`examples/platformer.ne`](examples/platformer.ne)_ _Source: [`examples/platformer.ne`](examples/platformer.ne)_
![War demo](docs/war.gif)
_Source: [`examples/war.ne`](examples/war.ne)_
## Quick Start ## Quick Start
```bash ```bash

BIN
docs/war.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

View file

@ -39,26 +39,51 @@ if [ $stale -ne 0 ]; then
exit 1 exit 1
fi fi
# Only rebuild docs/platformer.gif when platformer.nes or the gif # Only rebuild docs/{platformer,war}.gif when the underlying
# recorder itself changed — the gif regeneration takes ~20s due to # example sources, the gif recorder, or the harness changed — gif
# puppeteer's cold start, so we don't want to pay it on every commit. # regeneration takes ~20s per gif due to puppeteer's cold start,
# Skipped entirely if node_modules isn't installed in the emulator # so we don't want to pay it on every commit. Skipped entirely if
# harness; the CI `emulator` job is the authoritative check. # node_modules isn't installed in the emulator harness; the CI
# `emulator` job is the authoritative check.
#
# `examples/war.ne` pulls in `examples/war/*.ne` via include
# directives, so any file under `examples/war/` is treated as a
# trigger for rebuilding the war gif — the parser's preprocess
# pass sees them as part of the same translation unit.
changed_files=$(git diff --cached --name-only) 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 recorder_or_harness_changed=0
if [ -d tests/emulator/node_modules ]; then if echo "$changed_files" | grep -qE '^tests/emulator/(record_gif\.mjs|harness\.html)$'; then
echo " Rebuilding docs/platformer.gif (platformer.nes or recorder changed)..." recorder_or_harness_changed=1
(cd tests/emulator && node record_gif.mjs platformer 360 2 /tmp/platformer-precheck.gif >/dev/null) fi
if ! cmp -s docs/platformer.gif /tmp/platformer-precheck.gif; then
echo " STALE: docs/platformer.gif" check_gif() {
echo " rerun: node tests/emulator/record_gif.mjs platformer 360 2 docs/platformer.gif" name=$1
rm -f /tmp/platformer-precheck.gif args=$2 # extra args to record_gif.mjs (e.g. "4" for warmup)
exit 1 if [ ! -d tests/emulator/node_modules ]; then
echo " (skipping $name gif freshness check — tests/emulator/node_modules not installed)"
return 0
fi fi
rm -f /tmp/platformer-precheck.gif echo " Rebuilding docs/$name.gif..."
else # shellcheck disable=SC2086
echo " (skipping gif freshness check — tests/emulator/node_modules not installed)" (cd tests/emulator && node record_gif.mjs "$name" 360 2 /tmp/${name}-precheck.gif $args >/dev/null)
if ! cmp -s "docs/$name.gif" "/tmp/${name}-precheck.gif"; then
echo " STALE: docs/$name.gif"
echo " rerun: node tests/emulator/record_gif.mjs $name 360 2 docs/$name.gif $args"
rm -f "/tmp/${name}-precheck.gif"
return 1
fi fi
rm -f "/tmp/${name}-precheck.gif"
return 0
}
if [ $recorder_or_harness_changed -eq 1 ] || \
echo "$changed_files" | grep -qE '^examples/platformer\.(ne|nes)$'; then
check_gif platformer "" || exit 1
fi
if [ $recorder_or_harness_changed -eq 1 ] || \
echo "$changed_files" | grep -qE '^examples/war(\.(ne|nes)|/[^/]+\.ne)$'; then
check_gif war 4 || exit 1
fi fi
echo "All pre-commit checks passed." echo "All pre-commit checks passed."

View file

@ -1,10 +1,11 @@
// Record a GIF of a .nes ROM running in jsnes. // Record a GIF of a .nes ROM running in jsnes.
// //
// Usage: // Usage:
// node record_gif.mjs <rom-name> [frames] [stride] [output.gif] // node record_gif.mjs <rom-name> [frames] [stride] [output.gif] [warmup]
// //
// Example: // Examples:
// node record_gif.mjs platformer 360 2 docs/platformer.gif // node record_gif.mjs platformer 360 2 docs/platformer.gif
// node record_gif.mjs war 360 2 docs/war.gif 4
// //
// The recorder drives `harness.html` via puppeteer, collects one // The recorder drives `harness.html` via puppeteer, collects one
// canvas frame every `stride` NES frames for `frames` total, and // canvas frame every `stride` NES frames for `frames` total, and
@ -13,15 +14,23 @@
// 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 // `warmup` is the number of NES frames to advance before the first
// README. Any change to the compiler, the runtime, the harness, or // captured frame. The default of 30 skips past the reset stall and
// `examples/platformer.ne` that alters the gameplay you see in the // the platformer's auto-Title→Play handoff at frame 20; the war
// first ~6 seconds of the demo must be followed by // recording uses 4 instead because that demo opens on its menu and
// we want the title screen to be the gif's thumbnail.
//
// IMPORTANT: `docs/platformer.gif` and `docs/war.gif` are committed
// and embedded in the README. Any change to the compiler, the
// runtime, the harness, or the underlying `.ne` source that alters
// the gameplay you see in the first ~6 seconds of either demo must
// be followed by
// //
// node tests/emulator/record_gif.mjs platformer 360 2 docs/platformer.gif // node tests/emulator/record_gif.mjs platformer 360 2 docs/platformer.gif
// node tests/emulator/record_gif.mjs war 360 2 docs/war.gif 4
// //
// committed alongside the source change. The CI `emulator` job // committed alongside the source change. The CI `emulator` job
// regenerates the gif and fails if the committed copy is stale — // regenerates both gifs and fails if the committed copies are stale —
// gifenc + jsnes are deterministic, so the freshly-rendered bytes // gifenc + jsnes are deterministic, so the freshly-rendered bytes
// byte-match a valid commit. See `.github/workflows/ci.yml`. // byte-match a valid commit. See `.github/workflows/ci.yml`.
@ -66,11 +75,18 @@ await page.waitForFunction(
await page.evaluate((b) => window.nesHarness.loadRomBase64(b), romB64); await page.evaluate((b) => window.nesHarness.loadRomBase64(b), romB64);
// Warm-up: skip past the reset stall and any title screen so the // Warm-up: skip past the reset stall and (optionally) any title
// first captured frame shows real gameplay. 30 frames at 60 fps // screen so the first captured frame shows what we want as the
// covers ~0.5 s which is enough for the platformer example's // gif's thumbnail. 30 frames at 60 fps covers ~0.5 s which is
// Title → Playing auto-transition at frame 20. // enough for the platformer example's Title → Playing auto-
const warmupFrames = parseInt(process.env.WARMUP ?? "30", 10); // transition at frame 20. The war recording overrides this with
// `4` (positional arg below) so the title menu is the first frame.
// Positional arg wins; `WARMUP=…` env var is honoured for ad-hoc
// experimentation.
const warmupFrames = parseInt(
process.argv[6] ?? process.env.WARMUP ?? "30",
10,
);
await page.evaluate((n) => window.nesHarness.runFrames(n), warmupFrames); await page.evaluate((n) => window.nesHarness.runFrames(n), warmupFrames);
console.log( console.log(