Docs Design

Contents

Gamewright architecture

Status: proposed 2026-09-30. M0 in progress. Toolchain verified on the dev box 2026-09-30: Godot 4.7.2-stable, Node 24, pnpm 10, Mesa 25.2 (lavapipe and llvmpipe), Playwright Chromium.

Gamewright lets one person make a game by talking to an AI Game Director. The Director doesn't write a Godot project. It writes a Game Pack: small, versioned, schema-validated JSON documents that a person can read, edit and diff. One trusted Godot program, the player, runs any pack on desktop and in the browser. Everything the Director proposes arrives as a reviewable change set, and nothing is called done until the game has been played: by bots in a headless simulation, in rendered frames, in the web build, and finally by a person.

This document is the map. Decisions with real alternatives live in adr/. The evidence behind them is in research/. The milestone plan is ROADMAP.md.


1. Principles

  1. A game is data. A game is documents plus media, never scripts or Godot resources. One player runs every game (ADR-0002). That makes games checkable before anyone plays, safe to host, cheap to ship, and restylable.
  2. Grow the engine, never fork it. "Any game" comes from a vocabulary that grows: parameters, typed expressions, rule and behavior graphs, then new kits of reviewed engine code (ADR-0003). A gap the Director can't express becomes a KitRequest, which is the engine's roadmap signal.
  3. Always playable. A genre template supplies defaults for every document, so a new game is playable from its second step. Each step replaces defaults; nothing starts from zero.
  4. Everything is a proposal. AI output becomes a change set with a diff, previews and verification results. The person approves, tweaks, locks or regenerates at any granularity, and can always undo. Decide tools belong to the human.
  5. Done means played. A validator is not a playtest. Every game carries acceptance scenarios that every later change must keep green.
  6. Replayable by construction. Fixed-tick simulation, seeds by name, input keyed to the tick, state hashes (ADR-0004). Bots, tours, saves and bug reports all replay.
  7. Offline first. With no network and no API key, Tier 1 builders make the art, sound and music from recipes, and Claude Code can direct through MCP. Paid providers make a game richer, never make it work.
  8. The web is a first-class target. Every game must run and look right in the browser (Compatibility renderer, no threads, sample-mode audio). Desktop may add, never differ.

2. System overview

A diagram, as its Mermaid source.

flowchart LR
  user([Person]) -- chat --> web[Web studio<br/>gamewright site]
  cc([Claude Code / any MCP client]) -- MCP --> tools
  user -- terminal --> cli[gamewright CLI]
  web --> dir[Game Director agent]
  cli --> tools
  dir --> tools[Tool registry]
  tools --> cs[Change sets<br/>diff + previews + verify report]
  cs -- approve --> pack[(Game Pack<br/>documents in git)]
  pack --> val[Validators<br/>schema · semantics · static game analysis]
  pack --> bake[Bake<br/>template resolve · Expr compile · hashes]
  lib[(Library<br/>templates · packs · styles)] --> bake
  bake --> bundle[(Bundle)]
  bundle --> player[Godot player<br/>kits · builders · sim · bridge]
  player -- traces · frames --> ver[Verification ladder]
  ver --> cs
  player -- web export --> browser[Browser play]
  adapters[[Tier 2 adapters: image · 3D · audio]] -.-> pack

Three programs, one contract (the JSON Schemas in schemas/):

Core (core/, TypeScript on Node 24) Engine (engine/, Godot 4.7, typed GDScript) Web (web/, Next.js on Vercel)
Job Author, check and bake: load, validate, analyze, resolve templates, compile expressions, direct, orchestrate verification Play: load a bundle, generate worlds, simulate, build models and sounds from recipes, render, expose the playtest bridge Host: landing and gallery, play in the browser, later the Director chat, projects and accounts
Owns Pack I/O (the only writer of pack documents), schemas and codegen, the Expr compiler, static game analysis, change sets, the Director agent, the MCP server, the gamewright CLI, verification orchestration, Tier 2 adapters The player app, kits (simulation vocabulary), the Expr VM, graph runners, Tier 1 asset builders, render profiles, the save system, the playtest bridge, tours and shots, the engine CLI Pages, API routes that call the core's director and tools, bundle storage, the play origin
Reads Everything in a pack and the library Bundles, saves, tours Core APIs, bundles
Never Draws pixels or runs gameplay Writes pack documents, calls an LLM, loads scripts or Godot resources from a bundle Runs Godot on the play path

Why a TypeScript core. The Director needs the official Anthropic SDK and MCP SDK, JSON Schema validation with Ajv, and the same code in the CLI, the MCP server and the web site. Godot has none of these, and it can't run in a Vercel Function. Reelwright made the same split for the same reasons, and its foundation is copied rather than rewritten (research/reelwright-reuse.md).

