GBA Emulator
Pick up where the last visitor left offHow this works
One shared emulator and a live AI trainer, wired together through a single Firestore database.
Shared emulator
Everyone sees the same game. One visitor holds control via a 2-minute lease; the rest spectate. Your progress is snapshotted to the cloud, so the next person picks up exactly where you left off — same map, same party, same RAM.
Live AI trainer
A reinforcement-learning agent trains on Pokémon Emerald on a Kaggle GPU, reading the game's memory and learning by reward. Every 500 steps it streams its screen, stats, and current thought into the panel above — what you see is a real run.
Data flow
EmulatorJS runs the game; polls control + training state
Server routes authenticate and read/write Firestore via the Admin SDK
Two docs: gba/session (control + savestate), gba/training (live metrics)
The RL notebook writes training metrics straight into Firestore
The browser never touches the database directly — every read and write goes through the server. The trainer is the one exception, writing in with a service-account key.
Taking turns, without losing progress
- 1.Claim — You take the lease and the latest savestate loads into your emulator.
- 2.Play — A heartbeat every 30s keeps your 2-minute lease alive; spectators’ inputs are disabled.
- 3.Save — Your progress is captured on an interval, and again whenever you leave or the tab is hidden.
- 4.Hand off — Stop heartbeating (or release) and the lease frees — the next visitor resumes your exact save.
Stack: Next.js (App Router) · Firestore · EmulatorJS (mGBA core) · a DQN agent trained with mGBA + TensorFlow on Kaggle.
How the AI learns Pokémon
Reinforcement learning: reward the good, penalize the bad, repeat millions of times.
1. What it sees — the state vector
The AI doesn't process pixels. It reads 4 numbers directly from the GBA's RAM — enough to capture survival, progress, and wealth. Emerald XOR-encrypts money and shuffles save blocks in RAM, so the agent dereferences the SaveBlock pointers at runtime to find them.
| RAM address | What it reads | Normalized to | Example |
|---|---|---|---|
| 0x020244EC + 0x56/0x58 | Lead Pokémon HP / Max HP | hp / maxhp | 0.85 |
| SaveBlock2 flags 0x867–0x86E | Badges earned (0–8) | badges / 8 | 0.25 |
| SaveBlock1 + 0x0490 ^ encKey | Money (XOR-decrypted) | min(money / 100k, 1) | 0.12 |
| 0x020244E9 | Party size (1–6) | count / 6 | 0.50 |
[0.85, 0.25, 0.12, 0.50]2. What it can do — 7 actions
Each action is held for 8 frames then released for 8 frames before the next decision — 16 frames per step, ~60fps equivalent.
3. Rewards — what it's optimizing for
4. The neural network (DQN)
A Deep Q-Network maps the 4-number state to a Q-value per action — the estimated future reward of pressing that button right now. The action with the highest Q-value wins.
Two identical networks run in parallel. The live network trains every step; a frozen target network provides stable Q-value targets and syncs every 1,000 steps. Without this split, the agent chases a moving target and diverges.
5. Explore vs. exploit — ε-greedy
Early on the agent acts randomly to discover the game. Over time it trusts its own predictions more. ε (epsilon) controls the tradeoff — at each step, with probability ε the agent picks a random button; otherwise it picks the highest Q-value.
Decays by ×0.9995 per step. The panel above shows the live epsilon — watch it drop as training progresses.
6. Training mechanics
Every (state, action, reward, next state) tuple is stored. Training samples 64 at random — breaking the correlation of sequential steps that would otherwise destabilize learning.
MSE loss between predicted Q-values and Bellman targets, minimized by Adam (lr=1e-3, γ=0.95). Runs every step once the buffer has 64 entries.
The target network's weights are hard-copied from the live network every 1,000 steps. This prevents feedback loops where the network trains against its own rapidly changing outputs.