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
- 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.
- 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. - 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.
- 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.
- Done means played. A validator is not a playtest. Every game carries acceptance scenarios that every later change must keep green.
- 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.
- 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.
- 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), fromres://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 aresult.jsonplus 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 akit.jsonregistration: 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 generatesschemas/v1/vocab.schema.jsonfrom 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, whichgamewright new --startercopies 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.
- Read the bundle manifest. Check the engine range, schema major and every content hash.
- 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.
- 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. - 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'ssprite, 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
InputFrameper 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
pointersays 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 optionaltouchhint 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
RefCountedclasses 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 andpcg_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'sExpressionclass is not used (it can call arbitrary methods). - RuleSet runner.
{on: event, where: Expr, do: [actions]}, withonceand cooldowns. Actions are a closed union registered by kits.eachqueries carry amax, 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
CameraRigdocuments: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
AudioStreamWAVbuffers, 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 kindplan(design/m6-presentation.md §7). - UI from
Screendocuments: 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 acompactplace. 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 asGwJuice, a contract inruntime/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 aGamewright-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
gamewrightCLI,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 eventsschemas/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,
destructiveHintover 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.
- MCP first.
gamewright mcpexposes 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. - The in-app agent (the Studio, and CLI
gamewright director) uses the official Anthropic SDK:claude-sonnet-5-5by 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 chooseclaude-opus-5-5for the hardest changes orclaude-haiku-4-5for 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. - 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 projectgamewrighton the Pro teamxaxis-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.