cd /news/artificial-intelligence/claude-fable-vs-the-commodore-64-two… · home topics artificial-intelligence article
[ARTICLE · art-67251] src=pub.towardsai.net ↗ pub= topic=artificial-intelligence verified=true sentiment=· neutral

Claude Fable vs. the Commodore 64: Two Rungs Up the Benchmark Ladder

Anthropic's Claude Code running the Fable model built two new Commodore 64 games — Boulder Rush and ION RIFT — as part of an informal AI coding benchmark, adding autonomous gameplay and pixel-smooth scrolling to the agent's repertoire. The agent implemented a cellular physics engine, raster interrupts, double buffering, and SID music, though it initially failed to render the title screen correctly due to a charset issue. The human referee notes that the benchmark tests not just the agent against the hardware but the agent inside a loop where the human still knows the difference between 'it runs' and 'it's right.'

read16 min views1 publishedJul 21, 2026

A Boulder Dash tribute that plays itself through the emulator’s monitor port, then a pixel-smooth scrolling shoot-em-up with raster interrupts, double buffering, and a SID music engine — both built end-to-end by Claude Code running Anthropic’s Fable model.

By the human in the loop

A VIC-20 arrived in my house when I was eight; a Commodore 64 replaced it soon after, and I spent the years around ten playing exactly the kind of games this article is about. Forty years later, I find myself refereeing an AI coding agent on the same 64 kilobytes.

The proportions deserve stating up front. Every technique in these pages — the raster split, character scrolling smoothed by the fine-scroll register, the static Colour RAM of the great commercial scrollers, the double buffer flipped through $D018 — was invented by people who counted cycles on paper and waited through a cassette load to find out whether their interrupt held. The agent, by contrast, recompiled and reloaded dozens of times in an afternoon, with instant screenshots and a training memory full of their documented tricks. Forty years of solutions, paid for in sweat by others; the agent’s job was to choose them well and not miss a latch by a single frame. (It missed one. We’ll get there.)

This workspace has become an informal benchmark for AI coding agents. The rules never change: a real Commodore 64 toolchain (cc65, VICE with its remote monitor, Python tooling, a local vision model as the agent’s eyes), a mandatory build-reload-look loop, and a human who plays the results and pushes back.

Earlier sessions produced Meteor Storm and Dreadline, each documented in its own article. Dreadline set the previous technical ceiling: mixed C and assembly, a custom character set generated from bitmap art, multicolor sprites from an ASCII asset pipeline, VIC bank relocation.

This article documents the next two rungs, built with Claude Code running the Fable model:

There is a symmetry running through both sessions that only became clear at the end. The limited machine forced its original programmers into creativity; an ear and an eye trained on their games forced the agent into precision. The benchmark, it turns out, is not just the agent against the hardware — it is the agent inside a loop where the human still knows the difference between it runs and it’s right.

As always, the interesting part is not the final PRG files. It is watching the agent debug the machine, the toolchain, and itself.

Rung Project Techniques 1 snake, pong, invaders… conio character output 2 Stellar Assault 8 hardware sprites, SID sfx, attract mode 3 Frogger multicolor bitmap mode, custom linker config 4 Meteor Storm / Dreadline C + assembly, generated charset and sprites, VIC bank 1 5 Boulder Rush cave physics engine, custom charset, autonomous agent player 6 ION RIFT smooth scroll, raster IRQ split, double buffer, SID music

Rungs 1 through 4 tested whether an agent can write increasingly demanding code for this machine. Rungs 5 and 6 add two orthogonal axes: implementing somebody else’s rules faithfully, and closing the loop — the agent playing what it built.

A single evening’s target: a faithful Boulder Dash-style cave game. Character mode with a custom charset (dirt, boulders, gems, brick, steel, the miner, the exit), 40x22 caves generated from fixed seeds, a HUD, a timer, lives, and SID sound effects. The generator counts the gems it actually places, and the exit opens at 60% of them — ⌊count × 3/5⌋, minimum five.

Boulder Rush: cellular cave physics on a 1 MHz CPU — boulders fall, roll off rounded edges, and only kill while falling.

The physics engine is the heart of any Boulder Dash clone, and it fits a 1 MHz machine beautifully because it is pure cellular logic:

The first VLM look at the title screen reported garbage: hearts, clubs and box-drawing symbols where text should be. The screen RAM dump showed the text was written correctly — the charset itself was wrong.

