Add Rating

pithy add rating

No required prerequisites. Add auth if it is not already there — ratings bind to a player, so without it every route denies.

What lands in your repo

apps/<worker>/pithy.config.ts gains the registration, with an example game you are meant to replace:

rating({
  games: [
    {
      key: "duel",
      algorithm: "elo",
      xp: { win: 20, draw: 10, loss: 5 },
    },
  ],
  serverAuthoritative: true,
  basePath: "/rating",
  recordScope: "rating:record",
}),

apps/<worker>/wrangler.jsonc gains the DB binding, in every environment stanza. That is the only binding — no Durable Object, no Workflow, no namespace.

One migration runs, creating the rating and experience tables.

Declaring a game

Each entry declares a rated game, and the key names the pool its ratings live in.

games: [
  { key: "chess", algorithm: "glicko", xp: { win: 25, draw: 12, loss: 5 } },
  { key: "team-brawl", algorithm: "trueskill", xp: { win: 30, draw: 15, loss: 8 } },
],

Pick the algorithm against your roster size. elo and glicko are 1v1 only; trueskill is the one that rates teams. Wiring a 1v1 algorithm to a multi-player game fails on deploy rather than on the first recorded result — a ladder that silently produces nonsense for a month is the failure that check exists to prevent.

A game key is effectively permanent. It names the pool, and the pool is stored on every rating row.

Choosing between elo and glicko

elo is transparent, has one tunable, and is the algorithm your players already understand. When somebody asks why their rating moved by 16, you can answer.

glicko carries an uncertainty term alongside the rating, so a player returning after three months is rated as we are less sure about this number rather than as if they never left. It is better, and it is harder to explain.

Pick elo unless you have a reason. The reason usually arrives with your first wave of returning players.

The experience award is yours

xp is what an outcome is worth, per result. It is not derived from the rating change and does not fall — that is the point of having two numbers.

Draws award something rather than nothing in the example, deliberately: a game where a draw is worth zero is a game where players avoid drawing positions rather than playing them.

serverAuthoritative is on, and should stay on

With it on, recording an outcome requires the record scope, which you mint for your trusted server’s token and never for a device.

A rating a client can write is a rating a client can invent. Turn it off only if you have decided you accept that, and know that the ladder is then decoration.

Recording your first outcome

From your own route, after your own logic decided how the game resolved:

await rating.record({
  game: "duel",
  outcome: { winner: playerA, loser: playerB },
});

Both numbers move in one call: the skill rating adjusts both ways, and the experience total rises for everybody who played.

Wiring it to matchmaking

Composing matchmaking beside this needs no wiring at all. The queue reads the skill number to bucket waiting players, widening each player’s band the longer they wait until any opponent qualifies.

Without rating composed, that queue buckets by region alone — which works, and matches a strong player against a beginner as readily as anybody else.

Check it worked

pithy doctor reports rating under the Worker’s health, and the algorithm-versus-roster check runs there as well as at deploy. In dev, record an outcome between two seeded users and read both standings back.

ESC