From Arduboy to PlayStation: What It Takes to Build Games with AI on Every System

A year ago, Peakylight was my first real attempt at building something from scratch with AI. Since then I’ve used AI to build games for a 128x64 monochrome handheld, a $5 microcontroller, the NES, the Mega Drive, the Nintendo 64, the original PlayStation, the browser and a VR headset.

This post isn’t a list of games. It’s what I learnt climbing that hardware ladder: what each system demands, where the development time actually went, how the build pipelines grew up along the way, and what changed in how I work with AI because of it.

I built them roughly in the order web → Arduboy → ESP32 → N64 → PS1 → Mega Drive → NES. Here they’re ordered by hardware, from the smallest machine up, because that’s the order the lessons make the most sense in.

The Machines at a Glance

System CPU RAM Storage / Budget Games
Arduboy ATmega32U4, 8-bit @ 16 MHz 2.5 KB ~29 KB flash (FX: 16 MB flash chip) DownDraft, Stick Fighter, Sticks of Rage, Ether Echoes
NES 6502 @ 1.79 MHz 2 KB 32 KB PRG + 8 KB CHR (NROM) Lantern Moth
ESP32 (TTGO T-Display) Dual Xtensa LX6 @ 240 MHz ~320 KB SRAM 4 MB flash Another World ESP32
Mega Drive 68000 @ 7.67 MHz 64 KB 64 KB VRAM, 4 palettes x 16 colours Bridget (coming soon)
Nintendo 64 VR4300 @ 93.75 MHz 4 MB 2 MB cartridge (in my build) Another World N64
PlayStation 1 R3000A @ 33.87 MHz 2 MB 1 MB VRAM, 512 KB SPU RAM, CD-ROM Plattypus, Arduracer PSX
Web / WebXR Whatever you’ve got Lots Frame time on a phone or headset Aussie Meme Boss Rush (+VR), Sun Runner, Axis Rush

Every row taught me something different. The odd thing is that the hardest machines weren’t always the smallest ones.


1. Arduboy: 2.5 KB of RAM and a Lot of Lines

DownDraft gameplay

The Arduboy is where I learnt that AI writes code for the machine it imagines, not the one you have. With 2.5 KB of RAM and a 1-bit 128x64 screen, a single careless array is a crash, and float is a luxury you can’t afford.

What I built:

  • DownDraft: a 1-bit vertical descent platformer with energy-based jumps, a smash-through “plummet” and EEPROM high scores.
  • Stick Fighter: a 10-character fighter using skeletal (bone) animation instead of sprite frames. That’s the only way that many fighters fit.
  • Sticks of Rage: the same “Stick Engine” grown into a side-scrolling beat ‘em up with parallax and swarming enemies.
  • Ether Echoes: an 8-character, 4-shade grayscale fighter for the Arduboy FX, written in the ABC language.

Sticks of Rage

Ether Echoes title and Kael's stage art Ether Echoes stage background Ether Echoes: the title card and Kael’s 4-shade stage background.

The Pipelines

I tried three completely different pipelines on the same hardware:

  1. Browser IDE. DownDraft was built in Arduboy Cloud, with code, simulator and build output side by side. The build output is the most honest feedback loop on the platform: it tells you exactly how much flash and RAM you have left after every change.

    DownDraft in Arduboy Cloud 18,418 of 29,696 bytes of flash and 1,479 of 2,560 bytes of RAM. These numbers matter more than anything the AI says.

  2. Desktop twin. Stick Fighter and Sticks of Rage split into Engine, Game and Platform layers, so the same code builds for the Arduboy (arduino-cli) and for desktop SDL2. I also had the AI build a desktop skeletal animation editor to create and export poses.
  3. A niche language. Ether Echoes is written in ABC and packaged as main.abc plus a media folder. AI has seen very little ABC, so I was much more involved in that one.

Where the Time Went

Not in the gameplay code. It went into memory and tooling. The skeletal animation system and its editor took far longer than any fighter’s moveset. The SDL desktop build was a huge speed-up, but it also lied to me. On desktop, everything runs at a perfect 60 FPS with endless memory, so it’s easy to blow past the 32 KB flash limit without noticing.

Lesson: a desktop twin is worth it, but flash the real hardware often. The AI only respects a constraint it can see failing.


2. ESP32: Porting a Classic onto a $5 Board

Another World on ESP32

Another World on the ESP32 was my first port rather than an original game. It runs Éric Chahi’s bytecode VM at 50 FPS on a TTGO T-Display, with a custom capacitive touch gamepad PCB and Bluetooth audio.

Pipeline: PlatformIO, LovyanGFX for DMA display transfers, and a pack_assets.py --demo script that downloads the shareware demo and flashes it to SPIFFS. That keeps the repo free of copyrighted data. This was the first project where I wrote a proper AGENTS.md describing the architecture for the AI to follow.

Where the Time Went

  • RAM. The original engine wants four 32,000-byte framebuffers (128 KB). With only ~160 KB of free heap, the game’s 65 KB cinematic polygon table failed to allocate, and Lester stood on a black screen. The fix was aliasing down to two physical buffers.
  • Hardware gotchas no model would guess. GPIO 12 is an ESP32 strapping pin. Touching that pad at power-on put the board into a boot loop, so the controls had to be re-routed.

Lesson: on real hardware, half the bugs live outside the code. The AI is great at the VM, but someone has to hold the board.


3. NES: 2 KB of RAM and 27,000 Cycles a Frame

Lantern Moth title

Lantern Moth is my newest game, and the closest to the metal: hand-written 6502 assembly on a plain NROM cartridge with no extra chips. You’re a moth flying loops of light around cave creatures to cocoon them, with 15 stages, 3 bosses and 2-player co-op.

Lantern Moth co-op

The Constraints

Every frame has to fit in about 27,000 CPU cycles. The finished game has 136 bytes of general RAM free and 22 bytes of zero page. The core mechanic, detecting when a trail closes a loop and what’s inside it, is a polygon-fill problem. On a 6502 that means scanline parity with precomputed XOR tables: no flood fill, no stack, and the work spread across several frames so no single frame overruns.

The lighting is all palettes and attribute tables, so it costs no tiles. Moths and cocoons store only their left half and are mirrored. Stage maps are packed at 4 bits a cell.

The Pipeline

This one was the most complete pipeline I’ve had:

  • make runs Python asset generators, then ca65/ld65, then usage.py, which prints PRG, RAM and zero-page usage and fails the build if any of them runs short.
  • make test runs the ROM headless in jsnes through 134 checks, from clearing all 15 stages by honest loops to co-op revives and zero lag frames.
  • make stress flies two moths hard round every stage and reports the worst frame. The worst stress frame came in around 23,900 cycles.
  • Mesen with source-level debug symbols for anything the tests can’t see.
  • A full release kit: manual, box art, cartridge label and trailer.

Lantern Moth box art

Where the Time Went

The frame budget. Getting two moths, crossing trails, team loops and the light rebuild to fit inside one frame took most of the effort, by staggering work across frames, skipping work on busy frames and turning searches into bit tests.

Lesson: the README ends with a “Not verified” section. Sound was never actually listened to, the PAL path is untested, and boss pacing was tuned by scripts rather than by hand. I now ask for that section on every project. Knowing what wasn’t checked is as valuable as the test suite.


4. Mega Drive: Bridget (Coming Soon)

Bridget title screen

Bridget is a bridge-building physics puzzle for the Sega Mega Drive / Genesis, written in C with SGDK. You build from wood, steel and cable on a budget, then drive vehicles across and watch the Verlet physics decide whether you’re an engineer. It isn’t public yet, but it’s coming soon.

Bridget attract demo The attract-mode demo running in Mednafen.

The Constraints