How they talk.

  • Bundles. gamewright bake <pack> writes .gamewright/build/<hash>/: a manifest, resolved documents, compiled expressions and media. The player loads it from a path (desktop and dev), from res://game/ (a packaged desktop game), or from a URL (web).
  • Engine jobs. The core runs godot --path engine res://cli/cli.tscn -- <command> --job <dir> for simulation runs, shots, generator previews and asset bakes. The engine writes a result.json plus traces and PNGs. The last stdout line is always one JSON object.
  • The playtest bridge. In debug builds the player accepts a loopback connection (or reads a tour file) that can pause time, step N ticks, read state as JSON, inject input, and step until a predicate holds. Bots, tours, the Director and people's replays all use it.

3. The Game Pack

A pack is a folder, meant to be its own git repository. Documents are found by convention, <folder>/<slug>.<kind>.json, so tools, people and editors agree on where things are.

my-game/
  game.json                         GameManifest: template pin, packs, theme, style, seed, flow, platforms
  brief.json                        GameBrief: pitch, pillars, core loop, scope caps (the first proposal)
  theme/<slug>.theme.json           Theme: fiction, tone and never-list, factions, catalog, naming
  styles/<slug>.style.json          Style: palette, materials, render profile, lighting, UI tokens, audio palette
  stats/<slug>.stats.json           Stat and variable declarations
  archetypes/<slug>.archetype.json  Entities: tags, stats, components, abilities, visuals, cues
  abilities/  effects/  items/  recipes/  loot/
  brains/<slug>.sm.json | .bt.json  State machines and behavior trees (flat node lists)
  rules/<slug>.rules.json           Event → where → do rule sets
  timelines/  dialogue/  quests/
  worlds/<slug>.worldgen.json       Generator pipelines (passes); levels/<slug>.level.json for hand levels
  biomes/<slug>.biome.json          Climate envelope, grounds, scatter, spawns, weather, ambience
  models/<slug>.model.json          Tier 1 model recipes (builder + params + palette slots + budget)
  plans/<slug>.bodyplan.json        A body plan the library lacks: roles and motion presets (rare)
  sounds/<slug>.sound.json          SFX recipes; music/<slug>.score.json for score specs
  ui/<slug>.screen.json             Screens and HUD widgets bound by Expr
  input/  camera/  tuning/          Input map, camera rig, tuning knobs with ranges and reasons
  playtests/<slug>.playtest.json    Acceptance scenarios: seed, bot or tour, assertions, metric thresholds
  kits/                             Local kits (owner's machine only; see ADR-0003)
  assets/<slug>.asset.json          Asset: one media file the person brought in, pinned by hash, with its source
  assets/<slug>-<hash12>.<ext>      The file itself, frozen: a changed file is a new asset (design/assets.md)
  director/NOTES.md                 The Director's durable notes on the owner's preferences
  .gamewright/                      Caches, builds, sessions, proposals, traces (gitignored)

Conventions follow ADR-0005: JSON Schema 2020-12 that is also valid for Claude strict tools; closed objects; no recursion; anyOf + const unions; flat lists with parent ids; the envelope kind, schemaVersion, id, name, meta; kind-prefixed ids (arch.sentry, item.driftwood); units in field names; canonical files with JCS hashes; edits and locks addressed by item id.

Document kinds (first set; the full list grows with kits):

Group Kinds
Root GameManifest, GameBrief
Axes Template, Pack (library), Theme, Style
Mechanics (L0) Stats, Archetype, Ability, Effect, Item, Recipe, LootTable, Tuning
Logic (L1–L2) RuleSet, StateMachine, BehaviorTree, Timeline, Dialogue, Quest
World WorldGen, Biome, Level
Presentation Model, BodyPlan, Sound, Score, Screen, InputMap, CameraRig, Asset
Proof Playtest, Tour
Process ChangeSet, KitRequest

Locking. meta.status: locked freezes a document; meta.locks[] freezes parts of it by item id and pointer ("regenerate the sentry's brain, keep its body"). Generators and the Director treat locked parts as fixed inputs.

Dependency graph. Nodes are documents and derived artifacts (bundles, model previews, traces, verification reports). Edges come from references. gamewright impact arch.sentry answers "what changes if I edit this?"; gamewright status lists what is stale.

4. Templates, packs, kits; theme and style

(ADR-0006)

Kit      typed GDScript in engine/: components, actions, events, Expr functions, passes, BT tasks
 └ Pack  documents that use kits (crafting = recipe defaults + workbench archetype + make screen)
    └ Template  packs + sim mode + camera + controls + rules + archetypes + worldgen + UI + knobs + acceptance
       └ Game = template@version + overrides by id + generated content + theme + style
  • Kits live in engine/addons/gamewright/kits/<kit>/ with a kit.json registration: every component, action, event, Expr function, generator pass and BT task it provides, each with a parameter schema and a description written as a prompt. The core generates schemas/v1/vocab.schema.json from all registrations, so schemas, the Director's tools and the runtime always agree.
  • Templates and packs are data in library/, versioned with the engine. A game pins one template and may add packs. Templates never inherit; shared parts become packs. Body plans (library/plans/) are data too: the motion presets a rigged Model takes by name, which bake carries into the bundle of each game whose Models use them (§5.4). Starters (library/starters/<template>/) are games, one per template: the smallest game on it, playable at once, in its sample's look and sound, which gamewright new --starter copies as a person's new game (docs/design/m11-studio.md, S5). A copy pins the template like any game, so it is not the fork ADR-0006 rejects: it takes the template's upgrades for everything it doesn't override.
  • Theme says what the game is about. Style says how it looks and sounds, and chooses a render profile. Mechanics never reference style; presentation reads mechanics through cue names and visual slots. Restyling is one document.

First templates, in build order: action_survival (ortho 3D, the Tideline slice) → puzzle_grid and visual_novel (small; they prove the logic layer and the ladder) → platformer_2d → adventure (a top-down quest: a village, people to talk to, a wild place, an item to bring back) → twin_stick (one stick moves, the other aims and shoots; waves come through gates, the score grows, a short run is won or lost) → then roguelike and tower defense as recombinations. The test that the vocabulary is at the right level: the fourth template needs mostly packs, not new kits. The visual novel needed no new simulation kit, and the adventure none either: four small generic features (usables in kit.harvest, seek's talk goal, a talk that asks for its keys on touch, rules kept to a disc of the map). The twin-stick needed one, because shooting is a new mechanic and not a recombination: kit.ranged (shots that fly and hit through kit.melee's rules), with small generic features beside it (a second touch stick, the monkey's aim, the skirmish bot, a hint that names a stick, and routing a swarm cheaply: a lazily flooded route field and an opt-in reroute interval). All counted in docs/design/m4-templates.md.

5. The engine: one player for every game

5.1 Boot

engine/player/ is the app. It boots to a title or straight into a scene, given by the bundle's flow or by boot options (--bundle, --seed, --level <level id>, --at <place name>, --hour, --give, --tour, --shot, like unspent-ortho's BootOptions, so any state is reachable without clicking). When a person's game ends, the end shows for a second and then the press that leaves a title boots a new game to the title (GwPlayAgain): the app's, not the simulation's, so no state hash sees it and a tool run never does it.

The menu every game has is the player's too (GwPlayerMenu over the pure GwGameMenu, drawn by kit.ui on ui.pause and ui.options in the game's Style, the game's own Screen of that id when it has one). Esc, a pad's Start, the game's pause action or the touch controls' Pause opens it unless a view holds the input; its keys are its own and heard before the game's viewport, so nothing pressed for it reaches the game. While it is open no tick runs: a paused game hashes, tick for tick, as one never paused. It offers Continue, Restart, Options and Save and quit (the save slot continue, §5.6); the title, or the first page of a game with none, offers Continue while that save is on the device. Options (volume, music, sound effects, text size, and screen shake and flashes in a game with juice) are the person's, kept per game under user:// (IndexedDB on the web) and put on the stage for views (GwStage.gain, text_scale, shake, flashes), never in the simulation; a tool run plays at their defaults. The GameManifest's menu words it and turns off what a genre can't have.

  1. Read the bundle manifest. Check the engine range, schema major and every content hash.
  2. Reject anything that isn't a known document kind or an allowed media type (PNG, JPEG, WebP, OGG, MP3, WAV, glTF binary without scripts, fonts from the library), a picture or a sound told by its first bytes, never its name. Godot resources and scripts are never loaded from a bundle.
  3. Index documents by id. Instantiate the kits the template and packs name, in the order their declared dependencies give (after, provides, consumes), never by file name.
  4. Generate or load the world, spawn entities and warm up shaders, then start. Sounds and music bake after, a step at a time in a third of each frame, what is heard first first (design/m6-presentation.md §2): the web build has one thread, and a bed is seconds of work.

5.2 Simulation

  • One fixed tick (60 Hz by default; a kit may substep, as unspent's fight sim runs 8 ms slices). Rendering interpolates between ticks and never writes to the simulation.
  • Game state is plain data: entities (id, archetype, tags, stats, component state), world data (packed arrays for grids and heightfields), variables, clocks and timers, all serializable to JSON. state_hash() is SHA-256 over its JCS bytes.
  • Looks are not state. A component a kit.json marks presentation (kit.sprite's sprite, kit.actors' poses) is a look: only views read it, from the entity's Archetype, so it never enters an entity, a save, the state hash or the world digest (the state hash at tick 0). Nor do an Archetype's name, model and cues. A sprite one shade off keeps every hash. The lint that keeps simulation code off view-only kinds keeps it off looks too, and no expression can read one.
  • Input arrives as one InputFrame per tick: abstract actions and axes, never devices. The device layer, bots, tours and replays all produce input frames, so recording and replay come free (research/testing-verification.md §3).
  • The pointer is input too. A mouse, or a tap on the game's frame, reaches the simulation as world terms the view works out: where it is on the ground (on a 1/64 m grid) and, on a click, the thing under it, picked along the camera's ray. No pixel or camera float enters, so a replay needs no renderer. The InputMap's pointer says what it does: aim there (a twin-stick on a mouse), or walk to what was clicked and use it (point-and-click), or only tell the rules. A click is an order the simulation keeps (kit.move walks, kit.harvest uses), so a save mid-walk carries it. Menus and Screens answer the mouse as they answer keys, hit-tested against kit.ui's layout (design/pointer-input.md).
  • Touch is one more device. On a phone, a tablet or a touchscreen the player draws on-screen controls made from the game's InputMap: the stick or a d-pad under the left thumb, round buttons under the right (a twin-stick's second stick takes the right thumb, the buttons above it), small buttons along the top, each an action (an optional touch hint per action gives its label, place, order or when it shows). They sit around the game's frame, never on it, so no HUD is ever covered. A finger presses its action through Godot's input exactly as a key does, so the input frame, a replay and the state hash can't tell them apart. While a screen asks for actions (a title's start, an open menu's keys, a running talk's OK, Back and choice arms: GwStage.ask), only those show. They show from the first touch (from the start on a phone), a key hides them, and a tool run never has them (design/touch-controls.md). The frame they leave is small (390x219 points on a phone held upright), so the game's own text keeps to 11 points on the person's screen (§5.4).
  • Kit systems run in their declared order each tick over the entities that carry their components. Pure logic lives in RefCounted classes over packed arrays, tested headless; thin node systems draw. Systems talk through a typed event bus.
  • No hidden inputs. Simulation code never reads the clock or an unseeded RNG (tools/check-hidden-inputs.sh). Randomness comes from named PCG32 streams and pcg_hash.
  • Movement and collision are ours. Built-in kits use kinematic movers over our own world queries (tile grids, heightfields, AABB sets), not Godot physics bodies, so they replay. A template may opt into Godot physics and accept outcome-level tests.

