Caputchin
Marketplace game development

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-kit

The 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 RunFn

Deterministic primitives

The kit gives you the two things that most often break cross-runtime determinism, both seeded and reproducible:

  • cap.rng(seed) (and cap.rngFromState) - a seedable PRNG. Use it for every random choice instead of Math.random().
  • cap.math - deterministic transcendentals (sin, cos, ...) that agree across runtimes, instead of the platform Math ones 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

On this page