Elo
Monday, October 12, 2026
The Elo rating system provides a method to calculate the relative scores of two players in games like chess. Beating a stronger player should earn you more points than beating a weaker one, and the system adjusts each players ranking afterwards given the actual outcome versus the expected one.
We can implement a small version in Factor with just two words. One calculates the expected score; the other updates both players after a game.
Expected score
Let A and B be the players’ ratings. This formula gives A’s expected
score:
expected = 1 / (1 + 10^((B - A) / 400))
Equal ratings give an expected score of 0.5. A player
rated 400 points above their opponent has an expected score of 10/11,
or about 0.91; the opponent’s expectation is 1/11.
: elo-expected ( rating opponent -- expected )
swap - 400.0 / 10^ 1 + recip ;
Only the difference between the ratings matters: 1600 against 1400 gives the same expectation as 2600 against 2400.
A game scores 1 for a win, 0.5 for a draw, and 0 for a loss.
When draws are possible, expected score means the probability of a win
plus half the probability of a draw.
Updating the ratings
To update the ratings, compare the actual score with the expected score:
delta = K * (score - expected)
A' = A + delta
B' = B - delta
K controls how much ratings change after each game.
Using local variables, we can write this as:
:: elo ( rating-a rating-b score k -- rating-a' rating-b' )
score rating-a rating-b elo-expected - k * :> delta
rating-a delta + rating-b delta - ;
The score is A’s result, we add delta to A’s rating and subtract it from B’s.
A few games
Let’s give it a try:
IN: scratchpad 1500 1500 1 32 elo 2array .
{ 1516.0 1484.0 }
The players started equal, so A’s expected score was 0.5. A won,
scoring 1, and gained 32 * (1 - 0.5) = 16 points. B lost the same
amount. A draw between equally rated players changes neither rating:
IN: scratchpad 1500 1500 0.5 32 elo 2array .
{ 1500.0 1500.0 }
Now try players with different ratings. Each row is a separate game
with K = 32, rounded to two decimal places:
A before |
B before |
A’s result |
A after |
B after |
|---|---|---|---|---|
| 1800 | 1400 | Win | 1802.91 | 1397.09 |
| 1400 | 1800 | Win | 1429.09 | 1770.91 |
| 1400 | 1800 | Draw | 1413.09 | 1786.91 |
The favorite gains about three points for a win; the underdog gains
about 29. For a draw, the 1400-rated player scores 0.5 instead of the
expected 1/11, gaining:
32 * (0.5 - 1/11) = 13.09 points, approximately
The 1800-rated player loses the same amount. A draw is a better result than expected for the weaker player, so their rating goes up.
We can use the updated ratings for the next game. Here A wins, then
the players draw:
IN: scratchpad 1500 1500 1 32 elo
IN: scratchpad 0.5 32 elo
IN: scratchpad "A: %.2f, B: %.2f\n" printf
A: 1514.53, B: 1485.47
A is now rated higher, so the draw brings the ratings closer together.
We round only when printing, keeping the full values between games.
Choosing K
Using K = 16 halves the adjustment for the same ratings and result.
Let’s compare three wins by A with each K value. times repeats the
quotation using the updated ratings:
IN: scratchpad 1500 1500 3 [ 1 16 elo ] times
IN: scratchpad "K = 16: A: %.2f, B: %.2f\n" printf
K = 16: A: 1522.91, B: 1477.09
IN: scratchpad 1500 1500 3 [ 1 32 elo ] times
IN: scratchpad "K = 32: A: %.2f, B: %.2f\n" printf
K = 32: A: 1543.75, B: 1456.25
A larger K makes ratings respond faster, whether to a change in skill or a lucky streak. A smaller K gives steadier ratings that take longer to adjust.
Doubling K doesn’t quite double the total gain. With K = 32, A
becomes the favorite sooner, so later wins earn fewer points.
Game order matters
What if each player wins once, but we reverse the order?
IN: scratchpad 1500 1500 1 32 elo 0 32 elo
IN: scratchpad "Win then loss: A: %.2f, B: %.2f\n" printf
Win then loss: A: 1498.53, B: 1501.47
IN: scratchpad 1500 1500 0 32 elo 1 32 elo
IN: scratchpad "Loss then win: A: %.2f, B: %.2f\n" printf
Loss then win: A: 1501.47, B: 1498.53
A gains 16 points for the first win, then loses slightly more as the
favorite in the second game. Reversing the order reverses the advantage.
A rating period uses the starting ratings for every game and adds up the changes at the end. With a fixed K, game order no longer matters within the period, and both players here would stay at 1500.
This is a small per-game model; FIDE has additional rating rules. You could add placement games for new players or lower their K as they play more games. Mark Glickman’s Glicko system also tracks how uncertain each rating is.
Anyway, kind of fun to experiment with.