Sandbox
@plausibleventures/lattice

Plugin for agent-built isometric games in Claude Code and Codex

Lattice is a `/lattice` plugin for coding agents that turns one sentence into a playable isometric game in a browser. It combines agent skills, commands, and a nine-package TypeScript kit for drawing, input, loops, saving, simulation, and audio.

38 stars5 forksTypeScriptUpdated 25d ago
Who it's for

Builders who want their coding agent to generate and iterate on a browser game from a sentence.

What it delivers

You can go from a prompt to a playable, deterministic game without assembling the engine pieces yourself.

What it does

Agent plugin install

Installs into Claude Code, Codex, and Grok as a plugin you can call with `/lattice`.

Nine-package TypeScript kit

Includes `core`, `iso`, `draw`, `loop`, `input`, `audio`, `persist`, `sim`, and `ui` packages with no dependencies.

Skills and commands

Ships 12 skills plus `.claude/commands` and agent files to guide game-building workflows.

Deterministic game loop

Uses seeded randomness, replayable input, and fixed-step loops so the same seed and input can reproduce the same result.

Examples and gallery

Provides runnable examples in `examples/`, showcase games, and a gallery site for seeing the kit in action.

How to get it

  1. 1Then type one sentence
    /lattice a game where you plant an orchard and each evening choose to harvest or let it grow
  2. 2Which is the plain fact about this repository: it was largely built by agents. All but a…
    git log --format=%B | grep -c 'Co-Authored-By:'   # agent-co-authored
    git rev-list --count HEAD                          # total
  3. 3Nine of them, published in lockstep. Every one brings only the ones below it in the DAG,…
    npm i @latticekit/iso     # brings @latticekit/core, and nothing else
  4. 4They form a DAG and it points one way. core imports nothing; nothing imports ui.
    core ─┬─▶ iso ──┬─▶ draw ─┬─▶ ui
          ├─▶ loop  │         │
          ├─▶ sim   └─────────┤
          ├─▶ persist         │
          ├─▶ input ──────────┘
          └─▶ audio
  5. 5If none of those three is worth anything to you, the honest recommendation is Phaser.
    git clone https://github.com/plausibleventures/lattice
    cd lattice
    npm install
    npm run verify     # build, lint, docs, skills, tests, gallery, looking. nothing lands red
    npm run dev        # the demo game, on :5173
    npm run size       # per-package gzipped size against the budget

README

A valley at dusk rendered by Lattice: an isometric hillside with a lit shrine and a road of lamps

The grid underneath.

Type one sentence and get an isometric game you can play in a browser.

verify npm license site

▶ Nineteen worlds, running →


/lattice is a plugin for coding agents. Under it are nine TypeScript packages with no dependencies and no asset files — no images, no audio, no fonts — because there is nothing to load: every building is drawn from one color and every sound is built from oscillators.


Start here

Install the plugin in the agent you already use:

Claude Code/plugin marketplace add plausibleventures/lattice  then  /plugin install lattice@lattice
Codexcodex plugin marketplace add plausibleventures/lattice  then  codex plugin add lattice@lattice
Grokgrok plugin install plausibleventures/lattice

Then type one sentence:

/lattice a game where you plant an orchard and each evening choose to harvest or let it grow

The plugin picks the archetype, installs the packages in the order that works, writes the game, gets a screen up in the first minute, then opens it in a browser and looks at it — screenshots it, judges it against a harness, fixes what is wrong, repeats. The only other thing it may ask you is permission: to build blind when it cannot drive a browser, or to write into a folder that already has your files in it. Everything else it decides, and says which way it decided in one line.

It tells you when it is working blind rather than quietly doing it, because a suite that passes over a black screen is a failure this project has already shipped once.

What you get

a running gamea real page on a dev server, yours to keep and to edit. No engine account, no editor, no runtime
no assets to makeart is procedural and sound is synthesized. Nothing to draw, license, or pack
no sprite sheet to hallucinatethere is no asset path to invent, because there are no assets. An agent cannot reference an image that was never supposed to exist
the same result twiceseed + input log → the same pixel, so a fix stays fixed
12 skills, 34 named trapsthe parent that owns /lattice and eleven specialists. The traps are failures that compile, run, and produce a plausible-looking wrong game — written down so the agent does not re-discover them on your time