The 68000 has no floating point, so the physics core is pure Q16.16 fixed point. It has to produce exactly the same results on NTSC 60 Hz and PAL 50 Hz consoles, or a bridge that holds on one machine collapses on another. There’s no malloc anywhere. Everything is statically allocated, and the README puts the resident footprint under 18 KB of the 64 KB work RAM.

The Pipeline

Bridget was the most planned project. It started with a GAMEPLAN.md, an AGENT.md and a TODOLIST.md of 10 milestones and 100 steps, and the AI worked through them one milestone at a time:

  • src/core is portable C with no SGDK includes, so the physics is tested natively with gcc and again under ASan + UBSan.
  • The ROM builds with a vendored m68k toolchain, and CI produces a ROM byte-identical to the local build.
  • emeshot.py drives Mednafen headlessly, presses buttons and screenshots each screen, so every milestone has visual proof. The screenshots above came from it.

Where the Time Went

Into the physics and determinism. Making a structure behave the same way on both refresh rates is a bigger problem than drawing it.

Lesson: on a fixed-point machine, design the maths first. Splitting the core from the hardware layer is what made it testable at all.


5. Nintendo 64: The One-Day Port

Another World on N64

Once the Another World VM worked on the ESP32, the obvious question was “where else can it run?” The N64 port is built with LibDragon inside Docker, packs the game data with DragonFS into the ROM, and a build_rom.sh script pads the binary and signs the CIC-6102 boot checksum. Host-side unit tests check the checksum algorithm and the ROM header.

Where the Time Went

Almost nowhere, and that’s the point. All the commits for this one landed in a single day, because the hard work (the VM, the renderer, the audio mixer) already existed and only the platform layer was new.

Lesson: a clean platform layer pays off twice. The second port is nearly free.


6. PlayStation 1: Bare-Metal Rust and the Biggest Lessons

Both PS1 games are written in #![no_std] Rust and compile to MIPS R3000A machine code with the PSoXide SDK. That means 2 MB of RAM, 1 MB of VRAM, no operating system, and the GTE, MDEC and SPU coprocessors to drive by hand. These two projects were the longest by far: Plattypus has 140 commits over 14 days, and Arduracer PSX has 361 commits over 17 days.

Plattypus

Plattypus

Plattypus is a 3D tactical stealth game about a platypus breaking out of Healesville Sanctuary to get home to his new baby sister. It started on day one as a 2D platformer and was re-imagined as 3D stealth action within hours, which is the kind of pivot AI makes cheap. It has full-motion video cutscenes on the MDEC, a Red Book CD audio soundtrack, and a web player built on my fork of the PSoXide emulator.

Where the time went: the intro video. The FMV played at 6 FPS. The investigation measured each frame at 110 ms of work for a 66.67 ms budget (661%), and it records two diagnoses that turned out to be wrong. One was “the MDEC is backed up”, which was a misread status register. Another was “the decoder can’t run while the CD is reading”, which a six-way probe disproved. Three fixes were built and measured, and none of them reached 15 FPS. What finally worked was changing the codec so each frame needed 5 CD sectors instead of 7. That got it to 148 of 148 frames at 15 FPS.

Arduracer PSX

Arduracer PSX

Arduracer PSX takes my 8-bit ArduRacer FX time-trial racer to 32-bit colour, with 24 circuits, ghosts, AI rivals, car tuning, DualShock rumble and memory card saves.

The pipeline went fully multi-agent. An AGENTS.md sets the rules: every change in its own git worktree, every branch reviewed from four perspectives (architect, systems engineer, game designer, QA) before merging, clippy -D warnings, no panics in game code, and a tools/playtest reference driver that actually drives every circuit. make disc masters a BIN/CUE with CD audio tracks.

Where the time went: finding out what wasn’t real. The audits in that repo were the most humbling part of the year:

  • At one point no lap could ever be completed, cars moved ~30x too slowly, and the memory card never saved. Every write panicked.
  • Fixed::mul silently wrapped on overflow, so any distance over ~800 units came back as 0.00.
  • cargo test on the core crate ran zero tests and exited 0. The CI was green while testing nothing.
  • Whole features (force feedback, curb rumble, semi-transparency) were described as done but had no callers.

