An experiment in Loop Engineering with Claude Code — building the classic Snake game two different ways and comparing how the process, not just the result, differs.
This repository accompanies an article on Medium. The code here is the artifact; the article tells the story of what happened and why it matters.
The same game was built twice, from the same starting idea, under two very different workflows:
| Arm | Location | How it was built |
|---|---|---|
| One-shot | one-shot/snake.py |
A single prompt. Claude wrote the whole game — logic, rendering, and input — in one monolithic pygame file, in one pass. |
| Loop-engineered | snake/ + tests/ |
Spec-first, test-first, milestone-by-milestone. Each slice was verified before the next began. |
Both play the same game. The interesting difference is everything around the code: the contract, the tests, the guardrails, and the verification loop.
one-shot/snake.py is ~190 lines that do everything in one place: game
state, collision rules, food spawning, the pygame render loop, input
handling, and the speed ramp are all interwoven. It works, it's
readable, and there are no tests. It's exactly what you get when you ask
for "a Snake game" and accept the first answer.
The loop-engineered arm treats the build as an engineering process with a tight feedback loop, rather than a single request:
- A written contract first.
SPEC.mdpins down the engine API and every game rule (reversal handling, the tail-vacate collision edge case, seeded food spawning) before any code is written. - Work sliced into milestones.
MILESTONES.mdbreaks the build into M1–M5: movement, food/growth, collision, determinism, and finally the renderer. - Tests drafted before implementation. For each milestone, the acceptance tests were written first against an engine that didn't exist yet, then the implementation was made to satisfy them.
- Guardrails that constrain the agent.
CLAUDE.mdencodes the project rules Claude must obey — the engine may not import pygame, all randomness flows through a seededRandom, only one milestone at a time, never touch the tests, and "done" means zero failures, not "the code looks correct." - An independent verifier in the loop. A dedicated
loop-refereesubagent runs the suite after every change and reports only failures — it verifies, it never fixes. Each milestone was closed only on its green report.
The payoff is a clean separation the one-shot version doesn't have: a
pure, pygame-free, fully deterministic engine (snake/engine.py)
that is exhaustively tested, and a thin renderer (snake/render.py)
that draws it and owns all the timing.
-
26 automated tests, all passing, across the four engine milestones:
Milestone Focus Tests M1 Grid and movement 10 M2 Food and growth 7 M3 Collision and game over 4 M4 Determinism and speed hook 5 -
A deterministic engine: the same seed reproduces an identical game, tick for tick.
-
A clean boundary:
snake/engine.pynever imports pygame and knows nothing about time;snake/render.pyhandles all rendering, input, and the every-5-foods speed ramp. -
M5 (the pygame renderer) is human-verified by design — no automated tests, since it's the one layer that has to be seen to be believed.
A full turn-by-turn record of the build — every prompt, the agent's
reasoning, and each referee report — is in LOG.md.
The project uses pygame (renderer) and pytest (tests).
# Play the loop-engineered version
python -m snake.render
# Play the one-shot version
python one-shot/snake.py
# Run the engine test suite
pytest -qsnake/ Loop-engineered game: engine.py (pure logic) + render.py (pygame)
tests/ Acceptance tests, one file per milestone
one-shot/ The single-prompt version, for comparison
SPEC.md The engine contract and game rules
MILESTONES.md The M1–M5 build plan
CLAUDE.md Project rules the agent must follow
LOG.md Full record of the build session
.claude/ The loop-referee verification subagent
MIT — see LICENSE.