docs/SKILLS.md is the design: what the flow does, what it must never do, and what it is allowed to decide for you.


Three games nobody designed

Each was built by a different vendor's agent, in an empty directory, from one sentence, with no access to this repository — it installed @latticekit/* from npm like anyone else. The source is unedited, blemishes included, and two of the three carry a named defect the record states rather than hides.

gameagentthe sentence it was given
Before the BellGrokplace stalls and open gates to pull the crowd to your bakery before the market closesplay
Chime PathClaudehang chimes along a mountain path and tune each one, so the wind plays them in order as walkers passplay
Evenfall OrchardCodexplant an orchard and each evening choose to harvest or let it grow, and it keeps growing while the tab is closedplay

Source and the full provenance — the verbatim prompts, the transcripts, what was verified by hand, and the one thing that was changed — are in from-one-sentence/.

The gallery

Nineteen worlds: eighteen exhibits and the hero, each one proof of a capability that would otherwise be a claim. See them running →

Eight of the eighteen were built by agents from the written spec alone — Codex built Harbor, Wayfinding, Builder and Orbit; Grok built Idle and Instrument; Claude built Replay and Migration. Each was given one row of the exhibit table, the standard, and the tools, and was not allowed to read another exhibit's source: the test was whether the specification is followable, not whether an agent can pattern-match. Seven of the eight passed every row of the looking harness unaided; the exception was Replay, on legibility, for a text node too small for the check to measure. Every one of them carries its author's own notes on what the spec failed to say, in its README.md, verbatim — and each says what changed on the way into this repository, because all eight of them had to invent a bootstrap that examples/_shared provides and does not ship. The three games above are the ones whose source is unedited; an exhibit is not making that claim.

Which is the plain fact about this repository: it was largely built by agents. All but a handful of its commits carry an agent co-author trailer, and the count is deliberately not written down here — it changes with every push, and a number that goes stale on the next commit is worse than no number. Run it instead:

git log --format=%B | grep -c 'Co-Authored-By:'   # agent-co-authored
git rev-list --count HEAD                          # total

The eight exhibits and three games above are in the tree so the claim can be checked against source rather than believed.


Or use the packages directly

Nine of them, published in lockstep. Every one brings only the ones below it in the DAG, and nothing from outside the kit.

npm i @latticekit/iso     # brings @latticekit/core, and nothing else

This is a whole Lattice program — a surface, a camera, a palette, a loop, and a world drawn inside it. It is the file the landing page runs beside its own source.

import { createCamera } from '@latticekit/iso';
import { BASE_SLOTS, beginFrame, createCanvas2dSurface, createPalette, endFrame, isoBox } from '@latticekit/draw';
import { browserFrames, createLoop } from '@latticekit/loop';

const surface = createCanvas2dSurface(document.body.appendChild(document.createElement('canvas')));
const camera = createCamera(innerWidth, innerHeight, { zoom: 0.62 }), palette = createPalette(BASE_SLOTS);

createLoop({ clock: { now: () => performance.now() }, frames: browserFrames(), render: (_alpha, t) => {
  const pen = beginFrame({ surface, camera, palette, t, clear: 'sky' });   // erase, then paint the sky
  // Back to front is just the loop order in a 2:1 projection, so this city needs no depth sort.
  for (let gy = -7; gy < 7; gy++) for (let gx = -7; gx < 7; gx++) isoBox(pen, gx, gy, 1, 1, { color: 'metal', h: 2 + 5 * Math.sin(t + (gx + gy) * 0.4) ** 2 });
  endFrame(pen);
} }).start();

