Re: Factor

Factor: the language, the theory, and the practice.

Elo

Monday, October 12, 2026

#games #math

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.