Appearance
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 ciThe browser suite drives a real Chromium, which is a separate download:
bash
npx playwright install --with-deps chromiumNothing 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 startThree 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 stopWhich 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.
| Consumer | Offset from the worktree's base | Main checkout |
|---|---|---|
| vite, serving the game | the base itself | 7343 |
| visual regression rig | base plus 1 | 7344 |
| wrangler, serving the API | base plus 1000 | 8343 |
| drag harness | base plus 2000 | 9343 |
| idle harness | base plus 2001 | 9344 |
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 playThat 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 checkThat 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
| Symptom | Cause | Move |
|---|---|---|
| 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. |