Matchmaking operations
The ladder keeps Bradley–Terry ratings and chooses ranked opponents using a separate scheduler.
packages/server/src/ladder/scheduler.ts is pure; the ladder supplies the available pool, free
match slots, version/map evidence and queue ages. Ratings, combat, wire messages and replay
resolution are unchanged. There is no new database schema.
Selection
Every two seconds, a ranked round considers connected, queued, active bots within their quotas and pacing limits. Human games take priority. Same-owner and house-versus-house edges are excluded. Owner and global capacity are enforced at launch; if an owner's last slot fills, the remaining pool is re-optimized with its unavailable bots removed.
Predictions begin with Bradley–Terry on the queued version's matchmaking rating, including its predecessor prior. Each version pair's score is smoothed toward that prediction using 20 prior games. Each map's score is smoothed toward the pair prediction with another 20 prior games. Only rated, non-void games enter this evidence; draws count as half a win. The history cache is rebuilt from stored matches on restart and updated after a committed result.
Competition quality is the mean of 4*p*(1-p) over the configured maps, using a uniform map mix.
Thus two opposite map-specific blowouts score poorly even if their average expected score is
one half. Information is the mean of 1/sqrt(1 + map_games/20) over those maps. Pair scores add:
- quality, weighted by 2;
- waiting, weighted by 0.5, from the sum of the queue ages divided by 120 seconds (capped at 5);
- recent-game fairness, weighted by 0.25;
- information times quality, weighted by 0.2;
- a 0.3 placement bonus times quality when one rating deviation exceeds 150 and the other's is below 100;
- a penalty up to 0.25 for opponent familiarity in each bot's ten most recent ranked games.
Discovery has a separate allocation: approximately one of every seven ranked starts (14.3%) is reserved before competitive allocation. It considers every rating gap. It greedily favors less-tested pairs, using the same information measure above without multiplying by quality or applying a rating window. Older queue entries and stable bot IDs break ties. New versions start with no pair evidence. When a pair has 2–19 games and its observed score differs from the Bradley–Terry prediction by at least 0.25, its discovery score doubles for follow-up testing. The bonus ends at 20 games; established blowouts still receive coverage but lose priority to less-tested pairs. Maps remain balanced independently of outcomes.
The budget counts matches actually started. Each competitive start earns one credit; a reserved discovery start spends six. A batch can reserve against the starts it is about to make, so credit can temporarily be negative if other launches fail. Empty rounds and rejected launches neither earn nor spend credit. There is no accumulated timer-based quota.
When a pass cannot allocate any competitive match, leftover compatible bots play discovery
matches to avoid being stranded by rating filters. These idle-discovery starts neither earn
nor spend credit. They can increase the total discovery share beyond 14.3%, especially in small
or sparse pools. This is deliberate: the normal discovery budget controls the tradeoff when
competitive alternatives exist. All discovery matches retain same-owner and house-versus-house
exclusions, quotas, capacity and the two-sided immediate-rematch hold.
These tests can reveal A > B, B > C, C > A. Bradley–Terry remains the leaderboard summary;
version-pair and map evidence retains the cycle. Discovery creates opportunities for evidence,
not a guarantee of complete coverage while bots connect, change versions or leave. The simple
coverage policy has no special triangle detector, learned matchup model or second rating system.
The ordinary rating window is 400 points plus 20 per second of the shorter wait, capped at 1200. After 60 seconds, oldest eligible bots are served first against their best competitive available opponents; their own ages widen the window. An established matchup can pass the ordinary window if it has at least 20 games, expected score between 25% and 75%, and map quality at least 0.5. The 1200-point cap applies to competitive allocation. Discovery bypasses both rating filters, while both participants' ten-second immediate-rematch hold applies to every allocation. An overdue bot with no compatible opponent still waits. This policy is not a sixty-second SLA.
The remaining competitive pool is allocated by general-graph maximum-weight matching, using the pinned
MIT edmonds-blossom-fixed runtime dependency. Integer weights avoid floating-point precision
problems; candidates and edges use stable ordering. The free-match-slot constraint is solved inside
the matching problem, rather than truncating its chosen pairs. Exhaustive small-graph tests
check the solver, odd cycles, exclusions, capacity, version history, and overdue handling.
Each version pair plays its least-represented configured map next, with seed-based ties. This choice is independent of expected outcomes. Fighter IDs minimize the participants' combined ranked-seat imbalance; seeds break ties. The engine continues to randomize spawn assignment. Human games keep their map selection and human-first seats.
Traces and shadow mode
Every ranked round writes operator-only JSON Lines to <data>/matchmaking.jsonl, with one
rotated file, <data>/matchmaking.jsonl.1. Each file is limited to 10 MiB. Writes are serialized
asynchronously, flushed on shutdown, and created with mode 0600. Slow storage queues at most
four decisions; the next retained line reports dropped decisions. Fractional diagnostic values
are rounded to six decimal places to keep traces compact; selection uses the original values. Storage errors go to the
server log and do not stop matchmaking. Oversized decisions are reported rather than written.
A line contains at, round, policy, dropped, and attempts. Each attempt records:
decision.candidates: bot IDs, version labels, owners, house flags, matchmaking ratings and deviations, queue ages, recent ranked game counts and opponents;decision.options: configured maps, thresholds, reserved discovery slots and available match slots;decision.edges: competitive exclusions, windows, pair/map forecasts and score components;discoveryholds its separate score, rematch exclusion and early-result follow-up flag;decision.overdueanddecision.pairs: overdue reservations and proposals labelledcompetitive,discoveryoridle-discovery;budget.beforeandbudget.after: discovery credits before allocation and after actual starts;capacityExcluded,selected,started, andskipped: actual launch choices, match IDs, maps, seeds, fighter assignments, and chosen pairs rejected by capacity during the pass.
Traces contain no credentials and are not exposed over the public API. Copy both files over SSH before rotation when reviewing an incident; absence of a round outside their retention window does not establish that no round occurred. Overdue bot priority and the discovery budget reset when the ladder restarts, as does its in-memory queue.
Run npm run arena -- serve --house --matchmaking-shadow on a local ladder to start matches
with the previous greedy policy while recording weighted proposals. ARENA_MATCHMAKING_SHADOW=1
sets the same mode, including through the production compose configuration. Shadow mode keeps
the previous map and fighter ordering; its proposals are diagnostic and do not start games.
Capacity enforcement and leftover-pool re-optimization still apply. Shadow mode accounts for
hypothetical discovery purposes in proposal order, up to the number of legacy matches actually
started. This simulates the budget for diagnostics; legacy starts are not evidence that a proposed
discovery matchup was played.
For a rollout, deploy with shadow mode first, collect representative rounds, and compare proposed
quality, overdue service, discovery coverage across rating gaps and idle eligible bots. Use the
actual started[].pair.purpose fields to measure the active policy's allocation shares. Validate forecasts
on later completed matches. A prospective limited trial is needed to measure observed match
competitiveness and waiting under the new choices: shadow logs cannot supply outcomes for
unplayed alternatives. Turn shadow mode off to activate the weighted policy during an authorized
release. See DEPLOY.md for the deployment and draining procedure.