Route One Tactics Game Guide ← Back to site

How it's built

Solo project — design, engineering, tooling and infrastructure

Route One Tactics is a monster-collecting auto-battler: a deterministic battle engine, a data-driven balance pipeline, authoritative online multiplayer, and three build targets from a single Python codebase. This page is the engineering write-up.

PythonPygameWebAssembly (pygbag) WebSocketsFlaskSQLite GitHub ActionsPyInstallerCI/CD Simulation & balance analysis

Written for people who want to know how it works. You don't need any of it to play. Figures are from the current build and will drift as the project changes.

~40kLines of Python
166Pokémon
18Synergies
16Items
3Build targets
Play it in your browser →

Engineering highlights

The four decisions that shaped the project most.

Separating simulation from rendering

Problem Balancing an auto-battler by playing it is hopeless — a single match takes minutes and produces one noisy data point.

Approach Combat is resolved entirely by a headless engine that takes two teams and emits an event log (damage, abilities, KOs). The battle screen is a renderer replaying that log — it has no say in outcomes. The engine runs with no window and no display driver.

Result A full round-robin across every preset composition — over a thousand battles — runs in about 26 seconds, so balance changes are measured rather than guessed.

Balance as data, with a runtime override layer

Problem Tuning numbers shouldn't require a code change, a rebuild, and a release — but edits also shouldn't corrupt the source of truth.

Approach Stats, synergy breakpoints and item effects live in data modules. A web dashboard reads them, shows real win rates from played matches, and publishes edits as a separate overrides file that is merged in at lookup time. Base data is never rewritten.

Result Any balance patch is one reversible file, and the same override reaches the desktop game, the browser build and the public game guide.

One engine for single-player and authoritative multiplayer

Problem If the server and client resolve battles with different code, they will eventually disagree — and that class of bug is miserable.

Approach Online battles are resolved server-side over WebSockets using the same engine the offline game runs. Boards, items and synergies are serialised through one shared wire format, so there is a single implementation of the rules.

Result Online and offline can't silently diverge. The trade-off is that client and server must deploy together, which the release process enforces.

Retrofitting native-resolution rendering

Problem Every screen was written against a fixed 1280×720 coordinate space and upscaled, which looked soft on a 1440p display. Rewriting the layout of every screen wasn't realistic.

Approach Introduced a scaling indirection: layout code still passes 720p coordinates, and a painter multiplies by the render scale at draw time, rasterising text with correctly-sized fonts and requesting sprites at native pixel size. Around 360 direct draw calls were migrated to it.

Result The UI renders at the display's real resolution. The migration was validated by rendering every screen and diffing against the previous build pixel-by-pixel to prove nothing moved at the original scale.

Correctness and tooling

A game this data-heavy fails in data, not just in code — a unit missing a stat, a synergy referencing an effect the engine doesn't implement, two copies of a data file drifting apart. Those don't raise a neat exception; they crash a battle three rounds in.

So there's a preflight check that runs before builds and exits non-zero if anything is wrong, gating the pipeline. It targets the exact failure classes that have bitten the project before: duplicate unit IDs, abilities whose effect has no engine handler, and API drift between the player and wild-Pokémon classes.

Core modules also carry runnable self-test blocks, so a subsystem can be exercised on its own without launching the game.

A lesson that changed the approach: tuning against the preset AI compositions wasn't enough. Real players converge on whatever is strongest, so balance passes now also search the space of legal boards a player could actually build. Two changes that looked healthy against the presets were caught that way and reverted before shipping.

Architecture

The game is Python and Pygame — no engine, no scene editor, just a main loop and drawing code. An unusual choice at this size, and a deliberate one: full control over every frame, paid for by writing things an engine would give you free.

One codebase produces three targets:

TargetHowUsed for
DesktopPyInstaller (Windows & macOS) The full game, including online play
BrowserCompiled to WebAssembly with pygbag Play instantly, no install
HeadlessNo display driver Balance simulation and automated checks

Server-side, four pieces run independently:

ServiceResponsibility
Game server WebSocket lobbies and authoritative battle resolution
Staging server Identical copy for testing; sleeps when idle, woken on demand
Admin service Flask app: balance dashboard, stats API, SQLite storage
Static site This site and the browser build

From edit to playable

1
Change lands on the staging branch Nothing goes straight to what players see.
2
Preflight check gates the build Non-zero exit blocks the pipeline on bad data.
3
CI rebuilds the browser build and the guide The site's Pokédex, synergy and item pages are generated from the same data the game runs on, so documentation can't drift.
4
Verified on a password-gated staging site Whole site and game, deployed exactly as production is.
5
Merged to production; a tag ships desktop builds Release binaries are built, published, and the in-game update check is pointed at the new version.

Desktop builds deliberately don't run on every commit — they're the expensive step, so they're triggered on demand or by a release tag. A tag containing a hyphen (v1.2.0-uat.1) produces a pre-release that never reaches players; a plain tag (v1.2.0) is the real thing. Standard semver pre-release syntax doing real work in the pipeline.

Art and rendering

Character animations come from the PMD Sprite Collab community project — 191 animated sprite sets, with per-artist attribution listed in game under Settings → Credits.

Everything is pixel art, which makes scaling a correctness problem rather than a taste one: bilinear filtering turns a crisp one-pixel outline into a soft three-pixel smear. Sprites are scaled with nearest-neighbour sampling, snapped to whole-number multiples where possible, and only downscaled with smoothing — the one case where interpolation is the right answer.

Honest status

This is a solo, non-commercial project, still in development. Some things on this site are switched off deliberately rather than left half-working: desktop downloads and trainer statistics are both paused while their pipelines are finished.

The browser version is the real game, not a cut-down demo, and it's playable now.

Play it in your browser →