One person and Claude

How it was made

The engine, the games and this site, made by talking with Claude Code and measured on the real board.

In numbers

  • 16.489lines of C in the engine
  • 33example games
  • 117tests on every change
  • 239commits in the repository

One person and Claude. The engine, examples, tools and this site were written in conversation with Claude Code: every idea was tried in the simulator, measured on the board and corrected based on what the board showed.

The art, generated and made to fit. The looks that call for it (Neo Geo, CPS2, 32-bit) use AI-generated images. The engine's tools cut them out, scale them to the right height, split them into frames and reduce them to the console's palette. The low-res looks are drawn with code.

Measured before calling it done. Every example has a script that plays it automatically. On the computer, it checks each frame's checksum against the stored one; on the board, it logs the fps every second. Optimisations don't change a single pixel.

Truly free. The code is MIT; the examples' art, sound and videos are CC0. Take it, change it, build your own console.

One enemy, from request to game

The goblin from mazmorra, step by step, with the actual images.

  1. 1 · The request

    An original goblin enemy for a fantasy beat-em-up: small, wiry and hunched, green skin, huge pointed ears, a ragged brown loincloth, a rusty curved dagger. Full body, facing left. Style: a mid-1990s arcade sprite, crisp hard pixel edges, a bold dark outline, about 15 colours. Transparent background.

    Shortened from the original English request.

  2. 2 · The reference

    The goblin alone, full body: green, with pointed ears and a curved dagger

    The character alone. It's attached to every later request, so it doesn't change from one image to the next.

  3. 3 · The sprite sheet

    Ten poses of the goblin in two rows, on a blurred dark background

    Ten poses: walking, stabbing, taking a hit and falling. A transparent background was requested; it came out painted.

  4. 4 · Cut out

    The ten frames cut out, at the same scale and with their feet on the same line

    The engine's tools remove the background, split the frames, bring them to the same scale with their feet on the same line, and reduce them to 15 colours. Then each one is checked by eye.

  5. 5 · In the game

    Alba the swordswoman facing two goblins in a dungeon corridor

    In mazmorra they come in groups, at 60 fps on the console.

Six days in the repository

  1. The engine grows out of two adventure games and is measured on the board: the image goes straight to the panel, in strips.

  2. A virtual picture processing unit, like the Game Boy Advance's, and Lua games of any genre.

  3. Measured on the console and optimised. The web editor, for making games without coding.

  4. The looks of twelve consoles, first person, instructions for agents to make games, the site and the Library.

  5. The site and editor in three languages; Codex's games and IL·LUSTRES join the Library.

  6. The sound studio, a sequencer with a synth and effects. The site in tabs, with the Play tab and a page and a ranking for each game.

The whole process is in the repository: how the art is made (opens in a new tab), the Lua API (opens in a new tab), the instructions for agents (opens in a new tab).

ESP32-S3R8 · 240 MHz · octal PSRAM at 80 MHz · QSPI screen at 80 MHz

Measured on the console, not promised

Every figure in this section comes from the board: firmware that plays each game from a script and logs a performance line every second, or a benchmark that runs each scene five times. No emulator. At 60 fps, each frame has a 16.6 ms budget.

  • 71%of one core in the heaviest scene: affine, four layers and 128 sprites
  • 8,1msto send a whole frame to the panel; the wire alone takes 7.7
  • 3%of one core for sound during play
  • 117self-playing tests that check every frame, run on every change

Twelve looks, in frames per second

The slowest and fastest second of each example, played on the console. In grey: the first measurement, before optimisation.

Lookfps
Neo Geoduelo 60–61 fps
CPS2torneo 60–61 fps
Game Boybolsillo 58–61 fps
ZX Spectrumcinta 41–62 fps
PICO-8ladrillos 47–60 fps, was 46–55
LCD handheldhuevos 60–61 fps
PlayStation · Saturn 2Dgotico 60–61 fps, was 30–31
Super Nintendo · Mega Driveveloz 40–59 fps
Vectorrocas 61 fps
Game Boy Advanceplataformas 61 fps
Vertical arcadetormenta 30 fps, was 20
First personlaberinto 38–60 fps, was 20–31

Second by second

Each point is one second of play on the board, from the performance line logged every second while a script plays the game. Dashed: the first measurement; solid: today's. The first second loads the scene and doesn't count in the figures.

  • gotico 32-bit · the hall

    3060

    32,017,2 ms per frame

  • laberinto first person · walking

    3060

    40,920,3 ms per frame

  • ladrillos PICO-8 · playing

    3060

    18,817,6 ms per frame

  • tormenta vertical · waves

    3060

    48,732,3 ms per frame

These needed no optimising:

  • torneo CPS2 60–61
  • mazmorra CPS2 61
  • rocas vector 61
  • bolsillo Game Boy 58–61
  • huevos LCD 60–61
  • cinta Spectrum 41–62

How a frame reaches the screen

There is never a whole frame in memory: the screen is drawn in 15 strips of 32 columns. The two cores split each strip, left half and right half, in internal RAM, while DMA sends the previous strip to the panel.

In slow motion: a real frame takes 16.6 ms; here, 2.4 seconds. Sending a whole frame takes 8.1 ms, 7.7 of which is the wire itself.

Finding what's expensive

The same scene, replayed with one thing removed each time. In gotico, blending the two moonbeams on the fly ate 13 ms a frame: now they come pre-painted into the background, and the game went from 31 to 60 fps.

gotico, before the fix (ms per frame)

Measurement
16.6 ms = 60 fps
everything32.0 ms
no columns30.5 ms
no moonbeams19.0 ms
no moonbeams or glows16.3 ms
nothing on top16.2 ms

laberinto: the raycast before and after optimising it (ms per frame)

Measurement
16.6 ms
everything · before39.0 ms
everything · now24.4 ms
raycast only · before34.6 ms
raycast only · now22.1 ms
image only · before19.0 ms
image only · now16.3 ms

What drawing costs, on one core

A 240×160 logical frame of synthetic content, best of five runs, as a share of one core's time at 60 fps. The console has two cores.

Measurement
one whole core
One tile layer1.96 ms · 11.8 %
128 sprites, 16×162.24 ms · 13.5 %
One full-screen affine layer3.34 ms · 20.0 %
Alpha blend of two whole frames4.03 ms · 24.2 %
Mode 7: one affine layer, 64 sprites6.28 ms · 37.7 %
4 tile layers6.50 ms · 39.0 %
GBA mode 0: 4 layers, 128 sprites8.64 ms · 51.9 %
Everything: affine layer, 4 layers, 128 sprites11.88 ms · 71.3 %

And why nearly everything is drawn in internal RAM: the speed of copying 1 MB.

internal RAM → internal RAM381.0 MB/s
PSRAM → internal RAM84.7 MB/s
internal RAM → PSRAM48.5 MB/s
PSRAM → PSRAM31.9 MB/s

Source: docs/measurements.md (opens in a new tab), with the method and every measurement since day one. Measure it on your own board: idf.py -DBP_BENCH=ON build flash monitor