The cause is a classic cc65 detail: conio emits screen codes $41–$5A for uppercase text. Those codes are letters only in the C64’s second character set (lower/upper, at $D800 in char ROM) — not in the uppercase/graphics set at $D000 that had been copied. One changed source address fixed the entire alphabet.

The title screen as the VLM first saw it: the text is written correctly in screen RAM — it’s the charset underneath that’s wrong.

The toolchain already had a dual-loop agent architecture (a fast heuristic “System 1” and an LLM “System 2”) built around a convention: an agent input byte at $033C, with the same bit layout as the CIA joystick register.

Boulder Rush adopted the convention, and a new tool was written: demo_bot.py. It is a complete autonomous player that:

The first bot runs oscillated forever below a gem: down three cells, up one, down three, up one. Instrumented step-by-step logging made the cause obvious: a “tap” sent through the monitor has no reliable duration. Monitor connections the emulator, so a 0.18-second hold could last anywhere from one to four game moves. The bot overshot its target every time, in both directions.

The fix was not to calibrate timing — it was to change the contract. The game now consumes and clears $033C when it polls input, and polls only when the move cooldown has expired. The input became edge-triggered: one monitor write is exactly one move, regardless of host timing.

The next run — a single 240-second session on cave 1, fixed seed 1234 — collected a gem roughly every two seconds: 109 planned moves across two lives (11 + 11 gems), died once under a boulder (authentically), and kept playing on its next life while the session monitored gem-count milestones from the outside. “Without a single stall” is a measured claim: the bot’s anti-stall blacklist, which triggers after 20 planning cycles with no new gems, never fired once in the log. One run, one seed — a demonstration, not a statistic. Averaging across seeds is future work.

demo_bot.py at work: screen RAM in, weighted BFS in the middle, one edge-triggered byte at $033C out. Each step you see is exactly one monitor write.

Dreadline scrolls its deck one character at a time — visibly chunky, 8 pixels per step. The explicit goal for ION RIFT was everything Dreadline does not do:

Technique Dreadline ION RIFT Scrolling 1 char per step pixel-smooth, $D016 fine scroll at 50 fps Screen single double-buffered ($4400/$4800, flip via $D018) Raster IRQ none 2 interrupts per frame: steady HUD / scrolling playfield Terrain pre-authored bitmap tiles procedural, generated column by column Audio sfx 2-voice SID music engine + sfx on voice 3

The screen is split by raster interrupt: three fixed HUD rows (score, high score, shields, speed), a separator line, then 22 rows of playfield scrolling with the VIC-II fine-scroll register.

The scroll cycle is the classic C64 dance:

Terrain is procedural: a random walk for ceiling and floor thickness with a guaranteed flyable corridor, occasional towers, and sparse hires stars in the gap. Multicolor characters give the plating a metallic look — and the visual variety comes from texture, not hue, for a reason explained below. The game never stores a map — the world is generated column by column, forever.

The memory layout puts VIC bank 1 ($4000–$7FFF) at the centre: two screens, the custom charset, and sprite frames, with program code kept below $4000 and constant data above at $8000 — a linker config with an explicitly filled gap so the PRG loads across the video area it will later overwrite.

The raster split at work: everything below the separator moves at one pixel per frame; everything above it never moves at all. The terrain is generated column by column and never stored as a map.

The first boot looked glorious — scrolling plates, towers, stars, sprites, music — but every HUD letter was invisible while every digit rendered fine. Same family as the Boulder Rush bug, different layer: cc65 translates uppercase letters in C string literals to PETSCII $C1–$DA. Digits pass through unchanged. The game’s own text renderer now folds $C1–$DA down to screen codes $41–$5A.

The signature is worth remembering: digits fine, letters blank or wrong = PETSCII/screen-code mismatch.

First boot: everything works except the alphabet. Digits pass through cc65’s PETSCII translation unchanged; uppercase letters don’t.

The first music pattern was serviceable but polite. The human feedback was precise: “more tense, like R-Type.” The rework: an octave-hammering minor-key bass on the pulse voice, a faster step rate, and the lead arpeggio switched to sawtooth, cycling Am–F–G before leaning on an E major turn — the dominant that never quite resolves. Tension, in three tables of bytes.

A static screenshot cannot prove smooth scrolling. The toolchain’s vlm_look.py has a multi-frame motion mode, which turned out to hardcode a retired model name — so the session patched the tool itself to honor the -m flag, then captured four frames 0.4 seconds apart. The eyes in this session were gemma4:12b-it-qat running on local Ollama (~7.2 GB quantized); a reproducibility note for anyone rerunning the benchmark: the script's default still points at the retired model, so -m must be passed explicitly. The analysis confirmed terrain moving steadily leftward, HUD rows static, sprites coherent, no tearing reported.

