How I Built a 3D Mars Rally Game With AI Coding Agents
Mars Rally Championship, a free browser rally game, is 291 commits old. It started on 26 April 2026 from a game-jam starter pack, and most of the typing was done by AI coding agents working on branches: 54 merged pull requests came off agent branches, and 83 of the 291 commits carry an agent as the author rather than me. The thing that made it survivable was not prompting technique. It was a small set of machine-checkable invariants that fail CI the moment an agent writes code that is plausible and quietly wrong.
It started as a game-jam starter pack
The first commit is the Vibe Jam Starter Pack, dropped in whole: four standalone game starters and a bundle of shared agent skills. I kept the 3D one and deleted nothing, which is why projects/ and bonus/ are still in the repo as dead weight. Late April 2026, no plan beyond "make a rally car drive on Mars".
What it is now: a no-build browser game — 44 vanilla ES modules under public/js/, a self-hosted copy of Three.js resolved through an import map, zero bundler — plus a Next.js App Router site wrapped around it. The file I edit is the byte-for-byte file the browser runs. I wrote up that architecture separately in building a 3D rally game in Three.js with no build step; it turned out to matter for agents too, because a diff against a source file is a diff against production.
The honest scale
291 commits between 26 April and 6 September 2026. That is 19 weeks, roughly 2.2 commits a day, mostly evenings. About 38,000 lines across the game modules, the site, and the shared helpers, excluding the vendored engine.
The authorship split: 208 commits under my name, 83 under two agent identities. The repo's commit trailers show more than one agent was used — I moved between them over the summer, and I am not going to pretend I ran a controlled comparison.
Treat that 29% as a crude proxy and nothing more. Commit authorship measures who pressed the button, not who wrote the code. A large share of "my" commits are me fixing or trimming agent output and committing it locally; a share of the agent commits are mechanical renames across 44 files. If you measured characters typed, the agents wrote the large majority of this repo. If you measured decisions, they wrote almost none of it.
Guardrails are the actual work
This is the part people get wrong, so it is the part worth being precise about.
Agents are extremely fast at producing plausible code. The failure mode is almost never garbage. It is code that reads well, passes a skim, parses, type-checks, and violates an invariant nobody ever wrote down — because the invariant lived in my head, or in one comment, or in the shape of a file the agent never opened. Human review does not scale against that. You get three good reviews and then you get a tired one, and the tired one is the one that ships.
So the leverage is entirely in invariants a machine can check. Not "be careful", not a longer instructions file — a script that exits non-zero.
The import-graph check
./test.sh is a zero-dependency Node script, 25 structural checks, no npm install, runs in about a second. One check does something almost embarrassingly simple:
check('every js module is in the import graph', () => {
for (const f of JS_FILES) {
const base = path.basename(f);
if (!combined.includes(`./${base}`) && !combined.includes(`js/${base}`))
return `orphan module: public/js/${base}`;
}
});
With no bundler, an orphan module is invisible. Nothing errors — the file just never loads, and the feature looks complete in the diff and does nothing in the browser. Agents create orphan modules constantly: a new subsystem file, beautifully written, never wired into boot.js. This check has caught it every single time.
The drift guard
The garage, the pilot dossier and the achievement list live on the site, not in the game, and they operate on the game's own localStorage profile. That means lib/game-data.ts is a deliberate second copy of the game's cosmetics catalog, crate economy and medal thresholds. Two sources of truth by design, which is normally a smell.
The smoke test reads both files and diffs them: cosmetic ids must match exactly, and literals like the 500-credit crate cost and the gold/silver/bronze medal speeds must be present on both sides. Drift means either un-equippable items or wrong medals shown to players. Before the guard existed, an agent updating one side and not the other was routine.
The design-system gate
Any visual change to the site has to load a design-system skill first — tokens, type registers, imagery rules, verification steps. Without it, every agent invents its own spacing scale and the site stops looking like one artifact after about four sessions.
Rules about files that must never exist
Two of the sharpest guardrails are prohibitions. public/index.html must never be created, because it would silently shadow the Next.js landing page. Deploy-time placeholders live in exactly one document and nowhere else. Both are named checks that fail loudly, because both are the kind of "helpful" file an agent adds without being asked.
Tests that run the real game
52 Playwright tests across 11 spec files boot the actual game at /play, drive a stage, cross checkpoints, exercise the tutorial and the multiplayer lobby, and assert things about the site. This is the expensive guardrail — the job runs about 37 minutes and eats most of my CI budget — and it is the only one that catches "the code is fine, the game is broken".
| Guardrail | What it catches | What it costs to maintain |
|---|---|---|
| Import-graph check | New modules nothing imports — invisible in a no-build game | One line in boot.js per module; near zero |
| Catalog drift guard | Site's mirror of the game's constants diverging from the game | Both files must change in one commit, or CI fails |
| Design-system skill | Ad-hoc styling that fragments the site's look | The agent reads the skill before any visual edit; slower first pass |
| Never-create file rules | Files that would shadow real routes or leak deploy placeholders | Zero — two string checks |
| Lint for undefined globals | Bare-called handlers that were never registered anywhere | New globals must be declared in the ESLint config |
| Playwright runtime suite | Code that parses, lints and type-checks but does not drive | The big one: ~37 min of CI per run, plus flake triage |
| Content validator | Published claims the product cannot back; dead internal links | Keeping a facts sheet current as the game changes |
The general point is dull and I think it is the whole lesson: do not spend your budget on reviewing agent output, spend it on making agent output falsifiable. Every hour I put into test.sh paid back more than every hour I put into reading diffs.
What the agents were genuinely good at
Bounded, specified, verifiable work, and they were better at it than I am. The per-wheel tire model came out of a spec, a set of target numbers and three iterations. The deterministic replay pipeline — fixed substeps, seeded noise, recorded inputs, bit-exact ghosts — is the most intricate thing in the repo and an agent did most of it, because "the same inputs must produce the same output" is a property a test can assert.
They were also good at the work I avoid: writing the Playwright specs, SQL migrations, accessibility passes, and mechanical refactors that touch 40 files. And they read the whole repo before answering, which I stopped doing months ago.
What did not work
The findings below are the top of docs/ROADMAP.md, which I keep in the repo and public on purpose.
The engineering ran miles ahead of the product. After three months I had a deterministic simulator with real Mars constants, a per-wheel tire model, a ghost format, mobile touch controls and a genuine accessibility layer — and almost nothing wrapping it. No share surface anywhere. No way for a finished run to become a link someone else clicked. The sim was excellent and unencountered.
Content authoring, not code, became the bottleneck. For most of the project exactly one stage was authored. Stages exist as terrain squares; a stage becomes drivable only when someone hand-places a start position and a checkpoint route in the level editor. Three are drivable today. Everything downstream is gated on that manifest, deliberately — an unauthored stage is hidden from the stage list, its leaderboard, its guide page and the sitemap — so a missing route does not strand a broken page, it strands a whole surface that would have existed. No agent can fix this for you. It is taste and hours in an editor.
The leaderboard was trivially fakeable. For months the table accepted anonymous inserts with a permissive policy, and the submitted time was not checked against the submitted ghost. Any player with devtools could have posted a one-second run. That is now a server route that checks the time against a physics floor derived from route length, verifies the ghost's frame count against the claimed duration, confirms every checkpoint was actually traversed, and rate-limits per device. Worth noticing: agents built the leaderboard, and no agent ever mentioned that it was unauthenticated. I found it during an audit I asked for explicitly.
The site sat effectively unindexed while features kept shipping. A search for the domain returned essentially one URL, and it was a page that did not exist in the app. Meanwhile the roadmap kept filling with mechanics.
That last one is the honest summary of the whole failure mode. An agent will happily build you the next feature forever. It will never interrupt to tell you that your problem is distribution, or that the thing you should do this week is author a stage rather than refactor a subsystem. Vibe coding a 3D game gets you a 3D game surprisingly fast. It does not get you players, and it will not warn you about the difference.
What I would tell someone starting now
Write the invariants before the features. The first day is the cheapest time to add a script that fails when the repo stops being the shape you want, and you will never again know the shape as clearly as you do on day one.
Prefer a check in CI over a rule in a document. A rule in a document binds only the agents that read it that day. A failing exit code binds everyone, including future you at 1 a.m.
Keep an honest findings file in the repo and let the agents read it. Mine is blunt to the point of being unflattering, and it is the single most useful context I hand over at the start of a session.
Budget explicitly for the work agents cannot do: content, taste, and deciding what not to build. On this project that turned out to be the majority of the remaining work, and I planned for none of it.
Where to try this
The game is free, runs in the browser, needs no download and no account, and works on a phone or a school Chromebook. Start driving — a bare link auto-starts a stage, because the game has no menu screen and this website is the menu. The stage list shows what is actually drivable, the level editor page explains the tool that authoring bottleneck runs through, and about covers what the project is. The engineering companions to this post are deterministic ghost replays and the no-build Three.js architecture.