Kanto & Indigo: fast Pokémon simulator and self-play agents
A Rust simulator that reproduces Pokémon Showdown's Gen 1 battles exactly, ~900× faster. Agents trained on it by self-play beat TaurosV0, Metamon's best standalone Gen 1 OU policy.
- Context
- Independent project
- Year
- 2026
- Stack
- Rust, Python / PyO3, PyTorch, PPO self-play, Differential testing, Monte Carlo search
~900×
faster than Pokémon Showdown at replaying the same battles
884–916× on one CPU core, without text output
60.2%
score against TaurosV0, which its authors describe as Metamon's best standalone Gen 1 OU policy
12M-parameter agent vs 50M; 2,001 held-out-team games; 95% CI 58.0–62.3%
0
differences from Showdown over 81,496 test battles
6.46M decisions compared, cumulative across revisions
Overview
Problem
Pokémon Showdown is where competitive Pokémon is played and where game-playing agents are tested. Its simulator replays fewer than 8,000 Gen 1 turns per second on one core. Training an agent by self-play (the agent learns by playing against copies of itself) needs billions of turns. Faster engines exist, but none reproduces Showdown exactly.
Approach
I wrote Kanto, a Gen 1 simulator in Rust with Python bindings, by porting the code Showdown actually runs. To check it, I used differential testing: every battle is played in both simulators with the same teams and random seed and their choices, random-number calls and full state are compared at each decision. A batched environment runs thousands of battles in parallel.
On Kanto I trained Indigo, three agents of 2.8M to 12.0M parameters, with PPO (a standard reinforcement-learning algorithm) and a league: each agent plays itself, frozen earlier versions of itself and scripted bots. No human games were used. An optional search tries moves out before playing.
Results
Over 81,496 test battles (6.46M decisions), Kanto showed no difference from Showdown. It replays the same battles about 900× faster, or 24× faster when it also writes Showdown's text log. On held-out teams, the agents score 53.0–60.2% against TaurosV0 (50M parameters, described by its authors as Metamon's best standalone Gen 1 OU policy) and 77.8–81.3% against the search bot Foul Play. Search adds only a small gain against TaurosV0 (+2.8 points for the largest model; interval reaches zero). Limits: Gen 1 OU only, one training run per model, offline matches only, and exactness shown by testing, not proof.
What I built
- Ported the Gen 1 code Pokémon Showdown runs to Rust and built a differential tester that compares choices, random-number calls and full state against Showdown at every decision.
- Built a batched environment that steps 4,096 battles in parallel at about 1.0M steps per second on one thread, plus a client so trained agents play over Showdown's own protocol.
- Trained three agents (2.8M–12.0M parameters) by PPO self-play with a league of past versions, using no human games, and evaluated them on held-out teams with 95% intervals.
Results in detail
From an exact simulator to a trained agent
Kanto (steps 1–3) is the simulator; Indigo (steps 4–5) is the agent family trained on it
1Kanto simulator
A Rust port of the Gen 1 code Pokémon Showdown runs, function by function
a whole battle is one 760-byte value; copying it takes about 18 ns
Rust
Python bindings (PyO3)
2Differential testing against Showdown
Plays every battle in Kanto and in an unmodified Showdown 0.11.11 with the same teams, seed and choices
Compares legal choices, every random number call, every event and the full state at each decision
81,496 battles, 6.46M decisions, 0 differences
mutation testing:
catches 55 of 62
planted bugs
3Batched environment (VecEnv)
Steps 4,096 battles in parallel and writes NumPy arrays for both players
each player sees 752 numbers: only what a Showdown player could see
~1.0M steps/s
on one thread
4Self-play training with a league (PPO)
Each update plays 1,024 games against itself, 1,024 against frozen past versions and 448 against scripted bots (transformer recipe)
3.6B training samples per model; no human gameplay data
Indigo-MLP 2.8M
Indigo-S 3.2M
Indigo-M 12.0M
5Optional search at play time
Samples 64 guesses of the opponent's hidden team and plays 4 short rollouts of each move
settings chosen only in games between my own agents
64 × 4 rollouts
depth 3
Evaluation: held-out teams never used in training, against two outside agents (TaurosV0 and Foul Play) that receive Showdown's exact protocol from Kanto.
Scores against agents never seen in training
held-out teams · score = win rate with ties counted as half · bars show 95% intervals · policy alone, no search
Adding search, against TaurosV0
2,000 games per arm; both arms play identical matches (same seat, seed and teams)
| Model | Policy only | With search | Change [95% CI] |
|---|---|---|---|
| Indigo-MLP | 55.9% | 59.3% | +3.4 [+0.5, +6.2] |
| Indigo-S | 56.0% | 57.0% | +1.0 [−1.9, +3.9] |
| Indigo-M | 59.5% | 62.3% | +2.8 [+0.0, +5.5] |
Search gives a small gain: for Indigo-S it is indistinguishable from zero, and for Indigo-M the interval's lower end is at zero. By comparison, Indigo-MLP with the same search scores 67.6% against its own policy without search (development teams). One reading, not yet tested: the rollouts assume the opponent plays like the agent itself.
Each model was trained once (one seed), so differences between the three models may partly be seed-to-seed variation.
How much faster than Showdown
replaying the same 2,000 battles per team pool on one CPU core · speed-up over Showdown's BattleStream
Against the fastest Gen 1 engine
pkmn/engine (Zig) matches a patched Showdown, not the real one; both without text, same core
| Measure | Kanto | pkmn/engine |
|---|---|---|
| Time per battle update | 92 ns | 71–72 ns |
| Random self-play | 5.4M turns/s | 6.4M turns/s |
- Full RL environment (VecEnv, 4,096 battles, observations for both players)
- ~1.0M steps/s on one thread · 2.8–3.1M on eight
When Kanto writes Showdown's text, roughly 97% of its time goes to the log, the per-player views, the request JSON and input parsing. Leaving the text out is worth a factor of about 37 on its own.
NVIDIA DGX Spark, one Arm Cortex-X925 core at 3.9 GHz; Showdown 0.11.11 on Node.js 22.
Technical details
The full list of measured results behind this project.
Show all 10 measurements
| Measurement | Value |
|---|---|
| Replay speed on identical battles, one core: Showdown BattleStream / Kanto ProtocolBattle (same text) / Kanto engine (no text) | 7.69k / 185–189k (24–25×) / 6.80–7.04M turns/s (884–916×) |
| Kanto vs pkmn/engine, both without text: time per battle update; random self-play throughput | 92 ns vs 71–72 ns; 5.4M vs 6.4M turns/s |
| VecEnv steps per second, 4,096 battles, observations and masks for both players | about 1.0M on one thread; 2.8–3.1M on eight |
| Differential testing against Showdown 0.11.11, cumulative across simulator and harness revisions | 81,496 battles, 6,456,457 decision points, 0 differences |
| Mutation testing of the parity harness (single-site mutants detected) | 55 of 62 (6 survivors equivalent, 1 caught by a unit test) |
| Score vs TaurosV0 (50M parameters), greedy policy, held-out teams, 2,001 games: Indigo-MLP / Indigo-S / Indigo-M | 53.0% [50.9, 55.2] / 56.5% [54.3, 58.7] / 60.2% [58.0, 62.3] |
| Score vs TaurosV0 on the far subset (teams sharing at most 4 of 6 sets with any training team), 2,001 games: Indigo-MLP / Indigo-S / Indigo-M | 55.2% [53.0, 57.3] / 60.2% [58.0, 62.3] / 60.8% [58.7, 63.0] |
| Score vs Foul Play at 100 ms per decision, 1,002 games: Indigo-MLP / Indigo-S / Indigo-M | 77.8% [75.2, 80.3] / 80.0% [77.4, 82.4] / 81.3% [78.8, 83.6] |
| Paired search effect vs TaurosV0, 2,000 games per arm (difference in points, 95% interval): Indigo-MLP / Indigo-S / Indigo-M | +3.4 [+0.5, +6.2] / +1.0 [−1.9, +3.9] / +2.8 [+0.0, +5.5] |
| Head-to-head rating (Bradley–Terry, Elo scale, anchored at Indigo-MLP = 0), 4,000 games per pair | Indigo-S +10 [1, 18]; Indigo-M +46 [37, 55] |
Limitations (10)
- Gen 1 OU singles only. Other Gen 1 tiers, Gen 1 variants and later generations are not implemented; random battles are exact given the teams, but the random-team generator is not Showdown's.
- Exactness is established by testing, not proof: it holds for the states the fuzzer reached under random policies, and strong players may reach others, such as long stall endgames. It is pinned to Showdown 0.11.11 (re-checked against a later master).
- Showdown 0.11.11 itself crashes in a rare state (about one in 2,000 targeted battles); those battles have no ground truth and are excluded from the comparison.
- Observation equivalence between the training environment and the Showdown client view is established by testing on held-out-team battles, not proved.
- The bundled server is a development server (no authentication, ratings, spectators or timer), not a replacement for Showdown's.
- Each model was trained once (seed 0), so small differences such as Indigo-S vs Indigo-MLP may lie within seed-to-seed variation. Indigo-MLP also uses a different training recipe, so that comparison is not a pure architecture ablation.
- The search models the opponent by the agent's own policy. Its gain against TaurosV0 is small and not significant for Indigo-S; the explanation for the gap to self-play gains is an interpretation that has not been tested directly.
- No live-ladder evaluation: the PokéAgent Challenge ladder was offline during this study (its servers were shut down in October 2026), so every result comes from offline matches run on Kanto against the released agents. Scores against TaurosV0 and Foul Play do not map directly to a ladder rating, and no rated games were played against humans.
- Metamon's TaurosEnsemble could not be evaluated, and Foul Play was evaluated only at 100 ms per decision.
- All games run on Kanto with teams from one reconstructed-replay pool; training and evaluation teams are disjoint but come from the same metagame, and the agent never builds its own team.