5.3 The logic interpreters (L1–L2)

  • Expr VM. The core compiles each expression to a typed postfix array. The engine runs it on a ~200-line stack VM with a step budget. Roots: self, target, source, event, game, var, tune, level. Functions come from kit registrations. Godot's Expression class is not used (it can call arbitrary methods).
  • RuleSet runner. {on: event, where: Expr, do: [actions]}, with once and cooldowns. Actions are a closed union registered by kits. each queries carry a max, event chains stop at depth 8, and each tick has an action budget. Overruns are reported, not hung.
  • StateMachine and BehaviorTree runners over flat node lists, ticked by the simulation. Timeline for anything that waits (boss phases, cutscenes, ability windups). Dialogue and Quest runners share the rule vocabulary: conditions are Expr, effects are actions.

5.4 Presentation

  • Render profiles (chosen by the style): lit_ortho3d, lit_persp3d, flat_2d, pixel_2d. A profile owns its shaders, the colour door per renderer, light budgets and quality tiers. Compatibility is the baseline every game must look right in, because the web forces it and it renders windowless and bit-identically on the dev box. Every platform runs it. A style may later add a Forward+ desktop layer (volumetric fog, SSAO, more lights) that only enhances (ADR-0007).
  • Camera rigs from CameraRig documents: ortho3d (fixed yaw and pitch, texel snap, zoom, follow, a staging call, stipple occlusion of anything over the player), follow2d, platformer2d, fixed.
  • Tier 1 builders turn recipes into assets at load, cached by recipe hash: the mesh kit (primitives, sweep, lathe, scatter, tiered plants, building grammar, faceted rocks, terraced terrain), figure rigs, the sprite baker (renders any 3D recipe from fixed ortho angles to sprite sheets), torus-noise textures and corner tile sets, SVG icons, a Theme builder for UI, and the synth (sfxr-compatible SFX, layered synth graphs, score stems as exact-bar loops). Sounds are baked to AudioStreamWAV buffers, because web sample-mode audio can't generate sound live. Builders are versioned; a builder change that alters output is a new major, caught by golden hashes.
  • Things that move are rigged in data. A Model says how its thing can move: mesh.parts takes joints (a flat list with parents; each part rides one), and any rigged builder's Model may add or replace poses made of moves, keys per joint spread through the pose (swing, roll, turn, forward, up, stretch, with a phase so legs step in turn). A Sprite's clips sample those poses into frames. kit.platformer's view picks a body's clip from its state (hurt, climb, jump, fall, walk for a patrol, run, idle) and its frame from the tick, that state and its id: a jump plays through the arc by vertical speed. kit.json lists the clips each view plays, so the vocabulary shows them. It is all presentation, so the restyle gate holds a clip change to every state hash (design/m6-presentation.md §6).
  • Motion comes in presets. A BodyPlan (library/plans/: biped, quadruped, bird) names the roles a body's joints play (a quadruped's body, head, tail, four legs) and offers gaits and poses written as those same moves by role (walk, trot, gallop, hop, strut, flap, jump, fall, hurt). A Model names its plan (figure.person and machine.sentry bring their own, with their joints mapped in kit.json), maps any role its joints aren't named for, and takes presets by name as poses; its own moves then change any joint of them. The engine writes the presets out on the built skeleton, shifts scaled by the body's leg length over the plan's, so a rabbit bobs a rabbit's height. Bake carries the plans a game uses into its bundle; the vocabulary lists them as kind plan (design/m6-presentation.md §7).
  • UI from Screen documents: widgets in a flat tree, bound to state by Expr, themed from the style's tokens, drawn in whole multiples of a base pitch. Key glyphs are read from the live input map. Text a phone can read: the player tells the stage how many points a frame pixel takes on the person's screen, and when the style's pitch would draw text under 11 pt there (Apple's minimum), kit.ui draws at the least whole pitch that doesn't and lays out in the smaller room that leaves: scenery stays, everything else stays inside its parent, wrapped text reflows, the slate is all glass, and a widget may name a compact place. Views drawing text in Godot's face keep to the same 11 pt. A desktop shot is drawn for no screen and never changes (design/phone-text.md).
  • Audio routes through a mix sheet (category levels, variants, distance), and every emitted cue name must map to a sound (a validator checks it).
  • Juice (kit.vfx) answers events as audio's cues do, from the Style's vfx.effects: particles where it happened (a burst, or a trail behind an entity), a flash and a tween (squash, stretch, pop, hop, grow) on the entity it names, and a shake of the view. The view begins them inside the tick and puts them on the stage as GwJuice, a contract in runtime/present: the views that draw entities read how each looks now and how far the view shakes, and a flat view draws the particles; in 3D kit.vfx draws them itself, as camera-facing quads in one MultiMesh, and shakes the rig's camera by whole texels. Every effect is a pure function of the tick and a seed by name, so a shot at a named tick shows the same particles however many frames came before it. At most three flashes start in any second, only entities flash, and the person's Options turn shake down and flashes off (design/m6-presentation.md §8).

