← Back to work

EatWhat Tonight

A pixel-art survivors-like where a slime evolves by eating — fight, snack, pick evolutions, and let AI cook up foods that exist only in your run. It is also a methodology experiment: when almost all the code is written by AI, how do you make its output trustworthy?

Role · Solo developer + quality system design 2026 Indie game / Godot 4.7 Stack · Godot 4 / GDScript / DeepSeek / gpt-image-2 / PowerShell
EatWhat Tonight
925
commits · 24,865 lines of GDScript
129
automated checks (47 static + 82 runtime)
45 + 62
design specs and execution plans
6
verdict states · only one may be retried ↓

Eat to fill the evolution meter, then pick one of three cards: four branches, three tiers, plus golden combo cards on top. Win or die and you are back in the lobby, brewing looted seasonings, shopping, changing outfits, going again. Ask the chef for a dish and the game generates it on the spot — flavor text, pixel art, stats, its own evolution branch — for that run only.

The game lobby: cooking station, chef slime, wardrobe keeper and shop owner NPCs around the little player slime
The lobby packs ordering, brewing, outfits and intel into one screen — the AI generation entry point is the chef in the middle
PRODUCTION · How this was built

I did not write this code. I specified, reviewed and accepted it.

In one line: the AI produces the work, I set the standard, find the flaws, and decide whether it ships.

The scarce skill is not producing output, it is judging it — so the engineering weight moved from how to build it to how to know it is actually right.

  1. A feature idea
  2. Write the spec first what it must and must not do · 45 docs
  3. Then the execution plan broken into steps · 62 docs
  4. Pick the collaboration mode by risk low: just do it / medium: discuss first / high: two review passes
  5. The AI implements
  6. All 129 checks must pass anything red: locate → reproduce first → then fix
  7. Human review
  8. Into the optimization log 16 batches · including what was deliberately left unfixed
Write down what you want, then let the AI build it; nothing merges until the checks pass; everything you trip over goes back into the spec. No step in the loop rests on it looks fine to me.
AGENTS.md · the collaboration contract, tiered by risk
Low risk     tech debt / refactors / tests        implement  →  focused verification
Medium risk  player-facing UI / feel / numbers    discuss first  →  single review
High risk    cross-module behavior / release      implement  →  spec review  →  quality review
Which work can go straight to the AI and which has to be agreed with me first is fixed in a contract file, not decided by mood.

Specs come first. I write down what is wanted before the AI touches anything. For the same feature, a vague spec buys you a version that runs but is wrong; a tight spec usually lands first try. Across dozens of iterations that is an observable, repeatable causal relationship — which is why the 45 design specs and 62 execution plans are the least code-like and most decisive part of this project.

Every trap becomes a list entry. What accumulated over a year is not a feeling that AI is useful, but a catalogue of how AI fails inside a real engine project: the symptom, the mechanism, and the rule that avoids it next time. Three of them, expandable below.

AI failure modes · three of them
The check prints ALL OK, the verdict says FAIL
Symptom The script ends with ALL OK on screen, yet the harness reports a failure.
Cause The script referenced a global class name directly, so the engine had to compile that class entire dependency chain at load time. One bad link writes errors into the error stream before the script even starts — while the script own assertions still pass, because it has no idea anything blew up outside it.
Fix Guards never reference class names directly; they load by file path at runtime. And the verdict never trusts the script own conclusion alone — it reads the engine output too.
The process hangs with no output at all
Symptom It runs, then freezes: no error, no exit, eventually killed by the timeout.
Cause The retry was written as defer this to the next frame. But the engine drains its deferred queue until the queue is empty — anything pushed during the drain runs inside that same drain. So try again next frame became infinite recursion inside one frame.
Fix A retry has to actually cross a frame boundary (await the next frame, or use a timer). Deferred calls do not count as crossing one.
Check out the repo on another machine and dozens of checks fail at once
Symptom Green locally; on a clean checkout, dozens of checks all report could not find type.
Cause The global class table is written into a local cache by an editor scan. New classes were scanned into that cache long ago on my machine; a clean environment has no such cache, so nothing resolves.
Fix Let CI run from a fresh checkout every time — that is exactly what makes CI irreplaceable here: it supplies a machine that has never been used before.
QUALITY GATES · How it is verified

How do you make the checks passed worth believing?

A gate here is just an automated check script: if any one of them fails, the change does not merge. This section is about making those checks tell the truth.

Code that runs is not code that is right. Three illusions sit in between: the agent reporting its own success, a local cache hiding the problem, and one retry turning red into green. A fourth is worse — nothing errors anywhere and the output is still garbage.

L0
Static checks · the game never starts
47 checks · ~1.5 minutes · the only layer in CI