Three things about a Lattice game are the design rather than the API, and they are what docs/GUIDE.md builds out in the order you will need it:

  • A tap arrives as a tile, never as a pixel. No game on this kit converts a pointer position into a grid cell, because the conversion has to happen through the camera as it stood when the tick opened, and a game that does it in a handler does it through the camera as it stands now — a different tile on any frame where the map was moving.
  • The sorted list is walked forwards, and picked backwards. There is one sorted list, iso owns it, and renderFrame sorts immediately before handing it over, so no caller ever holds one and is tempted to improve it. Partitioning it — all the shadows, then all the bodies — is a stable reorder that looks harmless and makes a player tap one building and open the one behind it.
  • Your entities stay yours. No registry, no entity system, no scene graph, no component store. The kit hands back a permutation over the array you already had.

The nine packages

They form a DAG and it points one way. core imports nothing; nothing imports ui.

core ─┬─▶ iso ──┬─▶ draw ─┬─▶ ui
      ├─▶ loop  │         │
      ├─▶ sim   └─────────┤
      ├─▶ persist         │
      ├─▶ input ──────────┘
      └─▶ audio
packagewhat it is forenvironment
coreseeded rng, stateless hashing, noise, maths, easing, typed events, pools, formattingisomorphic
isothe three coordinate spaces, camera, depth sort, tile maps, footprints, hit-testing, pathsisomorphic
drawa Surface with a Canvas2D backend, color derivation, and the isometric solid kitbrowser + headless
loopwall-clock loop, fixed-step simulation, schedulers on two timelines, tweens, replayisomorphic
inputpointer/touch/keyboard into one replayable stream of intents, in tile coordinatesbrowser
audioWebAudio synthesis from declarative recipes, voice limiting, buses, a music deckbrowser
persistversioned saves, an explicit migration chain, injected storage, integrityisomorphic
simidle-economy maths in closed form: cost curves, flow, offline accrual, capacityisomorphic
uiDOM overlay primitives — panels, toasts, number rolls. Deliberately not a frameworkbrowser

.lattice/kit.json is the same table, machine-readable, with every package's exports and invariants. /api.json and /llms.txt are the versions an agent reads.


Determinism is checked, not claimed

Every kit says it is deterministic. The claim is only worth something if something breaks when it stops being true, so here it is, in packages/loop/test/replay.test.ts:

update(dt, tick) {
  x = (x + input + tick) | 0;
  y = (y + rng.int(0, 4)) | 0;
  if (nondeterministic && Math.random() < 0.5) x = (x + 1) | 0;   // ← the experiment
}

A session is recorded through a real loop, then replayed against the same game with that one line armed. With the flip off, divergedAt is -1; with it on, it is 63 — the first checkpoint, because 64 coin flips have happened by the time it is compared and the odds of all 64 landing tails are 2⁻⁶⁴, about 5.4 × 10⁻²⁰. A suite run once a second since the formation of the Earth would not have seen it. That is not a flaky test; it is a certain one with the arithmetic written down beside it. The falsification runs in both directions and both are asserted, which is the part usually skipped: a test that only ever fails proves nothing about the case it is meant to catch.

Math.random(), Date.now() and performance.now() are banned inside every package's src/ and npm run lint enforces it. Randomness arrives as a seeded Rng the caller owns; time arrives as a parameter.

The two-tier rule, which is the thing most kits get wrong by not knowing it exists

"Deterministic" was two claims wearing one word. ECMA-262 specifies + - * /, Math.sqrt, Math.imul and the bitwise operators exactly. It explicitly does not require sin, cos, pow, exp or log to be correctly rounded — so two conforming engines may disagree in the last bit, and a save file written on one will not verify on the other.

arithmeticpromisemay reach
Tier A+ - * /, sqrt, imul, bitwisebit-identical on every enginehashes, save files, replays, anything
Tier Bsin, cos, pow, exp, log, …correct to within an ulp or sopixels only — never hashed, never persisted

Tier B is not banned — a cost curve is b · rᵏ and there is no honest way around that. It is required to declare itself: mark the site @tier-b and the linter is satisfied. That makes every one of them greppable, so an auditor can ask of each in turn whether it ever reaches a save file.

