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
Codex codex plugin marketplace add plausibleventures/lattice  then  codex plugin add lattice@lattice
Grok grok 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 game a real page on a dev server, yours to keep and to edit. No engine account, no editor, no runtime
no assets to make art is procedural and sound is synthesized. Nothing to draw, license, or pack
no sprite sheet to hallucinate there 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 twice seed + input log → the same pixel, so a fix stays fixed
12 skills, 34 named traps the 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.

game agent the sentence it was given
Before the Bell Grok place stalls and open gates to pull the crowd to your bakery before the market closes play
Chime Path Claude hang chimes along a mountain path and tune each one, so the wind plays them in order as walkers pass play
Evenfall Orchard Codex plant an orchard and each evening choose to harvest or let it grow, and it keeps growing while the tab is closed play

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
package what it is for environment
core seeded rng, stateless hashing, noise, maths, easing, typed events, pools, formatting isomorphic
iso the three coordinate spaces, camera, depth sort, tile maps, footprints, hit-testing, paths isomorphic
draw a Surface with a Canvas2D backend, color derivation, and the isometric solid kit browser + headless
loop wall-clock loop, fixed-step simulation, schedulers on two timelines, tweens, replay isomorphic
input pointer/touch/keyboard into one replayable stream of intents, in tile coordinates browser
audio WebAudio synthesis from declarative recipes, voice limiting, buses, a music deck browser
persist versioned saves, an explicit migration chain, injected storage, integrity isomorphic
sim idle-economy maths in closed form: cost curves, flow, offline accrual, capacity isomorphic
ui DOM overlay primitives — panels, toasts, number rolls. Deliberately not a framework browser

.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.

arithmetic promise may reach
Tier A + - * /, sqrt, imul, bitwise bit-identical on every engine hashes, save files, replays, anything
Tier B sin, cos, pow, exp, log, … correct to within an ulp or so pixels 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:

operation ops/sec max latency
allocating add, result escapes 50,709,067 2.3168 ms
out-parameter add, identical work 40,714,999 0.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.

this status why you can check it
the 527 exported symbols stable in shape npm 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 exported tested 2,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 rule enforced the 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 package budgeted 83.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 plugin shipped it installs and runs in Claude Code, Codex and Grok. The three games above came out of it
the gallery complete eighteen exhibits and the hero, all nineteen built and running on the site
function signatures may change the 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 reference generated, not hand-kept /reference/ is read out of the packages' own .d.ts files at build time
a browser test matrix absent CI 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 want read
to build a game with it docs/GUIDE.md — start to finish, in order
a whole game, wired, at real scale examples/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 agent AGENTS.md — the eleven non-negotiables, and they are not a style guide
the numbers behind any performance claim docs/PERFORMANCE.md
why two packages split a responsibility the way they did docs/SEAMS.md
to send a pull request CONTRIBUTING.md
what changed in a release CHANGELOG.md
to report something that should not be public SECURITY.md

License

MIT. See LICENSE.