Skip to content

Run it locally

For: getting the game running on your own machine, either to develop against it or to look at it.

Once per machine

Node 24 or above is the floor. Continuous integration pins Node 24, and the desktop shell declares Node 22 or above, so 24 satisfies both.

Install dependencies from the lockfile rather than resolving fresh:

bash
npm ci

The browser suite drives a real Chromium, which is a separate download:

bash
npx playwright install --with-deps chromium

Nothing else is installed at machine level. Everything the loop needs lives under the repository.

Start the loop

The dev loop runs vite serving the game and wrangler serving the Worker API together, behind a per-worktree lock:

bash
npm run dev start

Three verbs and one flag cover the whole surface. Use start, stop and status, and add --fresh to discard the local universe and boot a clean one.

The loop prints the origin it answers on. Stop it with the verb rather than by closing the terminal, so the lock and the child processes are released:

bash
npm run dev stop

Which ports

Ports are not chosen per run. Each worktree claims one number in tools/worktree-registry.mjs, and every other port it may bind is derived from that one by a fixed offset.

ConsumerOffset from the worktree's baseMain checkout
vite, serving the gamethe base itself7343
visual regression rigbase plus 17344
wrangler, serving the APIbase plus 10008343
drag harnessbase plus 20009343
idle harnessbase plus 20019344

Registered worktrees sit two apart, which is what keeps each one's plus-one neighbour free. An unregistered worktree falls back to base 7399.

Two environment variables override the derived pair when you need them to: GW_DEV_WRANGLER_PORT and GW_DEV_VITE_PORT.

One loop per machine is the working assumption. A second loop started while the first holds the lock is the most common way to lose an hour to a port that is already bound.

Look at a build without the loop

To serve a built site with no API attached, build first and then serve it:

bash
npm run build
npm run play

That serves the dist/ directory over a locally bound port the operating system assigns, and prints the origin. It answers no /api route, so the game reaches its landing screen and no further. It refuses to start when the build output has no index.html.

Prove it

Run the narrowest suite that covers what you changed. The full gate is the arbiter:

bash
npm run check

That ends with the browser and dev-loop suites, which start real servers and are slow enough to keep out of the inner loop. Continuous integration runs the same command.

What the local universe is

The local loop runs the same Worker and the same Durable Object as a deployment. Its store is disposable and is wiped whenever you pass --fresh.

There is one universe, addressed by the name main. A request to any other universe id is refused with UNKNOWN_UNIVERSE and a 404.

When it will not start

SymptomCauseMove
A port is reported as already bound.Another loop holds it, in this worktree or another.Run npm run dev status, then npm run dev stop.
The loop starts and the game shows stale data.The local universe persisted from an earlier run.Restart with --fresh.
The API answers but the game does not load.Vite is down while wrangler is up.Stop and start the loop rather than restarting one half.
A suite fails only on this machine.A prerequisite is missing.Re-run npm ci and the Playwright install.