Docs Design

Contents

M4 design: the second to sixth templates

Status: draft, 2026-09-30. Read with ARCHITECTURE §4, ADR-0003 and ADR-0006, and research/generative-stack.md §5–§6. M4 proves "any game": two templates unlike the first, and a count of how many new kits each needed. The test for the vocabulary (ARCHITECTURE §4) is that the fourth template needs mostly packs; M10a's visual_novel is that test (updated 2026-10-01), M10b's adventure takes it again on the genre most people mean by a game (2026-10-02), and M10c's twin_stick takes it on real-time shooting, where it needed one new kit (2026-10-02).

puzzle_grid: turn-based rules on a grid (Sokoban-like)

Why second: it forces the logic layer to be general (rules, not bespoke systems), it's the cheapest way to prove the verification ladder with a real solver, and it's a common request.

Kits

  • kit.grid: a grid world of cells with layers (floor, object, overlay), object kinds with tags, and the player's avatar as one object. Turn-based: the simulation advances one turn per input action, not per tick; ticks between turns only animate. Undo and restart as actions, with a history of states (bounded). GwWorldQuery is implemented for the grid so generic kits still work where they apply.
  • kit.grid_rules: PuzzleScript-style rewrite rules as data. A rule is a pattern of cell contents along a direction ([ > player | crate ] → [ > player | > crate ]), with movement arrows, "late" rules that run after movement, and win conditions (all crate on target, no player, some key on door). Rules are a flat list with ids; patterns are flat lists of cells with parent rule ids (ADR-0005: no recursion). Execution is deterministic: rules in list order, each applied until it no longer changes the state, with a bound on iterations per turn (reported, never hung).
  • Solver (core, bake time). A breadth-first search over grid states with the same rule semantics (the TypeScript rules engine mirrors the GDScript one, checked against shared vectors like the Expr VM). It proves each level solvable, stores the shortest solution as the level's witness, and gives difficulty signals: solution length, branching, dead states reached.
  • Generator pass (grid.levels): place walls, crates and targets by reverse play (pull crates from solved positions), then verify with the solver; seeds by name path per level.

The template

InputMap (up/down/left/right/undo/restart), a grid camera (orthographic, top-down or ¾), a level select screen, win and level-complete flow, knobs (grid size, crate count, minimum solution length), and acceptance: every level has a witness, the golden path replays each witness, a monkey bot never crashes, and the heuristic bot solves the first three levels.

platformer_2d: a second physics mode

Why third: real-time 2D with jumping stresses movement, capability-aware reachability, and the sprite baker.

  • kit.platformer: a kinematic AABB mover against a tile grid: run, jump with coyote time and buffering, variable jump height, one-way platforms, ladders; all fixed-tick and deterministic.
  • Reachability (core): from the player archetype's numbers (run speed, jump apex, air time) compute which tile gaps and ledges are reachable, so generated rooms are checked before play.
  • Generator (rooms.chain): Spelunky-style: a solution path through a grid of rooms first, then room templates with guaranteed exits, then filler; each room validated against reachability.
  • Sprite baker (look track): render any 3D model recipe from a fixed camera into sprite frames, so 2D games get consistent art from the same recipes.

