@demystify/play-sdk
v0.2.0
Published
The Demystify Play GameModule contract — deterministic seeded RNG, replay recording and the golden-replay harness. Zero runtime dependencies.
Downloads
93
Maintainers
Readme
@demystify/play-sdk
The contract every Demystify Play game implements — deterministic seeded randomness, replay recording, and a conformance harness that proves a game behaves before it ships.
Zero runtime dependencies, by design: this module is imported by every game, by the Node test harness, and by anything that later verifies a score server-side. A dependency here is a dependency everywhere.
npm install @demystify/play-sdkWhy it exists
A game that cannot be replayed cannot be trusted, verified, or debugged from a bug report. This package makes replayability structural rather than something each game remembers to support.
The contract
import type { GameModule } from "@demystify/play-sdk";
const game: GameModule<State> = {
meta,
init(ctx, opts) { /* pure, integer-only */ },
update(state, input, ctx) { /* pure reducer, one fixed 16.667ms tick */ },
render(state, surface, alpha) { /* impure; floats welcome; never mutates state */ },
result(state) { /* score DERIVED from state, never accumulated */ },
describe?(state) { /* parallel accessible tree */ },
snapshot?(state) { restore?(snap, ctx) { /* resume after close */ } },
};update and render are split for one reason: update must produce a byte-identical state trace
from the same seed on Chromium, WebKit and Gecko. That is what lets a score be re-derived from a
seed plus an input trace instead of trusted — which in turn is what makes a verifiable leaderboard
possible later without touching a single game.
Seeded randomness
import { makeRng } from "@demystify/play-sdk";
const rng = makeRng("some-seed");
rng.int(6); // uniform 0..5 — rejection sampling, not modulo
rng.shuffle(items); // new array, never mutates
rng.sample(items, 3); // n distinct, throws rather than padding
const saved = rng.save(); // resume mid-run, exactlyint() rejects the ragged tail of the 32-bit range rather than taking a modulo. u32() % 7
over-produces low values — invisible on a three-choice board, and enough to skew a difficulty
generator drawing millions of times.
The daily seed
import { dailySeed, dailySeedWindow } from "@demystify/play-sdk";
await dailySeed("group-five", "2026-08-09"); // SHA-256(salt + gameId + UTC date)
await dailySeedWindow("group-five", "2026-08-09"); // the next 7 days, precomputedUTC so a shared result card means the same thing to everyone; gameId so today's answer in one game
does not help in another; SHA-256 for avalanche, so Tuesday is not a variation on Monday. The window
is precomputed at install, because the mechanic that brings a player back tomorrow must not be the
one that breaks offline.
Replay
import { ReplayRecorder, replayResult } from "@demystify/play-sdk";
const rec = new ReplayRecorder(gameId, seed, mode);
rec.record(frame); // per tick; only ticks where something happened are stored
const replay = rec.finish();
replayResult(game, replay, makeContext); // re-derives the scoreA three-minute session is ~10,800 ticks but perhaps 200 inputs, so a sparse trace is ~50× smaller and reproduces identically.
The conformance harness
import { conformance, assertConformance } from "@demystify/play-sdk";
assertConformance(conformance(game, { seed: "test", screenReader: "full" }));Checks determinism over 10,000 ticks, seed sensitivity, update purity, snapshot round-tripping and
describe presence. Each check is proved against a deliberately-broken fixture in this package's own
tests — a gate that has never been seen to fail is not a gate.
Licence
Apache-2.0 · Demystify Systems