Only data and rules: is a field wrong, is a file missing, does the algorithm still hold, is everything wired up. Fast enough to run on every single change.

L1
Runtime checks · the game actually runs
82 checks · 10–20 minutes · full local run

Boots the engine and runs real frames to verify systems working together: spawn counts, buff stacking, ability phases, whether the UI binds in time. Slow, but it catches what only appears once things are running.

L1'
Real-image regression · did AI art get worse?
on demand · deliberately outside the suite

Re-slices every real image already paid for and compares with last time: allowed to improve, never to regress. Kept separate so a missing corpus can never be the reason the suite is red.

L2
The part a human has to look at
UI bounds / overflow / font sizes + eyeballing + structured review

What machines cannot judge: composition, color, readability. Honestly, this part still has no automated criteria.

The fast layer runs on every change, the slow one runs in batches, and CI takes only the fast one — because what CI supplies is not speed, it is a machine with no local cache.
Once a check finishes, how do you decide whether it passed?
VerdictWhen it is givenBlocks mergeWhy it deserves its own state
TIMEOUTstill running after the timeout (180s default)yesA scene that fails an assertion often forgets to quit and just hangs. Without its own state it looks like it is still working
FAILa failure marker in the output (SCRIPT ERROR / FAILED / ...)yesThe check reporting its own failure
ERRLOGan engine error that is not on the whitelistyesEngine-level runtime errors. Before this state existed, that entire class of problem was missed
EXITCRASHall assertions passed, only teardown crashednoThe one failure whose cause is already understood — and therefore the only one allowed a single automatic retry
PASSexited cleanly and printed the pass markerno—
WARNno errors, but no pass marker eithernoSilent success is not allowed: no marker means the check may have verified nothing at all
Content first, exit code second — and the log, stdout and stderr are read together, because looking at only one of them badly undercounts the problems.
Principles

Four rules that got written down

None of them is generic engineering wisdom. Each is the shape this system took after reality corrected it.

Retries absorb diagnosed flakiness, nothing else

An early version retried any failure once. The suite did go green, and genuinely flaky bugs went green with it — something that reproduces 30% of the time shows as passing 91% of the time after one retry (both attempts hitting it is only 9%). So today only assertions passed, crashed on exit may be retried.

Freeze the old debt, zero-tolerance for new debt

A new rule meeting an old mess leaves two bad options: fix everything first (blocks work for weeks) or write it in a doc without checking it (no rule at all). The answer is a ratchet — like a wrench that only turns one way: freeze the existing 72 violations as a baseline and assert the total never exceeds it. Old ones do not block anyone, a new one breaks the baseline instantly, and every cleanup lowers the number for good.

CI is not a second run of what you already ran

It runs only the fastest 47 checks, and not to save time. A dev machine is full of local caches that hide problems; CI checks out from scratch, which is the equivalent of running on a machine nobody has used — missing classes and unsubmitted files only ever surface there.

Prove the gate can actually go red — in every direction

Raise the pass bar on purpose once and watch the eval report the pass rate falling from 1.000 to 0.000 and exit as a failure. A gate that has never been red is indistinguishable from a gate that does not exist. But one red run is not enough: a later audit found it missed three other ways of getting worse (end of this section). Every way a result can degrade needs its own red run — a gate you have only seen fail one way can only be trusted that one way.

EVAL AUDIT · Evaluating the evaluation

The eval system needs an eval too

I went back through the real local ledgers (one row per delivery, one per review) and checked them line by line against the code. Everything I found had the same shape: it ran every time, never raised an error, and was not doing what it claimed to do.

The worst one: of the 134 rows in the delivery ledger that served as the baseline, 126 had been written by the check scripts themselves. Only 8 were real deliveries.

