Game Packs and documents
In Gamewright a game is a Game Pack: a folder of small JSON documents, plus any media they name. It holds no code, no Godot scenes and no scripts. One player runs every game, on the desktop and in the browser (ADR-0002). Because a game is only data, it can be checked before anyone plays it, run safely from anywhere, shipped without its own copy of the engine, and given a new look without touching how it plays.
A pack is a folder
Here is part of Tideline, the first game made this way:
tideline/
game.json the game: its template, theme, style, world and player
brief.json the brief: its pitch, pillars and core loop
archetypes/sentry.archetype.json an Archetype: a kind of entity, here the machine
brains/sentry.sm.json a StateMachine: how the sentry patrols, notices and hunts
items/driftwood.item.json an Item
recipes/campfire.recipe.json a Recipe: three driftwood and two stones make a campfire
rules/tideline.rules.json a RuleSet: on an event, where a condition holds, do actions
worlds/tideline.worldgen.json a WorldGen: how the coast is generated, pass by pass
models/sentry.model.json a Model: a recipe the engine builds the sentry from
sounds/hit.sound.json a Sound: a recipe the engine synthesizes
ui/hud.screen.json a Screen: the HUD
playtests/proof.playtest.json a Playtest: a scenario every change must keep green
.gamewright/ what the tools make: bundles, reports, proposals
A file's name says what it is: <folder>/<slug>.<kind>.json, so driftwood.item.json is an
Item wherever it sits. The folders are yours to arrange. The game's manifest, game.json, and
its brief, brief.json, sit at the top. .gamewright/ holds only what the tools make, so keep
it out of git: put .gamewright/ in the pack's .gitignore.
A document
Every document starts the same way: its kind, the schemaVersion, an id, a name for
people, and meta (its status, where it came from, notes). The rest is the kind's own:
{
"kind": "Item",
"schemaVersion": "1.0",
"id": "item.driftwood",
"name": "Driftwood",
"meta": {
"status": "draft",
"provenance": {
"createdAt": "2026-09-30T00:00:00Z",
"createdBy": "user",
"license": "CC0-1.0"
}
},
"description": "A grey length of sea-worn wood. Burns well once dry.",
"bulk": 2,
"stack": 10,
"tags": ["fuel"]
}
Ids say what a thing is, with its kind first: item.driftwood, arch.sentry, rcp.campfire.
Documents name each other by id, and a change to a list names its entry by id too, never by
its place in the list, so an edit never lands on the wrong thing. Document
kinds lists every kind, where it lives and its fields.
The template underneath
game.json pins a template: "template": {"id": "tpl.action_survival", "version": "1"}. The template brings the kits the game runs, the controls, the camera, the
screens and its starting documents, so a game on it is playable from its first step.
The game's own documents then replace the template's, one by one, by id.
What documents can say comes from the engine's kits: the components an entity can
have, the actions a rule takes, the events it hears, the functions an expression calls.
Conditions and formulas are expressions in a small language like Python's:
self.health < 30, clamp(tune.alarm_radius * 2, 4, 20). When a game needs something no kit
offers, the answer is a new kit of reviewed engine code that every game can use, never code
inside one game.
Schemas are the contract
Every kind has a JSON Schema in schemas/v1/. The core checks every document against it, and the
engine's own types are generated from the same schemas
(ADR-0005). The rules that make documents easy to write and
hard to get wrong:
- Closed objects. A field the schema doesn't know is an error, with a suggestion when it is close to one it does.
- Flat lists. A tree is a list whose entries name their
parent, never objects nested deep. - Units in names.
timeMin,heightM,windupMs: a number says what it measures. - Values in fields, logic in expressions. Numbers and ids go in fields; conditions and formulas go in expressions.
The core writes, the engine plays
Only the core ever writes a document, and always in one canonical form, so a change shows as a
small diff and the same content always hashes the same. The Studio reads documents but never
writes them. Check the form, or hash a document, with the gamewright command line that came with
the Studio (installing says where it is):
gamewright fmt --check ~/Documents/Gamewright/Tideline
gamewright hash ~/Documents/Gamewright/Tideline/items/driftwood.item.json
Bake turns a pack into a bundle: the template and the game resolved into one set of
documents, expressions compiled, everything hashed, in .gamewright/build/<hash>/. The same pack
always bakes to the same bytes. The player loads a bundle, checks every hash, and refuses
anything that is not a known kind of document or an allowed kind of media: it never loads a
script from a game.
Locks and history
A document whose meta.status is locked is fixed; meta.locks fixes parts of one, by item and
pointer, such as "keep the sentry's body, change its brain". The Director treats locked parts as
given, and a change to one is refused. Lock from a terminal with changes lock, or ask the
Director's deciding tools in Claude Code.
A pack is meant to be its own git repository. When it is, every change you approve is a commit
with a Gamewright-Change: line naming the change, and changes undo applies its inverse.