Plattypus had a similar finding: a test harness that mirrored the game logic instead of testing it. That’s why “tests must exercise the real core crate” is now rule #3 in my AGENTS.md.

Lesson: green isn’t proof. AI is very good at producing things that look finished, like passing CI, sign-off tables and feature lists. On the PS1, the only thing that counted was a measurement or a reference driver actually finishing a lap.


7. Web and VR: Where It All Started

Aussie Meme Boss Rush

The browser was the easy end of the ladder, and the right place to start:

  • Aussie Meme Boss Rush: a neon Three.js boss rush against cybernetic Australian wildlife memes.
  • Sun Runner: a low-poly rail shooter diving into the sun.
  • Axis Rush: an F-Zero-style racer on a cylindrical track you can flip inside and out of, textured at runtime by my TexGen shader tool. Write-up.

Sun Runner

Axis Rush

Pipeline: Gemini CLI, a GEMINI.md of project rules, and a single index.html with procedural shaders and Web Audio instead of asset files. One file meant the AI always had the whole game in context, and “deploy” meant pushing to GitHub Pages.

Going VR

Four days after Aussie Meme Boss Rush shipped, it got a WebXR version. It turned out to be far more than a toggle. The desktop game used 2D screen-space hit detection, projecting everything onto the screen and checking where you clicked. In VR there is no screen to click, so targeting had to be rebuilt around controller raycasts from a blaster in each hand. It also gained haptic feedback on each shot and menus rendered onto in-world canvas textures.

Where the time went: debugging. You can’t open devtools inside a headset, so the VR build has its own on-screen debug console and simulated controllers for testing on desktop. Most of the VR commits are some version of “VR fixes”.

Lesson: a new input model means a new game. The rendering ported in an afternoon; the interaction didn’t.


What I Learnt Across Every System

1. Put the budget in the build. The best projects made the constraint impossible to ignore: Arduboy Cloud’s flash and RAM bars, Lantern Moth’s usage.py failing the build, Arduracer’s RAM budget check in CI. An AI respects a number that breaks the build. It doesn’t respect a sentence in a prompt.

2. Measure, don’t guess. AI explanations of performance problems sound authoritative and are often wrong. Plattypus’s FMV investigation went through two confident wrong diagnoses before measurements found the real bottleneck.

3. Green isn’t proof. Tests that run nothing, harnesses that mirror the code, features with no callers. Every bigger project needed a fresh-eyes audit that verified claims by running things.

4. Give the AI eyes. jsnes running Lantern Moth headless, emeshot.py screenshotting Bridget in Mednafen, Arduracer’s playtest driver. The projects improved fastest once the AI could see and play what it had built.

5. Separate the core from the hardware. The same pattern appeared everywhere: SDL plus Arduboy, a pure Rust core crate, Bridget’s src/core, and the Another World VM moving from ESP32 to N64. It makes code testable on a PC, and makes the next port cheap.

6. Write the rules down, and grow them. My agent files went from a few lines of style notes in GEMINI.md to AGENTS.md protocols with worktrees, multi-perspective reviews and banned patterns. Most of the improvement in the later projects came from those files, not from better prompts.

7. Ask for what wasn’t verified. Lantern Moth’s “Not verified” section is now my favourite part of any README.

8. The fun is still my job. Boss pacing tuned by scripts, combos that feel different on a keyboard than on real buttons, a VR game that needed new hands. The AI writes most of the code, but deciding whether it feels right means playing it on the real thing.


Turning the Lessons into an Agents File

Lessons are only useful if the AI actually follows them, and the place for that is the agents file it reads before every session. Here’s the AGENTS.md template I’ve distilled from all of the above. Every rule in it exists because one of these projects needed it.

# AGENTS.md