As built (2026-09-30)

  • kit.platformer: the mover as designed (run, jump with coyote time, a jump buffer and a jump cut, climbing, patrols), and touches (collect, checkpoint, exit, hurt). The core and the engine share it through testdata/platformer-vectors.json (25 traced cases), so the core's search moves bodies exactly as the game does.
  • Reachability searches with the game's own mover rather than computing tile gaps from the numbers: it can't disagree with play, and the witness it stores (inputs as runs like 32R 5- 14R 60RJ) replays tick for tick through the route tour verb, which must touch the exit on the witness's last tick. gamewright reach prints each level's witness.
  • rooms.chain lays a path through a grid of rooms, then fills the rest; the search proves the result completable, and the chain and rooms are written back at bake.
  • The sprite baker is kit.sprite, baking at load with a CPU rasterizer instead of rendering a SubViewport: the core's bake doesn't run Godot, and the CPU route gives the same pixels headless, windowed and on the web, so golden hashes can pin them.
  • New kinds: Room, PlatformLevel, Sprite. The sample is examples/burrow_run (in the gallery); the fixture it grew from is core/test/fixtures/games/platform_mini.
  • Not done: a 2D Style profile (the HUD falls back to the default 2 px pitch, which reads small). Asserts on platformer functions were not checked either (the core's assert checker knew only count and dist) until M3e moved asserts into the engine (m3-verification.md, decision 10): platformer_done(...) is checked now.
  • Later (2026-10-01): the platformer bot for sim --bot is kit.platformer's playthrough, a person's hands on each level's witness (m3-verification.md, decision 11), and the template ships test.playthrough (a casual player on 8 seeds; most runs must finish every level), so a Director-made platformer is measured per level (time, hurts and where, give-ups) without writing a Playtest.

visual_novel: the thesis test (M10a)

Why fourth: it has no simulation of its own, so everything it needs must come from the logic layer and the UI. ADR-0003 says the fourth template should need mostly packs; this one was built to need no new simulation kit, and to say plainly what was missing when it couldn't.

Saying it with what existed

Each thing a visual novel needs, against the vocabulary on 2026-10-01, before anything was added:

Need What existed Verdict
Flags a later line reads Stats vars, set_var, Expr when expressible
Endings end_game, outcome() expressible
A save that resumes on the same line kit.core saves vars and rule state expressible, if the story's position is a var
The player advancing a line or picking a choice RuleSets react to events missing: no kit emits an event for a key, so rules can't hear one. Only kit.core's title reads a press; grid, platformer and move read the frame in their own systems.
The story as a graph StateMachine no driver: only kit.brain_fsm runs machines, one per AI body, and it requires kit.move, kit.perception and kit.melee. A machine holds at most 64 states.
A line's words on screen a Screen label one label per line with when: var.at == 'n12': the words live in presentation, keyed by a string var, at most 128 widgets a screen, and nothing checks the story before play
Choices on screen, picked by keys a list widget rows come only from a kit's feed, none lists the choices open now, and a list's cursor is UI state a replay can't see
Words appearing as they are said label missing
Rereading the line before vars vars are scalars: a backlog would be N vars shifted by every line

Two smallest additions were weighed:

  1. An input.pressed event in kit.core, and a game-level StateMachine driver. Advancing and branching become expressible, but every line becomes a state whose onEnter sets var.line to an Expr string literal. The words scatter into actions, the 64-state cap holds a few minutes of story, rereading stays N shifted vars, and the core can't tell which endings a story reaches, because the story is generic machine state.
  2. A Dialogue kind, the L2 graph ADR-0003 and ARCHITECTURE §5.3 already name, in the shape research/generative-stack.md §7.2 gives it: lines {id, speaker, text, when, do}, links {from, to, choice?}, conditions in Expr and effects in the rule vocabulary, run by kit.core beside its RuleSets. Chosen: it is a new kind, not a new simulation kit, the Director writes a story as a story, and bake can check the graph.

The UI side needed no new kit either, only widgets and fields any game can use.

As built (2026-10-01)

  • Dialogue (schemas/v1/dialogue.schema.json, dlg.*): start, revealCps (default 40), speakers, lines and links, all flat lists with ids. A line goes on to the next line in the list unless a link from it says otherwise: jumps (links without choice) are tried in order and the first whose when holds is taken; choices (links with choice) are offered once the line is whole, fixed when the line is shown, after its do. A line whose when fails is passed over as if advanced (at most 64 a press). end ends the dialogue after a line; so does the last line. Writing top to bottom keeps a story short: the template's 13-line story needs 3 links.
  • kit.core gained the dialogue system (between core and rules) driving GwDialogueRunner (runtime/rules, beside the RuleSet and StateMachine runners), the start_dialogue action, the events dialogue.line, dialogue.chose, dialogue.ended, the pure functions dialogue_line(), dialogue_speaker(), dialogue_choosing(), dialogue_back(), the tour verbs read LINE and choose CHOICE, and the save key dialogue. Presses: advance or confirm (show the line whole, then go on; on a choice, take the marked one), up and down (move the mark), choose:<link> (take one by id, what tours and bots send), back (reread, 32 lines kept). The words shown so far are a function of the saved tick the line appeared on, so a view reads them and a load shows the same words.
  • The core checks a Dialogue's graph (core/src/game/dialogue.ts): lines, link ends and speakers exist; ids are unique; every line is reachable from start or a line a start_dialogue names (whatever the whens: the playtests prove the walks the flags allow); and no branch runs on, by list order, into a line another line offers as its own choice. A shown line whose do ends the game goes on to nothing, so an ending may sit just above another branch; with a when it can still be passed over, and then goes on. The check found a real bug in the template's first story: an ending line with end that a when passed over ended the story before the other ending.
  • kit.ui: the dialogue feed; a label's revealKey (the words said so far, drawn over the whole text's layout, so a word never jumps lines; L3 measures the whole text); a list's chosenKey (the game's pick, shown lit; such a list takes no menu focus); the sprite widget (a baked Sprite named by sheet or by a bound value like var.left, scale pitches a pixel, mirrored by flip; every Sprite is baked at bind when a screen draws one); look: fill frames (color fading to colorTo in 16 whole-pitch bands, from the Style's palette). Screen pitch pairs now reach ±1024, since a 3-pixel pitch at 1080 lines is 640 pitches wide.
  • The template (library/templates/visual_novel): kits core, ui, sprite and mesh; an InputMap of advance, up, down, back (mouse: click advances, the wheel rereads); a flat Style at pitch 3 for 1080 lines (the screen is 640 by 360 pitches at every size); one story Screen (ui.story) as the HUD: a backdrop filled from the palette, var.left and var.right as standing figures, the words on a pane with the speaker's plate, the choices on a pane of their own, the keys that move the story, and an end card reading var.ending; a title; the stage vars (scene, left, right, ending); rules.story (start dlg.story on game.start, end the game if a dialogue runs out); and a 13-line story of its own with one choice, a flag and two endings, so a game.json that only pins the template plays from title to ending (core/test/template-novel.test.ts proves both endings through Playtests). The fixture is core/test/fixtures/games/vn_mini.
  • The sample is examples/fog_signal (in the gallery). One night at a lighthouse: Tomas rows out for his sister before the fog, the lens's clockwork breaks, and a small light appears off the reef. 75 lines, three speakers, three choices, four flags and four endings; one reading is 46 to 48 lines, about 850 words, some six minutes. The flags change later lines (the shore sees your own lantern, or a light nobody should have; Tomas says he should have waited, or calls the lost boat a fair trade) and pick the ending. Its four scenes (the pier at dusk, the lamp room, the shore in the fog, dawn) are its own ui.story: fill frames in its Style's slots and 10 baked Sprites, three figure.person people, a wreck.hull and a rock.faceted, and five mesh.parts props (the Skerry, the pier, the boat, the lens, the tower). Nothing was added to the engine for it.
  • Proof: five Playtests with inline steps, one per reading, covering every answer to every choice, every ending and every flag-changed line (assert line:s_mine and assert not line:s_whose as they are read, the flags and the ending asserted at the end); the proof tour, all five readings in one run for the web rung; a resume Playtest (save in the lamp room, read on past a choice, load: assert same-state, and L3's saved and loaded frames are byte for byte the same); the look tour (L3: 11 frames, every text whole and at least 9 pixels tall at 360 rows); and the monkey (four games of 36000 ticks, one must end). A wrong flag assert turns L2 red.
  • Not done: a backlog list (back steps one line at a time, 32 kept); auto-advance and skipping read lines; picking a choice with the mouse (kit.ui has no pointer picking; a click advances); expressions (only figure.person's poses, and only idle is baked); L3's changed-after-input check takes a frame restored by load for a frame that failed to change, so the resume tour shoots once between them.

adventure: a top-down adventure (M10b)

Why fifth: it is the genre most people mean by "a game", and it is made of pieces every earlier template brought. A village with people to talk to, a quest given in words, a wild place with things that hunt you, items to find and use, and a way back to finish it. If the vocabulary is at the right level, this one should be documents: no new simulation kit, and only small generic features where something truly can't be said. The test is to try saying it first.

Saying it with what existed

Each thing an adventure needs, against the vocabulary on 2026-10-02, before anything was added:

Need What existed Verdict
A place to walk, seen from above kit.world_tiles: a WorldGen of terraces, grounds, water, props and named places expressible
A village here and a wood there ground and scatter rules bound level, slope, shore distance, moisture and patches of noise, all over the map missing: no rule can keep to one part of the map, so houses scatter everywhere and a wood can't be put at one end. Entity houses would need a move component to block a body, and the routes walk through bodies
The hero walks, swings, dodges, is downed and wakes kit.move, kit.melee (dodge, combatant downed, respawn place) expressible
People standing in the village Archetypes with figure.person models, spawned at named places by a RuleSet on game.start expressible
Talking to someone kit.harvest's use key works props and piles; RuleSets hear events missing: nothing tells a rule which person the player faces (harvest.refused has no target, and an Expr can't name an entity). The nearest thing, a villager's perception noticing the player, starts a talk whenever you pass and can't be refused
A conversation that fits the quest so far Dialogue (M10a): lines with when over var.quest and item_count(self, ...), do with give, take and set_var expressible
Standing still to talk; the keys being the talk's the dialogue reads advance or confirm missing: in a game that walks, E is both use and confirm, so the press that ends a talk starts it again, and W moves the hero while it picks a choice
Choosing on a phone touch shows play buttons; confirm and the menu arms only when a screen asks missing: nothing asks while a talk runs, so a phone has no Next and no way to pick a choice
A quest given in words, shown on the HUD a Stats var set by the Dialogue's do; a label bound to an Expr over it expressible
A talk panel kit.ui's dialogue feed, revealKey, chosenKey (M10a) expressible
Things that hunt you in the wild kit.perception, kit.brain_fsm, kit.melee, spawned at named places expressible
An item to find a Biome prop with a take rule (a chest, a shrine), placed by a count rule, with a site beside it (rule: prop) expressible, once the prop can be kept to the wood
Items to use a weapon: melee damage is an Expr, 2 if held(self) == 'item.hatchet' else 1; food: a Recipe with no outputs that a rule on crafting.made answers with add_stat the weapon is expressible. Food is not, found by building it: the core refuses a recipe that makes nothing (recipe.empty), and holding an item sends no event. Left as a gap at first (berries were eaten as they were picked, a rule on harvest.taken taking them back and giving health); closed by point 5 below
A reward and a win the giver's Dialogue takes the item and gives the reward; a rule on dialogue.ended calls end_game expressible
A bot that plays the quest kit.core's seek: reach, near, take, have, make, fight, survive, win missing: no goal talks, and a goal met once is met for good, so "talk to the giver" can't come twice
Music that follows the quest and the danger a Score's sections and layers over var.quest and watched(self) expressible

Six gaps; five were closed with the template, the sixth (using up a carried item) the same day, as point 5. The smallest generic additions, each tested and none naming a game:

  1. Rules kept to a disc (kit.world_tiles). Ground and scatter rules take atFrac and withinTiles: only tiles within that many tiles of that point. A village green, the houses round it, a wood at the far end, the stones of a ruin. Two fields on two structs, in the two spare ints the ground rules already carried; a rule without them is unchanged, so every existing world keeps its digest.
  2. Usables (kit.harvest). A usable component (verb, dialogue) makes an entity something the use key works when you face it: a person, a sign, a lever. Using it sends harvest.used (rules answer: open a gate) and starts its Dialogue. harvest_target stays the one rule, so the hint (use_state says usable, use_verb says talk) never promises what the key won't do. While a Dialogue runs, and on the tick one ends, the key is the conversation's and a body driven by input stands. The tour verb talk [ARCH] faces the person and presses use.
  3. A talk asks for its keys (kit.ui). While a Dialogue runs the HUD asks the stage for advance or confirm, back, and up and down while a choice stands, as an open app asks for the menu's keys; the touch layer shows exactly those, so a phone gets OK and the choice arms and loses the stick until the talk is over. A visual novel, whose story always runs, now shows its rocker only at a choice.
  4. Seek talks (kit.core). talk:<arch id> walks up to the nearest of them, talks, and reads the Dialogue to its end with the new read_all verb, taking the first choice on offer (so a game writes the choice that gets on with things first); bare talk goes to the nearest person with something to say. Goals met by a step finishing are met where they stand in the list, so talk:arch.warden … talk:arch.warden works.
  5. Using up a carried item (kit.inventory, kit.ui, kit.core; docs/design/item-use.md). An Item's use (verb, charges) says what using it costs; the carry app's confirm on its row presses use:<item>, kit.inventory spends a charge, takes one away when they are spent and sends inventory.used, and a rule says what it does. The usable's shape again, for a thing carried instead of a thing in front. The carry app's confirm now holds an item with no use, as its screen always said (nothing answered it before), and the key strip says the chosen row's verb. Seek's use:<item> goal and the use_item tour verb play it.

Gap three, standing still with the keys the talk's, is the second half of (2). No new kit, no new kind, no new document field outside kit registrations. Building it found two bugs the genre exposed, fixed at the root: the seek bot never counted a step that finished on the tick the game ended (the talk that wins it), because the runner stops asking for frames then; and the core's analysis checked a template folder on its own without its own player's starting items, so the hero's staff read as unobtainable.

The template (tpl.adventure)

  • Kits: core, world_tiles, clock, move, inventory, perception, brain_fsm, melee, harvest, mesh, ui, audio, actors. A game adds kit.music for a Score, kit.crafting for recipes, and kit.light_carried for a lantern.
  • InputMap: move (stick), run, use (talk, read, take), swing, dodge, carry, pause, confirm (E and Enter: on the title, in menus, and to go on in a talk), back (reread), up and down (menus and choices). Touch: Use, Swing, Dodge and Run under the right thumb, Carry along the top; in a talk only OK, Back and, at a choice, the arms.
  • Style: lit_ortho3d in daylight (the clock runs slowly, from late morning), a warm green palette, the slate restyled as a dark wooden board with parchment text, and the mix sheet.
  • CameraRig: the ortho rig a little closer than action survival's (13 m of view), so a person reads as a person.
  • World: gen.adventure, 72 by 72, a village on the west (a trodden green, a well, four houses) and a wood on the east (pines on moss, berry bushes at its edge, and a grassy glade with the chest the quest is for), with named places green, spawn, elder, wood_edge, glade, cache, den and thicket.
  • Cast: the hero (walks, runs, swings a staff, dodges, carries, is downed rather than killed and wakes on the green), the elder (a usable with dlg.elder), and the wood's beast (sees, hears, hunts, bites with a tell, has a soft side).
  • A quest of its own, so a game that only pins the template plays from title to win: the elder's Dialogue reads var.quest (0 not asked, 1 asked, 2 done) and what the hero carries; the chest in the glade yields the relic, and a beast keeps watch beside it; bringing it back ends the game won.
  • UI: the title; the HUD (health cells, the quest line under them, what is in hand, the message line, the use hint reading use_state and use_verb, and the talk panel: speaker plate, the words as they are said, the choices, the keys; an end card); the carry app.
  • Default Playtests: test.fuzz (the monkey on 4 seeds; it talks too, so every talk's keys get mashed), test.first_talk (seek talks to the nearest person with something to say and holds a minute: a newcomer is told what to do, the talk ends, and they aren't lost), and test.quest (seek plays the quest start to finish on 4 seeds: talk to the giver, reach the glade, fight its beast, take what the quest is for, come back, talk, win; most runs must not be downed). test.quest names the template's own cast, so a game that brings its own quest replaces it by id with its own goals; one that keeps the template's ids keeps it as it is.

Questions for the Director

  1. quest: "Who asks the player for help, what for, and what do they give back?" Becomes the giver's Archetype and Dialogue, the quest var's steps, the item, the reward, and test.quest's goals.
  2. places: "Where does the player start, and where is the wild place they are sent to?" Becomes the WorldGen's discs, grounds and props, and its named places.
  3. danger: "What lives in the wild place, and how is it beaten?" Becomes the foe's Archetype, its brain and its blows' tell and soft side.
  4. people: "Who else lives there, and what do they know?" Becomes more usables with their own Dialogues: hints, a gift, a sign.

As built (2026-10-02)

  • The template (library/templates/adventure, 45 documents) checks clean on its own and as a game (core/test/template-adventure.test.ts): a game.json that only pins it is L0–L2 green in the engine, 12 Playtest runs (the monkey, the first talk and the quest on 4 seeds each; the quest won in 1,600 to 1,800 ticks), and red when its rules leave the elder out ("there is no one with something to say").
  • The sample is examples/thornwick, 51 documents and three tours, nothing added to the engine for it. Thornwick's warden lost the shrine bell to the thornbacks of Briar Wood. Four usables (Maren, Odo, Wren, a signpost), 25 lines of talk (1,870 characters) with three choices at Maren's ask, a hatchet that halves the blows a thornback takes, berries, three thornbacks, the bell, a Score in D major whose layers follow the quest and the danger, and a 112 by 96 world: the village, a meadow with a tarn, a trail of trodden earth (eighteen small discs) through the wood to the shrine's flagstones and pillars.
  • Proof: verify --level L3 green: L1 the same state twice, L2 16 runs (the quest on 4 seeds in 3,428 to 3,619 ticks with two fights each and never downed, the quest without the hatchet on 2, the proof tour, the look tour, the fuzz and the first talk), L3 14 frames, every subject readable (the thornback 42 px tall, ΔE 45 from the grass) and every text whole. The web rung: the proof tour in Chromium ends at tick 4,363 on the desktop's state hash, and the phone visit is green (layouts upright and sideways, the stick moves, every button presses, text at least 11 CSS px); a scratch touch-only run walked to Maren, tapped Use, and the touch layer offered OK and Back, then the choice arms, then the stick again (docs/images/m10/thornwick-phone-choice.png). Fog Signal's web and phone proof, rerun with its rocker now shown only at a choice, is green and on the desktop's state.
  • How long it plays: the seek bot, walking straight and reading at once, wins in about a minute of game time; a person reads about 330 words and learns two fights, so about five to eight minutes for a first play. That is an estimate: no person has timed it.

What's missing

  • Using up a carried item: closed (point 5 above, docs/design/item-use.md). A use that refuses (only when hurt) is not built: a rule says what happened instead.
  • Doors and rooms: the world is one outdoor map; an indoors needs a second world or a kit.
  • Quests as data the core can check: a quest is a var and lines that read it. The core checks the graph, the playtests the walk; a Quest kind could let bake prove every step reachable.
  • Picking a choice by touching it, a backlog, and a talk that pauses the world: the beasts keep moving while you talk (the template keeps them away from the village).
  • A swing or dodge pressed mid-talk still happens; only the use key and walking are the talk's.

twin_stick: a top-down arena shooter (M10c)

Why sixth: it is the genre that asks most of the vocabulary's real-time side. One stick moves, the other aims and shoots, waves of foes come at you, the score grows as you survive, and a short run ends in a win or a loss. Most of that is the action survival play kits again (a world seen from above, walking bodies, things that notice you and hunt you, blows with tells). One part is not: nothing in the vocabulary flew. The test was again to say it first, and to count honestly what could not be said.

Saying it with what existed

Each thing a twin-stick needs, against the vocabulary on 2026-10-02, before anything was added:

Need What existed Verdict
An arena seen from above, with cover kit.world_tiles: a disc of floor (atFrac, withinTiles), boulders that block bodies, named gates expressible
One stick moves kit.move, driver input expressible
The other aims, apart from moving the input frame's aim, filled by a stick action named aim (GwDeviceInput); kit.move faces it, kit.melee strikes along it expressible on keys and pads
Shooting: a shot that flies, is stopped by a wall, can be stepped out of; a foe that shoots kit.melee: a blow that reaches at most 20 m at once, through walls, every body in its band missing: nothing flies. A long, thin blow is a hitscan beam through rocks that no one can dodge by moving, and a foe that spits can't be said at all
Foes that know where you are and come at you kit.perception (all round, 60 m, the player always heard) and kit.brain_fsm (hunt, attack) expressible
Waves over time RuleSets on: tick with vars and a clock var; spawn at a named place expressible (a spawn's at is a literal place, so one rule per gate, taken in turn by a var)
Clearing the dead between waves each over the foe tag, not target.alive, despawn expressible, with a trap found by building it (below)
A score that grows as you survive and as you kill vars; add_var on melee.killed and on tick with cooldownSec: 1 expressible
Score, wave and health on the HUD kit.ui labels bound to var.score, var.wave, cells on self.health expressible
A short run, won or lost end_game after the last wave, and on the player's melee.killed expressible. Playing again once a run is over was the player app's gap, closed on main the same day by GwPlayAgain (the press that leaves the title, after an end, boots a new game to the title), not in a kit
A second stick on a phone GwTouchPlan: the first stick action only missing
The controls in a hint: "[WASD] move [↑←↓→] aim" GwUiKeys: a stick action whole reads ? missing (small)
A fuzz that shoots the monkey treats every stick as the walk missing
An L2 bot that aims and shoots, so a Playtest can say the first wave is survivable seek's fight is kit.melee's tactics bot missing
A swarm chasing one body that moves, at 60 ticks a second in a browser kit.move asks kit.world_tiles' route every tick for every ai body; a route field is a Dijkstra over the rectangle round mover and target grown 24 tiles, rebuilt whenever the target's tile changes found by building it: Saltwell's sim spent 12 to 14 ms a tick, nine tenths of it routing (212 field builds in 40 s, about 160 ms each)

Seven gaps. One is a new mechanic and became the one new simulation kit; the rest are small generic features, each tested, none naming a game:

  1. kit.ranged (new: shooters and projectiles). A shooter fires shots that are entities of a projectile Archetype (<shooter>/shot.<n>, drawn by kit.actors from its model, saved like any entity), each flying straight and level until a wall (the world's edge, a blocking prop, a cliff two levels up), its range, or the first combatant its swept circle touches. Driven by input it shoots where the aim stick points as soon as it is pushed past half (aimFires, so a phone's right stick shoots with no button), or where it faces with fireAction held; an ai body shoots when a state machine's shoot asks, after a windupMs tell with its aim fixed when the tell begins, so it can be stepped out of. A volley fans count shots across spreadDeg; damage is an Expr of the shooter. Events ranged.tell, ranged.fired, ranged.spent; pure can_shoot and in_tell; the skirmish verb and bot (6 below). Why a kit and not a feature of one. A shot is not a blow: it lives after its shooter's window, moves on its own, is stopped by the world, and is a thing you see and step out of. Folding it into kit.melee ("close combat") would have bent that kit's whole description; a body that flies isn't kit.move's walker either. What a shot does when it lands is kit.melee's, though, so kit.ranged requires it and lands every hit through one new seam there: receive(by, from, t) (evaded in dodge frames, guarded in hurt frames, rung off plate, else open) and strike_from (hurt on the side it came from, stall on the working part, kill or down). kit.melee's own blows now go through receive too, and every gallery game's proof tour ends on the state hash it had before. So a shot meets dodges, hurt frames, plate and working parts exactly as a blow does, and melee.hit, melee.killed and the ladder's death and reward metrics name the shooter with no change to the core.
  2. A second touch stick (runtime/touch). The second stick action is a stick under the right thumb, level with the first, with the round buttons standing above it; each stick has its own finger, so both thumbs steer at once; the stage names them Left stick and Right stick (docs/design/touch-controls.md §2). The browser's phone visit holds both at once.
  3. A hint names a stick (kit.ui). GwUiKeys gives a stick action whole its first key each way, run together: WASD, ↑←↓→; on a phone, the stick's name.
  4. The monkey aims (kit.core). With a stick action named aim, the monkey pushes it too, on a stream of its own (bots/monkey/<n>/aim): half the time one way, half let go, 30 to 120 ticks at a time. A game without an aim stick never draws from that stream, so its frames are what they were (tested frame for frame).
  5. Routing a swarm (kit.world_tiles, kit.move). A route field is now flooded only as far as the mover asking needs: its frontier is kept, and it is expanded until the mover's own tile is final, which is all the walk down from it reads, so every answer is the whole flood's (tested against the whole flood over 400 routes on a generated coast). The flood reads a mask of the tiles a walker can stand on, made once per world revision, instead of calling can_step twenty times a tile. And kit.move's new rerouteMs (optional, with no default, so no body without it changes) lets an ai body ask for its route only that often, each on its own beat, walking to the point it was given in between and asking again at once on reaching it. Saltwell's sim went from 14 ms a tick to 4 or 5 (routing from 12 ms to about 2), and its proof tour runs at 58 ticks a second in Chromium. Every gallery proof tour ends on the same state hash as before both.
  6. The skirmish bot (kit.ranged). It plays a ranged fight as a fair person would: backs off a foe nearer than four metres, closes on one out of range, keeps clear of walls, strafes round the nearest foe, aims at the nearest whose plate does not face it with 8 to 15 ticks of reaction and a few degrees of error drawn from its own seeded stream, leads it by half the flight time, shoots while it is in range, and dashes a tell or a shot it sees coming. As a Playtest bot it plays until the run ends; as a tour verb, skirmish SECS [until LATCH], it stops at a moment to shoot.

Gap 7, playing again after a loss, was the player app's and closed on main (GwPlayAgain); both end cards say the key.

Building it found three bugs, fixed at the root and each pinned by a test that failed first: the monkey (test.fuzz) walked the player to the world's edge and fired outward, and the shot stood past the edge for a tick, breaking the in_world invariant (a shot fired point-blank into a wall or off the edge is now never made); Saltwell's spawn place, first written as "beside the well", found no tile and the player started in the map's corner (the world's own error said so, and L1 didn't fail on it: the place is now its own rule); and the wave rules cleared the dead on the very tick the last foe fell, before that tick's melee.killed rule could find the body to count it, so each wave's last kill went unscored (the proof's var.kills assert caught it: the dead are now cleared half a second later, and the playbook says why).

The template (tpl.twin_stick)

  • Kits: core, world_tiles, clock, move, perception, brain_fsm, melee, ranged, mesh, ui, audio, actors. A game adds kit.music for a Score.
  • InputMap: move (W A S D, the left pad stick), aim (the arrow keys, the right pad stick: it shoots as soon as it is pushed), fire (J, the left mouse button, RT: shoots where you face; never on touch), dodge (Space or Shift, the right mouse button, LB or B; Dash on touch), pause (never on touch) and confirm (Enter or E: begin, and play again; OK on touch, when asked).
  • CameraRig: ortho at 58 degrees with 15 m of view (27 m across), so a foe is seen coming from the meadow and the bolt's 11 m reach is most of the way to the screen's edge.
  • World (gen.twin_stick): an 80 by 80 wood with a clearing in the middle: a ring of trodden earth (12 tiles round, eight boulders on it for cover), a meadow with a few pines out to 22 tiles where the four gates are, and close-set pines beyond, so the world's edge is never in view from the clearing.
  • Cast: the ranger (walks at 4.2 m/s, shoots a bolt every 170 ms, dashes through bites and shots, five cells, killed rather than downed), the bolt (18 m/s, 11 m), the crawler (runs at 3.8 m/s, bites with a 0.38 s tell and lunges, two bolts) and, from the third wave, the brute (slow, six bolts, a 0.65 s tell and a bite of two cells). Both foes use sm.crawler (stand a moment as it comes in, hunt, attack; never give up) and reroute every 250 ms.
  • Waves (rules.twin_stick): five, of 6, 9, 12, 15 and 18 crawlers through the gates in turn (one every 0.9 s, at most 7 up), and from the third wave one brute more each; a breather of 3 s between; a second survived scores 1, a crawler 10, a brute 30, a wave 50; holding out through the fifth wins, the ranger falling loses.
  • UI: the title (the name, a line, begin: no control hints there, because a title's hints ask the touch layer for their actions, and the phone visit found the sticks and Dash shown before OK, Dash tapped first and the game never started); the HUD (health cells, the wave and how many, the score and the foes put down, the message line, a banner as each wave begins, the controls for the first ten seconds, and the win and lose cards with the score and play again, above the middle so the player shows beside them). Each has a compact place for a phone.
  • Style: late-afternoon light (the clock runs from 16:00), green grass, warm earth, the default green-phosphor slate, a mix where foes are heard from where they are.
  • Default Playtests: test.fuzz (the monkey on 4 seeds, aiming and shooting too), test.first_wave (the skirmish bot for 45 s on 4 seeds: shots fired, foes put down, most runs standing) and test.run (the skirmish bot through the whole run on 4 seeds: one at least must win, most must not fall).

Questions for the Director

  1. arena: "Where is the fight, and where do the foes come in?" Becomes the WorldGen: the arena's floor, its cover, what stands round it, and the named gates.
  2. foes: "What comes at the player, and how is each kind beaten?" Becomes each foe's Archetype (speed, health, a bite with a tell, or a shooter with a tell and a projectile of its own, plate where it is hard) and its points.
  3. weapon: "What does the player shoot, and how fast?" Becomes the hero's shooter and the projectile Archetype.
  4. run: "How long is a run, and what makes it a win?" Becomes the waves.

As built (2026-10-02)

  • The template (library/templates/twin_stick, 37 documents) checks clean on its own and as a game (core/test/template-twin-stick.test.ts): a game.json that only pins it is L0–L2 green in the engine, 12 Playtest runs (the skirmish bot wins all four of test.run in 5,438 to 5,623 ticks, about a minute and a half of game time), and red when its rules open no gate ("no run of 4 reached ranged.fired, melee.killed").
  • The sample is examples/saltwell (40 documents; nothing added to the engine for it). The last well on the salt road, held through one desert afternoon until the evening caravan rings its bells. It pins the template, replaces its waves, HUD, title and tuning by id, and brings its own: a salt pan round the well with red spires for cover, dunes with saltbush and a boulder field beyond (the six gaps the foes come through are all round, 20 tiles out); the keeper (an indigo robe, a sling of salt-glass, a dash, six cells); three foes, each beaten a different way: scuttlers in swarms that sting with a tell, spitters (from the third wave) that stop at 7.5 m, rear and spit a slow glob at where you stood (kit.ranged's shoot from sm.spitter: hunt, a spit state, back to hunting), and ramhorns (from the fourth) with a bone brow that turns shards (plate over their front 150 degrees) and a glowing back that stalls them, so they must be dashed past and shot from behind. Six waves (7 to 22 scuttlers, with one spitter and one ramhorn more each wave once they start: 103 foes), a line the first time each special foe comes, an afternoon that warms toward evening, and a Score in D harmonic minor (a drone; an oud-like line that plays in the breath between waves; frame drums whose level is how many foes are up; a pad from the fourth wave).
  • Proof: verify --level L3 green: L1 the same state twice; L2 14 runs, the template's three Playtests unchanged (the monkey shooting on 4 seeds; the first 45 s on 4 seeds, all standing; the whole run won on all 4 seeds in 10,830 to 15,403 ticks, three to four and a quarter minutes) and its own two (the proof tour, won at tick 9,898 with a tell, a ring, a stall and every foe's kill counted, the keeper stung once, a save, a quit to the title and a load that gives the same state; the look tour). L3 8 frames and 8 on a phone, every check green: the title, the keeper by the well, the first swarm, a scuttler hit, the keeper stung, a spitter's tell, a shard ringing off a ramhorn's brow and the end card (reached the quick way, standing in the fourth wave: L3 draws a tour twice within 900 s, and the whole run to the win, 11,600 ticks drawn, ran out of it on a busy box); the keeper 23 to 27 px tall and ΔE 52 to 77 from the ground, a scuttler 23 px (ΔE 57), a spitter 28 px (ΔE 37), a ramhorn 58 px (ΔE 38), every text whole and readable upright. The web rung: the proof tour in Chromium ends at tick 9,898 on the desktop's state hash, at about 58 ticks a second against the desktop's 60; the phone visit is green (the title offers only OK; both sticks held at once move and shoot, the frame 60% changed against 0.8% standing still; sideways the stick and Dash; every text at least 11 CSS px upright), and sound plays and holds in the browser. The score's seven stems bake at their levels and loop without a seam.
  • How long it plays: the skirmish bot, which kites and aims well and never misses by much, wins in two and three-quarter to four and a quarter minutes of game time. A person misses more, takes stings and must learn to get behind a ramhorn, so a first win is perhaps four to six minutes, and a loss comes sooner. That is an estimate: no person has timed it.

What's missing

  • Aiming with the mouse: on a desktop the arrow keys or a pad's right stick aim; the mouse's left button shoots where the body faces. A pointer aim needs the player app to turn the cursor into a world direction through the camera.
  • Pickups dropped by foes (health, a better sling): a LootTable and a pile can say it, and a rule on inventory.changed the effect; neither template nor sample uses them yet.
  • A shot that arcs (a lobbed stone) or homes; area damage. kit.ranged's shots fly straight.
  • The spitter's tell is a pose (in_tell in a pose rule); a ranged tell doesn't brighten an emissive slot as a blow's does (GwActorGlow reads only melee windows).
  • A score kept across runs (a best score): vars live in one game.
  • Tick rules run before that tick's events, which the wave rules had to work round (above); a next_tick or an after on a rule would say "half a second later" more plainly than a clock var.

Counting the vocabulary

Each template's report lists the new kits it needed and the packs it reused. The target for the fourth template (visual novel or twin-stick) is no new simulation kits.

Template New simulation kits New presentation kits New kinds Reused
puzzle_grid kit.grid, kit.grid_rules (kit.grid's view) Level, GridRules kit.core, kit.ui, the Expr VM, the ladder
platformer_2d kit.platformer kit.sprite Room, PlatformLevel, Sprite kit.core, kit.ui, kit.mesh's recipes as art, kit.grid's row reader, the ladder and web harness
visual_novel none (kit.core runs the Dialogue beside its RuleSets: one more system, action, three events, four pure functions, two tour verbs) none (kit.ui: the dialogue feed, the sprite widget, fill frames, revealKey, chosenKey) Dialogue kit.core (vars as flags, end_game, saves, rules), kit.ui (Screens, the title flow, the flat look), kit.sprite and kit.mesh (figure.person and mesh.parts recipes as people and props), the Expr VM, the ladder and web harness
adventure none. Kit features: kit.harvest's usable component, harvest.used, use_verb, the talk verb, and a talk holding the key and the body; kit.core's seek talk goal, goals that may repeat, and the read_all verb; kit.world_tiles's atFrac and withinTiles on ground and scatter rules; then kit.inventory's use (an Item's use, inventory.used, the use: and hold: presses, the use_item verb) and kit.core's seek use goal none (kit.ui: a running talk asks for its keys on touch; the carry app's default act, and the chosen row's verb on the key strip) none the action survival play kits (world_tiles, clock, move, inventory, perception, brain_fsm, melee, harvest), M10a's Dialogue and talk panel pieces, kit.mesh (figure.person people, mesh.parts houses and a rigged boar), kit.audio and kit.music, the seek and monkey bots, the ladder, the web harness and touch
twin_stick kit.ranged (shooters and projectiles: shots that fly, hit through a new kit.melee seam, receive and strike_from; the shoot action, three events, two pure functions, the skirmish verb and bot). Kit features: kit.move's rerouteMs; kit.world_tiles' route fields flooded as far as a mover needs (exact); kit.core's monkey pushing an aim stick none (the touch layer's second stick; kit.ui's hint naming a stick) none the action survival play kits (world_tiles, clock, move, perception, brain_fsm, melee), kit.mesh (figure.person, mesh.parts creatures on plan.quadruped's presets, rock.faceted, tree.tiered), kit.audio and kit.music, kit.ui's HUD and title pieces, RuleSets on tick as waves, the monkey, the ladder, the web harness and touch

The sixth template is the first since platformer_2d to need a new simulation kit, and the count says why plainly: shooting is a mechanic, not a recombination. Everything else a twin-stick is (the arena, the swarm and its tells, the waves, the score, the HUD, the run) was already sayable, and kit.ranged reuses kit.melee for everything a shot does once it lands. The cost of saying it was otherwise small features, and one performance gap (routing a swarm) that only building a real game in the genre found.

From docs/design/m4-templates.md in the Gamewright repository.