Getting out of the player's way
The controls got a four-stage rework: a tool palette instead of overloaded clicks, a camera that actually zooms, a game that shows you your own queued commands, and controls that teach themselves — all of it render-only, with the simulation untouched.
The last post was about making Phase 3 correct. This one is about making it playable — which turns out to be a different problem.
The simulation was solid. The way you talked to it was not. The old control scheme had grown one mechanic at a time, and it showed: the left and right mouse buttons each did three or four different things depending on which view you were in and what was under the cursor. Panning was hold-Space-and-drag. Game speed lived on the 1, 2, and 4 keys. Zoom did not really exist. None of it was written down anywhere in the game, and most of it you could only discover by accident. It worked fine for the one person who built it and was close to unlearnable for everyone else.
So the controls got a rework — four stages of it, issue #18, shipped over about a week. The whole point was to get the interface out from between the player and the game.
Stage 1: a tool palette and one honest gesture
The root problem was overloading. A single click meant “give an order” or “pan the camera” or “dig a tile” or “open a chamber menu” depending on context, and the player had no way to know which without trying it.
Stage 1 (#210) split that apart into three explicit tools — Command, Dig, Chamber — on keys 1/2/3, each with its own cursor and highlight, each resetting to a sensible default when you switch views (Command on the surface, Dig underground). Now the cursor tells you what a click will do before you click.
Underneath the tools is a single left-button gesture arbiter with one rule that holds everywhere: tap to issue a context order, drag to pan — the one exception being the Dig tool underground, where drag paints tiles. One code path decides tap-versus-drag, and one cancelGesture() cleans it up on every tool switch, view switch, menu open, and phase change, so a half-finished drag can never leak into the next thing you do. Hold-Space-to-pan is gone; Space is now just pause/resume, and game speed moved off the number keys onto a proper HUD widget (with =/- for the keyboard).
The Command tool keeps the parts that were actually intuitive — tap a food pile to forage it, tap your nest to rally, tap an enemy to attack — but now they live behind a tool you chose on purpose, instead of being one of four things a click might have meant.
Stage 2: a camera that actually zooms
Stage 2 (#215) added the thing every strategy game is expected to have and Subterrans did not: continuous, Google-Maps-style zoom and smooth pan, in both the surface and underground views, with the zoom anchored to the cursor so the map grows toward whatever you are pointing at.
Making that smooth meant changing how terrain is drawn. Re-rendering every tile every frame at every zoom level is wasteful, so terrain is now baked into a render texture once and only the tiles that actually change — a freshly dug tunnel, a new entrance — get re-stamped, tracked by a dirty set. The camera just zooms and pans over the baked image. When you zoom all the way out to see the whole nest, individual ant sprites would be sub-pixel noise, so they collapse into a batched dot layer with a little hysteresis so the swap does not flicker at the threshold.
The felt result is that you can now read the whole battlefield at a glance and then dive into a single tunnel fight, without the camera fighting you on the way. The strategic view and the tactical view are the same view at different zooms.
Stage 3a: the game shows you your own commands
This is the stage I am happiest with, because the idea is simple and the effect is large: the game should show you your own commands.
Two things came out of that (#217). The first is feedforward validity: when you are aiming a placement tool — a chamber, a tunnel, an entrance — the target tints green if the command is valid, red if it is geometrically impossible, and grey if your command queue is full. You know whether the click will work before you commit to it. The second is the paused-queue preview: when you pause, the orders you have queued but that have not been applied yet are drawn as ghosts, so you can see what hitting resume will actually produce.
The trick that makes this trustworthy is that both the cue and the real command are computed from the same projected world — a clone of the game state with your entire pending queue folded through the real command handlers. The preview is not a separate “what I think will happen” guess that can drift from the truth; it is the truth, run one step ahead. Green never lies, because the same code that paints it green is the code that would execute the order.
Doing this honestly required one carefully bounded change inside the simulation: the block of code that applies queued commands during a tick was lifted out into its own function so the renderer could call it on a throwaway copy of the world. The behavior is byte-for-byte identical — proven by a golden test that captured the exact output before the refactor and replayed it after — so determinism and replay are untouched. (More on why that mattered below.)
Stage 3b: controls that teach themselves
The last stage (#218) closed the loop the intro opened: a new player should discover pan, zoom, view-switching, and the tool palette without a manual or a tutorial wall.
That’s three reactive surfaces, all drawn through one device-glyph resolver so the hints can never disagree with the real bindings. There’s a hint strip that composes its text from the actual key glyphs rather than hardcoded labels; one-time, cross-session first-use nudges that fire on the effect — the first real pan, the first accepted zoom, the first dig-drag that actually enqueues marks — plus a gentle [Tab] prompt to discover the view toggle on your first input of a session; and hover tooltips on every control widget, including the ones whose purpose isn’t obvious (the stats panel tooltip teaches that you can click it for ant activity). All of the captions route through a single queue with an explicit policy about what preempts what, so two hints never stomp on each other and a nudge that gets dropped under load un-marks itself and fires again later.
The nice part is that none of it nags. Each hint shows once, when the moment is right, and then never again.
The discipline: the simulation never moved
The throughline across all four stages — and the constraint I am proudest of holding — is that this was render-only. A complete overhaul of how the game looks and feels, and git diff main -- src/sim is empty for three of the four PRs. The fourth moved exactly one block of code without changing what it does, with a golden test to prove it.
That matters more than it sounds. Subterrans is deterministic: the same inputs always produce the same round, which is what makes saves, replays, and the playtrace data from the demo trustworthy. If the controls rework had reached into the simulation, every one of those guarantees would have needed re-checking. Keeping the line between “how the game is presented” and “how the game is computed” clean meant a week of UI work cost the sim exactly nothing. The tool palette, the camera, the previews, the teaching layer — all of it is a new lens on the same untouched machine.
What’s next
The controls were the last thing standing between the demo and a stranger’s hands. The simulation is correct, the round has shape, and now the interface explains itself instead of needing me next to you to translate. So the question from the last two posts still stands, just with the excuses removed: is it fun for someone who did not build it? Play it — and now you should not need me to tell you how.