I’m in Hawaii this week. On vacation, the real kind, with a week off and nowhere to be. The picture is almost too on-the-nose: a laptop on a table on the lanai, a bay below catching the late light, a cold drink sweating next to the trackpad. And I’m building a video game. Not fixing a bug at work, not answering email. Building an ant colony simulation called Subterrans, a spiritual successor to the SimAnt I played into the ground as a kid. Agents are writing most of the code. I’m describing what I want, watching pull requests land, playing the result, asking for the next thing.
If you wanted a stock image for “vibe coding,” this is it. Relaxed human, beautiful place, AI doing the typing, software appearing as if by mood. That phrase has come to mean building by feel: you prompt, you accept, you ship whatever comes out, and the good vibes carry you. No plan, no rigor, just flow.
And from across the lanai it looked exactly like that. It was the opposite of that. The reason I could sit there and feel that loose is that almost nothing about the project is loose.
Subterrans runs on seven architectural rules I refuse to compromise on. The simulation is pure TypeScript with zero knowledge that a renderer exists. It advances in fixed 50-millisecond ticks. Every random decision in the entire game flows through a single seeded generator. No wall-clock time, no floating-point math, no classes, just flat data and pure functions. Saves are snapshots plus a log of every input, so any game can be replayed tick-for-tick. I wrote the long version of all seven on the devlog; the short version is that determinism is a property you either have everywhere or don’t have at all.
None of that is vibes. It’s the least vibey code I’ve ever been involved with. And it’s precisely what makes the vibey part possible.
Here’s the trick people miss. When the agent writes a feature, it can’t quietly break the simulation, because a lint rule fails the build the instant something outside src/sim/ gets imported into it. It can’t sneak in a Math.random() or a Date.now(), because those are banned at the door. It can’t introduce a bug that “only sometimes” happens, because the whole system is built so that nothing only-sometimes happens. Every pull request gets reviewed by me and by two AI agents independently before it lands. The structure isn’t there to slow the agents down. It’s there so I don’t have to babysit them.
That’s the inversion. The discipline is what buys the relaxation. The reason I could glance up from the screen and watch the water instead of the diff is that I’d already spent the up-front cost of making the project hard to get wrong. The rigor runs in the background, in the lint config, the fixed timestep, the dual reviewers, the planning docs I write before any code gets generated, so that the foreground can feel like flow.
Take the scaffolding away and the picture falls apart. Vibe coding without the rails is the thing that produces a demo that works once on your machine and corrupts its own save file on someone else’s. It feels great right up until the moment it doesn’t, and by then you can’t tell which of the last forty accepted suggestions introduced the rot. The relaxed feeling is real, but it’s borrowed against a debt that comes due with interest.
What I had on that lanai was the opposite arrangement. The debt was paid first. The architecture absorbs the chaos so I don’t have to. The agents move fast inside a box I built carefully, and the box is what lets me stop watching them every second.
So yes, I was vibe coding overlooking a bay in Hawaii. The vibes were lovely. They had nothing to do with why it worked. The view was the reward for the boring, unglamorous, non-negotiable work that happened long before I sat down with a drink: the work of deciding, in advance and in writing, exactly which things I would never let myself get wrong.
That’s the part that doesn’t fit in the stock image. It’s the only part that matters.