What the audit found · five of them
AssumedActuallyNow
The delivery ledger is the real baseline126 of 134 rows came from check scripts: check processes and the player shared one ledger, and the old test isolation only covered save filesThe source is decided from launch arguments and every check writes to an isolated directory; after a full 129-check run the real ledger still has 8 rows
The image regression gate was proven to fireOnly for a falling pass rate. A baseline silently lowered from 1.000 to 0.893, 4 unregistered new pages, failed cells rising from 3 to 6 — all of them passedLowering a baseline needs a written reason and leaves history; unregistered pages fail; failed-cell counts are compared too
CI catches engine errorsWhen errors flooded the log, the old gate missed 20 runs out of 20 — exactly when it matters mostThe new gate misses 0 / 20; a check now verifies that local runs and CI use the same engine version and error whitelist
A retry with the violation reasons lets the model fix its draftThe request was single-turn, so the model never saw its previous draft — a constrained re-roll. Hold the Line measured the same root cause: reasons alone rescued 12%, reasons plus the draft 75%Now multi-turn, with the draft included. This path has not fired here yet; 75% is a figure from the other project and only an expectation here
Art deliveries are guardedThat check read a copy excluded from the build and trusted a self-reported "ok" in the QA record for pixel quality — swap the image and it stayed greenIt now guards the files that actually ship and recomputes from pixels; every rule is mutation-tested: break it and the check must go red
None of these would ever have raised an error on its own: data has to be verified separately, and an alarm has to be tested in every direction it can fail. The samples are still small (8 real delivery rows, 9 review rows); after the fixes, a full 129-check run added zero rows to either.
Reviewers get evaluated too · 14 tasks reviewed one by one by AI subagents
Where the missed bug wasCountNotes
Inside the diff4The problem was right there in the change under review
Outside the diff5The problem was in a file nobody touched, which a per-task review structurally cannot see. The costliest one was here: a field was deleted while another file still assigned it, so every generation threw an error at the player — and two task reports labelled it "pre-existing"
Two projects combined. Misses inside and outside the diff have to be reported separately, or a blind spot in the process gets booked as a careless reviewer. The sample is small, and the review records of one project sat in a directory version control had ignored since day one, so they are gone and that part is inferred from timing. Keeping eval data is part of the eval system.
DATA PIPELINE · AI-made art

Two AI pipelines — the hard part is acceptance, not generation

One produces art offline, the other generates a whole asset set the moment a player orders a dish. Neither is call the API and use the result — everything interesting happens afterwards: is this usable, who decides, and by what criteria?

Generate → validate → post-process → accept → ingest. Five steps, none of them skipped.

Spec before generation. A project-wide art spec first pins down the hard rules — visual direction, pixel grid, palette and values, outlines and shading, frame rates, UI type — and then, most importantly, its last two chapters: the acceptance criteria for four asset classes and exactly how to review them. Assets that fail are rejected before ingest, instead of being used because they exist.

The look has to come apart. The hero evolves several times per run, so its appearance cannot be one flat image: the character is split into fixed regions, each generated and swapped independently, with a master rig keeping every part aligned. The difficulty is not making it look good — it is getting one model output to contain several regions that are all immediately usable.

One request delivers the whole set. The player orders a dish: DeepSeek writes structured flavor text, then gpt-image-2 paints the full set into a single 1024×1024 page — one food icon plus 3 evolution branches × 3 quality tiers of parts — which is sliced and assembled locally. Why not one request per part? Because this image API bills by pixels generated, not by number of calls, so painting everything in one page is the cheapest option.

  1. Dish text structured JSON from DeepSeek
  2. Assemble the page prompt the model is forbidden to invent a background
  3. One 1024×1024 page 1 icon + 3 branches × 3 tiers
  4. Slice by pixel blobs a hard grid cut fails — the model paints off-center and splatters, so it picks the connected blob instead
  5. Remove the pink fringe a magenta backdrop tints white steam pink; the affected channels get pushed back
  6. Normalize and align
  7. Assemble onto the character
One request delivers the whole set — the hard part is not getting the model to paint it, it is slicing a freely composed page into parts that still line up when assembled.
Same asset set, different canvas size
1536×1024
1372 tokens
1024×1024
1056 tokens
A 23% saving — note that it is not the 33% you would get from area, because this API prices canvas size in bands, not continuously. Claiming 33% collapses the first time someone checks.
Usable subject height is 256px in both cases; what was cut is horizontal background nobody wanted — so the 23% is free, with no loss of quality.

Turning “looks fine” into a number. An incident in August 2026: the vendor delivered and billed normally, text and abilities all succeeded, and the player still got the default template look — with nothing erroring anywhere. Three layers of failure, and not one of them was an error. Each was an absence of judgement.

  1. The model delivers and bills normally; text and abilities all succeed
  2. Slicing looks for sprites across 9 columns × 3 rows — the model painted one per branch × tier block instead
  3. That leaves 5 empty cells, over the threshold of 3
  4. The whole page is discarded fallbacks were settled per page back then
  5. The already-sliced food icon goes with it — the single most valuable cell of a paid delivery
  6. The player gets the template look, with nothing erroring anywhere the log said pipeline complete, and the NPC line blamed the vendor
Sliced without errors and sliced into something usable are two different claims. Three fixes, one per layer: converge the layout onto how the model actually paints, settle fallbacks per branch so the icon is never dragged down, and log every ingest into a ledger.
Every sliced cell gets five scores
MetricAcceptable rangeOut of range meansLevel
coverage · how full the cell is0.05 ~ 0.82too low: the cell is empty or just debris; too high: the backdrop was not strippedFAIL
edge_touch · pixels on the cell border≤ 0.20the subject was already cut off by the neighbouring cellFAIL
chroma_residue · leftover pink fringe≤ 0.02the magenta backdrop was not cleaned off the edgesFAIL
fragments · how many pieces≤ 3the subject shattered, or debris from a neighbour was merged inWARN
center_offset · distance from center≤ 0.25the framing missedWARN
Breaking the first three means the cell is unusable (FAIL); the last two mean usable but worth a look (WARN). The page rolls up into a pass rate: ≥0.95 passes, ≥0.75 warns, below that fails. Cells filled by a local placeholder count as failures — they are not model output and must not pad the score.

