@bgoodman/ladder
v0.0.2
Published
Deterministic TypeScript runtime for turn-based games
Readme
Ladder
Ladder is a deterministic TypeScript runtime for turn-based board, dice, and card games.
A Ladder game is a package of pure rules: serializable state, typed decisions, automatic transitions, legal choices, projections, and an outcome. Ladder provides seeded randomness, run-until-blocked execution, validation, actor-owned requests, trusted detached forks, checkpoints, and replay.
The model
A game alternates between two settled states:
- Blocked: the game is waiting for one or more actor decisions.
- Complete: the game has an outcome and no pending decisions.
Creating a session and submitting a decision always runs automatic rules until one of those states is reached. Applications do not drive internal phases or random effects step by step.
Actors are stable player identities registered when a game is created. A
viewer describes whose projection a runtime caller may see. Applications use
{ kind: "actor", actorId } for a player's frame and submit calls; a
transport maps authenticated users to those viewers.
import { createLadder } from "@bgoodman/ladder/core"
import { highRoll } from "./high-roll"
const ladder = createLadder()
const session = ladder.create(highRoll, {
gameId: "table-1",
config: { targetScore: 12 },
actors: [{ id: "alice" }, { id: "bob" }],
seed: "demo-seed",
})
const alice = { kind: "actor", actorId: "alice" } as const
const frame = session.frame(alice)
const request = frame.visibleRequests[0]
const result = session.submit(
{
requestId: request.id,
revision: request.revision,
actorId: "alice",
response: { optionId: "roll" },
},
alice,
)
if (!result.accepted) {
console.error(result.rejection)
} else {
console.log(result.frame.view)
}When controllers can answer every actor request, let Ladder own that loop:
import { runGame } from "@bgoodman/ladder/core"
const finalFrame = await runGame(session, {
controllers: {
alice: () => ({ optionId: "roll" }),
bob: () => ({ optionId: "roll" }),
},
resultViewer: { kind: "observer" },
})Controllers may be local strategies, terminal prompts, remote players, or test
fixtures. Their responses still pass through normal validation and replay
history. Use frame and submit directly when an application needs custom
interaction or recovery.
Set policySeed when a controller needs request-scoped randomness separate
from the game's rule RNG. Deterministic controllers can ignore it. Accepted
decisions remain the canonical replay input; controllers are not called during
replay.
The seed, game-definition version, config, actors, and accepted decisions fully determine the result. Replaying them produces the same state, requests, events, and outcome.
Define a game
A game definition supplies six things:
- A stable ID and version.
- Zod schemas for persisted data and decision payloads.
initialize,advance, anddecidetransition functions.- Pending decision requests, optionally with finite legal choices.
- State and event projections for actors, observers, and admins.
- A ranked outcome when play is complete.
import * as z from "zod"
import { defineGame } from "@bgoodman/ladder/core"
const configSchema = z.strictObject({
targetScore: z.number().int().positive(),
})
type Config = z.infer<typeof configSchema>
const game = defineGame({
game: { id: "example/high-roll", version: "1.0.0" },
schemas,
decisions,
initialize,
advance,
decide,
projectState,
projectEvent,
})Game state stays plain serializable data rather than extending a Ladder class.
defineGame provides inference and conformance checking around the complete
definition without adding runtime behavior. Game packages author Zod 4
schemas, infer their TypeScript types with z.infer, and let Ladder derive
JSON Schema Draft 2020-12 for MCP and other transport surfaces.
Start with Create your first game for a complete example. Use the API reference for the contracts and lifecycle.
Core guarantees
- Deterministic: rules are synchronous, pure, seeded, and replayable.
- Transport-neutral: the core has no HTTP, WebSocket, MCP, UI, or database dependency.
- Private by projection: authoritative state stays inside the session; callers receive viewer-specific state, events, and requests.
- Typed and runtime-validated: Zod schemas validate data and infer TypeScript types; Ladder derives JSON Schema for external tool surfaces.
- Stale-safe: request IDs and state revisions prevent old or duplicate responses from changing a game.
Documentation
- Create your first game
- UNO design probe
- Monopoly trade conformance fixture
- Sealed-bid decision-round fixture
- Yahtzee development and SearchModel fixture
- Development console
- Local AI and search
- AI API and roadmap
- Generated MCP surface
- API reference
- Core design
- Roadmap and reference games
Scope
Ladder owns game execution, not game content. Boards, cards, scoring rules,
trade rules, AI heuristics, and presentation belong to game packages.
Persistence, transports, authentication, experiments, and generated MCP tools
are extensions around the core. Trusted local rollout evaluation belongs in
the optional @bgoodman/ladder/ai extension; it is not an actor or MCP capability.
