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:
parent
add0df6ff1
commit
318a2f8bef
6 changed files with 107 additions and 41 deletions
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
28
CLAUDE.md
28
CLAUDE.md
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)_
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
_Source: [`examples/war.ne`](examples/war.ne)_
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
BIN
docs/war.gif
Normal file
BIN
docs/war.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 368 KiB |
|
|
@ -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."
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue