Generation I Pokémon as native executables. No emulator. No ROM interpretation at runtime. Just SM83 assembly translated, ahead of time, into portable C.
This repository ports Generation I Pokémon — Yellow, Red, Blue — to native code via gb-recompiled: a static recompiler that lifts SM83 (Z80-ish) machine code directly into C. Each game builds into its own native rom.exe. No emulator in the loop, only a small fallback interpreter for indirect jumps the static analyser couldn't resolve.
Sibling projects on the same toolchain: LinksAwakening, pokemon-gold, pokemon-crystal, oracle-recompiled.
| Pokémon Red | Pokémon Blue | Pokémon Yellow |
|---|---|---|
![]() |
![]() |
![]() |
All three boot as native executables rendered from their own translated C — no emulator at runtime. Frames are dumped straight from the recompiled engine's framebuffer.
A reinforcement-learning agent (PWhiddy's PokemonRedExperiments, unmodified) playing our recompiled Red — here exploring Oak's Lab and reading the Pokédex. Every frame is computed by the recompiled engine, not an emulator; recorded with bot/watch_agent.py.
The same agent crosses maps under its own steam — bedroom → Pallet Town → the "Wild POKéMON live in tall grass!" event → Oak's Lab:
| Game | Cart | Color | Status |
|---|---|---|---|
| Pokémon Yellow | MBC5, GBC | CGB palettes | ✅ Boots; GAME FREAK + Pikachu intro render with correct palettes |
| Pokémon Red | MBC3, DMG | Mono | ✅ Boots to the title screen and into gameplay. Also builds a headless rom_headless.dll driven by an RL bot (bot/). |
| Pokémon Blue | MBC3, DMG | Mono | ✅ Boots to the title screen (same GB_MODEL_DMG recipe as Red); also builds a headless rom_headless.dll. |
Red boot fix (DMG init). Red stayed white because the runtime hard-coded a CGB power-on state.
gb_context_createignored itsGBConfigand forcedA=0x11(the CGB signature) with the PPU in CGB mode. Red is a DMG cart and branches onAat$0100, so it needsA=0x01and DMG PPU/palette behaviour. The fix threadsGBConfig.modelthroughgb_context_reset(DMG →A=0x01B0,ppu->cgb_mode=false), defaulting to CGB whenconfig==NULLso Yellow is untouched.red/rom_main.cnow passesGBConfig{ .model = GB_MODEL_DMG }.
pokemon/
├── gbrecomp/ submodule → sp00nznet/gb-recompiled (the recompiler + runtime)
├── roms/ your legal ROMs (gitignored)
├── tools/ PyBoy diagnostic scripts + input scripts
├── yellow/ Yellow build dir (rom_main.c + CMakeLists committed; rom.c/rom_rom.c regenerated)
├── red/ Red — boots; also builds rom_headless.dll (platform_headless.c + rom_bridge.c)
├── blue/ Blue — boots; same headless build as Red
├── bot/ RL bot integration — PyBoy-compatible ctypes shim + differential tester + PPO training
└── screenshots/ images used in this README
Each game directory is self-contained, matching the pokemon-gold convention: tracked rom_main.c + CMakeLists.txt, generated rom.c / rom.h / rom_rom.c (gitignored, regenerated each recompile).
- Windows 11: Visual Studio 2022 Build Tools (C++ workload), CMake ≥ 3.20, SDL2 via vcpkg (
vcpkg install sdl2:x64-windows). - Python 3.11+ with
pyboy(pip install pyboy) — for the ground-truth trace capture. - A legally obtained ROM placed in
roms/.
git clone --recurse-submodules https://github.com/sp00nznet/pokemon.git
cd pokemoncmake -S gbrecomp -B gbrecomp/build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
cmake --build gbrecomp/build --target gbrecomp --config ReleaseThat produces gbrecomp/build/bin/Release/gbrecomp.exe.
# 1) point the recompiler at a rom-named copy so output files are rom.{c,h,_rom.c}
cp "roms/Pokemon Yellow Version - Special Pikachu Edition.gbc" yellow/rom.gbc
# 2) capture a PyBoy ground-truth execution trace
python gbrecomp/tools/capture_ground_truth.py yellow/rom.gbc \
-o yellow/rom.trace -f 18000 --random
# 3) recompile (writes rom.c, rom.h, rom_main.c, rom_rom.c)
gbrecomp/build/bin/Release/gbrecomp.exe yellow/rom.gbc \
-o yellow --use-trace yellow/rom.trace
# 4) configure + build
cmake -S yellow -B yellow/build \
-DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
cmake --build yellow/build --config Release
# 5) run
./yellow/build/Release/rom.exeUseful flags on rom.exe (from gbrecomp's runtime):
| Flag | Effect |
|---|---|
--dump-frames "100,400,900" |
dump frames at the listed numbers as screenshot_NNNNN.ppm |
--limit N |
cap total instructions (handy for headless smoke tests) |
--input FILE |
replay a recorded button script |
--trace, --trace-entries FILE |
dump every translated instruction address that executed |
ROM (.gb/.gbc) PyBoy ground truth gbrecomp
│ │ │
▼ ▼ ▼
[1 MB SM83 machine code] + [proven entry-points trace] → [~62 MB native C]
│
▼
cmake / MSVC + SDL2
│
▼
rom.exe (statically recompiled)
├─ translated game code
├─ gbrt runtime (PPU, APU, MBCs, save)
└─ SDL2 + Dear ImGui debug overlay
The --use-trace step is the key: it seeds the recompiler with every (bank, addr) PyBoy actually executed, solving the JP (HL) / PUSH-ret trampoline patterns Pokémon uses for predef / farcall / jump-table dispatch — control flow the static analyser cannot follow on its own.
See gbrecomp/GROUND_TRUTH_WORKFLOW.md for the detailed flow.
Beyond the SDL rom.exe, Red also builds rom_headless.dll — the same recompiled engine with no SDL/ImGui, exposing a tiny C ABI (gbrom_create/step/read/write/set_buttons/framebuffer/snapshot/restore). A Python ctypes wrapper (bot/pyboy_shim.py) presents that DLL as a drop-in subset of the PyBoy 2.x API, which lets two things run against our recompiled engine:
- Differential testing vs PyBoy — feed identical inputs to stock PyBoy and our engine, diff memory + screen. This proved the recompilation is cycle-faithful (the only startup difference is PyBoy's own boot splash; after phase-alignment the engines match to a handful of RNG/uninitialized bytes).
- An RL agent plays it — PWhiddy's PokemonRedExperiments
RedGymEnv, unmodified, withPyBoymonkeypatched to our shim. The agent walks out of the bedroom, into Pallet Town, and into Oak's Lab — driven entirely by our engine.
cmake --build red/build --target rom_headless --config Release # build the DLL
python bot/diff_harness.py # differential test vs PyBoy
python bot/run_phase_d.py # drive the RL env on our engine
python bot/train_ppo_ours.py # real PPO training on our engine
python bot/watch_agent.py --live # watch the trained agent (MP4 + live window)Full details — architecture, the boot-splash timing investigation, and how PPO training is wired — are in bot/README.md.
- ROMs are never committed. Provide your own legal copy under
roms/. - Each game's
rom.c(≈ 66 MB for Yellow) androm_rom.c(≈ 6.6 MB) are generated and gitignored; onlyrom_main.candCMakeLists.txtare tracked per game. - Re-running the recompiler overwrites the generated
CMakeLists.txtandrom_main.c, so re-apply local tweaks afterwards. For Red/Blue specifically: the committedrom_main.cpassesGBConfig{ .model = GB_MODEL_DMG }(a DMG cart needs it to boot) but the recompiler regenerates a CGB-defaultrom_main.c— restore the committed copy, or it will sit on a white screen. Yellow is a real CGB cart and needs no change. tools/pyboy_*.pyare project-specific PyBoy diagnostic scripts kept from the Yellow GAME FREAK palette debugging session.
MIT. The runtime (gbrecomp/runtime/) is MIT via the gb-recompiled submodule. ROMs are © Nintendo / Game Freak / Creatures — bring your own.