Degrade at a finer grain than you pay at. In that incident 5 empty cells voided the whole page, taking the already-sliced food icon with it. Now the icon and the parts settle separately, fallbacks settle per branch, and only a page where every branch failed can be dropped whole. The full page is also kept on disk forever, so the first response to a bad slice is not to buy another image but to re-slice offline with a different profile — which is exactly how the fried chicken page went from a full-page fallback to zero fallbacks, at a cost of nothing.

The whole chain is designed around failure. The model will misbehave sometimes, so every class of failure has an answer prepared in advance rather than patched in afterwards.

What can go wrongHow it is caught
The text model writes on a dark background into the prompt, contradicting the page magenta backdropBackground clauses are banned at generation and stripped again at assembly — which also fixes everything already cached
The returned JSON is polluted by code-fence noiseStrip the noise and retry, instead of declaring failure
A momentary network hiccupBack off by error class: connection / DNS / TLS / 429 / 5xx are retried; timeouts are not (inference takes tens of seconds anyway, so resending only doubles the wait); 400 / 401 / 402 fail immediately, since retrying cannot help
It really did failFall back to the signature-dish pool — crucially, signature dishes run through the same pipeline as generated ones. A fallback path that is weaker and never exercised will only invent a brand-new, untested failure when it finally fires
What the player seesThe chef NPC apologizes in character. The player perceives a character, not an error message
The last two are the easy ones to miss: a fallback path has to mirror the main path, or it becomes the breeding ground for the next bug.
−23%
cost per delivery (canvas accounting)
1.000
measured pass rate on real deliveries
0
cost to recover the incident (offline re-slice)
5
per-cell metrics + a page-level pass rate
GAMEPLAY

A combination space far larger than the content volume

Four branches × three tiers of evolution, plus golden combo cards; four utensil-themed auto-weapons that can be bought and upgraded through card draws; enemies drop typed, quality-graded seasonings, the brewing station melds three into one, and shop prices combine flavor-point value with recipes. Elite affixes, theme synergies, the recipe wall and boss variants multiply against each other, so every run spans far more combinations than the raw content would suggest.

Engineering

Technical implementation

Built solo in Godot 4 + GDScript, data-driven throughout.

Data-driven stages

Maps, waves, bosses, natural food and evolution pools all live in .tres resources; the Stage is just a shared skeleton — adding a stage requires almost no code.

Component combat

Characters are assembled from Health / Hitbox / Hurtbox / Mover / StatBoard components; projectile skills run through an object pool; four utensil-themed auto-weapons can be bought and upgraded through card draws.

Seasoning economy

Enemies drop typed, quality-graded seasonings; the brewing station melds three low-tier into one higher; shop prices combine flavor-point value with recipes — all implemented as pure static functions.

VERIFIABLE

Everything above has a document behind it

These are documents the project actually runs on — none of them was written for a portfolio, and none was rewritten for one.

DocumentWhat is inside
Quality gates and the AI collaboration eval systemlayered gates · the six-state verdict spec · how the noise whitelist was derived from measured frequencies · the AI failure catalogue · an audit of the eval system itself: what it found, how it was fixed, how the fix was re-verified
Reviewer miss-rate retrospectivetwo projects, 14 tasks · misses inside and outside the diff counted separately · which conclusions are inferred
Project-wide art remaster standardsvisual direction · hard rules · acceptance criteria and review method for four asset classes
Release-readiness reviewP0/P1/P2 triage · release gate checklist · a 30-second reproduction command for every claim
AIGC delivery-page quality standardcost accounting · the five thresholds and the reasoning behind them · fringe-removal constraints · how the regression eval is used, and when a baseline may go down
Optimization log16 batches, each with how it was reproduced, how it was verified, and what was deliberately left unfixed
The part of the release review worth reading is its evidence table: every conclusion carries a command you can re-run in 30 seconds — and a closing note marking which numbers came from live observation that day and cannot be verified from the repository.

One standard of judgement, two ways of producing. In the Liberty Engine ocean system it is hand-written HLSL, compute shaders and a GPU stability budget. Here it is an acceptance spec written for an AI.

— Across projects
Liberty Engine ocean system →Hold the Line →
Next project
Hold the Line →