Oct 23, 20261,688 words8 min read
Building Kessler Drift with a team of AI agents
The honest story of building a pixel-art Zuma-like roguelite with AI agents: the team, the decision log, the ideas the numbers killed, what QA caught and what fell short.

Introduction
Kessler Drift is a retro pixel-art marble shooter with roguelite runs. If you played Zuma or Luxor, the core is familiar: a chain of balls rolls along a track toward a hole, and you fire balls into it to make groups of three. Here the balls are space junk drifting toward a black hole called the Well, the shooter is a salvage tug, and every run is a contract: you clear stages to meet a Quota, pick upgrades, visit the Company Store and fight a boss called the Magpie at the end of the sector.
I didn't write the code, draw the sprites or compose the music. A team of AI agents did, with Claude acting as project manager and product owner, and me as the stakeholder making the big calls. In this article I will walk you through how that actually went: how the team was set up, how a vague idea turned into a playable build, which ideas got killed by numbers, what QA caught, and where it fell short.
This isn't a tutorial. It's the honest story of one project, told from its decision log and git history.

Setting up the team
My first message was short: be the PM of a Zuma-like roguelite, assemble a team, and run it in batches. References were Zuma, Luxor, Peglin, Bills Must Be Paid and Hades, and the style was retro pixel art. Claude came back with a charter and four roles, each one a short agent definition living in the repo under .claude/agents/: UX for direction and discovery, UI for art and screens, Dev for code, and QA for testing. Here is the QA one, trimmed:
---
name: qa
description: QA for the Zuma roguelite — test plans, bug reports, balance checks, automated smoke tests. Use after each build or milestone.
---
You are QA on a retro pixel-art Zuma-like roguelite. Before any work, read
docs/00-team/CHARTER.md, DECISIONS.md, the GDD and the latest CHANGELOG. [...]
You do not fix bugs; you report them to Dev and the PO.The important part is the first instruction. Agents start fresh every batch and remember nothing, so the docs folder is the team's memory: every agent reads the charter and the decision log before touching anything, and writes its output back as a document. Later I added two more roles, an Animator and a Sound designer, when the game worked but felt flat.
The decision log ended up being the most useful file in the project. Every call goes in, with who made it:
| Date | Decision | Rationale | Owner |
|------------|-----------------------------------------------------------|--------------------------------------------|-------|
| 2026-10-15 | Setting: B — Kessler Drift (space junk → black hole) | Avoids cultural-research overhead | Juan |
| 2026-10-16 | Salvage Manifests KILLED (Triple 73–74% of score > 70% cap) | Paper tests, paper-test-results.md | Juan |
| 2026-10-16 | Parallel CI runs must use separate PORT / CI_OUT | False failure in batch 4 | PO |Rows marked Juan are mine; rows marked PO are calls Claude made on its own, inside the scope I had approved. That split kept me out of the weeds without losing control of the direction.
Discovery and concept
The first batch was research. UX tore down the reference games and looked at recent roguelite trends, UI proposed an art direction (640x360, 16 px balls, a 32-colour palette) and pitched three settings, and Dev assessed the engine. The pitches were a yokai sushi conveyor, space junk falling into a black hole, and a folk-horror rosary. The team recommended the sushi one; I picked the space one because it didn't need cultural research.

The platform changed while discovery was still running. I first said Godot for a Steam release, then switched to web-first with an eye on native builds later. Dev compared Godot's web export with Phaser and gated the choice on a one-week web spike: a 15 MB download budget and five seconds to playable on desktop. The greybox came in at 10.3 MB gzipped and 2.8 seconds on localhost, so Godot stayed.

UX then turned the research into a concept: the Quota is the progress bar, chain reactions (cascades) multiply your score, and the fail state is Hull points rather than instant death. That last one was my call.
Building the vertical slice
The goal was a vertical slice: one full run, start to finish. It took five batches, each one ending with a QA pass and a build I could play: tracks loaded from data, then the run itself (map, rewards, Store, Dry Dock, random events, autosave), and finally the Magpie fight with its three phases. Version 0.6.1 was the first complete run.


Balancing was done by bots, not by feel. Dev wrote a headless bot that plays stages with novice, average and expert skill models, and UX used it to tune. The first pass played about 14,000 simulated stages and found the game far too easy: the average bot met every Quota in 21 to 55 seconds against a 55 to 100 second target. The boss went through the same process until the average bot won about 62% of fights.

