Skip to content
Subterrans

First players, first bugs

What changed in two weeks of going from private repo to embedded demo: real issue reports, the last Phase 1 gameplay seams, and one system that got smaller instead of bigger.

Two weeks ago the game lived in a private repo on my laptop. Today it’s embedded at /demo/play, the source code is on GitHub, and the issues page has real bug reports from people who aren’t me. That shift — from building in isolation to building under observation — changed what I worked on this week.

Here’s what landed.

Phase 1 is now actually playable end-to-end

The simulation has supported invading the enemy hive since Phase 09.1: a fighting ant standing on the enemy’s entrance descends into their grid, hunts the nearest hostile, and resolving combat against their queen triggers victory. That’s the win condition for Phase 1. But none of it was reachable from the UI — clicking on an enemy entrance did nothing visible, the keybind to swap underground views was undiscoverable, and there was no visual cue distinguishing “enemy entrance” from “any other surface tile.”

PR #29 wired up the four UI seams. Left-click on an enemy entrance now sets your rally point there. The X-keybind got a clickable button labeled “Your Colony / Enemy Colony.” Underground clicks on the spectator view became no-ops (instead of silently dispatching dig-marks against your own grid at the matching coordinates). And enemy entrances got a single-pixel red perimeter ring so they read as “rally here to invade” without any tutorial text.

The simulation work was already done. The lesson was that “feature ships when sim supports it” is wrong; the feature ships when a player can find it without reading the source.

One system got smaller

Phase 10 was originally going to add a dig-priority slider to the existing forage/dig/fight ratio that players control. After playtest, the slider got cut entirely. The behavior ratio is now two fields — forage and fight — and digging is auto-assigned based on outstanding marked tiles. Less UI, less to explain, and the simulation does the bookkeeping that a human would have done badly anyway.

This is the second time I’ve removed a control from the player surface in this project. The pattern: if a system has a single right answer most of the time, exposing it as a knob just lets the player make it worse. Auto-assign isn’t a smarter system; it’s a refusal to let the player accidentally starve the dig queue while staring at a UI element.

The first round of real issues

The issues page collected reports faster than I expected. The interesting ones, in rough order:

A pattern in this list: most of the bugs were input-layer or rendering-layer bugs surfacing simulation-layer symptoms. The sim itself, the part the seven principles protect, has been mostly fine. The friction is in the seams — the places where player intent translates into commands, or where world state translates into pixels.

How the game gets to the page

The library bundle work was the biggest mechanical chunk. The game builds itself as a single ESM module with a mount() entry point that the website’s Astro page imports dynamically. Cache-busted hashed filenames + a manifest.json so the website’s deploy can reference the right URL. A ready promise so the loading state knows when to clear. Source maps. A .d.ts file generated directly from the source so the public API can’t drift from its declared types.

It now takes a single push to game main to redeploy the live demo. The game’s CI bumps the website’s submodule pointer, the website’s deploy picks that up, the bundle builds with a fresh hash, and CloudFront serves it. None of that is interesting in isolation. What’s interesting is that the friction-to-ship is now low enough that I’ll fix bugs in the morning instead of batching.

The dual-AI review pipeline went live

Every PR on the public repo now gets reviewed by Codex automatically (skipped on owner-authored PRs, since I’m reviewing my own work via Claude already). The bug-report template emphasizes the F9 debug log, which produces the input-log + seed needed to reproduce a deterministic replay. The branch-and-PR workflow is now the rule, not the convention.

If you find something broken in the demo, I’d rather hear about it than not. The issue tracker is the place — the bug-report template walks you through the F9 debug log, which gives me a reproducible replay instead of guesswork.

More soon.