node-mgba
v0.2.6
Published
Scriptable, headless Game Boy and GBA emulator for Node.js powered by libmgba
Maintainers
Readme
node-mgba
Scriptable, headless mGBA emulator for Node.js — like PyBoy, but powered by
libmgbafor Game Boy, GBC, and GBA.
node-mgba provides native Node.js bindings to the C libmgba core. It is built for scripting, agent automation, and machine learning research, giving you direct programmatic control over the emulator:
- Headless & fast: Runs without a GUI window at 1,000+ to 3,400+ FPS.
- Controls & state: Step frames, inject button inputs, and save/load state in memory or to disk.
- Direct memory access: Read and write directly to memory buses (WRAM, VRAM, HRAM) without socket overhead.
- Screen & audio capture: Grab raw pixel buffers, encode to PNG/WebP, or stream audio/video frames.
- Plugin decoders: Optional high-level memory decoders (includes Pokémon Red/Blue state parsing).
Originally built to power the 24/7 autonomous agent on Gemini Plays Pokémon (web viewer).
Installation
pnpm add node-mgbaQuickstart
import { Mgba } from 'node-mgba';
// Load ROM (supports .gb, .gbc, .gba)
const emu = await Mgba.load('./game.gb');
// Advance frames and send input
await emu.controls.tick(60);
await emu.controls.press('A');
// Read memory
const playerX = await emu.memory.read8(0xD362);
// Capture screenshot buffer
const pngBuffer = await emu.screen.toPng();
// Close when done
await emu.close();Console Support & Limitations
| Platform / Model | Emulation & Controls | Direct Memory (read8, readBatch, slice) | Memory Snapshots (observe, GamePlugin.getState()) |
|---|---|---|---|
| Game Boy (DMG / SGB) | Supported (160×144) | Supported | Supported |
| Game Boy Color (CGB) | Supported (160×144) | Supported | Supported |
| Game Boy Advance (AGB) | Supported (240×160) | Supported (EWRAM, IWRAM, ROM) | Planned on Roadmap |
Note on GBA Memory Snapshots: GBA emulation, controls, audio/video streaming, savestates, and direct memory reads are supported. Multi-region snapshots (
emu.observe({ memory: ... })andGamePlugin.getState()) are currently limited to Game Boy (DMG/CGB/SGB) models and planned for GBA.
Common Tasks
Input & Frame Stepping
// Press returns a TurnResult with keyframes
const turnResult = await emu.controls.press('A', 8);
console.log(`Captured ${turnResult.keyframes.length} keyframes`);
// Hold a button across multiple ticks
await emu.controls.hold('B');
await emu.controls.tick(30);
await emu.controls.release('B');
// Run an input sequence
const sequenceResult = await emu.controls.sequence([
{ type: 'press', button: 'UP', holdFrames: 6, releaseFrames: 4 },
{ type: 'wait', frames: 10 },
{ type: 'press', button: 'A', holdFrames: 6, releaseFrames: 4 },
]);Screen Capture & Cropping
// Capture full screen as PNG or WebP
const pngBuffer = await emu.screen.toPng();
const webpBuffer = await emu.screen.toWebp({ quality: 85 });
// Crop a sub-region with optional integer scaling (e.g. 2x, 4x)
const croppedPng = await emu.screen.crop({
x: 16,
y: 16,
width: 32,
height: 32,
scale: 2, // 64x64 output
});Reading & Writing Memory
// Read unsigned integers
const byte = await emu.memory.read8(0xC000);
const u16 = await emu.memory.read16LE(0xC001);
const u32 = await emu.memory.read32LE(0xC003);
// Read GBA regions directly
const ewram = await emu.memory.readRegion('EWRAM', 0, 64);
const iwram = await emu.memory.readRegion('IWRAM', 0, 64);
// Write to memory
await emu.memory.write8(0xC500, 0x42);
// Read multiple addresses in one call
const [x, y, mapId] = await emu.memory.readBatch([0xD362, 0xD361, 0xD35E]);Savestates
// Save and load state files
await emu.states.saveToFile('./save.state');
await emu.states.loadFromFile('./save.state');
// In-memory state handles
const handle = await emu.states.save();
await emu.states.restore(handle);Waiting for In-Game Conditions
// Wait until memory matches a condition (or timeout is reached)
await emu.waitFor({
timeoutFrames: 300,
condition: async (instance) => {
const battleStatus = await instance.memory.read8(0xD057);
return battleStatus !== 0;
},
});Video & Audio Streaming
import { Mgba, WebSocketMediaSink, FfmpegRecordingSink, maskToButtonNames } from 'node-mgba';
// Stream video and audio chunks over WebSocket clients
const wsSink = new WebSocketMediaSink({
name: 'live-stream',
clients: () => wss.clients,
});
// Or record gameplay directly to an MP4 file
const recorder = new FfmpegRecordingSink({
outputPath: './gameplay.mp4',
fps: 60,
});
// Custom MediaSink with per-frame button input tracking
const customSink = {
name: 'input-tracking-sink',
onVideoFrame: (packet) => {
// packet.keys contains the 32-bit button bitmask active on this exact frame
const buttons = maskToButtonNames(packet.keys);
console.log(`Frame #${packet.frameIndex} rendered with active buttons:`, buttons);
},
onAudioChunk: (chunk) => {
// stereo PCM audio
},
};
// Pass sinks when loading the emulator
const emu = await Mgba.load('./game.gb', {
mediaSinks: [wsSink, recorder, customSink],
});Game Plugins & State Decoding
import { PokemonRedBluePlugin } from 'node-mgba/plugins';
// Attach game plugin
const pokemon = await emu.use(PokemonRedBluePlugin);
// Retrieve structured game state
const state = await pokemon.getState();
console.log(state.player.position, state.party);Real-Time Emulation & Autonomous Agent Loops
import { EmulatorController, GB_FPS } from 'node-mgba';
// Launch autonomous 59.73 FPS real-time execution in a worker actor
const controller = new EmulatorController({
romPath: './game.gb',
realtime: true,
fps: GB_FPS,
});
await controller.initialize();
// Send interactive key inputs or enqueue AI button presses
await controller.pressButtons(['START']);
// Stream 60 FPS video frames
controller.on('frame', (frame) => {
// Handle VideoPacket (RGBA pixel buffer)
});Performance
Measured on AMD Ryzen 9 5950X with Pokémon Blue (pnpm run bench):
| Metric | Throughput | Avg Latency | Notes |
|---|---|---|---|
| Headless Stepping | ~1,110 FPS | 0.90 ms / frame | Non-blocking worker_threads RPC |
| Direct Core Stepping | ~3,420 FPS | 0.29 ms / frame | In-process native core |
| Single Memory Read/Write | ~10,000 ops/s | ~100 µs / op | Direct WRAM bus access |
| Batch Memory Read (50 addrs) | ~6,390 batches/s | 0.16 ms / batch | Single IPC round-trip |
| Schema DSL Struct Decode | ~960,690 ops/s | 1.0 µs / struct | In-memory binary parser |
| Full Game State Decode | ~22,270 decodes/s | 44.9 µs / decode | Populated party, bag, map, box |
| Savestate Restore | ~2,280 restores/s | 0.44 ms / restore | Cycle-accurate state handle |
For full benchmarks and methodology, see the Performance & Benchmarks Guide.
Development & Testing
Running Tests
Tests run against bundled homebrew fixtures by default:
# Run test suite
pnpm test
# Run Pokémon Red/Blue plugin tests with a ROM dump
export POKEMON_ROM_PATH="/path/to/pokemon_blue.gb"
pnpm test
# Override test ROM or savestate path
export ROM_PATH="/path/to/game.gb"
export SAVESTATE_PATH="/path/to/game.ss0"
pnpm testEnvironment Variables
| Variable | Default | Description |
|---|---|---|
| MGBA_LOG_LEVEL | warn | Native mGBA log level (silent, fatal, error, warn, info, debug). |
| POKEMON_ROM_PATH | — | Path to Pokémon Red/Blue ROM for pokemon_red_blue.test.ts. |
| ROM_PATH | Bundled homebrew fixtures | Test ROM path (.gb, .gbc, or .gba). |
| SAVESTATE_PATH | Bundled homebrew fixture | Savestate fixture path (.ss0) for savestate tests. |
Interactive Web GUI Studio
node-mgba includes a built-in Vue 3 web interface for debugging emulation, testing input sequences, and inspecting real-time RAM decoding:
# Build the native shim, TypeScript, and GUI bundle
pnpm run build
# Launch the GUI server with a Game Boy ROM (opens at http://localhost:3456)
ROM_PATH="/path/to/game.gb" pnpm run gui
# Or run the GUI frontend in Vite development mode with hot-reloading
pnpm run gui:devNote: The GUI's state inspector and telemetry HUD panels are currently tailored specifically for Pokémon Red & Blue (displaying real-time party stats, inventory, badges, and map coordinates).
Subpath Exports
| Import | Description |
|---|---|
| node-mgba | Core emulator facade, controls, memory, and media sinks |
| node-mgba/plugins | Plugin base class (GamePlugin) and built-in plugins (PokemonRedBluePlugin) |
| node-mgba/schema | Declarative binary schema DSL for defining RAM decoders |
| node-mgba/testing | In-memory MockMemoryReader for unit testing decoders |
| node-mgba/browser | Browser media playback helpers (WebAudioPlayer, CanvasRenderer) |
