Building a Retro Arcade from Scratch
I built a three-game retro arcade entirely in TypeScript. No sprites. No asset files. Every pixel is drawn with code. Here's how it works and what I learned.
How I Built It: AI-Assisted Development using Good Practice Steering Techniques
This project wasn't built by opening a blank file and typing. I used Claude Code as a development partner, running it through a structured four-stage workflow: brainstorm, plan, work, review. Every stage produces a markdown document, so there's a complete written record of every decision made along the way.
Brainstorm comes first. I'd describe a rough idea, "I want a Donkey Kong game" or "Space Invaders feels too slow" and work through it as a conversation. What mechanics matter? What can we cut? What's the simplest version that's still fun? The output is a brainstorm doc that captures the decisions and rationale. Five brainstorms drove this project, from the initial scaffold through to the final game improvements.
Plan takes a brainstorm and turns it into a structured implementation document. This is where the technical approach gets nailed down; interfaces, constants, mechanics, phase-by-phase success criteria. The plan also runs through research against the existing codebase to make sure it builds on what's already there rather than reinventing it. Six plans were written across the project, each one a self-contained spec. I also implemented a form of LLM as a judge by feeding the plan into Codex an converging on a sensible approach between Claude Code and Codex.
Work executes the plan. Claude Code reads the plan, creates a task list, and works through it phase by phase; writing code, running builds after each change, committing at logical boundaries. The plan checkboxes get ticked off as each piece is completed. This is where the code actually gets written, but it's guided by the document rather than improvised.
Review is the quality gate. After implementation, a multi-agent review runs across the code looking at different angles; security, performance, architecture, and simplicity. Findings get written as prioritised todo files: P1 issues block progress, P2 issues should be fixed, P3 issues are nice-to-haves. This project generated 25 review findings across its lifetime, and many of them genuinely improved the final code. The simplicity reviewer alone caught overengineered auto-fire timers, unnecessary spotlight rendering, and per-body-part outline drawing that added complexity for minimal visual payoff.
The whole thing creates a documentation trail that reads like a project history:
docs/
├── brainstorms/ 5 brainstorm documents
├── plans/ 6 implementation plans
├── solutions/ Reusable patterns and best practices
todos/ 25 review findings (prioritised)
What I like about this approach is that every decision is traceable. If you want to know why the AI only has two states, there's a brainstorm doc that explains the trade-off. If you want to know why spotlights were dropped from the arena, there's a review finding with the reasoning. Nothing is buried in a chat log it's all in the repo.
What was Built?
A browser-based arcade with three classic games reimagined from the ground up:
- Donkey Kong: 10 hand-crafted levels with platforming, barrel dodging, fire enemies, springs, and a proper lives and scoring system.
- Space Invaders: A full alien invasion with marching grids, destructible shields, a mystery UFO, wave progression that gets harder as you play, and a machine gun power-up that drops from killed aliens.
- Street Fighter: A 1v1 brawler with three selectable characters, a two-state AI opponent, jump mechanics, punch/kick/block combat with knockback physics, and a best-of-three round system.
All three games live behind a landing page with game cards, high score tracking via localStorage, and keyboard navigation. Pick a game, play it, hit Escape to come back. Simple.
The Architecture
The whole thing runs on a small custom engine, about 300 lines of TypeScript sitting on top of PixiJS v8. The engine handles three things: the game loop, rendering, and input. Game logic lives in self-contained scenes.
Engine (300 lines) Game Scenes (~3,500 lines)
├── Game loop ├── Landing Page
├── Renderer ├── Donkey Kong (10 levels)
├── Input manager ├── Space Invaders
└── Sound └── Street Fighter
The core idea is a Scene interface. Every game implements six methods: init, update, render, onKeyDown, onKeyUp, and destroy. The engine doesn't care what game you're running. It just calls those methods in the right order at the right time.
Scene transitions are reentrancy-safe. When a game calls loadScene(), the engine doesn't swap immediately, it queues the new scene and processes it at the top of the next frame. This prevents the kind of nasty bugs you get when a scene tries to destroy itself mid-update.
The game loop uses a fixed timestep with an accumulator. Every tick is exactly 16ms of game time, regardless of how fast the browser is rendering. This makes game logic deterministic, physics and movement behave the same whether you're running at 30fps or 144fps. If the browser tab goes inactive and comes back, the accumulator is clamped to one second so the game doesn't try to process hundreds of missed ticks at once.
No Sprites, Just Rectangles
Every visual in this project is built from coloured rectangles drawn at the pixel level. The aliens in Space Invaders have animated legs made of tiny rects. The Donkey Kong character has a hard hat and swinging arms. The Street Fighter characters have punch and kick animations with extended limbs.
The renderer exposes one main drawing method: drawPixelRect(x, y, width, height, color). Text gets its own method, but all game art is rectangles.
This constraint turned out to be more freeing than limiting. There's no asset pipeline to worry about, no sprite sheets to manage, no loading screens. And it forces you to be creative with colour and shape.
For Street Fighter, I added darken() and lighten() colour helpers that shift RGB values by a fixed amount. Fighters get lighter heads and darker legs, which gives them visual depth without adding more geometry. The arena uses an 8-band gradient sky interpolated between two colours, which looks surprisingly atmospheric for a dozen rectangles.
#=Key Technical Decisions
Edge-triggered input: Jumping, firing, and menu navigation all use edge detection: pressed = down && !previouslyDown. This prevents the player from holding a key and getting repeated actions. It's a small pattern but it touches almost every game.
State machines everywhere: Each game uses a union type for its state ("playing" | "dying" | "gameOver" etc.) with a switch in update(). State transitions are explicit. This keeps complex game logic readable — you can look at any state handler and know exactly what's happening without tracing through flags.
Two-state AI: The Street Fighter AI uses just two behaviours: approach when out of range, attack when in range. It reactively blocks when the player swings. This is about 30 lines of code and produces fights that feel surprisingly natural. Overengineering AI with complex decision trees wasn't worth it for a rect-based brawler.
Single speed formula: Space Invaders aliens speed up based on two factors combined into one equation: wave number and how many aliens you've killed this wave. No lookup tables, no phase transitions. One line: Math.min(3.5, 1.0 + (wave - 1) * 0.2 + aliensKilled * 0.05).
The Numbers
The entire project is about 4,200 lines of TypeScript across 7 source files. No external game libraries beyond PixiJS for canvas rendering. No images, no JSON data files, no build-time asset processing. The production bundle is under 330KB gzipped.
Three complete games, a menu system, persistent high scores, sound effects, and smooth 60fps performance all from one npm run dev.
What I'd Do Differently
I'd add touch controls earlier. Right now it's keyboard-only, which locks out mobile. I'd also extract the colour helpers and state machine patterns into shared utilities — each game reinvents them slightly.
But honestly, the constraint of keeping things simple paid off. Every game is a single file (plus level data for Donkey Kong). You can read the whole Space Invaders implementation top to bottom in ten minutes. There's value in that.
