ADR-0047: The experience tier — pixel-addressed minigames on the cell-API
Sibling of: ADR-0046 — the other half of the foreground-surface split. ADR-0046 took the data tier (traditional-software surfaces as knEmacs modes); this ADR takes the pixel-rendered tiers (custom game surfaces on the cell-API). The two are siblings in placement (the foreground split), not in construction discipline — see the note under Decision 7. There is no “experience engine” symmetric to knEmacs.
Related / composes: ADR-0045 (the background substrate — the hunt keeps ticking under a minigame), ADR-0036 (the native framebuffer renderer — the single pixel surface, the system-tier blit, the 20 fps cap), ADR-0027 (the constrained cart cell-API + the privilege boundary), ADR-0040 (DeckRunner / screen router — gains the typed surface kind), ADR-0042 (first-party programs — a different surface category), ADR-0043 (mission objectives — consume the typed outcome), ADR-0006 (cart format / schema — the return schema), ADR-0005 (NoshAPI — gains experience-tier verbs; future amendment), ADR-0015 (the Null-cart chrome-exception precedent). Design sources (mined, not embedded): the BearLibTerminal Design Overview (foo.wyrd.name/en:bearlibterminal:design), RandyGaul/cute_headers, raylib’s reasings.h / raymath.h, and PICO-8 / TIC-80.
Context
Section titled “Context”ADR-0046 drew the scope line and built one side — the data tier, where traditional-software-shaped programs are modes on the knEmacs engine. It deferred the other side: the “minigames that pop up during a run” — piloting a drone, making moves against an ICE on the ICEBREAKER board. This ADR is that other side.
Two independent axes describe a foreground surface, and conflating them was the original error this ADR (post-review) avoids:
- Address axis — cell-addressed (8×8 glyphs on the grid) vs pixel-addressed (arbitrary-size sprites at pixel offsets).
- Interaction axis — event-driven + command (idle until input; keys→commands) vs active-loop + control (ticks every frame; keys→controls + trackpoint).
The placement heuristic Josh landed on — traditional software → mode; custom/bespoke game surface → cell-API — is really a statement about the address axis (bespoke game surfaces want pixels). It does not dictate the interaction axis. The ICE board is the proof: it is bespoke and wants the full pixel surface (pixel-addressed), yet it is turn-based — discrete-state, command-driven, event-redrawn (mode-like interaction). So it is neither a knEmacs mode nor a real-time experience; it is a third thing.
The renderer (ADR-0036) is a real pixel framebuffer underneath the 8×8 cell grid — which is what makes a pixel-addressed tier possible — but it is a single, process-lifetime-stable surface, the pixel blit is a privileged system-tier primitive, the firmware chrome rows are non-negotiable, and the redraw is capped at 20 fps. Those four facts bound everything below; see Foundation reconciliation.
Forcing functions
Section titled “Forcing functions”- ICEBREAKER’s ICE board and the drone set-pieces are next to design; the surface kinds, the render/capability contract, and the invoke/resolve lifecycle should be fixed first.
- A run weaves between data-tier surfaces and pixel-rendered set-pieces; the screen router needs to know which kind it routes to, to drive transitions and switch input/tick behavior.
Constraints
Section titled “Constraints”- Monochrome amber, arena-bounded, no-malloc-in-hot-paths, single-threaded Pi Zero 2 W; carts are KEC Lisp in a constrained (cart-tier) KEC context that, by design, cannot resolve system-tier primitives.
- The runtime already owns its renderer (ADR-0036), PSG audio, and input — an off-the-shelf terminal gamekit would import a second, competing renderer/IO layer (termbox2 was already evicted by ADR-0036). The device’s real gap is game logic, not a screen layer.
Decision
Section titled “Decision”-
Three foreground surface kinds, along the two axes (not a binary):
- mode — cell-addressed + event/command. The knEmacs data tier (ADR-0046).
- tactical — pixel-addressed + event/command. Bespoke, full-render, but discrete-state and command-driven. The ICE board charters this kind on day one.
- experience — pixel-addressed + active-loop/control. Real-time, keys+trackpoint as controls. The drone pilot.
(The fourth quadrant — cell-addressed + active-loop — is unused.) tactical and experience are the two pixel-rendered kinds this ADR governs; they share the render/capability contract below and differ only on the interaction axis.
-
Pixel rendering is system-tier; carts get it invoke-only. The pixel blit (
render_bitmap/render_fill_rect/render_set_pixel, ADR-0036) is a privileged system primitive — it is not bound into a cart-tier KEC context (ADR-0027’s boundary holds, or the cell-API sandbox is decorative). A cart invokes a tactical/experience surface and supplies its content and logic (sprite data, behavior tables, per-tick lambdas); the runtime owns the pixels. The cart-facing surface is constrained, system-mediated verbs (e.g.draw-spriteby handle,move-toward,ease-to) — never raw pixel primitives. “Bespoke” means a cart’s own content/logic, still invoke-mediated — not raw pixel access. -
The render primitive is one blit. An 8×8 glyph, a multi-cell sprite, and a full-content-area splash are the same
render_bitmap()operation with size + sub-cell pixel offset + alignment + a 1-bit transparency mask (monochrome — mask yes, color blending no). One blit keeps the cart-facing draw verb-set tiny. -
Surfaces composite inside the cart-usable content area; the chrome rows persist. Row 0 / Row 74 are firmware-owned and non-negotiable (Spec Hygiene Rule 5); a tactical/experience surface draws within the Rows 1–73 pixel band, and the status/action chrome stays live (you want battery/timer/mode visible mid-set-piece). A modal full-surface takeover (suspending the chrome) is not granted by this ADR; if ever wanted it requires an explicit exception + Josh + a canonical-spec.md update, on the precedent of ADR-0015’s Null-cart CIPHER exception.
-
The screen router carries the typed surface kind. The type lets the router drive transitions/animations and switch the two contracts at the seam: the runtime contract (mode/tactical are event-driven; experience is an active per-frame tick loop) and the input contract (mode/tactical take keys→commands; experience takes keys→controls + trackpoint).
-
Lifecycle: push → play → typed outcome → pop. A mission phase or cart handler pushes a surface onto the router (interrupting the data-tier flow); it runs; it resolves to a typed outcome; the outcome feeds the phase chain / objective model (ADR-0043). A minigame is a phase gate with a real-time (or turn-based) resolution. Outcomes are cart-declared schema, materialized in UDS: at cart load the cart registers its return schema into Universal Deck State and owns the outcome→state mapping; the runtime/objective layer consumes typed returns without hardcoding any minigame’s outcomes (composes ADR-0006 schema + ADR-0043 objectives).
-
Build-then-extract an experience kit — runtime-reusable, invoked across carts. Start a surface bespoke in its first cart; extract the kit (the one blit primitive + painter’s-order draw + sub-cell offset + mask + the constrained verbs) when a second needs the same machinery. Pure-logic helpers (collision, easing, vector math) live in the C/system layer and are exposed to carts as high-level verbs, not raw primitives.
Construction asymmetry with ADR-0046 (deliberate). The data tier has one shared engine, built framework-first (knEmacs). The pixel tiers are bespoke-first, extract-a-kit-later. The kit is a harvested convenience, not a mandatory substrate — do not expect an “experience engine” symmetric to knEmacs. The siblinghood is in placement, not construction.
-
Composition with the background tier. A surface runs in the foreground while the ADR-0045 scheduler keeps background systems ticking (the hunt), surfacing/intruding via the event bus. ADR-0045 (background) + ADR-0046 (data foreground) + this ADR (pixel foreground) are the three layers of a run — the “minigame with a hunt in the background” that opened this design line.
Foundation reconciliation (renderer + sandbox)
Section titled “Foundation reconciliation (renderer + sandbox)”This ADR asserts nothing the ADR-0036 foundation and the cart sandbox don’t fund:
- Capability boundary — resolved invoke-only (Decision 2): pixel-blit stays system-tier; carts never get raw pixel primitives. The ADR-0027/0036 privilege boundary is preserved.
- Chrome rows — resolved composite-in-1–73 (Decision 4): Rule 5 holds; modal takeover is deferred behind an explicit exception.
- Render budget — sized here, not assumed:
- “Layers” = painter’s-order redraw into the single, process-lifetime-stable ADR-0036 surface (background → entities → HUD, in draw order) — not N retained 1024×600 RGB565 buffers (~1.2 MB each, incompatible with the no-malloc/fixed-arena discipline). No compositor, no per-layer allocation.
- Double-buffering / page-flip / tearing is a presentation follow-on, now owned by the single SDL3 host on both targets (KMSDRM on the Pi). (2026-07-27: there is no separate
/dev/fb0backend and never will be — one SDL3 host runs on both targets over KMSDRM. See ADR-0036 Amendment Log.) Tear-free on-device presentation is still not promised; the work moved, it did not land. - The 20 fps redraw cap (ADR-0036, shared single-threaded with the ADR-0045 scheduler + PSG audio) is the action-minigame ceiling. Sub-cell motion is smooth in position (pixel offset) but the frame rate is 20 fps; fast action set-pieces must be designed to that ceiling.
Options Considered
Section titled “Options Considered”Option A: Pixel-rendered tiers (tactical + experience) on the cell-API — system-owned pixels invoked by carts, typed router, schema-in-UDS outcomes, build-then-extract kit. (ACCEPTED)
Section titled “Option A: Pixel-rendered tiers (tactical + experience) on the cell-API — system-owned pixels invoked by carts, typed router, schema-in-UDS outcomes, build-then-extract kit. (ACCEPTED)”Chosen because it gives bespoke game surfaces the full pixel surface the heuristic sends here, keeps the cart sandbox real (invoke-only), separates render from interaction so turn-based and real-time surfaces each get the right contract, gives the run a clean push→play→outcome lifecycle feeding objectives, and composes with the background hunt — all within the single-surface / system-blit / chrome-row / 20 fps foundation.
Option B: Build minigames as knEmacs modes (force them into the engine).
Section titled “Option B: Build minigames as knEmacs modes (force them into the engine).”Rejected. The buffer/cell model is for traditional structured-data surfaces; a drone pilot is not a buffer. The heuristic sends bespoke game surfaces to the pixel tiers, not the engine.
Option C: Pure-cart minigames with raw pixel access — each cart hand-rolls its own.
Section titled “Option C: Pure-cart minigames with raw pixel access — each cart hand-rolls its own.”Rejected on two counts. It re-pays the machinery per cart (no reuse), and giving carts raw render_bitmap collapses the ADR-0027/0036 capability boundary — the cell-API sandbox becomes decorative. Invoke-only + build-then-extract gets the reuse and keeps the sandbox.
Option D: Embed an off-the-shelf terminal gamekit (ncurses / notcurses / BearLibTerminal / raylib).
Section titled “Option D: Embed an off-the-shelf terminal gamekit (ncurses / notcurses / BearLibTerminal / raylib).”Rejected — wrong layer. Those are renderer/IO frameworks that duplicate and compete with the native renderer the runtime already owns (ADR-0036); termbox2, the closest, was already evicted by ADR-0036. The device’s gap is game logic, not a screen. Instead: mine the design (BearLibTerminal’s layers + sub-cell offset + alignment + mask-compositing model is directly transferable) and lift pure-logic single-headers (collision/easing/vector-math) — see Open.
Trade-off Analysis
Section titled “Trade-off Analysis”Option A buys full pixel render + cross-cart reuse + a clean run-composition + a preserved sandbox, at the cost of (1) a typed surface kind + two contracts in the screen router, (2) an experience kit to build (mitigated by build-then-extract), and (3) a third surface kind (tactical) to keep the turn-based/real-time distinction honest. All accepted: the pixel tiers are first-class in runs, invoke-only keeps them from puncturing the sandbox, and the painter’s-order/20 fps sizing keeps the render claims funded.
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Bespoke surfaces get the full pixel surface — arbitrary sprites, smooth sub-cell motion, painter’s-order layering, masking — without a competing renderer and without breaking the sandbox.
- Runtime-reusable across carts; missions invoke shared set-pieces.
- Render and interaction are separated, so turn-based boards and real-time set-pieces each get the right contract.
- Clean push → play → typed-outcome lifecycle feeding objectives; the cart owns its outcome contract, so the runtime stays generic.
- Composes with the background substrate — the three-layer run model.
- One blit primitive ⇒ a tiny cart-facing draw API.
Negative / Accepted costs
Section titled “Negative / Accepted costs”- The screen router gains a typed surface kind and two contracts.
- An experience kit must be built (build-then-extract is the mitigation).
- Three surface kinds instead of two.
- Painter’s-order redraw (no retained-layer compositor) and the 20 fps ceiling bound what action set-pieces can do; tear-free on-device presentation is still unproven under the SDL3/KMSDRM host.
Follow-on work this ADR creates
Section titled “Follow-on work this ADR creates”- Typed surface kind in the screen router (ADR-0040), with the mode/tactical/experience contract table.
- The experience-tier invoke verb + the constrained draw/sprite/easing verbs in NoshAPI (ADR-0005 amendment, future) — explicitly not raw pixel primitives.
- The cart return-schema → UDS mechanism (ADR-0006 + UDS + ADR-0043).
- Build ICEBREAKER’s ICE board (the first tactical surface) + the drone (the first experience), design-for-extraction → the kit.
- Evaluate the lift candidates (
cute_c2.h,reasings.h,raymath.h) + the SDK-side asset pipeline.
Explicitly Not Decided (open — recorded so nothing here reads as committed)
Section titled “Explicitly Not Decided (open — recorded so nothing here reads as committed)”Posture only. Left open:
- The minigame roster and per-surface mechanics / controls / layouts. Per-cart and per-program design. (The kinds are decided; the tactical interaction contract details — turn structure, selection model — are per-design.)
- The exact experience-kit API and NoshAPI verbs. The PICO-8
spr/map/sfx/btnshape is the illustrative reference, not the committed surface — and whatever lands is constrained/system-mediated per Decision 2. - A modal full-surface (chrome-suspending) takeover — not granted here; an open question requiring an explicit exception + Josh (ADR-0015 precedent).
- Which external libraries are actually vendored.
cute_c2.h/reasings.h/raymath.hare lift candidates to evaluate, not committed dependencies; the SDK-side asset pipeline (cute_tiled/cute_aseprite/cute_png) and any PICO-8/TIC-80-style on-device authoring tooling are directions, not decisions. - The exact UDS return-schema format and versioning.
Documentation Updates (REQUIRED — Spec Hygiene Rule 3)
Section titled “Documentation Updates (REQUIRED — Spec Hygiene Rule 3)”-
docs/adr/ADR-0047-experience-tier-minigames.md— this file. -
docs/adr/README.md— index row added (sibling to ADR-0046). -
docs/adr/ADR-0005-ffi-surface.md— future amendment: experience-tier invoke + constrained draw/sprite verbs (when they land; non-blocking). - Per-surface design docs note their kind (tactical/experience), pixel-addressed construction, and their cart return schema. (Non-blocking; per-program/per-cart.)
Narrative
Section titled “Narrative”ADR-0046 split the foreground and built one half — the deck’s traditional software, modes on the knEmacs engine. This is the other half: the set-pieces that interrupt a run, where you stop reading the system and start acting in it — flying the drone, fighting the ICE. The first cut of this ADR called those “experiences” and waved at “the full rendering surface,” but a coherence pass against the renderer caught the seams: the pixel blit is the system’s to do, not a cart’s, so a cart invokes a surface and the runtime draws it; the chrome rows are non-negotiable, so a set-piece lives inside the content area with the battery and the timer still showing; and “layers” on a device with one surface and no spare megabytes means painter’s order, not a stack of buffers, at twenty frames a second. The pass also found a quieter thing: bespoke and real-time aren’t the same axis. The ICE board wants every pixel but takes its turns — pixel-rendered, yet command-driven — so it isn’t a mode and isn’t a real-time experience; it’s a third kind, and naming it keeps a turn-based board from being forced to fly like a drone. The renderer to draw all this, we already built; the game logic we lift as small libraries; the design we take from the fantasy consoles and the pseudo-terminals without taking their engines. And underneath, the background keeps ticking — the run you’re not looking at, the thing hunting you — exactly the picture this whole line started from, now with all three layers named and none of them claiming more than the foundation can pay for.