5.5 Worlds

A WorldGen document is a flat list of passes ({id, pass, after, params}); passes are kit code. First passes: noise.height, climate.fields, biome.assign, terrace.levels, water.sheets, scatter.props, place.sites, place.spawns, path.guarantee, verify.reach. Each pass draws from its own named seed stream, so adding a pass never moves another pass's output. Every generated thing gets a stable id from its stream (region.r0/prop.pine.17). Generators build solvable structure first and store a witness; a capability-aware reachability check reads the player archetype's movement. Worlds stream in chunks with a draw budget per chunk.

5.6 Saves

A save holds the seed, the world stamp, and deltas: taken props, placed builds, entity states, inventory, clocks, variables. Kits register their own save keys. The world stamp is a content hash of the engine's simulation version, kit versions, generator documents and root seed. A changed stamp is detected, never bumped by hand; the save is refused by name or migrated. Saves are JSON, never Godot variants with objects. On the web they live in IndexedDB. A person reaches one slot, continue, through the menu (§5.1): Save and quit writes it, Continue loads it, and a refusal is said in plain words ("This save can't continue: the game has changed.") while the game runs on. A save written before looks left state (2026-10-01) is migrated on load: its look components are dropped, which gives the state today's engine reaches by the same play (nothing in play ever wrote them).

