Blog

282 Commits to the Sunset: Building an 80s Arcade Racer in Four Days with Claude Code

Key art: a coastal highway splitting in two at sunset
The TEB RUN title screen: a chrome logo over a sunset coastal road
The title screen isn’t a picture of a race. It is the race, driving itself behind the logo.

On 26 September 2026 I made the first commit of a browser game. By 29 September there were 282 of them, and a few more days made it work on phones: a sprite-scaling pseudo-3D racer with 15 stages, a road that physically splits in two, five endings, a car radio with three original songs, rain, snow, traffic, crash spins and a three-letter high-score table. It has no game engine, no framework and no runtime dependencies. I didn’t type most of the code. Claude Code did, split into parallel agents, each working from a written plan. My job was taste, decisions, play-testing, and saying no. This is how it went, including the part nobody writes about: an image model quietly drew somebody else’s car into my game, and I had to take it out.

By the numbers (at the end of the four days, 29 September)

Calendar time4 days (26–29 September 2026)
Commits282
StackTypeScript, Vite, Canvas 2D, Web Audio API
Runtime dependencies0
Game bundle~240 KB of JavaScript
Source~21,000 lines in 95 TypeScript files
Tests~28,000 lines; 2,389 unit tests in 132 files (Vitest), plus end-to-end tests (Playwright), all green
Spec and plans~66,000 lines of Markdown across 11 tracks
Content15 stages, 5 levels, 5 endings, 4 cars, 3 songs
Art~186 PNG sprites and illustrations, ~115 sprite names, every one with a procedural fallback

At launch: 340 commits, ~25,400 lines in 117 TypeScript files, 2,820 unit tests in 158 files.

The idea and the constraints

I wanted an 80s sunset-coast arcade racer: palm trees, a convertible, a sky going orange, a checkpoint timer that is always slightly too short.

The constraints were the interesting part:

  • It runs in a browser tab. Open a link, you’re driving. No install.
  • No engine, no framework, no runtime dependencies. Canvas 2D for pixels, Web Audio for sound, TypeScript for everything else. The shipped bundle is about 240 KB of JavaScript.
  • No cookies, anonymous stats only. A strict Content-Security-Policy; high scores live in localStorage. The only network call is an anonymous, cookie-free visit counter on the website version; the portal builds make none.
  • It must survive without its art. This turned out to matter a lot.

How I worked with Claude Code

The shortest honest description: I wrote down what I wanted before anyone wrote code, and then I kept writing things down.

First, a design spec. How the road looks, how the car feels, what a stage is, how the fork works. Not a wish list, a contract.

Then, tracks. The work was split into 11 tracks, each a slice of the game that could mostly be built on its own:

TrackScope
AEngine: the pseudo-3D projection
BAudio
CArt pipeline
DUI screens
ERace logic
FStructure: 15 stages, the fork graph, 5 endings
GIntegration and polish
HDriving and road feel
IRace “moments”: countdown, start gantry, checkpoints, crowds
JAudio 2: sampled engine, richer music, “bring your own radio”
KWeather and road surfaces

Each track got its own written implementation plan: small tasks, the files they touch, the tests that prove them. Spec and plans together came to about 66,000 lines of Markdown, which is more than the game’s source and tests combined. That ratio looks absurd until you remember who the readers are.

Then parallel agents. Claude Code executed the tracks with subagents working at the same time, each in its own git worktree so they couldn’t step on each other. Every task was test-first: write the failing test, make it pass, commit. Between steps there were code-review passes, and then the track was merged back.

To give a feel for the granularity, here is what a task in a track plan looked like. This snippet is illustrative, written for this article; it is not copied from the repository.

### Task 7: Checkpoint carries leftover time

Files: src/game/race.ts, tests/game/race.test.ts

1. Write a failing test: a car crossing a checkpoint with 12 s left
   gets the stage bonus added to those 12 s, not a reset.
2. Run the test, confirm it fails for the right reason.
3. Implement the minimal change in the checkpoint handler.
4. Run the full suite. Commit: "feat(race): carry leftover time".

Nothing clever. That was the point. A plan at this level is something an agent can execute without guessing and a reviewer can check without archaeology. Written plans are what made the parallelism possible: agents don’t share a head, so the plan is the shared head.

And the human part. I played the game. A lot. That caught things no test would: the time limits were retuned three times after I played, because “technically finishable” and “fun” are not the same number. I said no to features that were fine but not this game. I made the calls on feel, on art, and later on rights. Claude Code is very good at doing what the plan says. Deciding what the plan should say is still the job.

The radio screen: three songs to choose from, a car selector reading CAR: RETRO, and a big dashboard radio
Before every race you tune the radio. It’s the most important decision in the game, and it has nothing to do with driving.

The fun technical bits

A road made of slices

The road follows the classic pseudo-3D technique, the one Jake Gordon explains beautifully in his “How to build a racing game” tutorial (javascript-racer, MIT licensed). If you’ve ever wondered how 80s racers faked 3D, read it. Our code was written from scratch, but it walks the same path, and a few projection constants and easing functions match his, because good constants are good constants. Thank you, Jake.

Picture the road as a long list of thin horizontal slices stretching away from you. For each slice you know its distance, and dividing by that distance tells you how high on the screen it sits and how wide it is. Draw the slices back to front as trapezoids, alternate the stripe colours, and you have a road rushing towards you. For a curve, nudge each slice sideways a little more than the last. For a hill, lift the slices. Trees, signs, traffic and your own car are flat sprites, scaled by the same distance. None of it is real 3D, and all of it reads as speed.

On top of that sit the things that make it a game: Lo/Hi gears; drifting that punishes you for holding the steer too long; solid walls you can spin against; a dirt shoulder you can use as an escape route; rain, snow and different road surfaces; traffic; crash spins; and a checkpoint timer that carries your leftover time into the next section, so a clean stage buys breathing room for a messy one.

The road that splits in two

This is my favourite thing in the game. There are 15 stages across 5 levels, and at the end of every stage you choose where to go next. But there’s no menu. The road literally widens, a strip of terrain grows up in the middle, and it becomes two full carriageways. You choose your next stage by choosing a side. Keep left, keep right, commit. The paths lead to five different endings.

The fork is at once geometry (the road must widen and split believably), logic (a graph of stages) and presentation (you should feel the choice coming). Giving the structure its own track, with its own plan and tests, kept it from turning into a tangle.

In-game: a red roadster on a beach stage, HUD showing TIME, SCORE, LAP, speed and the stage name PALM BAY
Palm Bay. The HUD tells you your speed; the palm trees tell you the truth.

Audio as code

The radio offers three original songs: “Mirrorball Mile” (disco, 122 bpm), “Amber Lagoon” (chill-out, 92 bpm) and “Tunnel Nine” (techno, 138 bpm). None of them are audio files. The songs are written as note data in TypeScript and played by a small six-channel sequencer on Web Audio. The drums and every sound effect are synthesized at runtime. The only recorded audio the game ships is the engine, built from two public-domain (CC0) recordings.

And because a car radio should play your music, the source build has a “bring your own radio” folder: drop in your own files, run one script, and they show up on the dial.

A font you can read in the diff

The text you see in the game uses a hand-authored 8x8 bitmap font, written as source code. Every glyph is eight strings of # and .. You can review a letter in a pull request by squinting at it. There’s something satisfying about a font you can code-review.

Procedural fallbacks: the game without its pictures

Here’s the design decision that paid off the most. The game has about 115 sprite names, and every single one has a procedural fallback drawn in code. If an image is missing, the game draws a simpler version of it with canvas calls. With zero image files, the game runs identically: same road, same stages, same collisions, just humbler visuals.

That started as resilience: a missing sprite should never break a race. Later it became the thing that made open-sourcing possible, which I’ll come back to.

The title screen uses the same idea in reverse: its attract mode isn’t a video. It drives the real race engine behind the logo.

Art with an image model

About 186 PNG sprites and illustrations in the game were generated with ChatGPT’s image model from written prompts. Every prompt carried the same pasted style block, so the palette and pixel feel stayed consistent across dozens of subjects. Each image was then checked by eye at high zoom and run through a small Python/Pillow pipeline: background removal, cropping, resizing to exact target dimensions, and binary alpha (a pixel is either there or it isn’t, which is what sprites want).

“Checked by eye at high zoom” is doing a lot of work in that sentence. Some things the model did that we had to fix:

  • Fake transparency. Asked for a transparent background, it sometimes painted a grey checkerboard, the pattern image editors use to show transparency. We wrote a filter to detect it and remove it.
  • Seamless strips with a seam. Scrolling backgrounds must loop, and the model rarely makes its left edge meet its right. Our first fix was to mirror each strip, which loops perfectly and leaves an obvious fold down the middle. Now we cut each strip at the pair of columns that match best, and blend only a few pixels.
  • Clouds cut in half at the loop point. Same problem, different sky.
  • A square gantry. The start gantry has to span four lanes. The model kept drawing it almost square, like a garden gate.
  • A brake light that barely braked. The “brake” frame of the car came out with the lights barely lit. We rebuilt it from the straight-ahead frame by lighting only the tail lights.
The start line: a steel gantry with a checkered banner, a traffic light showing READY, and a crowd
The start gantry, now properly four lanes wide. It took some convincing.

The lesson isn’t “image models are bad”; for a solo developer they’re astonishing. It’s that an image model is a collaborator that will hand you something almost right and never tell you which part isn’t. A human eye has to look at every frame.

The part nobody writes about: getting the rights right

Before going public, I had research agents survey the legal side: trademark, copyright, car design, audio, platform rules. It’s not a substitute for a lawyer, but it’s a cheap way to find the questions you need to ask one.

The good news came first. Game mechanics and the genre aren’t protected. Pseudo-3D roads, checkpoint timers, branching routes, sunsets: fair game. The title is different from anything it’s inspired by. Fine.

The real problems weren’t things I had done. They were things the image model had quietly smuggled in.

My red car looked too familiar. The player car’s silhouette drifted towards a well-known 1980s sports car, and the model kept adding a tiny badge on the nose, the wheel centres and the rear panel, even when the prompt said “no badge”. The badge came back. Three times.

The traffic had registration papers. A few of the traffic cars looked like recognisable real models, one with readable lettering on its tailgate. None of it ever shipped.

The fix was to redraw all of them as invented cars. And the way that finally worked was to stop describing what to avoid and start describing shapes: two rectangular tail lights, one central exhaust, smooth flanks, plain wheel caps. Geometry, not identity.

Two lessons I’ll carry into every project that touches an image model:

  1. Describe forms, not names. Ask for “a red 80s convertible seen from behind” and you get the red 80s convertible, the famous one, because that’s what the phrase means to a model trained on the internet. If you want something original, you have to specify something original.
  2. Never type a brand name in a prompt. Not even in a negative. “Not like X” puts X in the prompt, and the model draws what it reads. The anti-badge prompts were, in effect, badge prompts.
The four player cars side by side: a red roadster, a mint-and-cream 1950s retro roadster, a candy-red super speedster and a pearl-white 80s sports roadster
Four cars, zero badges, no family resemblance to anyone. It took more prompting to invent a car than to copy one.

Audio had its own surprise. Seven drum samples came from an archive labelled “CC0”. Following the trail showed they actually came from a commercial sample pack whose licence forbids redistribution. The “CC0” label had been added by an anonymous uploader, not the owner. A licence label is a claim, not a proof. The samples were removed and replaced with synthesized drums, which is why the drums are now generated at runtime along with every other effect.

Even the vocabulary got cleaned up: the docs describe the game by its genre, and the inspiration is credited once, at the end.

The resulting licence split is simple. The code is MIT. The art, music, name and texts are reserved. The two engine recordings stay CC0, as they came.

And this is where the procedural fallbacks earned their keep a second time. The public repository contains the code and the tools, but not the illustrated sprites. Check it out and the game still plays, drawn entirely by the fallbacks. The full-art version lives on the website. Open code, reserved art, no compromise on either, because a decision made early for robustness made it possible later.

After the four days

The four days gave me a game. The days after gave it to phones, and to people who aren’t me. Between 30 September and 2 October another 58 commits went in, for 340 in total: on-screen touch controls and optional tilt steering, tested on a real iPhone; near-square screens that play in letterbox; a radio you can read at phone size; sprites shipped as WebP and loaded in phases, so only the first stage loads before the title; three builds from one codebase (a personal one, the website with cookie-free stats, and a portal build that makes no outside calls); a pause menu; and a share card at the end of every run.

The last change was the difficulty, and it came from play-testing, not from a test report. NORMAL was too hard. A crash in NORMAL now costs about three seconds, every stage on the first four levels has three more seconds on the clock, and a simulated expert driver who crashes twice now reaches the end on all 16 routes. HARD is unchanged. At launch the game is about 25,400 lines in 117 TypeScript files, with 2,820 unit tests in 158 files.

What worked, what didn’t, what I’d do differently

What worked

  • Writing before coding. The spec and the 11 track plans were the backbone. Agents working in parallel, in separate worktrees, only works if each of them knows exactly what “done” means without asking the others.
  • Test-first, always. Thousands of unit tests (2,820 at launch) sound like a vanity number until you realise they’re what let 11 streams of work merge into one game without anyone holding it all in their head.
  • Procedural fallbacks for everything. Robustness first, then the open-source release for free.
  • A second play-tester at home. TEB were the three letters I typed into arcade high-score tables in the 80s. My teenage son grew up hearing about those cabinets, and he became TEB RUN’s first play-tester: he picked the four cars from the dozens we had generated, argued for five stages per run, asked for mud and snow on the road, and kept me honest about the difficulty.

What didn’t, or at least not by itself

  • Tests can’t feel a game. The time limits were tested. Then I played, and they were wrong. Three rounds of retuning came from sitting in the driver’s seat, not from a test report.
  • Image models need a human on every frame. Checkerboards, seams, square gantries, dim brake lights, a badge that wouldn’t die. None of it was flagged; all of it was visible at high zoom.

What I’d do differently

  • Run the rights review earlier. I’d do it right after the first batch of art, not before launch. Redrawing a car is cheap early and annoying late.
  • Write art prompts as shape specs from the start. Tail lights, exhaust, panels, wheel caps. Describe the object, never the reference.
  • Treat every “free” asset as unverified until the licence traces back to the actual owner.

Try it

Tune the radio. Keep left, or keep right. Watch the timer.


About. TEB RUN is inspired by Sega’s 1986 Out Run and other 80s arcade racers; it is not affiliated with or endorsed by Sega.

Credits. Built by Teb with Claude Code. The pseudo-3D road follows the classic technique described in Jake Gordon’s “How to build a racing game” tutorial (javascript-racer, MIT); the code was written from scratch, and some projection constants and easing functions match the tutorial. Sprites and illustrations generated with ChatGPT’s image model from written prompts, then checked and processed by hand and with a Python/Pillow pipeline. Engine sound built from two CC0 recordings. Music, drums and sound effects written and synthesized in code.