Configure
One entry per game, each naming an algorithm and — if you want a shared ladder — a pool. Rate and rank puts this beside a leaderboard, and the reference has every field.
rating({
games: [
{ key: "duel", algorithm: "elo", xp: { win: 20, draw: 10, loss: 5 } },
{ key: "ffa", algorithm: "trueskill", players: 4, pool: "global" },
],
serverAuthoritative: true,
recordScope: "rating:record",
})Every game is validated at assembly. An unknown algorithm, a roster the algorithm cannot rate, a team format on an algorithm with no team model, or params it refuses — all fail on deploy.
Then pithy migrate. One table, and nothing to provision beyond the database you already have.
Record an outcome
POST /rating/games/duel/outcomes
{ "ranks": { "u1": 1, "u2": 2 } }Finishing place per user id. 1 wins; ties share a place.
The body carries the result and nothing else — the pool comes from config, the algorithm from the pool, the timestamp from the server’s clock.
Read
| Route | |
|---|---|
GET /rating/games/:game/me | The caller’s skill, XP, rank and games |
GET /rating/games/:game/players/:userId | Another player’s |
A hidden rating returns null for skill and everything else as normal.
There is no public surface
A rating with no authenticated player has nothing to key on.
Rating reads core’s auth seam, never the auth package’s internals. Without auth composed every route denies — which is the right failure rather than an open one.
Mount elsewhere with basePath.
Two numbers, and what moves them
| Moves on | Direction | |
|---|---|---|
| Skill | Every real result | Both ways |
| XP | A result against a stranger, or a friend when sharedRoomCounts is on | Up only |
Friends cannot farm each other to climb. Skill still updates either way, because a game against a friend is real evidence of strength.
Levels
levels: [{ key: "bronze", from: 0 }, { key: "gold", from: 2000 }]Worst to best. Classified on read from stored XP, so adding one needs no backfill.
Registering your own algorithm
registerRatingAlgorithm(yourAlgorithm);In your Worker entry, before the capability assembles. Then name its id in a game.
Declare an id, a params schema with defaults, a state schema, the player counts you support, and pure initial, update and skill functions.
Your params and player bounds are validated exactly as the built-ins are.
Feeding matchmaking
Name the pool in a matchmaking game’s skillPool and its queue buckets on that pool’s skill number.
Omit it and the queue buckets on region alone — matchmaking degrades rather than breaking when rating is absent.