5.7 The playtest bridge

Built into every debug player, and the reason verification works for any game:

  • Stepping: pause, step N ticks, step until an Expr predicate holds or a timeout passes.
  • State: entity and world state as JSON; typed queries (count(tag), player.health, var.x, in_region(id), event_seen(name)).
  • Input: inject input frames; record and replay them with checkpoint hashes every 60 ticks. A desync reports the first mismatching checkpoint.
  • Tours: a line DSL modelled on unspent-ortho's (press, hold, walk to <name>, until, await, shot NAME with SUBJECT). Staging is always by name, never by coordinate, because every generated world is new.
  • Shots: frame-server capture at a named tick, with the subject check and the adapter check.
  • Traces: a fixed schema per tick or event (inputs, player state, events, counts, timings) that metrics are computed from.

6. The core

  • Load and validate. Strict JSON parse; schema validation with plain-language diagnostics (pointer, message, hint); semantic validation (references, unique ids, trees are trees, actions valid for their event, units); static game analysis: item obtainability from starting inventory through loot, recipes and harvests; quest and objective reachability; unreachable states; rules that can never fire; economy sources and sinks; physical feasibility from constants (jump apex against required steps, run speed against gaps).
  • Expr compiler. Parse the Python/GDScript subset, typecheck against a symbol table generated from kit registrations plus the game's stats, variables and knobs, and emit postfix arrays. Errors carry a pointer and a column.
  • Bake. Resolve template + packs + game overrides by id, apply id-addressed edits, compile expressions, run bake-time solver passes (constraint puzzles), re-encode media, hash, write the bundle. Deterministic: the same pack and library give the same bundle bytes.
  • Change sets and history. Proposals live in .gamewright/changes/. Each operation carries the file's base hash, so a stale proposal never applies. Approve writes atomically, validates the whole pack first, and commits to git with a Gamewright-Change: trailer if the pack is a repository. Undo is an inverse change set. Variation groups hold N options; approving one rejects its siblings.
  • Engine jobs and verification (§8). Tier 2 adapters (§9).
  • Surfaces. The gamewright CLI, gamewright mcp (stdio), gamewright serve (the session daemon the Studio supervises: JSON-RPC 2.0 on a localhost WebSocket with a per-launch token, its methods and events schemas/v1/session.schema.json), and library functions the web API calls. One tool registry behind all of them.

7. The Game Director

An agent with tools. Every tool that changes anything returns a proposed change set, never a direct write.

The pipeline (research/generative-stack.md §1.2). Each stage emits documents, passes its gate, and becomes a change set. Stages 2 to 4 open together at C1 and make the first playable: a game needs a place before anyone can play it (docs/demos/m2-pilot.md). Only presentation waits for C2.

# Stage Writes Gate Human
1 Brief GameBrief: pitch, pillars, core loop (verbs, resources, goals, fail states), scope caps, template fit every loop verb maps to kit vocabulary or a KitRequest C1: approve brief and template
2 Template and systems GameManifest, Stats, RuleSet, Tuning boots headless; template bots finish a session —
3 Entities and mechanics archetypes, abilities, effects, items, recipes, loot, brains refs, Expr types, obtainability —
4 World biomes, worldgen, levels, quests, dialogue N seeds generate with witnesses; objectives reachable C2: play the first playable (template visuals)
5 Presentation theme, style, models, sounds, score, screens frame property checks; judge on the contact sheet C3: approve look and content
6 Tuning knob values within declared ranges bot sweeps hit the template's target metrics continuous

Tools (design level, never node level):

  • Read: game_summary, get_document, list_documents, search, impact, vocabulary (what kits can do), estimate_cost.
  • Propose: propose_brief, create_game (template pin), create_* per document kind, revise (id-addressed pointer edits), propose_slice (one idea over several documents, checked as one pack), vary (N options over chosen pointers), generate_world, restyle, file_kit_request. Each may build on pending proposals it names (after); the person approves those first.
  • Run: validate, bake, playtest (ladder rungs with a JSON report), preview (shots and contact sheets as images), play_url.
  • Decide (the human only; buttons in the UI, destructiveHint over MCP): approve, reject, lock, unlock, undo, redo.

The repair loop. Tool inputs and outputs pass schema, semantics, static analysis and the verification rungs the stage requires. Diagnostics go back with pointers and hints, at most three rounds per call, then the tool fails visibly. The Director can never mark its own work passed: reports come from validators and the engine.

Context. Tools, the system prompt, the vocabulary digest, the brief, the theme and style summaries and director/NOTES.md form a stable cached prefix. Each stage sends only its own tools, inside Claude's strict-tool budget (20 tools, 24 optional parameters, 16 unions per request). Whole-document authoring tools are non-strict with the full schema and the repair loop.

Surfaces and who pays.

  1. MCP first. gamewright mcp exposes the registry to Claude Code, where Claude is the agent and the person's own plan pays. This needs no API key, and it's how the owner directs games from day one.
  2. The in-app agent (the Studio, and CLI gamewright director) uses the official Anthropic SDK: claude-sonnet-5-5 by default, which made the more finished whole game from nothing for about a third of Opus's price a change (2026-10-06); the person can choose claude-opus-5-5 for the hardest changes or claude-haiku-4-5 for small ones. A turn never starts a call its budget cannot pay for. Tests replay recorded fixtures keyed by request hash; recording is an explicit, paid command.
  3. Bring your own key on the web, then a small hosted quota backed by a per-user ledger.

Sameness and drift. vary gives numbered options for key decisions; a similarity check compares a new game's spec and traces with its template and the owner's earlier games; pillars from the brief are checked at each gate.

8. Verification: the ladder

Each rung is a command with a JSON report, an exit code and a time budget. A rung runs only when the rungs below are green. The Director reads reports, never raw logs (research/testing-verification.md, research/generative-stack.md §8).

