Back in April 2025 I spent a weekend with Codex making a small Godot game called LeLion. The pitch fits in a sentence: a lion head flies over a grey city, picks up colour dots, and vomits a rainbow to paint the buildings, whilst a flying saucer and a ladybug try to kill it. Nine commits in ten days, most of them titled « On continue » (we keep going), then I put it down. The thing more or less ran, the painting worked through a shader and a pixel mask, and that was that.
Yesterday (yep, 17 months as the crow flies) I picked it up again with Claude Code, and by the end of the day there was a finished web game, and a GBA ROM (for #retrogramming).
Waking up the prototype
The first thing Claude did was read everything and tell me, plainly, what state it was in. Two signal connections pointed at methods that didn't exist (one of them because of a capital letter). Three dated copies of the main script lived next to the real one, because in 2025 I knew git but apparently didn't bother. The victory condition counted the transparent sky as something to paint, so it was unreachable. Seven emitters of 7,500 GPU particles each. One saucer, one ladybug, to debug, and then nothing ever again.
We agreed on phases: clean up, structure, gameplay, polish. Claude installed Godot through Homebrew and drove everything headless. It wrote a smoke test as a SceneTree script that loads the main scene without a display, unlocks a colour, makes the lion vomit on the city, provokes a defeat, then a victory, and asserts on the way. That script grew all day, from 12 assertions in the morning to 136 yesterday evening, and it caught a good half of the regressions before I even saw them.
A few Godot gotchas for anyone trying the same (but I'm sur Claude will reengineer it anyway, for a few tokens). godot --check-only doesn't load autoloads, so every reference to a singleton is flagged as an error; ignore it and trust a real run. A --script harness compiles before the autoloads exist, so you fetch them with root.get_node("GameState") rather than by name. And when you run the game windowed to take screenshots, wait in seconds with create_timer, not in frames, because without vsync the OpenGL window runs at several hundred frames per second and your "60 frames" is a quarter of a second.
Those screenshots helped Claude see the mess the project was in. The colour pickup's collision shape was offset by (195, 54) from its own origin, so it was never where the code put it. The lion's mouth marker sat at (186, 191) on a 128 pixel sprite, well outside the lion, and the vomit was a handful of one pixel particles (I was beginning with Godot and Codex, OK?). Claude located the actual mouth by scanning the sprite for pink pixels, moved the emitter there, and placed the paint zone at the landing point of a projectile fired at 45 degrees with the same velocity and gravity as the particles.
Making it a game
From there it was a long list, and I'll go fast. Three difficulty modes (easy with three hearts and heart pickups, normal with three hearts, hardcore with one). Three levels, two of them procedurally generated skylines, plus a title screen with best times. A rainbow star that doubles the jet for eight seconds. A pause menu. Screen shake, a red flash and a knockback when you're hit, confetti when you win, paint drips running down the buildings, a bit of inertia and lean on the lion. A procedural chiptune loop written by a Python script that outputs WAV files, later split into three synchronised tracks so the arpeggios and the melody come in as the town gets painted, with a minor key theme for the boss level. Touch controls that only appear on touch screens. Settings with volumes, fullscreen, a CRT filter and a language toggle, the whole game translated to English through Godot's CSV translations. A READY? VOMIT! intro, a CONTINUE? countdown when you die, an animated end-of-level summary, an arcade mode chaining the nine level and difficulty combinations, and an attract mode that plays a demo if you leave the title screen alone for fifteen seconds. CI on GitHub Actions builds the web export and deploys it to GitHub Pages on every push.
The boss deserves a paragraph. The Village level was too easy, so we added a giant painter's head, 65 percent of the screen tall, who comes in from one side, walks to the centre, pauses, walks back, and returns from the other side; you paint the half he leaves free. There was no sprite, so Claude drew an SVG silhouette (beret, nose, eye, brush with a red tip) and we iterated on the moustache three times: first it looked like two pieces, then it curled up, then I asked for it to curl down and hold the brush. The collision polygon is generated from the SVG's alpha at runtime, so replacing the file replaces the hitbox. Then a bug I noticed: the sprite flipped when the boss came from the right, but the collision didn't. Fixed together. Progress is now a real coverage measurement (the paint mask downscaled to a grid every 200 ms), which made 85 percent honest and, as a consequence, hard. Normal now asks for 90, hardcore for 95.
And then, a GBA ROM
Late in the day I asked whether the game could exist as a Game Boy Advance ROM, with the constraints of the era. Godot doesn't export to GBA, so the honest answer was a rewrite in C. Claude proposed a scale: 2000 by 648 pixels becomes a 480 by 72 town scrolling on a 240 by 160 screen, the 128 pixel lion becomes 32 by 32, the jet lands 35 pixels ahead and 45 down instead of 136 and 190, the boss becomes a composite of four hardware sprites. We created a second repository and went.
No toolchain got installed on my Mac. The build runs inside the official devkitPro Docker image (make docker), and the same image builds the ROM in CI. For tests, since the Homebrew build of mGBA was linked against an ffmpeg that no longer exists, Claude rebuilt it from source and then wrote a 150 line C harness on top of libmgba: it loads the ROM, runs frames, presses keys, reads memory, pokes memory, and dumps the screen as PPM. The game writes a small debug block at a fixed address in EWRAM (lion position, camera, progress, lives, state), and the test scripts assert on it. Screenshots come out as PPM, a tiny Python script turns them into PNG, and I got to look at them.
The town is a mode 4 bitmap (8 bit paletted), painted directly in an EWRAM buffer and copied into the visible page with one DMA per row. The sky gradient is drawn the same way, with fixed-source DMA fills. Sprites do the rest: the lion, ten drops along the arc, pickups, the saucer, the ladybug, the painter in four parts. Music runs on the actual Game Boy channels (two squares, the wave channel with a triangle for the bass, noise for kick and hat), sequenced from the same note data as the Godot version, and the sound effects are 8 bit samples on the two DirectSound FIFOs. Records and settings go to cartridge SRAM. Seven phases, thirteen commits, four tagged releases, all built and tested by CI, the ROM attached to each release.
The GBA taught us things the Godot version couldn't. libtonc's clamp() excludes its upper bound, so the lion stopped one pixel short of the edge and the camera two. RGB15() is a function, not a constant, so it can't initialise a static table. Non-square hardware sprites use the size code 64 for both 64 by 32 and 32 by 64; I'd used 32 and the painter lost his collar. And two bugs I found by actually playing in mGBA: pressing left on the difficulty row showed an empty label, because key_hit() returns the key's bitmask and not a boolean (right is 16, left is 32, and 16 modulo 3 happens to be 1, which is why right worked). Then the game lagged with four colours or more whilst vomiting: the paint stamp did three software divisions per pixel on a processor with no divide instruction, up to 441 pixels a frame. A lookup table and a multiply fixed it, the hot loops moved to IWRAM as ARM code, and a test now holds A with seven colours for 240 frames and checks that the frame counter advanced by exactly 240.
I asked whether it was worth optimising more, in case we add things. Measured, not guessed: a probe records the scanline reached when the frame's work is done. Worst case in play, boss on screen, seven colours, vomiting and scrolling, is 41 percent of the frame. So no. A test will go red if a future feature pushes that past three quarters.
Last thing, since I thought of it: the released ROM read two test hooks from the debug block, so anyone with an emulator's memory editor could poke one word and win. Those hooks now compile only into a separate lelion-debug.gba; CI tests that one and publishes the clean one, with a final check that the release ROM ignores the pokes. A memory editor can still write your lives directly, of course. That's true of every game ever made.
The web version is at w3cdotorg.github.io/LeLion, the GBA ROM in the releases of the second repo; mGBA runs it, and it should run on a flash cart, though I haven't tried one yet (will try today on a RG35XXSP — not an affiliated link)). If you do, or if the chiptune sounds wrong on real hardware (I haven't heard it on anything but an emulator), tell me in the comments.
Session ledger
Since I'd like to be transparent with costs in tokens, here is the actual count, pulled from the local transcript Claude Code keeps for the session (an 18 MB JSON file, which tells its own story). One session, one model (Claude Fable 5.1), from 07:37 on the 4th to a last exchange the next morning, so roughly fifteen hours of actual work with a night in between.
| My messages | 39 |
| Model calls | 280 |
| Tool calls | 319 (224 shell commands, 78 file or image reads, the rest browser automation) |
| Output tokens | 561,242 |
| Input tokens, fresh | 7,700 |
| Input tokens written to the cache | 2,450,708 |
| Input tokens read from the cache | 125,947,570 |
The big number is the one to read carefully. A coding agent re-reads its whole context at every call, so 280 calls over a context that ended up around half a million tokens gives a hundred and twenty-six million tokens of cache reads. Those are billed at a fraction of fresh input, which is the whole point of the cache, and they don't mean the model "read" that much new material; it kept re-reading the same day. The genuinely new material is the two and a half million tokens written to the cache (my messages, every tool result, every screenshot description) and the half a million tokens it produced: the code, the commands, the commit messages, the screenshots' analyses, and the explanations you've just read a summary of. For scale, that output is about the length of four novels, most of it GDScript, C, Python and shell, and a fair amount of it thrown away along the way.