And yet the subtlest bug of the whole session got through. It was found by the human, not the tooling: “it scrolls, but it stutters — maybe a double buffer problem?” Static screenshots looked perfect; the multi-frame motion analysis had passed. The cause was one frame of timing skew: the main loop runs early in the frame, before the raster split, so a new fine-scroll value took effect immediately — while the $D018 buffer flip it belonged with was latched by the interrupt one frame later. Once per coarse step, one frame showed the old buffer at the new scroll position: a 7-pixel back-jump at 6 Hz. Invisible in any single frame, and below the temporal resolution of four frames sampled 0.4 seconds apart — but unmissable to a human eye watching motion.

The fix latches the fine scroll and the buffer flip together in the top interrupt. Text screens went fully static as a consequence (text inside a fine-scrolled zone wobbles by construction, so scrolling now ignites when the game starts).

The rework also made the engine honest about its budget. The first build scrolled Colour RAM every coarse step, with true per-tile accent colours — at roughly 15,000 cycles per flip, more than a PAL frame allows. Colour RAM has no second buffer on a C64, so the final build makes it fully static: every cell holds the same colour, and terrain variety survives through the other three colour sources a multicolor character has — background and the two global registers $D022/$D023, which cost nothing. Ceiling, floor and towers are told apart by pattern density on those four shared colours, not by tint: variety by texture, not by hue — the same compromise the commercial scrollers of the era made. The row copy was spread across four frames.

And a missed-frame counter at $033D turned “feels smooth” into a number with a definition. A missed frame is a main-loop deadline overrun: the raster interrupt ticks a flag once per PAL frame, and if the loop finds more than one tick accumulated when it comes up for air, it was still working when a new frame began — the excess goes into the counter. It does not count interrupt misses (the IRQ always runs), partial row copies (those are just work inside the loop), or monitor-connection s (emulation is frozen, frames do not elapse). The number: 38 overruns in ~39,000 frames — 0.1% over roughly thirteen minutes of mixed title, demo and agent play.

Like Boulder Rush, ION RIFT plays itself — but a 50 fps shmup broke the Boulder Rush recipe. Reading a kilobyte of screen RAM per decision is too slow, and too hard on the emulator’s fragile monitor port. The replacement contract is a small shared-memory ABI in the tape buffer:

Address Meaning $033C edge-triggered input (fire) $033D missed-frame counter $033E hold input (steering) + watchdog heartbeat $0340+ six telemetry bytes, refreshed every frame

The telemetry: ship Y, the first flyable pixel line under the ceiling at the ship’s column, the top of the floor/tower at that column, the nearest enemy ahead (Y, and horizontal distance in half-pixels, $FF for none), and the game state (title / playing / game over).

The steering byte is guarded by a watchdog with a design worth pausing on. The game merges only the low five bits into input; the heartbeat is bit 7, which the input path never sees. But the game doesn’t inspect that bit — the watchdog triggers on any change of the raw byte value, resetting a 25-frame TTL. An unchanged byte counts the TTL down; at zero the game clears it. So the agent can re-send the same direction with bit 7 alternating: the hold refreshes without the input twitching. A dead agent can never pin the ship for more than half a second, and since a real joystick never writes $033E, there is no ambiguity between the two.

ion_bot.py flies the corridor, dodges, fires, and restarts its own game overs at roughly 3 Hz. Architecturally it is a standalone System 1-style tool — pure reflex logic, no LLM in the loop — that speaks the same conventions as the toolchain's dual-loop agents, so it can be plugged in later. It plays honestly but mortally: with a 0.3-second reaction time, a dart crossing 60 pixels between decisions is often unavoidable. The in-game demo autopilot, reacting every frame, flies far better. The gap between the two is a clean measurement of what reaction latency costs in an action game. The obvious next step is feeding the $0340 telemetry to the toolchain's existing neural fast loop — which, like everything else here, runs host-side and writes $033C; a controller living inside the C64 would be a different project entirely, and is not this one.

Every entry but the last was found by the toolchain loop — build, reload, screenshot, look — plus the emulator’s monitor as a memory microscope:

