sentencetogame.com

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.

ComponentAt hour fourNeeded for a second sessionDelta
Core loopPresent, tuned to one difficultySame loop, but a reason for it to feel different at run fiveSmall code, large design
Input and collisionPresent, usually correctNothing moreClosed
Art and HUDPresent, coherentNothing moreClosed
ContentThree to five hand-written levels, or one spawn functionEnough distinct situations to outlast the player's pattern recognitionThis is the expensive axis
DifficultyA constant, set once at generationA function of the last run, or a ramp inside the runCheap, rarely done
Feedback on failureA game-over screenA cause of death the player can nameCheap, almost never done
PersistenceAbsentAbsent is survivable if the loop is goodSeparate problem
Return reasonAbsentAt least one hook the player meets at the moment of deathCheap, 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:

ArchetypeS (distinct decision states)N per runP 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:

  1. 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.
  2. 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.

#HookWhat it changesCostFailure mode
1Personal best with deltaTurns the next run into a comparison instead of a repetition~5 linesShowing the best but not the difference; the player cannot tell if they improved
2One modifier per runMakes run two a different problem, not a harder version of the same one~30 linesModifiers that scale numbers instead of changing decisions
3Difficulty band from last runBends the curve without adding content~15 linesBand too steep; the player reads it as the game cheating
4Seed of the dayTurns private repetition into a shared comparison~20 linesSeed not displayed, so nobody can compare
5Ghost trail of your best runMakes your own past the opponentHoursBuilt 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.

BlockQuestionPassIf it fails
A. BuildDoes it run without a console error on a phone?Yes, first loadNothing else in this document applies yet
A. BuildCan a new player start playing without reading anything?Yes, under 5 secondsFix the title screen before touching the loop
A. BuildIs the core loop fun for 60 seconds with no score at all?YesStop. The loop is the problem, not retention
B. First sessionDoes the first run end in under 3 minutes?YesShorten it; a long first run hides a thin loop
B. First sessionCan the player name what killed them?3 of 3 testersFingerprint 3: add the cause line
B. First sessionDid anyone ask "what happens if I…"?At least 1 of 3The loop has no visible depth; do not add hooks yet
C. Second sessionDoes the death screen show a delta, not just a best?YesFingerprint 4: hook 1
C. Second sessionDo two runs differ in what the player must do?Yes, by run 2Fingerprint 1: hook 2
C. Second sessionDoes the fifth run score higher than the first?Yes, clearlyFingerprint 2: hook 3
C. Second sessionDid any tester start run 2 without being asked?2 of 3The hooks are not landing; check hook 1 first
C. Second sessionIs there a reason to share a run?Seed or score visibleHook 4
D. VerdictCan you name the single next change in one sentence?YesYou have not finished the post-mortem
D. VerdictIs that change variance or content?VarianceIf content, the project is no longer an afternoon project
D. VerdictWould you play it again tomorrow, unprompted?YesShelve 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.