Rung What Where
L0 Spec schema, semantics, static game analysis, physical feasibility core, < 1 s
L1 Boot headless boot, 600 idle ticks at --fixed-fps 60, log gate (any SCRIPT ERROR, SHADER ERROR, push_error fails), invariants, two runs give the same hash headless Godot, < 15 s
L2 Play the golden-path tour; a monkey agent on 8 seeds; the template's heuristic agent (human-like reaction delay and noise) on 8 seeds (a platformer's playthrough plays every level as a casual person, per level: time, hurts and where, give-ups; an action survival game's seek plays its first minute as a newcomer, makes its first thing and must not be lost); an explorer search where state snapshots. Gates: solvable, reachable, no softlocks, no crashes, deterministic, and metric ranges over a Playtest's runs (most runs finish every level). Signals: skill gap, difficulty curve, time to first reward, pacing, action diversity, unfair deaths headless Godot, parallel, 30–120 s
L3 Frames shots at named moments (title, first control, first reward, first threat, win, lose); property checks: not blank, changed after input, subject present, every subject readable (its height and its colour difference from the ground around it, measured from the engine's own shadow-only re-shot; luminance contrast is reported, not gated, because accepted frames read by hue), and text a person can read whole (none cut or clipped, none under 18 px at 1080 rows); every shot that shows text again as a phone held upright shows it beside its touch controls (none cut, over another text or under a pane, none under 11 pt), by the web proof's own rules Compatibility on llvmpipe under Xvfb, windowless and bit-identical; GPU when a display is free
L4 Judge grounded questions only: "where is the player" scored against where the engine measured its pixels (finding the player gates), first-minute comprehension and glitch notes as advice; off unless asked for, cached by frame, budgeted the Anthropic API, or the local Claude Code CLI on a dev box; cents per run
L5 Web no-threads export, headless Chromium (GPU flags locally, SwiftShader on CI), unspent-ortho's fail list, the golden path in the browser with the outcome compared to native Chromium
L6 Human the person plays; the session is recorded as input frames with checkpoints; anything they report becomes a replay test the person

Look-only changes keep play. A proposal that touches only what views read (Style, Theme, Model, Sound, Sprite, Screen, CameraRig, Score, and an Archetype's looks, name, model and cues) must end every scenario in its base's exact state, or it is red with restyle.changed_play; a lint keeps simulation code from reading those kinds and looks.

Acceptance scenarios. Each approved feature adds a Playtest to the pack (seed, tour or bot, assertions, thresholds). Every later change set re-runs them before it can be approved. This is the answer to "fix one thing, break another".

Frames are for looks, bots are for play. Pixel-only AI players finish under 1% of VideoGameBench, and VLM glitch detection has high false-positive rates. Bots read state; the model judges frames with questions the engine can check.

9. Assets

Tier 1 is the product (research/asset-generation.md). unspent-ortho ships a lit 3D game with no imported art, but as bespoke code. Gamewright turns that into recipes the Director writes and versioned builders execute. The Director composes archetypes (tree.tiered with tiers: 4), not primitives; every build emits a contact sheet the Director can look at; numbers decide acceptance.

Tier 2 adapters sit behind narrow capability interfaces (image.generate, image.tileset, mesh.from_image, audio.sfx, audio.music, audio.voice), with model ids in configuration, because hosted models shut down monthly. One pipeline:

request (asset spec + style + references)
  → generate → freeze raw bytes on receipt (content hash)
  → normalize (palette snap, material replacement, sprite bake, mix sheet)   [deterministic]
  → QA (palette ΔE in OKLab, silhouette at play size, poly and texel budgets, loop seams, loudness)
  → proposal with a before/after contact sheet → approve → frozen asset + provenance record

The provenance record keeps provider, model id, request, date, cost, the licence and its terms URL, and the normalize chain. A validator refuses a commercial game that contains a non-commercial, research-only or region-restricted asset, and the records generate Steam and itch.io AI disclosures. First adapters: image (gpt-image-2.5 hosted, FLUX.2 klein 4B open), PixelLab for pixel tilesets, ElevenLabs SFX, ACE-Step for music; 3D last.

The person's own files come in the same frozen way (built 2026-10-09, design/assets.md). An Asset pins one picture (PNG, JPEG, still WebP; at most 4096 px a side and 8 MP) or sound (Ogg Vorbis, WAV, MP3; at most 60 s) by its hash, says what its bytes are, and records where it came from (who made it, its licence, whether AI made it): the ledger the store disclosures are filled from. The person imports it (a drop on the Studio, File ▸ Import…, gamewright import) as their own change; a Sprite with sprite.sheet cuts it into clips and a Sound with sound.file plays it through the same bake as a recipe. The Director uses Assets and never writes one.

10. Determinism

Summary of ADR-0004:

Promise Scope Test
Same pack + library ⇒ byte-identical bundle any platform bundle hash snapshots in core tests
Same bundle + seed ⇒ same world digest any platform, native and web seed vectors and world digests in native and web builds
Same bundle + seed + input frames ⇒ same state hash every tick one platform and engine version replay tests; determinism rung in L1
Same state + camera ⇒ same frame one renderer and driver shot tests on llvmpipe; property checks elsewhere

Seeds come from name paths: seed(child) = u32le(sha256(le32(parent) ‖ path)). Both languages implement deriveSeed, PCG32 and pcg_hash against testdata/vectors.json. Decisions use integers; floats that feed decisions are quantized.

11. The web platform

(research/web-platform.md)

  • Play. One no-threads Godot web player per engine release, immutable-cached, loads a bundle fetched at start. No server-side Godot on the play path. Games run in a sandboxed iframe on a separate registrable domain, with a strict CSP and no cookies there. Later, a custom template with javascript_eval=no. On a phone or tablet the player's own touch controls (§5.2) make every game playable without a keyboard, and its text stays readable on the small frame they leave (§5.4); the page only keeps the browser's gestures off the canvas. The web proof's phone visit checks both (tools/web.sh --touch).
  • Site. Next.js 16 (App Router) in web/, Vercel project gamewright on the Pro team xaxis-projects. The play origin is a second static project, gamewright-play, at gamewright-play.vercel.app (vercel.app is on the Public Suffix List, so it is a separate site); a custom play domain is a configuration change. Its deploy proves every gallery game in Chromium against production and keeps the previous bundles so the site's links never break.
  • Director on the web. AI SDK for streaming; one director turn per request at first, Vercel Workflows for long unattended runs later; Vercel Sandbox with a Godot image only for server-side verification and thumbnails.
  • Data. Today the sample games' bundles ship as static files with the play origin. When people make games: bundles in Vercel Blob (drafts private, published public, served through a same-origin rewrite on the play project); Better Auth on Neon Postgres via Drizzle; a per-user spend ledger.
  • Phases. Landing page (now) → gallery of playable demos → Director preview (invite, BYO key, hosted MCP) → accounts and projects → community (publish, remix, moderation).

12. Quality gates

Gate Where CI
Biome, tsc, GDScript formatter, schema lint, hidden-input check all yes
Core unit tests (Vitest): parsing, schemas, Expr, analysis, bake, change sets, tools core yes
Cross-language vectors: deriveSeed, PCG32, pcg_hash, JCS, Expr evaluation core + engine yes, native; web in the web job
Generated GDScript compiles under warnings-as-errors; a negative control must fail core + engine yes
Engine tests (gdUnit4, headless): kits, VM, runners, builders' golden hashes, save round trip, replay determinism engine yes
Example games: L0–L2 ladder on the sample packs CLI yes
Shots of sample games on llvmpipe (engine regression goldens) engine yes, under Xvfb
Web export boot of the sample games in headless Chromium web yes
Director stages against recorded fixtures core yes, replay only
Tours and perf on a GPU dev box no: scheduled local sweeps through tools/heavy.sh

13. Repository layout

docs/                ARCHITECTURE.md, ROADMAP.md, adr/, research/, design/
schemas/v1/          JSON Schemas: the contract (vocab.schema.json is generated from kits)
core/                TypeScript: @gamewright/core, the `gamewright` CLI, MCP server, Director
  src/foundation/    copied from Reelwright: strict JSON, pointers, JCS, seeds, schema machinery, codegen
engine/              Godot 4.7 project: the player
  addons/gamewright/ runtime/ (util, spec, sim, expr, graphs, bridge, save, present), kits/<kit>/
  player/ cli/ tests/
library/             templates/, plans/, starters/, packs/, styles/: data versioned with the engine
examples/<game>/     sample Game Packs, one folder each (tideline first)
web/                 Next.js site (Vercel project gamewright)
play/                play-origin static project (later)
tools/               toolchain.json, installers, doctor, check scripts, heavy.sh, deploy scripts
testdata/            cross-language vectors

14. Toolchain (pinned in tools/toolchain.json)

Tool Version Notes
Godot 4.7.2-stable + export templates official Linux zips and .tpz with SHA-512
Node.js / pnpm 24 / 10.30 the core runs TypeScript directly (type stripping)
gdUnit4 6.2.1 vendored
GDScript formatter GDQuest 0.27 pinned binary
Xvfb + Mesa Ubuntu 24.04 packages windowless frames (llvmpipe for Compatibility, lavapipe for Forward+)
Playwright Chromium pinned web boot and in-browser verification

15. Risks and open questions

  • Vocabulary ceiling. Data-first builders have a history of narrow launches (Roblox 4D shipped with two schemas). Mitigations: kits grow from logged KitRequests, local kits give the owner an immediate escape hatch, and the fourth template is the test.
  • GDScript performance. Interpreted Expr, graphs and builders in GDScript may be slow for big worlds. unspent-ortho's world takes 25 s to generate. Budgets are tests; a GDExtension (with wasm builds) is the fallback for hot kernels, decided per kernel by measurement. So far GDScript holds when the hot loops make no calls and no small arrays: Tideline's 192x192 world took 4 s to generate and 1.5 s to draw its first nine chunks, and takes 0.35 s and 0.3 s (native) since noise is read a grid at a time (GwValueNoise.grid, the same bits as point by point) and per-tile and per-vertex work reads packed arrays directly.
  • Native vs web float drift. Decisions come from integers; the web job runs the vectors and world digests; long float trajectories aren't compared across builds.
  • Hosted model churn and licences. Model ids are configuration; outputs are frozen on receipt; licence records are validated.
  • Name. Unchecked for trademarks. The site is gamewright.diy; the play origin is gamewright-play.vercel.app until the owner chooses a custom domain for it.
  • No Anthropic API key on the dev box. MCP-first direction works without one. The in-app Director, fixture recording and the hosted chat need a key.
  • Godot contribution policy bans AI-generated substantial code in engine PRs. Anything Gamewright sends upstream must be written by a person.

From docs/ARCHITECTURE.md in the Gamewright repository.