It changes real decisions. iso's A* heuristic is the integer octile metric 14·min(dx,dy) + 10·|dx−dy| rather than a Euclidean one, because a path reaches a save file. sim's cost curve is exponentiation by squaring rather than Math.pow, because a player is charged that number. There are deliberately no sine or expo easings in the kit. And a color is stored as the player's hue, never as the #rrggbb it derives to, because the derivation needs cbrt.


Numbers, not convictions

The rule that the hot path allocates nothing costs every call site an output parameter, and comparing mean throughput says the rule is wrong:

operationops/secmax latency
allocating add, result escapes50,709,0672.3168 ms
out-parameter add, identical work40,714,9990.0274 ms

Allocating wins the mean by about 25%, and that is not measurement error — V8's nursery is a bump allocator and an object that dies in the same iteration is nearly free. Look at the second column. The allocating form's worst observed call is 85× slower: that is the garbage collector, showing up exactly where a mean cannot see it, and a 2.3 ms pause inside an 8 ms budget is not a slow frame, it is a dropped one. A game protects its frame-time tail, not its mean.

docs/PERFORMANCE.md has the rest, including the sprite bitmap cache that was measured and then not built — a perfect cache, 100% hits, saves 2.10 ms of an 8 ms budget and buys four new ways to render something stale — and the row that says at what scale it reopens.


Is this ready?

Today is v0.1.1, all nine packages, on npm. The honest answer has three parts: what is stable, what is not, and what will break before 1.0.

thisstatuswhy you can check it
the 527 exported symbolsstable in shapenpm run lint fails the build if a package exports a name .lattice/kit.json does not list, so the reference cannot drift from the code
behavior of everything exportedtested2,648 tests across 100 files. npm run cover reports 99.82% of statements and 100% of functions against a 90% floor. Coverage is not part of the gate, so run it rather than trusting this line
the layering and the determinism ruleenforcedthe clock and the random source are lint errors inside a package. CI also runs the whole suite twice in separate processes and diffs the recorded streams, because a stray Date.now() or a Set iteration order is what a single run cannot catch
the size of each packagebudgeted83.02 kB gzipped for all nine. 12 kB per package by default, with draw at 12.5 and input at 16, each override argued in the manifest rather than raised quietly
the /lattice pluginshippedit installs and runs in Claude Code, Codex and Grok. The three games above came out of it
the gallerycompleteeighteen exhibits and the hero, all nineteen built and running on the site
function signaturesmay changethe first publish was 2026-08-18 and nobody outside this repository has depended on them yet. That is the whole reason the version starts with a zero
the API referencegenerated, not hand-kept/reference/ is read out of the packages' own .d.ts files at build time
a browser test matrixabsentCI runs the suite in Node on 20.19, 22 and 24. The browser floor below is read off the compiler target and the built output, not off a test run

Versioning, and what a breaking change means here

Semver, with the pre-1.0 rule stated rather than assumed: a minor bump may break source compatibility, a patch never does. The nine packages version and publish in lockstep — one number for the whole kit — because they are a DAG that only ever imports along its own layering, and a visitor who installs draw at one version and iso at another has found a way to be wrong that costs nothing to close.

Two kinds of breakage matter here and only one of them is about code:

  • Source breaks — a renamed symbol, a changed signature. Loud, immediate, and your compiler finds every one of them. These are what the version number is about.
  • Artifact breaks — a change that makes something already written down invalid. A save file, a replay log, a shareable seed. These are silent, and they are the ones this kit spends its rules on: persist refuses a version mismatch by name instead of guessing, stepMs is a compatibility constant because it appears in every recorded session, and docs/SEAMS.md is the list of every place the two are connected. A change of this kind ships with a migration or it does not ship.

What it needs from a browser

It is Canvas2D, and that is the whole rendering story: no WebGL, no WebGPU, no WebAssembly, no workers, and no OffscreenCanvasdraw's backend uses a detached <canvas> deliberately, because OffscreenCanvas has no toDataURL. Beyond the canvas it asks for requestAnimationFrame, ResizeObserver and Pointer Events with setPointerCapture. @latticekit/persist uses localStorage through a swappable adapter, and @latticekit/audio uses AudioContext, which on every browser needs a user gesture before it makes a sound. Neither is required by anything else.