Ideas the numbers killed
My favourite part of the process was how cheaply ideas died. I asked the team to explore deck-building for the balls you shoot. Before anyone wrote game code, UX built a paper simulation, and it showed that letting your deck decide colours would raise shots with nothing to aim at from 4.7% to 16.8%. So deck-building became about effects, not colour.
Then the two top candidates went through bot tests with pass and kill criteria written in advance. Salvage Manifests, named combo types like Balatro's poker hands, died because plain three-ball matches were 73 to 74% of all score, so the combos barely mattered. Overtime Push, a risk-reward option after meeting the Quota, died because the best case added 7% Scrip against a 15% bar. Each test cost a fraction of a day instead of a prototype.

The art pipeline
There's no hand-drawn pixel in the game. Every sprite comes from a Python script that draws it pixel by pixel on the locked palette, and a checker fails the batch if any PNG uses a colour outside it. Here is the start of the function that builds a 16 px scrap ball:
def scrap_index_map(symbol, frame, size=16, radius=7.0):
"""Return a size x size array of LUT names (or None) for one scrap frame."""
c = size / 2.0
grid = [[None] * size for _ in range(size)]
inside = np.zeros((size, size), bool)
for y in range(size):
for x in range(size):
if math.hypot(x + 0.5 - c, y + 0.5 - c) <= radius:
inside[y, x] = True
# ...outline pixels, shading bands and the colour-blind symbol follow
Because the art is code, changing it is cheap: UI regenerates a whole sheet when a rule changes. UI also ran a colour-blind check on the six scrap colours and drew a symbol on every ball, so the colours still read apart for colour-blind players. The Animator joined later with its own generators for squash frames, sparks, transitions and boss tells, and a motion spec with timings in frames that Dev implemented.


Audio
The Sound designer made every sound itself with a small synth toolkit written in Python and numpy, so there are no licensing questions. The style is chip-tune with a crunchy junk-metal clank on every impact. Chain reactions climb a D minor pentatonic scale, one file per step, and the manifest tells Dev exactly how to play each event:
"cascade_step": {
"files": ["sfx/cascade_step_02.ogg", "...", "sfx/cascade_step_11.ogg"],
"pick": "by_index",
"bus": "SFX",
"volume_db": -6,
"pitch": [1.0, 1.0],
"hook": "on_match with depth >= 2",
"notes": "Never randomise pitch: it is a D minor pentatonic ladder (D5 ... C7)."
}The final set is 90 files (sound effects, three music loops and two danger layers) in 2.41 MB, under a 3 MB budget. The catch: the sandbox the agents ran in has no speakers. Audio was checked through waveforms, spectrograms and loudness numbers, and I was the first person to actually hear any of it. It sounded good, which was a relief.

QA loops
QA never fixed anything; it reported bugs with a seed, steps and severity, and Dev fixed them in the next batch. Every batch ended with one command that runs the unit tests, replays recorded runs, exports the web build and drives it in a browser:
PORT=8862 CI_OUT=/tmp/ci-dev-2 game/tools/ci.shA few catches that convinced me the loop was worth it:
- A scoring bug where chain-reaction multipliers were one step too high. The tests passed anyway, because they had been written against the wrong numbers.
- Loading a replay from a link froze the browser tab, a regression introduced by the art integration.
- Reloading the page on the death screen brought a dead run back to life.
- A false CI failure caused by two agents running the test suite on the same port at once. The fix became a rule in the decision log.
The game is deterministic: the same seed and the same inputs give the same state hash. That made the polish work safe, since every animation change had to leave the replays bit-identical, which proved it changed only how the game looks.

Shipping the web build
Each build shipped as a zipped Godot web export. The single-threaded export needs no special server headers, so any static server works:
cd kessler-drift-0.7.2
python3 -m http.server 8000I asked for a hosted link a couple of times, from a computer without the repo. It didn't work out: the agents' sandbox can't accept outside connections, and the engine file was too big for the page hosting available, so I stuck with the zip. Version 0.7.2 is the build embedded above.
What worked and what did not
What worked:
- Docs as memory. A charter, a decision log and a handoff note were enough for fresh agents to pick up where the last ones stopped.
- Pass and kill criteria written before a test. They turned "I like this idea" into a number that could say no.
- Separate roles that review each other. The Animator and Sound designer checked Dev's integration of their specs and found real problems.
- Me only making the big calls: setting, platform, fail state, what to explore next.
What didn't:
- The sandbox has no GPU and no speakers, so frame rates, feel and sound need a human.
- The cloud container restarted a few times mid-batch. Work survived because it was committed to a work-in-progress branch, but each restart cost time.
- Agents wrote long. Almost every document came in over its word target.
- The slice never formally passed its exit criteria. Firefox, real phones and multi-player playtests are still open.

That's it! You can play it full screen at /games/kessler-drift. The next steps are the carry-over fixes, a decision on two new Modules that passed the second round of tests, and a real playtest with more people than me.

