---
slug: /gambeta-gba/
url: https://zajdband.com/gambeta-gba/
markdownUrl: https://zajdband.com/gambeta-gba/index.md
title: Gambeta on a Game Boy Advance
description: >-
  I wanted to play Gambeta, my web football game, on a handheld. Claude ported it to the Game Boy
  Advance: the whole game, compiled from the web's own code, with eleven agents working at once.
  What it took, the bugs I found by playing it, and the ROM to download.
published: '2026-11-06T15:00:00.000Z'
updated: '2026-11-06T15:00:00.000Z'
language: en
category: Games
tags:
  - gambeta
  - gba
  - games
  - agents
takeaways:
  - >-
    Gambeta looks like a Game Boy Color game, but the port is for the Game Boy Advance: the Color's
    hardware would have constrained it much more. On the Advance, one step of the match engine takes
    8% of a frame.
  - >-
    The GBA game is not a rewrite of the web game. Its dialogue, signs and story pages are the web's
    own TypeScript, compiled to bytecode for a small VM: 1307 scripts, and all 1268 that a new game
    can reach say exactly what the web says.
  - >-
    The match engine gives the same result bit for bit on the GBA as on the computer: one hash over
    a whole match, checked on every change.
  - >-
    Eleven agents ported scenes at the same time because adding a scene was one thing: a C module, a
    generator and a check, each found by name. What they duplicated anyway was merged later, and a
    check now refuses a second copy.
  - >-
    The two bugs I found by playing were a walk that limped once per tile and music that slowed down
    when the game was busy. Both were arithmetic.
wordCount: 1901
readingMinutes: 10
license: https://creativecommons.org/licenses/by/4.0/
draft: false
---