Read this before changing anything. GAMEPLAN.md says what we're building, TODO.md says
where we are, and this file says how to work.

## 1. The target machine
| Resource | Limit | Current use | Where it's checked |
|---|---|---|---|
| CPU / frame | e.g. ~27,000 cycles (NES NTSC) | | `make stress` |
| RAM | e.g. 2.5 KB / 2 KB / 64 KB / 2 MB | | `make size` |
| ROM / flash | | | `make size` |

- Write code for this machine, not for a PC. If a technique assumes floats, a heap,
  an OS or plenty of memory, it's wrong here until proven otherwise.
- Use fixed point and say the format (e.g. Q16.16). Multiplies must saturate:
  no silent wraparound.
- No dynamic allocation on constrained targets.
- If a change moves any number in this table, update the table in the same commit.

## 2. Budgets live in the build, not in prose
- The build prints resource usage after every compile and FAILS if a budget is exceeded.
- A stress test reports the worst frame. A new worst frame is a regression.
- Never "fix" a budget failure by raising the limit without a written reason.

## 3. Core and platform are separate
- core/ is portable game logic and maths that builds and tests on the host.
- platform/ is display, input, audio and SDK glue. Keep it thin.
- Desktop builds are for fast iteration only. Before calling a feature done,
  check it on real hardware or an accurate emulator.

## 4. Testing
- Tests exercise the REAL core code, never a copy or mirror of it.
- A test command that runs zero tests is a failure. Check the count, not the exit code.
- Give yourself eyes: headless emulator runs, scripted screenshots, a bot that plays.
- Same inputs give the same result on NTSC and PAL. CI builds match local builds.

## 5. Evidence over assertion
- Measure, don't guess. A performance claim needs a measurement and the command
  that produced it. Otherwise label it a hypothesis.
- When a diagnosis turns out wrong, record that. Don't quietly rewrite history.
- Never write a report, sign-off, playtest or hardware result that did not happen.
- A feature is done only when it has a caller, is reachable, and was seen working.

## 6. Always include a "Not verified" section
Every README, PR and milestone summary ends with what was NOT tested and why:
no hardware, no audio output, emulator lacks PAL, tuned by script not by hand.

## 7. Workflow
- One branch or git worktree per change. Never commit directly to main.
- Review from four perspectives before merging: architect, systems/hardware,
  game designer, QA.
- Format, lint (warnings as errors) and test before every commit.
- No panics on shipping code paths. A save that fails must say so.

## 8. Hardware gotchas
- One line per bite, e.g. "ESP32 GPIO 12 is a strapping pin: high at boot means a boot loop."

## 9. The human owns
- Game feel, difficulty and pacing sign-off.
- Real hardware testing, and any claim about it.
- Scope changes and cuts. Propose them; don't make them silently.

Where the rules came from:

  • Budgets in the build: Arduboy Cloud’s flash and RAM bars, and Lantern Moth’s usage.py failing the build.
  • Core and platform separate: the SDL desktop twin of Stick Fighter, and the Another World VM moving from ESP32 to N64.
  • Tests exercise the real code: Plattypus’s harness that mirrored the game, and Arduracer’s cargo test running zero tests.
  • Saturating maths: Arduracer’s Fixed::mul wraparound, which turned long distances into 0.00.
  • Measure, don’t guess: Plattypus’s two wrong FMV diagnoses.
  • Never write a result that didn’t happen: sign-off tables and feature lists that looked finished but weren’t.
  • “Not verified”: borrowed straight from Lantern Moth’s README.
  • Four-perspective review and worktrees: the Arduracer PSX multi-agent setup.
  • Hardware gotchas: the ESP32 strapping pin that no model would have guessed.
  • The human owns the fun: every game on this list.

A year ago I wouldn’t have believed I’d ship a PlayStation game in bare-metal Rust or an NES game in 6502 assembly. Bridget is next out of the oven. After that, who knows. Maybe the Saturn?

Updated: