Directing a game I couldn't have written


I don’t really write frontend. TypeScript is a language I reach for when something forces me to, I know enough React to read it and not enough to be trusted with it, and I actively avoid CSS. Tube Race, which I put live a few weeks ago, is a React 19 and TypeScript game with a MapLibre canvas, container queries, a hand-rolled confetti burst and a follow-camera that keeps your next moves in frame. I wrote almost none of it by hand. It is also the most polished thing I have shipped this year, and I think it is genuinely good. Those two facts sitting next to each other are the reason this post exists.

If I didn’t write the code, the fair question is what I did instead. The honest answer is that I did the two things the model couldn’t do for itself: I said precisely what the thing was, and I checked whether what came back actually worked. Specification and verification. Take the typing away and those two turn out to be most of the job, and they are harder than they look.

The project started as a spec, not a repo. Before any code existed there was a SPEC.md that all three build streams (a Python data pipeline, a pure TypeScript game engine, and the React UI) had to code against, with a line at the top saying that if anything was ambiguous, that file won. Some of what it pins down is ordinary schema. Some of it is load-bearing in a way that has nothing to do with being able to write the language. The engine, for instance, is forbidden from calling Date.now() or Math.random(): the date is always passed in, and any randomness is seeded from a hash of the date string through a small PRNG. That single rule is what makes the daily puzzle identical for everyone and a shared score honest, and it is a decision, not a line of code. I couldn’t have written the Dijkstra engine quickly, but I could specify that a line change costs four, that you’re scored against the true optimal, and that the engine must be pure and deterministic and DOM-free so it could be tested in isolation. The spec is where my actual expertise went.

The build then ran in rounds. I’d set a batch of work, an agent would build it overnight, and I’d wake to a draft PR and a written hand-off log.1 The logs became the artefact I relied on most. Each one ended with two sections: “decisions that may need your call”, and “honest gaps”. A typical round closed with a status line like 263 tests passing, tsc clean, vite build clean, Playwright verified at 1440 and 390 pixels with no console errors. That status matters, but it was the honest-gaps section that earned my trust.

The part nobody warns you about is verification, because it’s the part that breaks when you can’t read the code.

My usual way of trusting a change is to read the diff. That route is mostly closed to me here; I cannot meaningfully review hundreds of tests of TypeScript I could not have written, and skimming the implementation tells me less than it would in Python. So trust had to come from the outside rather than the inside, from behaviour rather than from code review. Three things did that work. A pure engine, unit-tested to the point of determinism, so the same inputs always produce the same score and I can reason about the logic without reading a line of React. Playwright playthroughs that drive the real game at desktop and phone widths and check what a player would actually see. And me, playing it every day, which catches the things a test never will.

Playwright is the one I’d single out, because it’s what makes directing a UI you can’t read tractable at all. It drives the real, rendered game in a headless browser the way a player would: it clicks through the onboarding, plays an actual solve move by move, checks the result card shows the right score and the enriched station panels, runs the whole thing again at a phone width, and treats a single console error as a failure. The agent writes those playthroughs and runs them itself, then hands me back screenshots and a verdict. It isn’t asserting that the code it wrote is correct, it’s opening the browser and showing me the game doing the thing, which lands the check at the one altitude I can actually judge from, behaviour, rather than the one I can’t, the React underneath. When a run reports that it solved Stratford International to Notting Hill Gate in an optimal sixteen and the result card rendered clean, I don’t need to read the component to believe it. And it catches what reading never would: at an early fixed zoom, a long Elizabeth-line hop from Stratford to Liverpool Street landed just off the visible map, a legal move the player simply couldn’t see, and only driving the real game surfaced it. The fix was a follow-camera that always frames your current station and every move open to you. There’s even a small dev-only handle wired into the map so the browser driver can read station coordinates back, testability built into the app on purpose so the agent could check its own work. Written test specs describe what should happen; Playwright is the thing that goes and confirms it did, in the real browser, without me in the loop.

The single most useful sentence in any of those logs was one where the agent admitted it had not managed to screenshot a particular on-map marker under test, because the follow-camera timing made it fiddly to drive headlessly. That admission was worth more to me than another green tick. It told me exactly where the verification was thin, and therefore where I had to go and look myself. A tool that reports its own blind spots is one you can supervise. A tool that reports only success is one you’re trusting blind, and the difference is the whole game when you can’t audit the work directly.

The other thing the model wouldn’t do is decide whether the game was any good. The sharpest call in the build was deleting a mechanic I had written into the spec as core. Originally, every station you visited stayed revealed, so you slowly built up a map as you explored. It was specified, implemented, and working. Playing it, it was worse than I’d expected: the board filled with dead history and the eye lost the thread of where you actually were. So it went. The map became minimal, showing only where you are, where you can go and where you’re heading, and the onboarding was rewritten to stop promising a reveal that no longer happened. No test would ever have flagged that, because nothing was broken. It simply wasn’t good, and knowing that difference is not yet something you can hand off.

The obvious objection is that I didn’t really build this, and I’d rather meet it than dodge it. If building means typing the implementation, then mostly, no, I didn’t. If building means deciding what the thing is, defining what correct means, catching where it’s wrong or merely mediocre, and owning whether it ships, then that was all mine and it was the larger part of the work. Both are true at once. The typing was real work I didn’t do; the rest was real work I did.

I’m clear-eyed about where this falls down, too. I’m dependent in a way I’m not on a Python project: if a subtle rendering bug slipped past the tests and the playthroughs, I’d be poorly placed to open the file and fix it, and I’d lean on the agent to. The method only works because the game’s behaviour is observable and cheap to check. You can watch it, drive it headlessly, seed it deterministically, and diff a score against a known optimal. On a system whose correctness is hard to observe from the outside, the same approach would leave me trusting far more than I could verify, and I’d be right to be uneasy about it. The competence gap is survivable here precisely because the verification gap isn’t.

I’ve written before about what this shift looks like across a team, where the problem is consistency and distribution. This was the same shift shrunk to a party of one, in a language I don’t speak. I couldn’t write the game. I could say what it should be and tell whether it was, and this time that was enough.


1 Each round ran on its own branch as a draft PR, and nothing reached main without me reading the log, playing the build, and merging deliberately. The overnight part was the building. The merge was always in daylight and always a decision I made, which is the one step I was careful never to automate away.