All projects

Reinforcement learning · 2026

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.

Code Paper in preparation
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

Bar chart titled 'The same battles, about 900× faster than Pokémon Showdown'. Showdown 7.69k turns per second (1×); Kanto with the same text output 185–189k (24–25×); Kanto engine without text 6.80–7.04M (884–916×).

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

  1. 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.
  2. 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.
  3. 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

  1. 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)

  2. 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

  3. 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

  4. 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

  5. 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.

Figure 1. How the pieces fit: an exact simulator, checked against Showdown battle by battle, feeds a batched environment; agents learn on it by playing themselves and earlier versions of themselves, and can add a short search at play time.

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

vs TaurosV0

50M parameters; its authors' best standalone Gen 1 OU policy · 2,001 games each

Indigo-MLP2.8M parameters
53.0% [50.9, 55.2]
Indigo-S3.2M parameters
56.5% [54.3, 58.7]
Indigo-M12.0M parameters
60.2% [58.0, 62.3]

vs Foul Play

search-based Showdown bot at 100 ms per decision · 1,002 games each

Indigo-MLP2.8M parameters
77.8% [75.2, 80.3]
Indigo-S3.2M parameters
80.0% [77.4, 82.4]
Indigo-M12.0M parameters
81.3% [78.8, 83.6]

Adding search, against TaurosV0

2,000 games per arm; both arms play identical matches (same seat, seed and teams)

ModelPolicy onlyWith searchChange [95% CI]
Indigo-MLP55.9%59.3%+3.4 [+0.5, +6.2]
Indigo-S56.0%57.0%+1.0 [−1.9, +3.9]
Indigo-M59.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.

Figure 2. Scores against two outside agents on held-out teams, with 95% intervals. All three models beat TaurosV0, a 50M-parameter agent, and Foul Play; adding search helps only a little against TaurosV0.

How much faster than Showdown

replaying the same 2,000 battles per team pool on one CPU core · speed-up over Showdown's BattleStream

Showdown BattleStream7.69k turns/s · always writes its battle log
1×
Kanto ProtocolBattle185–189k turns/s · writes the same log, byte for byte
24–25×
Kanto engine, no text6.80–7.04M turns/s · what training and search use
884–916×

Against the fastest Gen 1 engine

pkmn/engine (Zig) matches a patched Showdown, not the real one; both without text, same core

MeasureKantopkmn/engine
Time per battle update92 ns71–72 ns
Random self-play5.4M turns/s6.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.

Figure 3. Replay speed on the same battles on one CPU core. Writing Showdown's exact text, Kanto is 24–25× faster; without text, which is what training and search use, it is 884–916× faster.

Technical details

The full list of measured results behind this project.

Show all 10 measurements
MeasurementValue
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 throughput92 ns vs 71–72 ns; 5.4M vs 6.4M turns/s
VecEnv steps per second, 4,096 battles, observations and masks for both playersabout 1.0M on one thread; 2.8–3.1M on eight
Differential testing against Showdown 0.11.11, cumulative across simulator and harness revisions81,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-M53.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-M55.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-M77.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 pairIndigo-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.