Vu Nguyen
← Essays & Opinions
8 min read

I Built a 3D Game on the ACE Train

I built Badge In, a 3D survival-horror game, from my phone on the ACE train with one bar of signal. Here is the rig, and the agent loops that did the heavy lifting.

aiagentsprototypingworkflow

Badge In is a survival-horror game about returning to the office. You badge in on a Monday morning, the floor has gone wrong, and you fight your way up to the CEO's all-hands armed with a stapler. It has fixed security-camera shots in the style of the original Resident Evil, rigged 3D coworkers, five rooms and a boss fight. It is about ten thousand lines of TypeScript across eleven pull requests, built over two days.

I made almost all of it from my phone. A lot of it on the ACE train, where the internet comes and goes.

This is not a stunt post. The phone was simply what I had with me. What interests me is how little the device mattered once my job changed from typing code to directing work. That is the part worth writing down, because it is the state of the possible right now, and most of it is setup anyone can copy.

The desk was never the point

For most of my career, building software meant sitting at the machine. The editor, the terminal, the build and the browser all lived on one computer, and you had to be in front of it. Away from the desk you could think about the work, but you could not do it.

Agentic tools move the human to a different part of the loop. If the agent writes the code, runs the tests and opens the pull request, my job is to say what I want, judge what comes back, and decide what happens next. All three of those fit on a phone. None of them need a keyboard, and none of them need a good connection for more than a few seconds at a time.

The rig

The setup is four pieces, plus a fifth that closes the loop. The real computer stays at home and does all the work. The phone is a remote control.

  • Tailscale puts my Mac and my phone on the same private network. The Mac is reachable from anywhere, with no ports opened to the internet and nothing to configure when I change networks. It is free for personal use up to a generous number of devices. My home router, a GL.iNet Flint 2 (GL-MT6000), runs Tailscale itself, so the home network is always on the tailnet.
  • Moshi is the terminal on my phone. It connects to the Mac over Tailscale and holds the session when the train goes through a dead zone, so a dropped signal costs me a reconnect rather than the work.
  • Wispr Flow turns my voice into text. Almost every prompt was spoken, not typed. You can tell from the transcript: "launxh" made it through, and so did "advertiser reviews" when I meant adversarial ones. The agent understood both.
  • Claude Code runs on the Mac with the full repository, a shell, a browser and git. It is the harness that turns a spoken sentence into a pull request.
  • Vercel builds a preview deployment for every pull request. The thing I check is a link I open on the same phone, so I am playing the real build, not reading a description of it.
PHONEwispr flow voiceMOSHImobile terminalTAILSCALEprivate networkMACclaude code harnessPULL REQUESTgithub + ciPREVIEW LINKvercelOPEN IT ON THE PHONE

The important property is that the work does not live on the phone. If the train loses signal for ten minutes, the agent keeps going on the Mac. When I reconnect, I am looking at progress, not a frozen upload.

Claude Code as the harness

The model is the engine. The harness is everything around it: the repository, the tools, the permission rules and the instructions it reads before it does anything. That is what makes it safe to stop watching.

My repo has a CLAUDE.md that the agent follows on every task. Never commit to main. Every feature gets its own git worktree and branch. Pull requests go through Aviator so stacked branches stay in order. Dependency versions are pinned exactly. Lint, type-check and tests pass before anything is called ready. I wrote those rules once, at a desk. On the train I never have to check whether the agent committed to main, because it can't.

The guardrails pushed back on me too. When I said "merge everything", Claude Code's permission check refused to merge without review until I explicitly allowed it, and the repository's branch rules refused any pull request without green CI. It was slower in the moment. It is exactly what you want when you are approving work from a phone with one bar of signal.

Start from a reference, not a spec

My first real prompt was one sentence: a 3D shooter like Resident Evil, set around a return to office. That was the whole brief, and it was enough, because a good reference carries hundreds of decisions in a few words.

Claude came back with a mapping table. Zombies became hollow-eyed coworkers. Healing herbs became coffee. The typewriter save room became the photocopier. Keys became badge levels, and the final boss became the CEO at the all-hands. Then it asked three questions: fixed cameras or over the shoulder, dark comedy or straight horror, and what to call it. I answered in one line: "yes fixed cameras, go with dark comedy." Then: "Yes please keep going til done."

That shape matters on a phone. Typing a spec is expensive. Choosing between options is cheap. The most useful thing I can ask an agent for is a short list of decisions with a recommendation attached.

Where the multiplier comes from

One agent writing code is useful. The real gain came from pairing agents so that one checks the other, and from never making me the first line of quality control. Most of what I typed from the train was not code direction at all. It was asking for a loop.

One agent makes, another grades

