An AI Made Game in an Afternoon: The Honest Post-Mortem
Short answer: an AI made game built in one afternoon will run more often than you expect and will lose the player before the second session almost every time. The afternoon is enough for everything that happens inside a session — input, collision, loop, art, HUD — and structurally short of the one thing that produces a second session: a difference between run one and run two that the player can feel without being told. This is the post-mortem for that difference. Four fingerprints that mark an afternoon build, an arithmetic way to estimate when your game runs out of situations, five return hooks ranked by what they cost, and a worksheet with pass/fail lines so the verdict is a decision instead of a mood.
The build is the part that goes well. The post-mortem is the part that gets skipped, which is why the same game gets rebuilt from scratch three weekends in a row instead of being finished once.
What an afternoon actually buys
Four hours of prompting produces a surprisingly complete artifact. It is worth listing what is present at hour four against what a second session requires, because the two lists barely overlap and the gap is not evenly distributed.
| Component | At hour four | Needed for a second session | Delta |
|---|---|---|---|
| Core loop | Present, tuned to one difficulty | Same loop, but a reason for it to feel different at run five | Small code, large design |
| Input and collision | Present, usually correct | Nothing more | Closed |
| Art and HUD | Present, coherent | Nothing more | Closed |
| Content | Three to five hand-written levels, or one spawn function | Enough distinct situations to outlast the player's pattern recognition | This is the expensive axis |
| Difficulty | A constant, set once at generation | A function of the last run, or a ramp inside the run | Cheap, rarely done |
| Feedback on failure | A game-over screen | A cause of death the player can name | Cheap, almost never done |
| Persistence | Absent | Absent is survivable if the loop is good | Separate problem |
| Return reason | Absent | At least one hook the player meets at the moment of death | Cheap, entirely absent |
Seven of those eight rows are closed or cheap by hour four. The row that decides whether the game has a life is the last one, and it is the row no generator will ever fill for you, because a return reason is not a feature of the game — it is a relationship between two sessions, and a forward pass through a single session has no place to put it.
That is the whole post-mortem in one line: the afternoon produces a game that works, and a game that works is not yet a game that gets played twice.
The arithmetic of running out of situations
Before the fingerprints, the estimate, because it converts "nobody came back" from a feeling into a number you can argue with.
A game with fixed content has a finite number of distinct situations in it. Not visual variations — situations where the player's correct action changes. Count those, and you can estimate how many runs the game can support before it is solved.
S = number of distinct situations the game can produce
(count only states that change what the player should DO)
N = new situations a typical run shows the player
P ≈ S / N plays before the game stops surprising anyone
Three archetypes, counted honestly:
| Archetype | S (distinct decision states) | N per run | P before saturation |
|---|---|---|---|
| Endless dodger with one obstacle type and a speed ramp | ~5 (one obstacle, three lanes, two speeds that matter) | 3 | ~2 plays |
| Arena shooter, three enemy types, one arena | ~12 (nine pairings plus three solo states) | 4 | ~3 plays |
| Wave defence with five hand-built waves | ~20 (five waves × four states each) | 20 | ~1 play |
Read the third row carefully, because it is the one that fools people. A game with twenty situations that shows all twenty in the first run has one run of content, no matter how long that run is. Content volume is not the variable. Content volume relative to what a single run exposes is the variable.
Two levers follow, and only two:
- Raise S. More levels, more enemy types, more mechanics. Linear cost, linear gain, and every addition brings new bugs. This is the expensive axis and it is the one an afternoon cannot pay for.
- Raise variance. Keep S where it is and change which situations a run draws from, or how they are weighted. Cheap, and it multiplies the number of runs before the player sees the same shape twice.
An afternoon cannot buy lever one. It can buy lever two in about ninety minutes, which is why every recommendation below is a variance change rather than a content change.
Four fingerprints of an afternoon build
These are the shapes that show up again and again in games generated in a single sitting. Each one is stated as what you see, why the generator produced it, and a test you can run in two minutes. Run all four before you write a single line of the post-mortem, because the verdict depends on which ones are present.
Fingerprint 1 — Every run is the same run
You see: run three feels like run one with a different score attached. You already know where the first enemy comes from.
Why: randomness in generated code almost always lands in the cosmetic layer — particle direction, background stars, idle animation phase. The gameplay-relevant draws are either absent or seeded from a constant, because "make it random" is interpreted as "make it look alive," and because a fixed spawn table is easier to write correctly than a weighted one.
Test: play three runs and write down one thing that differed in what you had to do. Not what you saw — what you did. If the page stays blank, this fingerprint is present.
Direction: move randomness from the cosmetic layer to the decision layer. One modifier drawn per run, applied before the first frame, changes the problem the player is solving without adding a single new asset.
Fingerprint 2 — The curve was set at generation time
You see: your score at run six is roughly your score at run one, plus or minus luck. The game does not appear to notice that you got better.
Why: difficulty in a generated build is a set of constants — enemySpeed = 2, spawnInterval = 900, hp = 100. A constant is not a curve. There is no term in the code that reads "how is the player doing" and writes back into those numbers, because that requires the game to observe itself, and a forward pass writes the game, not a loop around it.
Test: compare your first-run score with your fifth-run score. If the fifth is not clearly higher — roughly double is the heuristic, since a player who has learned the pattern should be able to survive meaningfully longer — the difficulty is a wall you memorised, not a curve you climbed.
Direction: make three of those constants functions of the previous run's score, clamped to a band. Ten lines, and the game starts responding to the person playing it.
Fingerprint 3 — Death is a wall, not a lesson
You see: you die, the screen says the score, you press restart, and you are back at the beginning with no idea what specifically killed you.
Why: generated death handling is a state transition, not an explanation. state = 'gameover' is one line; "you died because two enemies converged while your dash was on cooldown" is a diagnostic system, and diagnostics are the first thing to be dropped when the generator is optimising for a runnable file.
Test: die once, then say out loud, in one sentence, what killed you and what you would do differently. Then have two other people do the same. If any of the three produces "I just got overwhelmed," this fingerprint is present.
Direction: capture the cause at the moment of death — the last damage source, the last input, the distance to the nearest threat — and print one line of it on the game-over screen. It costs less than a menu and it converts a restart into a hypothesis.
Fingerprint 4 — The result screen is a dead end
You see: a screen with three elements: "Game Over", the score, and a button. Nothing to compare against, nothing that moved, nothing that says the next run would be different.
Why: the result screen is written last, from the perspective of "the session ended," not from the perspective of "the next session begins." A generator that has never modelled a second session cannot write a screen whose job is to start one.
Test: open the game, die on purpose, and look at the screen for ten seconds. Answer two questions: is the previous best visible, and is the difference from it visible? Is there one thing on this screen that will be different next time? Two nos means this fingerprint is present, and it is the single highest-value thing in this entire page to fix.
Direction: previous best, the delta, and one line about what changes next run. Five lines of code, and it is the difference between a score and a stake.
Five return hooks, ranked by what they cost
Every hook below changes variance rather than content, which is why they fit in an afternoon. They are ordered by cost, and the order matters: implement one, run the three-run test, and stop as soon as the test passes. Adding all five to a game that fails fingerprint 1 is how a second afternoon gets spent making the same mistake more thoroughly.
| # | Hook | What it changes | Cost | Failure mode |
|---|---|---|---|---|
| 1 | Personal best with delta | Turns the next run into a comparison instead of a repetition | ~5 lines | Showing the best but not the difference; the player cannot tell if they improved |
| 2 | One modifier per run | Makes run two a different problem, not a harder version of the same one | ~30 lines | Modifiers that scale numbers instead of changing decisions |
| 3 | Difficulty band from last run | Bends the curve without adding content | ~15 lines | Band too steep; the player reads it as the game cheating |
| 4 | Seed of the day | Turns private repetition into a shared comparison | ~20 lines | Seed not displayed, so nobody can compare |
| 5 | Ghost trail of your best run | Makes your own past the opponent | Hours | Built before 1–4 exist, so there is no best run worth chasing |
Hook 1 — the delta, not the best
A personal best alone is a number the player has no relationship with. The delta is the relationship.
// on death
const best = Number(localStorage.getItem('best') || 0);
const delta = score - best;
if (delta > 0) {
localStorage.setItem('best', String(score));
showGameOver({ score, best: score, delta, label: `new best, +${delta}` });
} else {
showGameOver({ score, best, delta, label: `${delta} from your best` });
}
The label matters more than the number. -180 from your best produces a second run; Best: 1240 produces a shrug, because it does not say what this run was.
Hook 2 — modifiers that change decisions, not numbers
This is the hook that buys the most runs per line of code, and the one most often implemented backwards. enemySpeed × 1.2 changes how the same run feels. enemies leave a trail changes what the player does.
const MODIFIERS = [
{ id: 'trail', text: 'Enemies leave a damaging trail',
apply: g => { g.enemiesLeaveTrail = true; } },
{ id: 'fragile', text: 'One hit ends the run',
apply: g => { g.playerHp = 1; } },
{ id: 'gravity', text: 'Gravity is 40% stronger',
apply: g => { g.gravity *= 1.4; } },
{ id: 'magnet', text: 'Pickups pull toward you',
apply: g => { g.pickupMagnet = 320; } },
{ id: 'fog', text: 'You can only see a radius around you',
apply: g => { g.viewRadius = 220; } },
{ id: 'mirror', text: 'Controls are inverted',
apply: g => { g.invertX = true; } }
];
function pickModifier(seed) {
return MODIFIERS[Math.floor(rand(seed) * MODIFIERS.length)];
}
The test for whether a modifier is a real one: does it change the first decision of the run? One hit ends the run changes it immediately — you approach the first enemy differently. Enemies are 20% faster does not change any decision, it only changes the margin. Six decision-changing modifiers give six runs a distinct shape, and six runs is usually past the point where a small game would otherwise be exhausted.
Hook 3 — a difficulty band, not a difficulty slider
// before the first frame of a new run
const last = Number(localStorage.getItem('lastScore') || 0);
const band = last < 500 ? 0 : last < 1500 ? 1 : 2;
const SPEED = [2.0, 2.6, 3.2][band];
const SPAWN = [900, 720, 560][band];
Three bands are enough. The failure mode to avoid is a continuous mapping from score to difficulty, which makes a bad run compound into a harder run and reads as punishment. A band resets the difficulty conversation at each tier, so a player who had a terrible run still gets a fair next one.
Hook 4 — a seed you can say out loud
Once runs are seeded, the seed becomes content: two people can play the same run and compare. Display it on the title screen and on the game-over screen, and accept it from the URL (?seed=4821) so a run can be sent to someone.
The cost is nearly zero if hooks 2 and 3 already read from a seed instead of from Math.random(). That is the reason to introduce the seed while adding the modifier, not after.
Hook 5 — the ghost, last
A ghost of your best run is the strongest hook in the list and the only one that requires the run to be recorded, stored, and replayed. It is worth building only after hooks 1 through 4 are in place, because a ghost is a comparison and there is nothing to compare against until a best run exists and is worth beating.
The paste-back prompt
If you would rather have the generator write hooks 1 through 3, this is the block to paste, with the file attached and no other changes requested.
Retention patch. Keep the game exactly as it is: same loop, same art,
same collision, same functions. Do not add levels, do not add mechanics.
1. Add a seeded RNG. Every random call in gameplay reads from it. Seed
comes from ?seed= in the URL, or from Date.now() if absent. Store the
seed and display it on the title screen and the game-over screen.
2. Add a MODIFIERS array of six entries. Each entry has an id, a short
player-facing text, and an apply(game) function that changes a rule,
not a multiplier of an existing number. Pick exactly one modifier per
run from the seeded RNG and show its text for three seconds at the
start of the run.
3. Read lastScore from localStorage at the start of each run and pick a
difficulty band: 0 under 500, 1 under 1500, 2 above. Each band sets
enemy speed and spawn interval from a fixed table of three values.
Do not interpolate between bands.
4. On death, write lastScore. Compare the run score with the stored best.
Show the best, the delta with its sign, and either "new best" or
"N from your best". Never show the best without the delta.
5. Record the cause of death: last damage source, distance to the nearest
threat at the moment of death, and the player's last input. Print one
line of it on the game-over screen.
6. Do not change anything else. Do not rename existing functions. Do not
rewrite the game loop.
Line 2 is the one that gets ignored most often. Generators default to multipliers because multipliers are the obvious interpretation of "modifier," and a run that is 20% faster is not a different run.
The post-mortem worksheet
Four blocks, fourteen questions. Each has a pass condition, and each failure has exactly one prescribed response — that is the point of writing it down, because the tempting response to a retention problem is always "add content," and content is the axis an afternoon cannot afford.
| Block | Question | Pass | If it fails |
|---|---|---|---|
| A. Build | Does it run without a console error on a phone? | Yes, first load | Nothing else in this document applies yet |
| A. Build | Can a new player start playing without reading anything? | Yes, under 5 seconds | Fix the title screen before touching the loop |
| A. Build | Is the core loop fun for 60 seconds with no score at all? | Yes | Stop. The loop is the problem, not retention |
| B. First session | Does the first run end in under 3 minutes? | Yes | Shorten it; a long first run hides a thin loop |
| B. First session | Can the player name what killed them? | 3 of 3 testers | Fingerprint 3: add the cause line |
| B. First session | Did anyone ask "what happens if I…"? | At least 1 of 3 | The loop has no visible depth; do not add hooks yet |
| C. Second session | Does the death screen show a delta, not just a best? | Yes | Fingerprint 4: hook 1 |
| C. Second session | Do two runs differ in what the player must do? | Yes, by run 2 | Fingerprint 1: hook 2 |
| C. Second session | Does the fifth run score higher than the first? | Yes, clearly | Fingerprint 2: hook 3 |
| C. Second session | Did any tester start run 2 without being asked? | 2 of 3 | The hooks are not landing; check hook 1 first |
| C. Second session | Is there a reason to share a run? | Seed or score visible | Hook 4 |
| D. Verdict | Can you name the single next change in one sentence? | Yes | You have not finished the post-mortem |
| D. Verdict | Is that change variance or content? | Variance | If content, the project is no longer an afternoon project |
| D. Verdict | Would you play it again tomorrow, unprompted? | Yes | Shelve it now, in an afternoon, instead of in three months |
Two rules govern the worksheet:
Rule one: blocks are ordered, not scored. A failure in block A makes blocks B through D meaningless. A failure in block B is not fixed by anything in block C. The most common mistake in an AI made game post-mortem is answering block D first — deciding "it needs more levels" — and then working backwards to justify it.
Rule two: the C-block failure has exactly one allowed response, and it is not content. If runs do not differ and the death screen is a dead end, adding levels raises S while leaving N unchanged, so P barely moves. Hook 2 is thirty lines. A new level is an afternoon and a new bug surface.
Five things not to do in the afternoon
Each of these is a common instinct, and each one spends the afternoon without touching the row that matters.
- Add levels. Raises S linearly at the highest cost per unit of the four levers, and leaves variance at zero. A game with ten identical runs is one run with nine repeats.
- Add a second mechanic. Doubles the bug surface and usually lands on the same difficulty band. A dash and no dash are two games only if the level design knows the difference, and generated levels do not.
- Do an art pass. Art is read as a promise of depth. Better art on a shallow loop produces a sharper disappointment than plain art does, and it makes you less willing to shelve the project.
- Polish the title screen. A beautiful title screen in front of a loop that saturates in two runs just delays the moment the player finds out.
- Add a leaderboard before a personal best exists. A leaderboard with no personal best is a screen full of strangers' numbers with no relationship to your own play. It is the most expensive way to learn nothing.
The pattern behind all five: each one adds surface, and none of them adds a difference between run one and run two. Surface is what the afternoon is good at producing, which is exactly why it is the wrong thing to spend the second hour on.
The honest verdict
An AI made game in an afternoon is a real artifact, and the honest post-mortem does not say otherwise. It says three things, in this order: the loop works, the loop saturates in a handful of runs, and the fix is variance rather than volume.
That is a better outcome than it sounds. A game that saturates in three runs but has a working loop is a game you can fix in ninety minutes. A game with twelve levels and a loop nobody wants to repeat is a game you cannot fix at all, and it is the more likely outcome of a second afternoon spent adding content.
So the post-mortem's last line should not be "what I built." It should be "what would make someone press start twice," written as a single sentence, with the answer classified as variance or content. If the answer is variance, the project has an afternoon left in it. If the answer is content, the honest thing is to shelve it now — in an afternoon, on purpose — instead of discovering it three weekends later, which is how most AI game maker projects end.
If you are still at the stage where the game forgets the run on refresh, that is a different failure with a different fix; it is covered in the persistence audit at [/ai-generated-video-game](/ai-generated-video-game). If the loop itself feels broken rather than shallow, start at [/why-ai-generated-game-feels-broken](/why-ai-generated-game-feels-broken) instead, because no return hook rescues a loop that does not work.