Symptom Instrument Cause Hearts instead of letters VLM + screen RAM dump wrong char ROM half copied Cave bottom missing PIL pixel analysis screenshot taken mid-draw (transient) “VICE went crazy, CPU pegged” jiffy-clock speed measurement emulator launched without audio, speed regulation lost Bot oscillating forever instrumented step logging monitor taps have unreliable duration HUD letters invisible, digits fine own eyes + VLM PETSCII literals vs screen codes One-frame scroll stutter human eye only fine scroll and buffer flip latched one frame apart

That last row is the honest one. The multi-frame motion check verified gross scrolling — direction, HUD stability, tearing — and passed, correctly, because at its sampling rate the defect does not exist. A 7-pixel glitch lasting one PAL frame needs either frame-exact capture or a human watching motion. The loop finds a lot; it does not find everything, and knowing where its resolution ends is part of the benchmark.

The quieter lesson: several fixes landed in the toolchain, not the games — the reload tool’s wrong default entry address, the motion mode’s hardcoded model, the emulator launch environment on a snap-packaged desktop. A benchmark that lets the agent repair its own instruments measures something a static benchmark cannot.

One open item, stated plainly: the very first ION RIFT build was once seen dropping to the BASIC READY. prompt mid-demo — the signature of a stray BRK. A breakpoint trap was planted on the KERNAL BRK path and the demo left running under watch; the crash has never reproduced in over forty minutes of continuous autoplay, and no evidence of a stack overflow or an IRQ race has surfaced. The honest ledger reads: one observation, two credible explanations that are not the game at all. VICE 3.7.1's remote monitor layer was separately caught dying mid-session (vice_network_send: internal error in loop) — an emulator-side failure that can look exactly like a guest crash — and, in the same session, the reload tool was jumping to the wrong entry address ($0810 instead of $080D), producing BASIC-idle states that also read as "crashed to READY." The trap stays set. Retro development keeps you humble.

Area Choice Mode character mode, custom charset at $3800 Physics bottom-up cave scan, parity move-flags, rounded-object rolling Caves seeded procedural, 40x22, 60% gem quota Agent I/O $033C edge-triggered input byte Autonomous player demo_bot.py: screen-RAM read → weighted BFS → one write per move Binary ~8 KB

Area Choice Video VIC bank 1: screens $4400/$4800, charset $5000, sprites $5800 Scrolling $D016 fine scroll, 50 fps, coarse shift in ca65 assembly spread over 4 frames Buffering double-buffered screen RAM, $D018 flip latched with fine scroll in the raster IRQ IRQ 2 raster interrupts/frame (HUD split + playfield), KERNAL chained once Terrain procedural ring-buffer model, multicolor tiles + hires stars, static colour RAM (texture, not hue) Assets single assetgen.py pipeline: MCM/hires tiles + MCM sprite frames Audio 2-voice pattern engine (pulse bass, saw lead) + sfx voice Agent I/O $033E watchdog-guarded hold input + six telemetry bytes at $0340, refreshed per frame; demo autopilot built in Diagnostics missed-frame counter at $033D: 38 overruns / ~39,000 frames (0.1%) over ~13 minutes Binary 31.9 KB PRG on disk; ~9.8 KB real code+data (the rest is padding plus the filled VIC bank gap)

Two games in one arc, testing two different things. Boulder Rush proves the agent can implement somebody else’s rules faithfully — cellular physics whose edge cases generations of players know by heart — and then step outside the machine and play its own game through the emulator’s monitor port. ION RIFT proves it can climb the hardware ladder deliberately: interrupts, fine scroll, buffer flips and colour RAM compromises, chosen and combined the way C64 programmers actually did it.

And one thing neither game proves, worth saying out loud: the loop’s instruments have a resolution limit, and the one-frame stutter sat below it. For now, that gap is covered by an eye trained on these games forty years ago — the symmetry the preface promised, closing where it opened. Frame-exact capture and differential frame analysis are the obvious next rung; until then, the human in the loop is not a formality. He is an instrument.

The benchmark continues waiting for new models and trying new games.

A note on AI assistance: This article was developed with the help of AI tools (Claude by Anthropic). AI assisted with structuring arguments, drafting prose. All results have been personally validated.

Claude Fable vs. the Commodore 64: Two Rungs Up the Benchmark Ladder was originally published in Towards AI on Medium, where people are continuing the conversation by highlighting and responding to this story.

── more in #artificial-intelligence 4 stories · sorted by recency
── more on @anthropic 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/claude-fable-vs-the-…] indexed:0 read:16min 2026-07-21 ·