@demystify/play-runtime
v0.2.0
Published
The shared Demystify Play engine — fixed-timestep loop, input intents, DOM/canvas surfaces, packaged audio, the zero-network trap and the shared game mini-kit.
Maintainers
Readme
@demystify/play-runtime
The shared engine every Demystify Play game runs on: fixed-timestep loop, device-independent input, DOM and canvas surfaces with theme awareness, packaged audio, a zero-network trap, and the mini-kit primitives games reuse.
npm install @demystify/play-runtimeBudget: ≤35 KB gz, because it loads on every game page.
The loop
import { createLoop } from "@demystify/play-runtime";
const loop = createLoop({ tick: (i) => step(i), draw: (alpha) => render(alpha) });
loop.start();Fixed timestep, always. If update took a variable delta, the same input on a 60 Hz phone and a
120 Hz laptop would produce different states and replays would not reproduce. Two failure modes are
handled explicitly: catch-up is capped so a long stall drops time instead of spiralling, and the loop
stops on visibilitychange/blur — a rAF loop left running in a hidden tab is the single biggest
battery complaint driver for web games.
Note:
TICK_MSis 1000/60, which is not representable in binary floating point. Repeated subtraction leaves a residue, so an exactly-N-ticks frame silently produced N−1 ticks until an epsilon was added to the accumulator comparison. The bug made games run slow at a rate that varied by machine.
Input — intents, not devices
import { createInput, bindInput } from "@demystify/play-runtime";
const input = createInput(); // "left" | "action" | "undo" | …
const unbind = bindInput(el, input, cellFromEvent);A game never sees a KeyboardEvent. That buys four things at once: keyboard completeness becomes
structural (there is no mouse in the vocabulary), gamepad and touch come free, replays are
device-independent, and the automated keyboard-only test drives the same path a player does. Pointer
position is reported in grid cells, never pixels — a pixel would make a replay depend on the
viewport.
Canvas that follows the theme
import { createCanvasSurface, readTheme, watchTheme } from "@demystify/play-runtime";The DOM follows a theme for free; a canvas does not, so a canvas game that hardcodes its palette is
permanently one theme. watchTheme watches both the data-theme attribute and the
prefers-color-scheme query — watching only one leaves a canvas stuck in the wrong palette when the
OS appearance changes.
The zero-network trap
import { withNetTrap } from "@demystify/play-runtime";
const { violations } = await withNetTrap(() => playASession());Replaces fetch, XMLHttpRequest, WebSocket, EventSource, navigator.sendBeacon and
new Image().src with recording throwers — including the two people forget: sendBeacon fires
during unload and is invisible to most capture tooling, and an image src is a request that never
looks like one. Packaged data: URIs are allowed. Intended for tests and development; production
relies on CSP plus an end-to-end capture.
The mini-kit
makeGrid / flood / neighbours (immutable, edge-clipped, reading-order deterministic) ·
createUndoStack · createCountdown / createCooldown (ticks, never wall-clock) ·
createHintBudget / createStuckDetector · STATE_MARKS / CATEGORY_MARKS.
State marks bind colour and glyph as one indivisible value, so "never colour alone" cannot drift.
categoryMark refuses a ninth category rather than reusing a colour — a board needing more than
eight simultaneous categories has a working-memory problem no palette fixes.
Licence
Apache-2.0 · Demystify Systems