The first version of the game had boxy, code-generated people and flat textures. I said so: "The game texture and models are sub par." A pass of better code-generated textures helped, but not enough. So instead of guessing at the next fix, a critic agent, briefed as a demanding art director, played the game in a browser and came back with a candid grade and a prioritised plan.

Then I asked what it would take to set up Blender. The answer was one install command. The agent installed Blender with Homebrew, added the MakeHuman plugin and free CC0 skins, clothes and animation libraries, and drove all of it with headless Python scripts. Each character is built, rigged, animated and exported as a model file by a script that lives in the repo, so it can be rebuilt.

The maker did not trust its own output. Before wiring a model into the game it rendered a preview and looked at it. The first coworker came out with stretched strips of geometry, so it rendered each mesh on its own until it found the one that was wrong. Only once the renders looked right did the models go into the game, and later review and QA agents checked them there, the way a player would see them.

I have never opened Blender. I do not know how to use it. That is the point.

BRIEFreference + tasteMAKERagent builds itEVIDENCErenders, playtestsGRADERcritic, report onlyYOUkeep or redoFINDINGS

The rule I took from it: the agent that made something is a poor judge of it. Split the maker from the grader, give the grader a point of view, and make the grader report only, so it cannot quietly fix things and hide what it found.

Agents that play the game

A game is a bad thing to validate by reading code. So I had agents play it. A QA agent drove a real browser with Playwright scripts, walking through rooms, firing, dying and saving, and reported what broke. A critic agent wrote about how it felt. Neither was allowed to change a file.

A later playtest found the most interesting problem of the project. With classic tank controls and fixed cameras, a camera cut could flip which way "up" meant, so you would run into a room and suddenly start running back out of it. That finding became camera-relative controls, which hold your direction through a cut until you let go of the stick.

Review from several angles at once

Before cleaning anything up, I asked for reviews along a few dimensions, because I wanted the game to be easy to tweak later. Five review agents ran in parallel, each with one job: maintainability, correctness and resource lifecycle, performance and asset weight, test coverage, and UX with accessibility and the site's design system. A single reviewer asked to look at everything tends to find the obvious things. Five narrow reviewers each go deep.

Fix in parallel, then try to break the fix

When a playtest turned up a list of bugs, two agents fixed them in parallel on separate parts of the code, and then each ran an adversarial review whose only goal was to break the fixes. That round caught things a friendly review would not: a fire key already held down could press a menu button that opened under your thumb, and key auto-repeat could refire the stapler.

Kick off, nap, review

The rhythm that emerged was asynchronous. I would kick off a task on the train, put the phone away, and sometimes take a nap. When I came back there was a summary, a pull request and a Vercel preview link. I opened the link, played for a minute, and decided.

My feedback was mostly about feel, in plain words: "the game play feels slow in pace", "i like running as the default and keep the game controls simpler". Turning that into movement speeds, turn rates and pickup rules is exactly the kind of work the agent is good at.

Half of my messages were some version of "where are we now?" The other half were checks: "Have you pushed these changes up for me? If so did you actually modify and do anything?" Once, the honest answer was a flat "No", with a list of what had and had not happened. When you cannot see the screen, you want an agent that tells you that plainly, and you should ask.

Prototype first, then make it maintainable

The order matters. The lobby, combat, the building and the boss fight all landed in the first day, as stacked pull requests small enough to review on a phone. Only once I liked the game did we turn to keeping it: one pass pulled every tuning number into named tables with a README that maps each kind of tweak to the file that holds it, and another hardened the tests.

The last mile was the least glamorous part. Linux WebKit in CI draws the game at a frame or two per second, so tests that waited on game time timed out, and the branch rules refused to merge anything red. Fixes went back down the stack until all eleven pull requests merged green. A prototype without that discipline is a demo. The cleanup is what makes it something I can change next month without fear.

What it cost

The first question I get is how many tokens this burned. The honest answer: it all ran on the $20 a month Claude Pro plan. No API bill, no team seat, no separate budget for the subagents. Tailscale is free for personal use, and the Mac and router at home are hardware I already owned.

That is part of what makes this the state of the possible rather than a demo. You do not need a research budget to work this way. If you want to try it, my referrals page has a Claude link that gets you a free month of Pro, and one for Wispr Flow.

The state of the possible

What changed is not that AI writes code. It is that the loop of decide, build, check and decide again no longer needs me at a keyboard. The machine does the building and the checking. I bring the reference, the taste, the no, and the call on when it is done.

If you want to try it, the pieces are ordinary. Keep a machine at home that is always on, and reach it with Tailscale and a mobile terminal. Write your rules down once so the agent follows them without you. Ask for decisions, not essays. Never let the maker grade its own work. Make every pull request produce a link you can open on your phone.

That is the dream of prototyping: the distance between an idea on a train and a link I can play is a few spoken sentences and a nap.


← Essays & Opinions