DEV Community

Cover image for I vibe coded a mobile game in 8 working days. The decision log was the real product.
Edwin Vermeer
Edwin Vermeer

Posted on

I vibe coded a mobile game in 8 working days. The decision log was the real product.

Quadrim is four in a row played in a 4×4×4 cube. The game is also known as Qubic: sixty-four cells, seventy-six winning lines, and most of those lines are invisible until someone uses one on you. It's a Flutter app for iOS and Android with 31 computer opponents, from one that barely notices a threat to one that plays the solved game perfectly.

I built it with Claude Code, on the €15-a-month Pro plan. Apart from some later tweaks and additions, it took 8 working days spread over 4 weeks. It's on the App Store, where it got almost no attention, so I've turned the repository into a tutorial instead:

https://github.com/evermeer/Quadrim

The repository holds the whole process, not just the code: the original rough idea, four rewrites of it, the spec, 48 tickets, every prompt in order (workflow.md), and a log of 396 decisions. That log turned out to be the most valuable thing I made, and this article is about why.

The loop

The workflow is described in full in CodingAgentOrchestration. In short:

  1. Clarify. Give the agent the rough idea and ask it to restate the goal, list assumptions and unknowns, and ask questions until nothing is ambiguous. No code yet.
  2. Brainstorm several approaches for each open question.
  3. Spec. Turn the result into a spec with acceptance criteria.
  4. Tickets. Cut the spec into small tickets, each one buildable, testable and mergeable on its own.
  5. Review. Have specialist agents (an architect, a security engineer, a product manager) review the spec and the tickets before any code exists.
  6. Build. Implement one ticket, run a code review, commit, and move on to the next.

One sentence ran through every prompt: "Add all decisions to the decisions file."

What a decision looks like

Every non-obvious choice gets a numbered entry with three parts: the question, the decision, and what it costs. Here's a short one, D295:

Question: Image.asset with only a height reports a size of zero until its bytes have decoded. Does that matter?

Decision: Every image is wrapped in a SizedBox whose dimensions are known before decode.

It cost this ticket three separate bugs, all with the same shape. The New Game button laid out 316×0 and couldn't be tapped. The logo was a zero-width gap.

The rule is stated once, with its reason, and from then on it's a fact the agent reads at the start of every session, not something it has to rediscover.

Two conventions made the log work:

  • Entries are never rewritten. When a decision is reversed, the old entry gets a "superseded by" line and a new entry is added beside it. Wrong turns stay visible.
  • Numbering never restarts. D01–D217 in the first edition, D218–D391 in the second, D392 onward now. Code comments cite them (// D369), so any odd line of code leads to its reason.

Here's what 396 of them taught me.

1. A hang is not a failure

At one point the full check script had been started ten times in a session. Exactly one of those runs wrote an exit line, and that one failed early, before it reached the engine tests. Nobody noticed for five tickets.

The cause was a test asking the search to solve exactly from a position with 58 empty cells: an instruction to solve Qubic, which never returns. Three wrong diagnoses came before the right one, and the decision log records all of them (D178).

The real finding was the habit, not the bug: a failing test announces itself; a hanging one looks exactly like a slow one. Every instinct says "give it a bit longer". Since then every long-running command gets a hard timeout, and the search asserts that it is never asked for an exact solve of more than 16 empty cells.

2. Measure instead of arguing

AI agents are very good at producing a plausible explanation of why opponent A should be stronger than opponent B. It took a while to learn to ignore all of them.

  • Styles form a ring. I spaced one tier of the ladder by which squares an opponent prefers. Measured head to head, corners-first beat centre-first, centre-first beat centre-only, and centre-only beat corners-first. Rock-paper-scissors, inside the very thing the tier was built on (D315). Which squares an opponent prefers is a style, and styles can't be rungs on a ladder (D336).
  • A cheaper measurement can reverse the answer. To save time, calibration ran at 10% of the search budget. For the strongest opponents that didn't just blur the result, it flipped it: one opponent read +83 against the next rung up at 10% and −115 at full budget. Their strength lives in a handful of expensive positions per game, and those are exactly what the reduction cuts.
  • One bad reference point skews everything averaged with it. Each rating estimate is "a neighbour's rating plus a measured gap", so one wrong neighbour shifted every estimate near it. Switching from the mean to the median of those estimates fixed it (D341).

The rule that came out of this: every claim that one opponent is stronger than another has to come from a run of the duel tool, not from an argument about the code. The largest single gain in the project came from exactly that kind of measurement: pointing the forcing search at the opponent's threats instead of only your own was worth about 300 Elo (D169).

3. A check protects the file it reads, not the thing you ship

The app promised that computer play makes no network call. To enforce it, check.sh asserted that the Android manifest didn't declare INTERNET.

Then in-app purchases arrived, and with them a Google telemetry library, three dependencies deep, that declares INTERNET. The release bundle shipped with the permission. The check still passed, because it read the source manifest, and the permission came in through the merged one (D356).

The log is honest about one more thing: the first conclusion was "the check survives unchanged". It came from reading the two dependencies that came to mind instead of asking the build tool what it actually produced. That wrong conclusion is recorded too, because its shape is the lesson: list the inputs you thought of, and you'll miss the one you didn't.

4. Rules that matter become checks

A rule that lives only in a document gets broken the first time a session doesn't happen to read it. So the important ones became greps in check.sh:

  • The engine package may not import Flutter, dart:io, a clock or an ambient random source. That's what makes "the same position and seed give the same move" true, and self-play reproducible.
  • The strategy dictionary may only be used if it's credited in the app. So the check fails if nothing in app/lib shows the credit (D174). A note in the repository isn't where a credit counts.
  • No user-facing text outside the localisation files.

The agent breaks a check the same way a person would: by accident, on a busy day. The difference is that the check is still there the next day.

5. Fold the log back into the brief

After 391 decisions I asked the agent to rewrite the idea document with every decision folded into it, and the war stories turned into general rules. The result, idea-v4.md, is the document I'd hand someone who wants to rebuild the game from scratch: the answers arrive with the questions instead of months later.

That's also my advice if you want to use this repo as a tutorial. Don't start from the code. Start from idea-v4.md, run the workflow, and compare what your agent decides with what's in the log.

Where I was needed

The agent wrote the code. I wrote the idea, answered the questions and made the decisions, and some of them were mine alone:

  • Scope and taste. What the game is, which opponents exist, and how monetisation works (a free daily limit, an optional unlock and an optional subscription).
  • Saying "measure it". Most of the lessons above started with me not believing a plausible explanation.
  • The world outside the repository. Store accounts, signing keys, App Store review and the Play Console. Some of those tickets simply can't be done by an agent.
  • The art. I made the design in Claude Design and took screenshots of it. I asked Microsoft Copilot for four completely different visual takes on that design, picked one, and had Copilot write the prompts for every image the game needed. Then I generated the images with ChatGPT. The prompts are in the repo too (doc/avatars-overview.md).

The numbers

Working days 8, over 4 weeks (plus later tweaks and additions)
Idea rewrites 4
Decisions logged 396
Tickets 48
Commits 183
Code ~18,000 lines of Dart, ~16,000 lines of tests, 800+ test cases
Tools Claude Code (Pro plan, €15 a month); Claude Design; Microsoft Copilot and ChatGPT for the art
Computer opponents 31, the strongest playing from Oren Patashnik's 1980 proof that the first player can always win

Try it

If you try the workflow on your own idea, I'd love to hear where your agent needed you.

Top comments (0)