The engine kit (optional)
This page is optional. Everything here is one convenient way to produce a conforming run; you can ignore the kit entirely and ship a bare run you wrote by hand. Reach for @caputchin/engine-kit when you would rather write ordinary game logic and have determinism, the replay loop, and trace encoding handled for you.
The kit re-exports the whole replay contract surface, so kit users have a single import site.
npm install @caputchin/engine-kitThe idea: write a reducer, get a run
The kit's core move is to let you express your game as a pure reducer: a function that takes the current state and one tick's input and returns the next state. From that reducer plus deterministic primitives, the kit's toRun adapter produces a run(seed, config, trace) that conforms to the contract by construction.
import { defineEngine, toRun, cap } from "@caputchin/engine-kit";
const engine = defineEngine({
setup(seed) {
const rng = cap.rng(seed); // deterministic, seeded RNG
return { score: 0, targets: spawn(rng), rng };
},
tick(state, input) {
// pure: same (state, input) always yields the same next state
return applyInput(state, input);
},
result(state) {
return { passed: state.score >= 3, score: state.score, durationMs: state.elapsed };
},
});
export const run = toRun(engine); // a conforming RunFnDeterministic primitives
The kit gives you the two things that most often break cross-runtime determinism, both seeded and reproducible:
cap.rng(seed)(andcap.rngFromState) - a seedable PRNG. Use it for every random choice instead ofMath.random().cap.math- deterministic transcendentals (sin,cos, ...) that agree across runtimes, instead of the platformMathones that may not.
Optional shims
applyShim()- neutralizes the non-deterministic globals (the wall clock,Math.random) so an accidental call fails loud in development instead of silently diverging at replay.applyDomShim()- a minimal headless DOM for the framework path, so a reducer that touches a tiny DOM surface still replays in the no-DOM isolate.
Run it locally before publishing
The kit's replay harness runs a trace through your engine the same way the server will, and selfCheck runs a battery of cases and reports any non-determinism before you publish:
import { selfCheck } from "@caputchin/engine-kit";
const report = selfCheck(engine, { cases: myCases });
if (!report.ok) console.error(report.violations);A clean local selfCheck is the best predictor that the marketplace's publish self-check will pass (it is the same determinism standard). The kit also ships a CLI for running this from package.json scripts or CI.
Trace encoding
encodeTrace / decodeTrace give you a compact, versioned trace format so the input stream your live game emits is exactly what your run decodes at replay. Using them on both sides keeps the two in lockstep.
When to skip the kit
Skip it when you already have deterministic logic (a fixed-point simulation, a WASM module compiled for spec-floats) or when your game is simple enough that hand-writing run is trivial. The platform never knows or cares whether the kit produced your run; it only loads and replays the contract.
See also
- The replay contract: the
run/ seed / verdict this kit produces. - Build a marketplace game: the game whose logic this kit can back.
- The caputchin.json manifest: shipping the produced
runas the artifact.