I wanted to play [Gambeta](https://gambeta.futbol) on a handheld, one of those cheap ones that run emulators, like the R36S.

Gambeta is my football game: you arrive at the boarding house of Club Atlético Zapallitos and in one season you have to get the club promoted. It is a web game that looks like a Game Boy Color, and I had already [written about its music](/gambeta-music/). It is about 50,000 lines of TypeScript on Phaser.

This is the ROM itself, in a Game Boy Advance emulator that runs in this page. Press play, skip the intro with START if you like, and write your name:

<div data-toy="play"><p><em>Interactive: the Gambeta ROM in a Game Boy Advance emulator, with keyboard and on-screen controls. Nothing loads until you press play. Needs JavaScript.</em></p></div>

As in the other posts in this series, I did not write the code. Claude did, in Claude Code. I asked for things, played the result and said what was wrong.

## Why the Game Boy Advance

Gambeta looks like a Game Boy Color game, so the Color was the obvious target. I ruled it out, because its hardware would have constrained the port much more: an 8-bit CPU at 8 MHz, 32 KB of RAM and four colours per palette. Gambeta's match moves a dozen players with real physics every frame, and that alone needs more than the Color has.

The Game Boy Advance has a 32-bit ARM CPU at 16.78 MHz, 288 KB of RAM, sixteen colours per palette and a C compiler that knows it. The match engine runs on it as is: one step takes 8.3% of a frame, and 15.9% at its worst.

## The match is the same match

The engine that plays the match is C that the web game shares. On the GBA it has to play exactly like on the computer, not roughly: a test plays a full match with every rival, hashes the state of every step and compares the hash with the computer's. It is `c4954b81` on both. Every change to the port runs it.

So what changes from the web is how the match looks. Each rival has its pitch, drawn by the web's own drawing code: dirt, mud, the coast, concrete with railway tracks, a synthetic pitch. Luz y Fuerza plays at night, with a third of the pitch dark that moves and flickers, and blackouts:

<video controls playsinline preload="none" poster="/gambeta-gba/luz.png" src="/gambeta-gba/luz.mp4" style="width: 100%; height: auto; image-rendering: pixelated; border-radius: 12px;"></video>

And at Ferro the train crosses the pitch, and the camera shakes:

<video controls playsinline preload="none" poster="/gambeta-gba/ferro.png" src="/gambeta-gba/ferro.mp4" style="width: 100%; height: auto; image-rendering: pixelated; border-radius: 12px;"></video>

Both are friendlies played by the game's own AI, so nobody holds the controls in these videos.

## The story is compiled, not rewritten

Gambeta's story lives in more than a thousand small functions: what each person says, depending on the week, on what you did, on whether you won. Rewriting them in C would have been a second game to keep in sync with the first one.

So they are compiled. A partial evaluator reads each function's TypeScript with the real game loaded around it. Whatever does not depend on your saved game, it computes on the computer. Whatever does (your name, the flags, the results of each round) becomes bytecode for a small virtual machine written in C, which runs on the GBA. The pages of the short stories between chapters are compiled the same way, and so are the conditions that decide who is in the street each week.

Here are three people from the club. The box shows what their compiled bytecode says when it runs in that virtual machine, for each saved game you choose with the buttons. The bytecode is the same in every case; only the saved game changes:

<div data-toy="compiler"><p><em>Interactive: three scripts from the game, the TypeScript they come from, the size of their bytecode, and what that bytecode says for different saved games. Needs JavaScript.</em></p></div>

Today that is 1307 scripts. To know they are right, the build runs each one in a copy of the virtual machine on the computer and compares what it says with what the web game says on a new game: 1268 of 1268 match. The other 39 are the short stories' pages and endings, which a test plays on the GBA instead.

After a match, the neighbourhood paper comes out with the result, a headline that depends on the rival and the moment, your mark and something someone from the club said. Then the experience and the level:

<video controls playsinline preload="none" poster="/gambeta-gba/historia.png" src="/gambeta-gba/historia.mp4" style="width: 100%; height: auto; image-rendering: pixelated; border-radius: 12px;"></video>

## Eleven agents at once

The web game has eleven minigames, and each one is between 200 and 1,000 lines. Porting them one by one would have taken days. So Claude first made adding a scene one thing, with nothing to wire by hand:

- a C module `gba/<scene>.c` with a `start` and a `frame` function, found by name;
- a generator that turns the web's own art and data into C, found because it is in a folder;
- a check that plays the scene with a tape of button presses in a headless emulator and reads the result from memory.

Then eleven agents each took a part, in their own copy of the repository: the drills, the story minigames, the music, the missing dialogue, the pitches of the match, the title and the intro. Claude merged their work as they finished.

They did duplicate things, because they could not see each other's code. Four agents wrote the coach's explanations inside a drill, four wrote a tool to turn the web's drawings into GBA tiles, five wrote a pixel canvas. Merging those into one of each removed 847 lines, and a check now fails the build if a second copy appears.

<video controls playsinline preload="none" poster="/gambeta-gba/futtenis.png" src="/gambeta-gba/futtenis.mp4" style="width: 100%; height: auto; image-rendering: pixelated; border-radius: 12px;"></video>

That is the football tennis drill, with the hero played by the drill's own AI.

## The music is the web's synth, recorded

The web plays its songs on four voices like a Game Boy's, and the Game Boy Advance still has the Game Boy's sound chip. So the songs go to those channels as they are: the melody on a square wave, the harmony on a 25% pulse, the bass on the wave channel and the drums on the noise channel.

The sound effects are trickier, because the web builds them from oscillators with pitch slides and filters. The generator does not translate them. It runs the web's `audio.ts` against a fake `AudioContext` that records everything it is asked to do, and turns each recorded tone into a voice for a software mixer on the GBA. To learn what the arguments do, it runs each effect again with them changed.

## Two bugs I found by playing

The checks passed, and then I played it, and two things felt wrong.

**The walk limped.** A step is 16 pixels in 15 frames, so the walking speed was 273/256 of a pixel per frame. Fifteen frames of that is 4095/256, one 256th of a pixel short of a tile. So every tile took a sixteenth frame that moved by almost nothing, and running made it worse. The fix measures a step in 1/240 of a pixel, where the web's speeds are exact, and carries what one step overshoots into the next.

These two kids hold right with the code from before and after the fix. Each bar under them is one frame:

<div data-toy="limp"><p><em>Interactive: two kids walking across a strip of tiles, one with the old arithmetic and one with the fix, with a graph of how many pixels each one moves every frame. Needs JavaScript.</em></p></div>

In slow motion the old one stops on every tile, and after fifteen tiles he is a whole tile behind.

This is the walk now, in the club: walking, then running with B, then a chat with the Negro.

<video controls playsinline preload="none" poster="/gambeta-gba/barrio.png" src="/gambeta-gba/barrio.mp4" style="width: 100%; height: auto; image-rendering: pixelated; border-radius: 12px;"></video>

**The music slowed down.** The sequencer advanced once per game frame. When a frame took too long, loading a map for example, the music fell behind. Now the vertical blank interrupt counts real time and the sequencer catches up. A check makes every game frame take three times as long and checks that the song still moves at the right speed.

Neither bug broke a test, because the tests read where the hero is, not how he walks there. Both checks exist now.

## The one that ate the save

My favourite came from merging. After two drills went in, the story check failed: the coach said I was not a member of the club, right after the president had signed me up. The flag was set, and then it was gone.

The GBA has 32 KB of fast memory, and all the game's variables were there, after the code that needs to be fast. With the new drills they filled it, and the saved game ended up at the very top, where the stack grows down from. Every deep function call wrote over the save. All the variables moved to the 256 KB of slower memory, the fast one keeps what really needs it, and the match is exactly as fast as before.

This is that memory with the real sizes from the build that broke. Add the scenes one by one and watch the saved game climb towards the stack, then make a deep call:

<div data-toy="memory"><p><em>Interactive: a map of the Game Boy Advance's 32 KB of fast memory, with the variables of each scene, the saved game and the stack growing down into it, before and after the fix. Needs JavaScript.</em></p></div>

## Play it

[Download the ROM](https://gambeta.futbol/gba/gambeta.gba): `gambeta.gba`, 2.9 MB. It runs in any Game Boy Advance emulator, like mGBA on a computer, on emulator handhelds like the R36S, and on a real Game Boy Advance, GBA SP or Game Boy Micro. Or play it at the top of this page.

The web game has a cheat, and the port has it too: the old code from the eighties, the one that starts with up, up, down, down. It works anywhere in the game, and on this page.

## A cartridge of your own

It also runs on a real Game Boy Advance, from a cartridge. [Gambeta's Game Boy Advance page](https://gambeta.futbol/merch/cartucho/) has the ROM, what to buy, how to flash it and the label: 43 by 22 mm, the size of a GBA label, with the title's pitch at dusk, the game's letters and the kid with the ball.

<img src="/gambeta-gba/cartucho.jpg" alt="A grey Game Boy Advance cartridge with the Gambeta label: GAMBETA, RPG DE FUTBOL and the kid with the ball on the pitch at dusk" style="width: 100%; height: auto; border-radius: 12px;">

The cartridge in the photo is a real one with a blank label, and the design is mapped onto it in perspective. To make one:

- **A cartridge you can write to.** One that fits is insideGadgets' [GBA 32MB, 256Kbit FRAM Save](https://shop.insidegadgets.com/product/gba-32mb-256kbit-fram-save-flash-cart/). It saves 32 KB, the same as the game, and FRAM keeps the save without a battery.
- **A writer.** The [GBxCart RW v1.4 Pro](https://shop.insidegadgets.com/product/gbxcart-rw/), with a USB-C cable.
- **The program.** [FlashGBX](https://github.com/lesserkuma/FlashGBX), free, for Windows, Mac and Linux. Plug in the writer, insert the cartridge, open FlashGBX in Game Boy Advance mode and write the ROM. It takes a few minutes.
- **The label.** Print it at its real size on vinyl or sticker paper, cut it to 43 by 22 mm with slightly rounded corners and stick it on.

If you would rather not flash anything, an [EverDrive GBA X5 Mini](https://krikzz.com) takes the ROM from a microSD card.

## Numbers

- About 17,000 lines of C for the GBA, plus 7,000 shared with the computer build, and 13,000 lines of TypeScript of generators and checks.
- A 2.9 MB ROM.
- 23 checks that play the game in a headless emulator on every change, and a match frame that takes 31% of the time on average and 77% at its worst.

<script type="module" src="/gambeta-gba/toys.js"></script>
