Custom game recipe¶
This page is a source-backed pattern, not a promise of a generated game API.
Start from the actual TypeScript Engine, Chronicle, Stack, Space, and
ActionRegistry modules, and keep all persistent writes on the dispatch path.
Minimal state and dispatch¶
import { Engine } from "../engine/Engine.js";
import { ActionRegistry } from "../engine/actions.js";
ActionRegistry["game:advance"] = (engine, { playerId }, context) => {
const game = engine.session.state.myGame;
if (!game || game.phase !== "play") throw new Error("game is not active");
if (game.currentPlayer !== playerId) throw new Error("not your turn");
return context.mutate("game:setState", {
key: "myGame",
patches: [
{ path: ["turn"], value: game.turn + 1 },
{ path: ["currentPlayer"], value: (game.currentPlayer + 1) % game.players.length },
],
});
};
const engine = new Engine();
await engine.dispatch("game:setState", {
key: "myGame",
value: { phase: "play", turn: 0, currentPlayer: 0, players: ["a", "b"] },
});
await engine.dispatch("game:advance", { playerId: 0 });
game:setState accepts either a whole-key value or nested patches; values
must be JSON-serializable. The registered handler receives an execution context:
use context.mutate() for internal registry mutations so the outer dispatch
owns one history/policy/rule lifecycle. Validate before writing. Dispatch is not
transactional if code mutates and then throws.
For cards, use Stack/Source and Space for canonical containers. For turns,
use the real game-loop actions (game:loopStart, game:setActiveAgent,
game:nextTurn, game:loopStop) rather than inventing action names. For rules,
use RuleEngine; for persistence, use engine.useStorage(adapter),
persist(), and resume().
Design checklist¶
Write down entities, invariants, visibility, conflict policy, authority, replay
needs, document-growth limits, and failure behavior. Add fixed seeds to tests.
Read engine/ACTIONS.md and docs/TESTING.md; do not call session.change()
from ordinary game code.