Radarvan is a statistics-tracking application for Command & Conquer: Generals
Zero Hour. It scrapes published games, parses the resulting replay files (via
the cncstats binary) to extract per-player match data, stores everything in
PostgreSQL, and serves a wide range of statistics — player and general win/loss
records, match details (APM, spending, kill maps), leaderboards, draft
randomization, OpenSkill ratings, pairwise synergy, and an ONNX-based win
predictor — to a React single-page app.
- Backend: FastAPI + SQLAlchemy on PostgreSQL, Pydantic for the wire schema, Alembic for migrations.
- Frontend: React 19 + Vite + Material-UI (MUI), recharts for charts. The
TypeScript API client under
src/api/is auto-generated from the backend's OpenAPI spec (do not edit it by hand). - Storage: Replay
.repfiles and their parsed JSON live in S3 (s3://generals-stats/radarvan/dev/), accessed viafsspec/s3fs. - Win prediction: an N-model ONNX ensemble (
ml_ensemble/) served withonnxruntime- every prediction runs all N replicates and reports the mean plus the spread across them; player ratings use OpenSkill. - Tooling: Python ≥3.14 managed with
uv, linted/formatted withruffand type-checked withmypy(strict); TypeScript in strict mode, formatted and linted with Biome.
The fastest path is the containerized dev stack — Postgres, backend and frontend, all hot-reloading, against a local database you can safely break:
cp .env.example .env # fill in AWS / cncstats / Discord keys
make up # frontend :5173, api :8000, postgres :5433
make db-snapshot # optional: pull production data down...
make db-restore # ...and load it locallySee LOCAL_DEV.md for the full workflow, including how to test a migration before it reaches production.
To run the servers directly on the host instead, install both Python and Node dependencies:
make install # uv sync + npm installThe backend reads configuration from environment variables (set them in your
shell or a .env file):
DATABASE_URL— PostgreSQL connection string (required).DEV=1— relaxes the session cookie so login works over plain HTTP for local development.
Scheduled jobs (scraping, nightly stats, game-night recaps) are not run by the web
server; Heroku Scheduler runs python -m radarvan.jobs <job> on one-off dynos.
Discord OAuth login needs additional variables (DISCORD_CLIENT_ID,
DISCORD_CLIENT_SECRET, DISCORD_REDIRECT_URI, SESSION_SECRET) — see the
local auth.md setup notes (gitignored, not part of the public tree).
alembic upgrade head # apply database migrations
fastapi run radarvan/main.py # start the API server (matches Procfile)npm start # Vite dev server; proxies /api → localhost:8000The Makefile is the canonical entry point for formatting, linting,
type-checking, and CI. Run make help to list every target.
make all— format + auto-fix lint + type-check across both Python and TypeScript (run before pushing)make check— Python lint + format-check + mypymake ts-check— TS format-check + Biome lint + tscmake test—uv run pytest(make test-fastskips the slow ML tests)make build— checks for both languages, thenuv buildmake ci— the same non-mutating gates CI runs
To regenerate the TypeScript API client after changing backend routes or
api_types/, run make gen-client (no server needed). CI fails if src/api/ is
stale.
CLAUDE.md is the comprehensive guide to the codebase — module
layout, data flow, key patterns, and gotchas. Other docs cover specific
subsystems:
SYNERGY_METHODOLOGY.md— the statistical methodology behind the player-synergy stats (wins together minus the rating model's pregame expectations).auth.md— Discord OAuth2 login setup (app registration, env vars, theuserstable migration). Kept as a local, gitignored doc.
Production runs on Heroku (radarvan-5e9c302c60e6.herokuapp.com). The Procfile
launches the FastAPI server, which also serves the built frontend as static
files.
A few binary/data artifacts are intentionally committed to the repo because the deployed Heroku server loads them at runtime and there is no separate artifact store wired up:
mapparse— a map-geometry parser binary thatradarvan/missing_maps.pyandradarvan/map_upload.pyshell out to. Override its location with theMAPPARSE_BINenv var. Provenance below.ml_ensemble/— the N-model win-prediction ensemble (model-*.onnx) and their shared vocabulary (vocab.json), read from the repo root byradarvan/ml_inference.py. Override the directory withML_ENSEMBLE_DIR. Seeml/bootstrap_matrix.pyfor how the replicates are produced.
Model-training code and its heavier dependencies live under ml/ and are
installed only on demand (uv sync --group ml), never in the production app.
Upstream: https://github.com/bill-rich/mapparse. The source is not vendored here — the committed binary is the only copy in this repo, so this section is what makes it reproducible.
| Upstream revision | 673567269893914c9a8c073f1375127e99a38242 |
| Built | 2026-07-04T19:41:54Z |
| Working tree | dirty (vcs.modified=true) — local edits not in the upstream commit |
| Toolchain | go1.26.5, GOOS=linux, GOARCH=amd64, CGO_ENABLED=1 |
| SHA-256 | f1463691d74e439058cde85b4ad960648e582b771de784e35e08d6a527ad0e8c |
tests/test_mapparse_provenance.py pins the hash, so replacing the binary is a
red build rather than a silent change in map geometry. Everything in the table
above except the upstream URL is readable back off the binary itself with
go version -m mapparse, which is the first thing to run if this section and the
file ever disagree.
Two things follow from the dirty build. The upstream commit alone does not
reproduce this binary, so anyone rebuilding needs whatever local changes produced
it — recover them by diffing a clean build's --json output against a stored
jsons/*.json. And a rebuild changes the hash, which is exactly what
MapData.mapparse_bin_hash is for: POST /api/reparse_maps finds the rows parsed
by the older build. Update the table and the pinned hash in the same commit as any
new binary.
Rebuild (once the local changes are recovered):
GOOS=linux GOARCH=amd64 go build -o mapparse .