You want players to feel rewarded for playing. You also want matches to be fair. So you build a single number that goes up when they play and is used to pick their opponents.
Six months later your most dedicated player has the highest number in the game, gets matched against the actual best players, loses constantly, and stops playing.
The number was doing two jobs and they pull in opposite directions.
Two numbers
pithy add rating gives every player two, per pool, in your own D1:
- MMR — a skill rating. It moves both ways with every result. It is an estimate of how good you are, and it feeds matchmaking.
- XP — an experience total. It only ever rises. It rewards showing up, and it drives rank.
Separating them is the whole design. A player on a losing streak keeps their rank and their sense of progress while their MMR quietly finds the level where their matches are competitive again. One number cannot do that, because rewarding participation and estimating ability are different questions with different correct answers.
The algorithm is a choice, not a hardcode
Three ship built in, and which one is right depends on your game rather than on fashion:
| algorithm | shape | why you would pick it |
|---|---|---|
elo | 1v1 | transparent, one tunable, everybody understands it |
glicko | 1v1 | Glicko-2, carries an uncertainty term |
trueskill | any roster | the only one here that rates teams |
The uncertainty term in Glicko-2 is worth understanding, because it fixes a specific unfairness. A player who has not played for eight months has a rating from a version of themselves that no longer exists. Elo treats that number as current. Glicko-2 tracks how confident it is, so a returning player’s rating moves faster until it has caught up — they are not rated as though they never left.
And if none of the three fit, registerRatingAlgorithm takes your own.
A mismatch fails on deploy
Here is the detail I would want in any library like this.
Elo and Glicko-2 are 1v1 algorithms. They have no meaningful definition for a four-player free-for-all. If you wire one to a game with a four-person roster, you have made a category error.
That fails on deploy, not on the first recorded result.
The difference matters more than it sounds. A runtime failure means the mistake is discovered by a player finishing a match, in production, with a result you now cannot rate. A deploy failure means it is discovered by you, with the config file open, before anything has happened.
Pools
Ratings are per pool, and pools are named.
So several game modes can share one ladder, or each can keep its own. A player’s ranked-duel skill and their casual-brawl skill are separate numbers if you want them to be, and the same number if you do not.
A pool is also what matchmaking buckets its open queue on, which is what makes this the skill source that capability needs. Compose both and the queue pairs on real skill; compose only matchmaking and it pairs on region alone.
A device cannot report that it won
Recording an outcome is server-authoritative by default and requires the rating:record scope.
That sentence is doing a lot of work. If a client can post “I won”, your ladder is fiction within a week — and unlike a cosmetic leaderboard, a corrupted rating pool degrades everyone’s matches, because the matchmaker is now pairing on numbers that mean nothing.
How this differs from a leaderboard
Leaderboards deliberately left this out, and the split is clean:
A leaderboard ranks what has happened. A rating estimates what will.
A leaderboard is a record — these are the top scores this week, and they are a fact. A rating is a prediction — this player is probably about this good, and here is how confident we are. You want both, and you want them to be different tables, because a fact and an estimate should not be stored as though they are the same kind of thing.
What you still own
The algorithm’s tunables are yours, and the defaults will not be right for your game. Elo’s K-factor decides how violently a rating swings; too high and one bad night erases a season, too low and a genuinely improving player never catches up.
Deciding what counts as a result is also yours. A forfeit, a disconnect, a draw against a much stronger opponent — those have to mean something, and only you know what.
Ratings bind to an authenticated player, so compose auth too. Without it, every route denies.