Sprint 4 Design Pack — GWP-264
Story narrative
Section titled “Story narrative”The clip system itself is shipped (per docs/software/cartridges/authoring/clip-system.md Status: “Implemented in emulator; prototype/production firmware pending”), and there’s an existing kn86-attract/ Remotion-backed TypeScript DSL for authoring scenes deterministically. The runtime side — attract_init, attract_tick, region-aware playback — is in place. What’s missing is content + cart-side wiring + the cart-author UX for producing clips outside the kn86-attract toolchain.
The task title says “build attract clip pipeline + 4 launch-cart clips” but reading the existing docs reveals the pipeline is mostly built. Reframe: this task is really “(a) close the cart-author authoring loop so cart contributors who aren’t TypeScript developers can ship clips, and (b) ship 4 launch-cart attract reels using whatever pipeline shakes out as the right one.” The kn86-attract Remotion DSL is excellent for marketing-controlled attract content; it’s overkill (and a wrong-tool match) for cart-author–contributed gameplay-snippet clips. Two pipelines, one binary format.
The marketing dispatch T02 (“KN-86 attract mode shipped”) wants this verifiable end-to-end on the emulator: boot → cart-attract reel plays → loops cleanly. Today, boot drops to bare deck terminal (per docs/software/runtime/bare-deck-terminal.md) when no cart is loaded, and into the cart’s home screen when a cart is loaded. There’s no “attract reel” beat between cart-load and cart-home today.
What “shipping 4 launch-cart attract clips” means in practice:
For each of the 4 launch carts (icebreaker, neongrid, blackledger, depthcharge), produce a 4–8 second .kn86clip of representative gameplay — not a marketing-style highlight reel, but a gameplay window that an idle deck would naturally cycle to demonstrate “this cart, in motion.” Cycle them in attract mode when the deck has been idle for ~30 seconds (configurable; firmware decision). Loop cleanly so a glance at an idle deck on a shelf reads as alive.
Player-facing semantics with worked example
Section titled “Player-facing semantics with worked example”The operator’s experience:
Idle deck (cart inserted, no input for 30 seconds) → attract mode kicks in → the deck cycles through a small reel of clips. With ICE Breaker loaded, the operator sees ~6 seconds of network-intrusion gameplay (ICE/trace/node/packet animations playing on the main grid; CIPHER fragments ticking on the OLED in their normal cadence). With NeonGrid loaded, ~6 seconds of grid-patrol-evasion. With Black Ledger loaded, ~5 seconds of transaction-tracing UI updating. With Depthcharge loaded, ~7 seconds of sonar sweep with contact resolution.
The clips are not narrative cutscenes and not boasting headline-screens. They’re gameplay-in-motion windows. The deck, idle on a shelf, looks like it’s thinking — that’s the design goal. Operator-on-couch glances over, reads the screen for ~3 seconds, gets the idea of what the cart does, returns attention to whatever they were doing. Operator-at-desk picks up the deck and the attract loop dies the moment input arrives (per the existing input-dispatch path).
Worked example: ICE Breaker attract clip storyboard.
6 seconds (180 frames at 30 fps) showing one short network-intrusion run:
- Frames 0–30 (1s): A network topology renders (5 nodes connected by lines, drawn with
gfx-line/gfx-circleover the cart-author’s chosen layout). Cipher emits one fragment to OLED:target acquired. - Frames 30–90 (2s): A trace cursor walks from the operator-node to a target-node, animating one cell per ~12 frames. The active edge highlights. PSG tone climbs slightly each hop.
- Frames 90–120 (1s): Target node “compromises” — flashes inverse-video for ~10 frames, then renders
[BREACHED]annotation. - Frames 120–180 (2s): Status row updates with
1 NODE COMPROMISED PAYOUT 800 ¤ THREAT 3. Pause on the result; loop point at frame 180.
The clip authors as 180 pre-rendered frames, packed in .kn86clip format, played via attract_tick() driving the existing region-aware playback machinery. The cart’s own gameplay code is not invoked during attract — these are baked frames, not live gameplay. Pre-rendered guarantees deterministic playback and zero CPU cost during attract.
Acceptance criteria expanded (≥6 testable items with file paths)
Section titled “Acceptance criteria expanded (≥6 testable items with file paths)”tools/make_clip.py(new) — sequence + cues →.kn86clip. Authoring CLI that takes a directory of frame-text files (e.g.,frame000.txt…frame179.txt, each an 80-line × 80-col ASCII layout in the KN-86 Code Page) plus an optionalaudio_cues.json(mapping frames to PSG calls) and emits a single.kn86clipfile conforming to the binary format owned bykn86-emulator/src/clip.c. Reproducible — same inputs → byte-identical output. This is the cart-author UX path — non-TypeScript-fluent contributors ship clips by writing ASCII frame files in any text editor.- The TypeScript-DSL Remotion pipeline (
kn86-attract/) remains the marketing-content path for marketing dispatch attract reels. Both pipelines emit the same.kn86clipbinary.tools/make_clip.pyis additive, not a replacement. - 4 launch-cart attract clips delivered under
kn86-emulator/carts/<cart>/assets/<cart>.kn86clip:icebreaker.kn86clip— ~6s network-intrusion gameplay window per the worked example above.neongrid.kn86clip— ~6s grid-patrol gameplay.blackledger.kn86clip— ~5s transaction-tracing UI.depthcharge.kn86clip— ~7s sonar sweep with contact resolution. Each produced via eithertools/make_clip.py(if the cart-author UX is the right tool) orkn86-attract/(if the marketing pipeline is). Use whichever fits the cart’s content; the binary format doesn’t care.
- Each clip is 4–8s representative gameplay; loops cleanly; audio cue fits PSG budget. “Loops cleanly” = the last frame’s pixel state is contiguous with the first frame’s pixel state (no visual snap on loop). PSG audio cues are non-jarring on loop point — fade out by end-of-clip or terminate audio cleanly before loop boundary.
- Emulator boot sequence loads from cart’s clip subdir before falling through to bare-deck attract reel. When a cart is loaded and idle timeout fires, attract mode looks for
<cart_root>/assets/*.kn86clipfirst. If found, plays one or cycles all. If not found, falls back to the bare-deck attract reel. Implementation:attract.cgains a clip-dir resolver that checks the loaded cart’s asset path before the runtime-default path. tests/test_attract_clip_load.ccovers:- Known-good
.kn86clipparses + plays first frame correctly. - Corrupt-header clip fails gracefully (no crash; logs to debug overlay; falls through to fallback reel).
- Clip too large to fit attract memory budget rejects with logged warning + falls through.
- Clip-dir resolver picks cart-side clip over runtime-default when both exist.
- Clip-dir resolver picks runtime-default when no cart-side clip exists.
- Loop boundary is clean (last frame == first frame pixel state — assertion-friendly because both are deterministic byte arrays).
- Known-good
- T02 dispatch’s “attract mode shipped” claim verifiable on emulator end-to-end. Manual test: boot emulator with each launch cart loaded → wait 30 seconds → attract clip plays → press any key → clip terminates → cart’s home screen returns. Document the test sequence in the PR.
docs/software/cartridges/authoring/clip-system.mdgains a “Cart-author quickstart” subsection describing thetools/make_clip.pyworkflow for non-TypeScript contributors. One-page max — not a full tutorial, just enough to unblock cart authors who want to ship clips without learning Remotion.
Cross-references (cart specs that consume + ADRs that constrain)
Section titled “Cross-references (cart specs that consume + ADRs that constrain)”docs/software/cartridges/authoring/clip-system.md— design source. Existing pipeline doc; this task adds the cart-author quickstart.docs/marketing/dispatches/Transmission-02-outline.md— dispatch consumer; verifiability is one of the AC items.kn86-emulator/src/attract.c— existing implementation. Clip-dir resolver is the single significant code change here.kn86-emulator/src/clip.c— existing parser; the binary format spec lives here. Per the task constraint, this file is NOT modified — bothtools/make_clip.pyandkn86-attract/produce the same format that the existing parser consumes.kn86-attract/— existing TypeScript Remotion DSL. Pipeline is correct; not modified by this task. Quickstart doc cross-references it as the alternative pipeline.- All 4 launch cart design bibles — visual sources for the gameplay-window storyboards. Reference each cart’s home + 1-2 representative gameplay screens to inform clip content.
- No ADR is touched by this task. The decisions (clip-system architecture, kn86-attract DSL, region-aware playback) are all in the existing doc, not in ADRs.
Edge cases (≥3)
Section titled “Edge cases (≥3)”- Cart with no clip ships. Most carts in v0.1 may not have clips authored yet. The attract-clip resolver must fall back to the bare-deck attract reel cleanly. No silent failure — log to debug overlay so a cart author knows their cart isn’t shipping a clip when they expected it to.
- Clip references glyphs not in the KN-86 Code Page. The
make_clip.pyASCII-frame input is constrained to KN-86 Code Page glyphs (perdocs/software/api-reference/grammars/character-set.md). If an input frame contains a glyph outside the set (e.g., a Unicode em-dash in a stray paste), the encoder must reject with a clear error citing the offending frame + character position. Don’t silently substitute. - PSG audio cues collide with cart’s gameplay audio. Attract mode happens between gameplay sessions, so collision is unlikely during attract — but if attract is interrupted by input (operator returns), the PSG state is whatever the last cue left it as. The transition back to live gameplay should reset PSG state (call
sound-silenceon attract exit) so the cart’s first SFX after exit isn’t competing with a stuck attract tone. - Clip authoring without TypeScript / Remotion knowledge. The cart-author UX issue.
tools/make_clip.pyis the answer — but the docs need to explain what the input directory structure looks like (one ASCII text file per frame, namedframeNNN.txt, exactly 80 lines × 80 cols, optionalaudio_cues.jsonschema). Quickstart doc covers this. - Loop-point audio snap. Worst case: last frame plays a tone at full volume; first frame restarts the same tone. Operator hears a click at the loop boundary. Mitigation: clip authors should fade audio out by end of clip OR avoid PSG calls in the last 30 frames. Document in the quickstart as an authoring rule.
- Idle-trigger duration (30 seconds) is configurable but currently uncalibrated. What’s the right duration for “deck looks alive on a shelf” without being so eager that it interrupts an operator-at-desk pause? Recommendation: 30s default; expose as a deck-state preference that the operator can adjust (or hide behind a debug flag for v0.1). Not load-bearing for this task — the firmware exposes the value, the carts don’t see it.
Engineering hand-off notes
Section titled “Engineering hand-off notes”- Files owned:
tools/make_clip.py(new),kn86-emulator/carts/icebreaker/assets/icebreaker.kn86clip(new), 3 more cart-side.kn86clipfiles. Possiblykn86-emulator/src/attract.c(additive — clip-dir resolver only). - Files added-to:
kn86-emulator/tests/test_attract_clip_load.c(new),docs/software/cartridges/authoring/clip-system.md(cart-author quickstart subsection). - Files NOT touched:
kn86-emulator/src/clip.c(parser already correct, per task constraint),kn86-emulator/src/psg.c,kn86-emulator/src/oled.c.kn86-attract/TypeScript DSL is also not modified. - Expected PR size: ~250 lines
make_clip.py(encoder), ~50 linesattract.c(resolver), ~150 lines tests, ~80 lines quickstart docs, ~120 KB binary (4 clips × ~30 KB each). Single C+Python engineer with art-direction consult on clip storyboards from Gameplay Design. - Test strategy: TDD for the resolver + encoder (reproducibility, format validity, error paths). Manual end-to-end test for the boot-→-attract-→-input flow per AC #7.
- Dispatch shape: C engineer + Gameplay Design consult — the engineer ships the encoder + resolver + tests; Gameplay Design (or Josh directly) authors the storyboards for the 4 launch-cart clips. Two halves can run in parallel after the format-and-pipeline is locked. Light dependency on GWP-263 (12×24 font cut): if GWP-263 lands first, the clips are captured against the new font; if not, they’re captured against the 8×8-scaled font and may need re-capture later. Not blocker; just ordering preference.
- Dependency check:
clip-system.mdciteskn86-attract/andkn86-emulator/src/clip.cas already shipped. Verify before dispatch — if either has regressed, the task is blocked on getting them back. - Watch for: the clip storyboards are the load-bearing piece (the format is solved; the content is the work). Don’t hand off to an engineer who’ll generate filler clips — Gameplay Design should provide the storyboards (or Josh approves them) before encoding.
- Cart-author UX question to resolve early: is
tools/make_clip.pythe right surface, or should it be a Lisp-side primitive in NoshAPI (e.g.,(clip-author <frames> <audio-cues>)invoked from a cart’s authoring REPL)? Recommendation: standalone Python tool for v0.1 (cleaner surface, easier to iterate). A Lisp-side authoring primitive is a future task if cart authors want it.
Open questions for Josh
Section titled “Open questions for Josh”- Cart-author UX choice. Standalone
tools/make_clip.pyPython script (recommended for v0.1) vs Lisp-side authoring primitive (more in-deck, more friction to ship). Confirm. - Storyboard authoring ownership. Gameplay Design agent authors all 4 launch-cart clip storyboards (recommended) vs Josh writes them inline (more hands-on but slower). Recommendation: Gameplay Design with Josh review at storyboard stage before encoding.
- Idle-trigger duration default. 30s (recommended; matches existing convention from comparable retro-handheld attract modes) vs 60s (less eager) vs 15s (more eager). Configurable post-launch; pick a v0.1 default.
- Cart-side vs runtime-default clip resolution. Always prefer cart-side (recommended; carts ship with their own attract content) vs always cycle bare-deck reel + cart clips together. Recommendation: cart-side clip overrides bare-deck reel when present.
- Audio fade-out at loop point. Hard authoring rule (
make_clip.pyenforces silence in last 30 frames) vs soft guideline (documented in quickstart, author’s responsibility) vs auto-fade (encoder mixes a fade-out automatically). Recommendation: soft guideline + encoder warning if PSG calls happen in last 30 frames. Auto-fade adds complexity; hard rule restricts authoring.