Safari and Firefox are both in. The packages compile to ES2022, and the newest syntax actually present in the built output is private class fields and Array.prototype.at, which puts the floor at roughly Chrome 92, Edge 92, Firefox 90 and Safari 15.4 — spring 2022. Older targets transpile the packages like any other dependency; they ship as ES modules and nothing in them is pre-minified.


Why not Phaser, Pixi or Three

Because each of them is better than this at what it is for, and none of them is for this.

Three is a 3D renderer. An isometric game is a 2D projection with a sorting rule, and adopting a scene graph, a camera stack and a material system to obtain a coordinate transform is a large dependency for a small idea. Pixi is a very fast 2D renderer and would draw a Lattice game beautifully — it is also a renderer and nothing else, so the projection, the depth sort, the pathfinding, the seeded noise, the save migrations and the sound are still yours to write, and that is most of what is in these nine packages. Phaser is the closest comparison and the fairest one: a complete engine with scenes, physics, input, audio and a loader, a decade of documentation that every agent has already read, and a community that has answered your question. If you want a game engine, use Phaser.

Three things here are not on that list. It is deterministic by rule rather than by discipline, which is what makes a replay land on the same pixel and a seed a link you can send. It has no asset pipeline at all, so there is nothing to load, nothing to license, nothing to pack, and a recolor is a runtime value. And it is written to be handed to an agent: the manifest, the invariants, the cross-package contracts and the traps that cost this project real time are all machine-readable at /api.json, which is a thing you can check in ten seconds rather than a claim.

If none of those three is worth anything to you, the honest recommendation is Phaser.


Working on it

git clone https://github.com/plausibleventures/lattice
cd lattice
npm install
npm run verify     # build, lint, docs, skills, tests, gallery, looking. nothing lands red
npm run dev        # the demo game, on :5173
npm run size       # per-package gzipped size against the budget
if you wantread
to build a game with itdocs/GUIDE.md — start to finish, in order
a whole game, wired, at real scaleexamples/demo/src/
to know which package owns a thing.lattice/kit.json, then that package's README.md
to work on it, as a human or an agentAGENTS.md — the eleven non-negotiables, and they are not a style guide
the numbers behind any performance claimdocs/PERFORMANCE.md
why two packages split a responsibility the way they diddocs/SEAMS.md
to send a pull requestCONTRIBUTING.md
what changed in a releaseCHANGELOG.md
to report something that should not be publicSECURITY.md

License

MIT. See LICENSE.

Files in the repo

Repository payload28 top-level entries
  • .claude
  • .claude-plugin
  • .github
  • .lattice
  • docs
  • examples
  • from-one-sentence
  • packages
  • showcase
  • site
  • skills
  • test
  • tools
  • .gitignore
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • CODE_OF_CONDUCT.md
  • CONTRIBUTING.md
  • LICENSE
  • package-lock.json
  • package.json
  • README.md
  • SECURITY.md
  • tsconfig.base.json
  • tsconfig.check.json
  • tsconfig.json
  • vitest.config.ts

Discussion (0)

Ask about usage, or say what you built with it

Sign in to join the discussion.

No comments yet. Be the first to say what this is good for.

More plugins

Makes your AI agent think like the laziest senior dev in the room. The best code is the code you never wrote.

138k
1 add

Graphs that teach > graphs that impress. Turn any code into an interactive knowledge graph you can explore, search, and ask questions about. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, and more.

82k
code-yeongyu/
oh-my-openagent

OmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.

69k

Persistent Context Across Sessions for Every Agent – Captures everything your agent does during sessions, compresses it with AI, and injects relevant context back into future sessions. Works with Claude Code, OpenClaw, Codex, Gemini, Hermes, Copilot, OpenCode + More

94k

Opinionated Oxlint rules for rejecting low-evidence TypeScript and JavaScript patterns

4.3k