Skip to main content

Hacker-first, scriptable GBA emulator with HTTP API

Project description

gbax

PyPI Python CI License: MPL-2.0 Platform

A hacker-first GBA emulator. Play with your keyboard, drive it from HTTP.

gbax is a pip-installable Game Boy Advance emulator built for people who like typing commands. It plays games in a window with a keyboard, like any emulator — and it also exposes its entire state over a local HTTP API, so any script, shell pipeline, or LLM in any language can read pixels, peek memory, and press buttons.

$ pip install gbax
$ gbax download "pokemon emerald"
$ gbax play emerald

Pokémon Emerald, in a window, with sound, in three commands.

$ gbax serve emerald
gbax serving Pokemon - Emerald Version (USA, Europe).gba on http://127.0.0.1:8420
  mode=step  rom_sha1=f3ae088181bf583e55daf962a92bb46f4f1d07b7
  endpoints: /mode /step /speed /frame /buttons /memory /frame_count

$ curl -X POST localhost:8420/buttons -d '{"buttons":["a","right"]}' -H 'content-type: application/json'
$ curl -X POST 'localhost:8420/step?frames=4'
$ curl localhost:8420/frame -o frame.png

That's the headline: it's an emulator you can pipe.

🤫 Pst, that whole "let's do science on GBA games" is the public reason you're here, but we all know you just want to play Pokémon on Linux. Go ahead, that's allowed too.

Status

  • Alpha. v0.3.0. Works on Linux x86_64 — the wheel bundles the libretro core, so pip install gbax is a one-step setup. macOS / Windows / ARM are PR-welcome.
  • MPL-2.0. Same license as the underlying mGBA core.
  • No ROMs bundled. gbax download pulls from the public No-Intro mirror at archive.org. Use it for games you own; respect your local laws.

What's here

Play

gbax play <rom> opens an SDL window, wires the keyboard, plays audio.

  • D-pad: arrow keys · A: X · B: Z · L: A · R: S · Start: Enter · Select: Right-Shift
  • Ctrl+1Ctrl+9 — save state to slot N (auto-persisted to ~/.gbax/saves/<rom-sha1>/)
  • Shift+1Shift+9 — load slot N
  • F12 — screenshot to ~/.gbax/screenshots/
  • Tab (hold) — fast-forward at 8×

Slots survive restarts. Open a game, save in slot 3, close the window, open the game again, Shift+3 — you're back.

Cheats

$ gbax cheats emerald | head -3
  1-hit-kill                           1-Hit Kill
  max-money                            Max Money
  walk-through-walls-l-r               Walk Through Walls [Press L+R]
…

$ gbax play emerald --cheats max-money,walk-through-walls-l-r
cheat ON: Max Money
cheat ON: Walk Through Walls [Press L+R]

Pin cheats to keys so the same hotkey toggles the same cheat across sessions:

$ gbax pin emerald F1 max-money
$ gbax pin emerald F2 walk-through-walls-l-r
$ gbax pin emerald F7 complete-pokedex
$ gbax pins emerald
  F1  →  max-money
  F2  →  walk-through-walls-l-r
  F7  →  complete-pokedex

Pins persist to ~/.gbax/pins/<rom-sha1>.json. In play, pressing F1 toggles the pinned cheat directly — autoloading it from the catalog if needed. Unpinned F-keys fall back to "toggle the Nth currently-active cheat."

Over the API: POST /cheats/<slug>/enable, POST /cheats/<slug>/disable, POST /cheats/custom for ad-hoc codes, DELETE /cheats to clear.

The libretro-database snapshot (~6700 GameShark / Action Replay / Code Breaker codes covering 512 GBA games) ships in the wheel — no network at runtime.

Automation: Controller, Scenarios, Tournaments

In-process Python automation. No HTTP needed.

import gbax

with gbax.Controller("pokemon emerald") as g:
    g.press(["start"], frames=2)
    g.wait(60)
    g.press(["a"], frames=2)
    print(g.read_u32(0x02024284))
    g.screenshot("/tmp/run.png")

For repeatable runs and tournaments, define a scenario (a small Python file with setup / observe / score / done) and have one or more players race through it:

$ gbax scenario create "pokemon emerald" --name catch-snorlax
$ gbax train --rom emerald --scenario catch-snorlax --player ./my_bot.py
$ gbax tournament --rom "mortal kombat" \
    --scenario mk-arcade-easy \
    --player "python -m gbax.data.bots.press_a" \
    --player "python -m gbax.data.bots.random"

Scenarios choose their own observation shape (raw bytes, structured dict, framebuffer) and scoring criteria. Two reference scenarios ship in the wheel: mk-arcade-easy (Mortal Kombat Advance ladder) and smb3-world-1-1 (Super Mario Advance 4, World 1-1). Full design at docs/automation.md.

Library

$ gbax search "metroid"
    1. Metroid - Zero Mission (USA).zip  (4.0 MB)
    2. Metroid Fusion (USA, Australia).zip  (5.5 MB)
    …

$ gbax download "metroid fusion"
match: Metroid Fusion (USA, Australia).zip
  size: 5.5 MB
  downloading… 100%  (5.5/5.5 MB)
saved: /home/<you>/.gbax/roms/Metroid Fusion (USA, Australia).gba

$ gbax list-roms
  Pokemon - Emerald Version (USA, Europe).gba  (16.0 MB)  sha1:f3ae088181
  Metroid Fusion (USA, Australia).gba          ( 8.0 MB)  sha1:fbe10b78b6

Search is instantaneous (~13 ms) — the full 3555-entry No-Intro GBA index ships in the wheel. gbax download is the only thing that touches the network.

Serve

gbax serve <rom> boots the emulator in step mode and exposes a FastAPI control surface on 127.0.0.1:8420. In step mode the emulator is paused by default; a controller posts /step?frames=N to advance. That's what makes slow controllers (an LLM that takes 2 seconds to think, an RL agent that runs in Python) actually viable — the game waits.

GET  /mode                                  → "step" | "free"
POST /mode                {mode}              switch
POST /step?frames=N                          advance N frames
POST /speed               {multiplier}        free-run wall-clock speed
GET  /frame_count

GET  /frame                                  PNG of current frame
GET  /frame?fmt=raw                          240×160×3 RGB888 bytes

GET  /buttons                                → ["a","right",…]
POST /buttons             {buttons}           replace held set

GET  /memory?addr=…&len=… → {data: "deadbeef…"}
POST /memory              {addr, data, width} write hex

The address space gbax exposes is the full GBA bus — IWRAM at 0x03000000, EWRAM at 0x02000000, VRAM at 0x06000000, OAM, I/O, ROM, BIOS. So a Pokémon-aware controller can read 0x02024362 and know your party's HP.

Free-run mode (POST /mode {"mode":"free"}) advances at wall-clock 60 fps (or faster with /speed), and /buttons writes still take effect. Use this when you want a human at the keyboard and a script reading state.

Architecture

flowchart TB
    subgraph clients[" "]
        direction LR
        kbd([Keyboard])
        http([HTTP client<br/>script · LLM · shell])
    end

    subgraph cli["gbax CLI (Typer)"]
        play["gbax play"]
        serve["gbax serve"]
        other["search · download · list-roms · …"]
    end

    sdl["SDL renderer<br/>window + audio + input"]
    api["FastAPI server<br/>/mode /step /frame /buttons /memory /speed"]
    rt["EmulatorRuntime<br/>load · step · framebuffer · memory · save slots · ticker"]
    lr["LibretroCore<br/>~300 LOC cffi shim over the libretro ABI"]
    so["mgba_libretro.so"]

    kbd --> sdl
    http --> api
    play --> sdl
    serve --> api
    sdl --> rt
    api --> rt
    rt --> lr
    lr --> so

    classDef ext fill:#eef,stroke:#33a,stroke-width:1px;
    classDef core fill:#fef9c3,stroke:#a16207,stroke-width:1px;
    class kbd,http ext;
    class so core;
  • LibretroCore is a ~300-line cffi wrapper around the libretro ABI. It dlopens mgba_libretro.so, captures the framebuffer + audio + memory-map callbacks, drives input. Swapping in another libretro core (vba-next, gpsp) is mostly a one-line config change.
  • EmulatorRuntime is the thread-safe gbax-shaped API on top: load, step, framebuffer, memory, save states, free-run ticker.
  • The SDL window and the FastAPI server are independent clients of the runtime. They don't know about each other.

Step-mode controller loop

When you gbax serve, the emulator is paused. A controller drives it:

sequenceDiagram
    autonumber
    participant Ctl as Controller<br/>(script / LLM / RL)
    participant API as FastAPI
    participant RT as EmulatorRuntime
    participant Core as mgba_libretro

    loop every decision
        Ctl->>API: GET /frame
        API->>RT: framebuffer()
        RT-->>API: 240×160×3 RGB
        API-->>Ctl: PNG

        Ctl->>Ctl: think (any wall-clock time)

        Ctl->>API: POST /buttons {a, right}
        API->>RT: set_buttons(...)
        RT->>Core: retro_set_input_state
        Ctl->>API: POST /step?frames=4
        API->>RT: step(4)
        RT->>Core: retro_run() × 4
    end

The game waits for the controller. That's what makes a 2-second-per-decision LLM viable, and what makes RL training reproducible.

Why libretro and not mGBA's Python bindings directly? Because the upstream bindings are brittle on modern toolchains and require building libmgba with a specific feature set. The libretro ABI is stable, well-documented, and the core is a single self-contained .so. See know-how/building-libretro-core.md.

Install

pip install gbax

One command on Linux x86_64 and you're done. The wheel ships a prebuilt mgba_libretro.so inside it, so there's no cmake, no apt-get, no $GBAX_CORE_PATH to set — gbax play emerald works on a fresh box.

The bundled core is built from mgba-emu/mgba at the tag pinned in .mgba-version — currently 0.10.5, MPL-2.0, upstream license shipped alongside at gbax/cores/LICENSE.mGBA. Want to swap in a debug build, a different mGBA version, or another libretro core? Point $GBAX_CORE_PATH at your own .so; the env var always wins.

Outside Linux x86_64 (macOS, Windows, ARM, ancient glibc) pip falls through to the sdist, which carries no binary. gbax play will exit at startup with instructions; bring your own core and set $GBAX_CORE_PATH. PRs adding more platform wheels are welcome.

Full coverage in docs/installing.md: lookup order, supported distros, version bumps, compliance.

Examples

Pipe the framebuffer into ImageMagick

$ gbax serve emerald &
$ for i in $(seq 1 60); do
    curl -s 'localhost:8420/step?frames=1' > /dev/null
    curl -s localhost:8420/frame > frame-$i.png
  done
$ convert -delay 5 frame-*.png loop.gif

Have an LLM play Pokémon

import base64, requests
from openai import OpenAI

g = "http://localhost:8420"

while True:
    requests.post(f"{g}/step?frames=4")
    frame = requests.get(f"{g}/frame").content
    response = OpenAI().chat.completions.create(
        model="gpt-5",
        messages=[{
            "role": "user",
            "content": [
                {"type": "text", "text": "Press one button to make progress."},
                {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64.b64encode(frame).decode()}"}},
            ],
        }],
    )
    button = response.choices[0].message.content.strip().lower()
    requests.post(f"{g}/buttons", json={"buttons": [button]})

(The LLM rendered above is illustrative — gbax makes no assumption about your controller.)

Read your Pokémon party from a shell

$ # EWRAM byte 0x2024284 is the start of the party block in Pokemon Emerald
$ curl -s 'localhost:8420/memory?addr=33718916&len=4' | jq -r .data
01000000

Roadmap

Status Slice
gbax play — keyboard + audio + save state slots that survive restarts
gbax serve — HTTP API for memory / framebuffer / buttons / step / speed
ROM library — search, download, list-roms against archive.org
Cheat codes — vendored libretro DB (~6700 codes), F1–F9 toggle, /cheats API
YAML user scripts — Ctrl+H runs a sequence of presses + memory pokes
Recording / replay — deterministic input log + divergence detection
Per-game plugins — Python plugins expose /state and /actions for Pokémon, etc.
Bundled libretro core — pip install gbax ships a working emulator on Linux x86_64
macOS / Windows / aarch64 wheels

Full design at vault/Atlas/Architecture/2026-06-09-gbax-design.md (in the companion vault, not this repo).

Credits

  • mGBA by endrift — the emulator core doing the actual heavy lifting. MPL-2.0.
  • No-Intro — the canonical ROM-naming and SHA-1 reference.
  • archive.org — hosts the No-Intro snapshot we point at by default.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gbax-0.3.0.tar.gz (984.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gbax-0.3.0-py3-none-manylinux_2_28_x86_64.whl (870.6 kB view details)

Uploaded Python 3manylinux: glibc 2.28+ x86-64

File details

Details for the file gbax-0.3.0.tar.gz.

File metadata

  • Download URL: gbax-0.3.0.tar.gz
  • Upload date:
  • Size: 984.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for gbax-0.3.0.tar.gz
Algorithm Hash digest
SHA256 5f00dbb7503c5fc8050ae95caf1865332c3607d135297563c8c8f9332a3bec89
MD5 fa023dd2eab57fb4d7f6d6b5622b06e3
BLAKE2b-256 2957b2f47f60329dea7f6a6da3342dc5e9c84147a59fb4232512bd054e695d2f

See more details on using hashes here.

Provenance

The following attestation bundles were made for gbax-0.3.0.tar.gz:

Publisher: release.yml on apiad/gbax

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file gbax-0.3.0-py3-none-manylinux_2_28_x86_64.whl.

File metadata

  • Download URL: gbax-0.3.0-py3-none-manylinux_2_28_x86_64.whl
  • Upload date:
  • Size: 870.6 kB
  • Tags: Python 3, manylinux: glibc 2.28+ x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for gbax-0.3.0-py3-none-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 c0f1e97fd034bba572f310871f10de83892e3dd162a55dd4f27fab13ca89adcb
MD5 424f6668660a205b4293b95d68e26853
BLAKE2b-256 60f9e767e293cd2f80c60f5c2f90069ab002b42ef998bd684bba282d64e4e2ef

See more details on using hashes here.

Provenance

The following attestation bundles were made for gbax-0.3.0-py3-none-manylinux_2_28_x86_64.whl:

Publisher: release.yml on apiad/gbax

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page