Search & planning
Clone a battle, enumerate legal choices per slot, step each joint action, keep the best. Battle has no interior sharing, so a copy is a full independent state.
v0.5.0 · pre-1.0 · Gen 9 doubles
vgc-engine is a from-scratch Pokémon battle simulator in Rust, designed for the people writing search and learning code: MCTS rollouts, endgame solvers, self-play data. Fixed-size state, a heap-free step(), cheap clones and a Python module you can import.
Pokémon Showdown is the compatibility oracle. On a sealed sample of 1,300 real Reg M-C battles, 97.9–98.9% replay identically to Showdown. Good enough for local self-play, not yet a replacement for Showdown as the final evaluator. See the evidence.
01 · Who it's for
Pokémon Showdown is the reference simulator, but it is JavaScript, event-driven and allocation-heavy. That is fine for a ladder. It is a bottleneck when a search tree wants to clone a battle, try every joint action and step each branch. vgc-engine is written for that loop.
Clone a battle, enumerate legal choices per slot, step each joint action, keep the best. Battle has no interior sharing, so a copy is a full independent state.
A pyo3 module, vgc_engine, plugs into Python training and evaluation code. State round-trips through compact bytes or inspectable JSON.
The vgc-solver crate enumerates outcome frontiers and solves small endgames, returning mixed policies for both sides with provenance and node counts. Every solve stops at a hard time budget and still returns a usable answer.
A vgc CLI for damage, speed and EV questions, team legality checks for the Champions regulations, and a differential harness against Showdown.
02 · Inside the engine
The design takes its discipline from pkmn/engine and its answers from Pokémon Showdown, then aims both at modern doubles.
Battle state is a plain fixed-size struct. Handles, not pointers: MonId, MoveId, SideRef. The project rule is no heap allocation inside step(), and an allocation probe over 500 battles reported 0 step allocations at the 2026-10-06 finish-line check.
build.rs turns @pkmn/dex JSON into Rust source for species, moves, items, abilities and the type chart. Champions is a data overlay, not a fork of the code.
Slot targeting, spread-move damage, redirection, the Protect family, weather, terrain, screens, Tailwind and Trick Room live in shared modules. Since 0.5.0, legal_choices can also offer your partner as a target (opt-in ally_targets; off by default because it changes the action space).
Standard Gen 9 doubles with Terastallization, and the Champions overlay (Reg M-B, Reg M-C) with Mega Evolution, Stat Points and Champions move data, selected by format id.
Behind the chance feature, Battle::step_chance() returns every possible next state of one turn with its prior. Today it wraps a record-and-replay pass. Native per-site branching is planned.
The ps-rng feature adds a bit-exact port of Showdown's PRNG with its draw order and a per-draw trace, for seeded differential testing. Off by default, and absent from default builds.
03 · How a frontier is built
This is how vgc-solver enumerates outcomes, replayed on a toy model in your browser. Pick a joint action and step through the four passes.
Loading the toy model…
The four passes match the documented v1 algorithm in vgc-solver: record the visited draw sites, take the cross product of their outcomes, replay each cell with a keyed RNG, and merge by canonical state hash. The toy keeps three site kinds per hit (accuracy, crit, 16 damage rolls). Real turns have more.
04 · Quickstart
There is no PyPI package or GitHub release yet, so you build from source. You need Rust (stable), Python 3.9+ and curl. The data build reads the @pkmn/dex JSON from VGC_DEX_DIR; the steps below fetch the same pinned copy CI uses.
git clone https://github.com/colefoster/vgc-engine.git
cd vgc-engine
# @pkmn/dex JSON for build.rs (localdex 0.1.1, as in CI)
mkdir -p .dex
curl -fsSL https://registry.npmjs.org/localdex/-/localdex-0.1.1.tgz | tar xz -C .dex
export VGC_DEX_DIR="$PWD/.dex/package/data"
# Build and install the Python module (vgc_engine 0.5.0).
# ps-rng enables ps_seed and Showdown sessions; without it they raise.
python3 -m venv .venv
.venv/bin/pip install maturin
.venv/bin/maturin build --release -m crates/vgc-engine-py/Cargo.toml \
--features ps-rng -i .venv/bin/python -o dist
.venv/bin/pip install dist/vgc_engine-0.5.0-*.whl
.venv/bin/python -c "import vgc_engine; print('ok')"
# Optional: the vgc CLI
cargo build --release -p vgc-engine-cli --bin vgc
./target/release/vgc calc chomp rillaboom eq
# After the Install tab, inside .venv. Choices are still plain tuples.
import json, vgc_engine
team = json.dumps([
{"species": "rillaboom", "moves": ["protect"]},
] * 2)
b = vgc_engine.Battle.from_teams(team, team,
format="gen9doublescustomgame", seed=7)
# Choices are (kind, actor_slot, move_or_team_idx, target_side, target_slot)
p1 = [b.legal_choices(0, s)[0] for s in (0, 1)]
p2 = [b.legal_choices(1, s)[0] for s in (0, 1)]
branch = b.clone() # independent copy, no shared state
result = branch.step_move(p1, p2) # "continue" | "p1_win" | "p2_win" | "tie"
snapshot = branch.to_bytes() # compact; to_json() for inspection
again = vgc_engine.Battle.from_bytes(snapshot)
import vgc_engine
# Anytime endgame solve under a hard 1.5 s budget; returns mixed policies.
sol = vgc_engine.solve_endgame_anytime(b, time_budget_ms=1500)
sol["value"], sol["provenance"], sol["depth_completed"]
sol["truncated"], sol["truncation_reason"] # best-so-far if over budget
row_action = sol["row_joint_policy"][0][0] # an executable joint choice
# One budget across K sampled opponent sets; own action by EV
w = vgc_engine.solve_endgame_worlds([b, b.clone()], side="p1", time_budget_ms=1500)
w["chosen"], w["chosen_ev_own"], w["robust"]
# Exact search with depth and node limits (also deadline-bounded)
sol = vgc_engine.solve_endgame_exact(b, max_depth=1, node_budget=1_000)
# Or plug in your own batched leaf evaluator (a model, a heuristic…)
sol = vgc_engine.solve_endgame(b, lambda obs: [0.0] * len(obs))
# Champions team legality from a Showdown export ([] means legal)
problems = vgc_engine.parse_and_verify(paste_text, format="regmc")
# Quick damage range
r = vgc_engine.calc("chomp", "rillaboom", "eq")
r["min"], r["max"]
# Test the default workspace, as CI does (needs VGC_DEX_DIR)
cargo test --workspace --exclude vgc-engine-py
# The `vgc` fast-calc front end: terse specs and aliases
cargo run --release -p vgc-engine-cli --bin vgc -- calc chomp rillaboom eq
cargo run --release -p vgc-engine-cli --bin vgc -- survives chomp lando eq
cargo run --release -p vgc-engine-cli --bin vgc -- outspeeds chomp lando
# Rebuild a position from a real replay, or run a local Showdown-protocol sim
cargo run --release -p vgc-engine-cli --bin vgc -- recon --log replay.json --turn 5
cargo run --release -p vgc-engine-cli --bin vgc -- sim
# Opt-in instrumentation lives behind Cargo features
cargo test -p vgc-engine-core --features chance
cargo test -p vgc-engine-core --features ps-rng
Verified on macOS arm64 with Python 3.9 and 3.12 from a fresh clone of main at 0.5.0 (2026-10-24). On Linux, CI runs cargo test, and wheels for x86_64 serving hosts are built natively with --features ps-rng.
Full build notes, the Python API and protocol docs live in the repo.
Read the README on GitHub05 · What you can do with it today
Clone, step joint actions, read HP and terminal results.
Battle.clone · step_move · legal_choicesEnumerate every next state of a turn with its prior, deduplicated.
step_chance · vgc-solverSolve for mixed policies under a hard deadline, alone or across sampled opponent sets.
solve_endgame_anytime · solve_endgame_worlds · solve_endgame_exactRebuild the position at turn N of a Showdown replay, filling hidden sets from a usage prior.
position_from_replay · from_position · vgc reconPersist states mid-game and restore them elsewhere.
to_bytes · from_bytes · to_jsonCheck a Showdown export against Champions Reg M-B or Reg M-C rules.
parse_and_verifyRanges, survival, speed tiers, best move, matchups, EV thresholds.
vgc calc · survives · outspeeds · ko-evs06 · Evidence ledger
Every number here comes from a document in the repo, labelled by when it was measured. The headline: about 98–99% of unseen real Reg M-C battles replay turn for turn exactly as on Showdown. That is good enough for local self-play. The original phase gates are still formally open, and Showdown stays the final evaluator.
legal_choices, checked against Showdown on 7 positions. Off by default.to_bytes / from_bytes round-trips again.main since 0.5.0replay_budget are checked before every simulated turn on every solver path; panics are caught.truncated with a reason, never nothing.Only 16.5% of real Reg M-C battles were fully clean under forced RNG then. Most divergences were deterministic mechanics bugs, not RNG, and the PRNG port was already bit-exact. That set the fix-one-mechanic-at-a-time workflow. Read the study
| Phase | Gate | Status |
|---|---|---|
| 1 · Skeleton | Workspace, codegen, Python import, corpus report | Done |
| 2 · Top mechanics | 1,000-replay differential vs Showdown, turn-agreement thresholds | Open |
| 3 · Long tail | ≥95% turn agreement on a ≥10k-battle corpus; Tera; Champions | Open |
| 4 · Perf & integration | Replace Showdown in one consumer rollout path, with a measured ≥10× speedup | Awaiting decision |
The Phase 2 and 3 gates name a replay corpus that isn't available, so they haven't been formally run. The 1,300-battle samples above are the closest measurement, not a substitute. Tera and the Champions overlay landed ahead of the Phase 3 gate; phase numbers follow gates, not features. See PLAN.md and the finish-line checklist.
07 · Roadmap
The long-term goal is one shared codebase across all generations and formats. Today, only Gen 9 doubles is being built.
simulate-battle stdio simchance frontier (record/replay wrapper)ps-rng Showdown PRNG mode08 · Straight answers
For local self-play, the 2026-10-11 accuracy report says yes: about 98–99% of unseen real Reg M-C battles replay turn for turn identically. As the final evaluator, not yet. The remaining errors cluster in rare interactions, and a policy trained only on engine self-play could learn to exploit one. Keep a periodic check against Showdown, and treat Showdown as ground truth.
Not on the evidence so far. In pre-registered tests in a downstream bot project, overriding a trained play model with the solver's endgame choice showed no measurable win-rate change. What the solver does guarantee is a correct answer for the position it was given, inside a hard time budget.
The design targets order-of-magnitude speedups for rollouts, and that is what the Phase 4 gate will measure. No speedup is claimed here until a representative workload is agreed and re-measured.
Gen 9 doubles: the standard Tera-legal rules and the Champions overlay (Reg M-B, Reg M-C). Earlier generations have not been started. Singles exists as a format value, but doubles is the target.
Yes, for testing. Build with ps-rng and seed a battle with a Showdown seed string. Draw order is aligned at the call sites the engine can mirror, with a trace of every draw.
Not yet. There is no PyPI package or GitHub release; build the wheel from source as in the quickstart.
MIT or Apache-2.0, at your option. Built binaries include data from @pkmn/dex and Pokémon Showdown (both MIT); see NOTICE and ATTRIBUTION.md.