Skip to content

Commit

Permalink
Merge pull request #581 from aarew12/elo-fide-table
Browse files Browse the repository at this point in the history
Use FIDE table for elo calculation
  • Loading branch information
ornicar authored Oct 8, 2024
2 parents ea0fc5c + 63694c4 commit 7daecfe
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 19 deletions.
82 changes: 69 additions & 13 deletions core/src/main/scala/Elo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,28 @@ object KFactor extends OpaqueInt[KFactor]:
* */
object Elo extends OpaqueInt[Elo]:

final class Player(val rating: Elo, val kFactor: KFactor)
final class Game(val points: Outcome.Points, val opponentRating: Elo)

/* 8.3.1
* For each game played against a rated player, determine the difference in rating between the player and their opponent.
* A difference in rating of more than 400 points shall be counted for rating purposes as though it were a difference of 400 points. In any tournament, a player may benefit from only one upgrade under this rule, for the game in which the rating difference is greatest. */
def playersRatingDiff(a: Elo, b: Elo): Int =
Math.min(400, Math.max(-400, b - a))
def computeRatingDiff(player: Player, games: Seq[Game]): Int =
computeNewRating(player, games) - player.rating

// https://en.wikipedia.org/wiki/Elo_rating_system#Mathematical_details
def computeNewRating(player: Player, games: Seq[Game]): Elo =
val expectedScore = games.foldMap: game =>
val prd = playersRatingDiff(player.rating, game.opponentRating)
1 / (1 + Math.pow(10, prd / 400d))
getExpectedScore(prd)
val achievedScore = games.foldMap(_.points.value)
val ratingDiff =
Math.round(player.kFactor * (achievedScore - expectedScore)).toInt
Math.round(player.kFactor * (achievedScore - expectedScore))
player.rating + ratingDiff

def computeRatingDiff(player: Player, games: Seq[Game]): Int =
computeNewRating(player, games) - player.rating
/* 8.3.1
* For each game played against a rated player, determine the difference in rating between the player and their opponent.
* A difference in rating of more than 400 points shall be counted for rating purposes as though it were a difference of 400 points. In any tournament, a player may benefit from only one upgrade under this rule, for the game in which the rating difference is greatest. */
def playersRatingDiff(a: Elo, b: Elo): Int =
Math.min(400, Math.max(-400, b - a))

def getExpectedScore(ratingDiff: Int): Float =
val absRatingDiff = ratingDiff.abs
val expectedScore = conversionTableFIDE.getOrElse(absRatingDiff, 0.92f)
if ratingDiff <= 0 then expectedScore else 1.0f - expectedScore

def computePerformanceRating(games: Seq[Game]): Option[Elo] =
val winBonus = 400
Expand All @@ -46,3 +47,58 @@ object Elo extends OpaqueInt[Elo]:
case Outcome.Points.Half => 0
case Outcome.Points.One => 1
(ratings + points * winBonus) / games.size

final class Player(val rating: Elo, val kFactor: KFactor)

final class Game(val points: Outcome.Points, val opponentRating: Elo)

// 8.1.2 FIDE table
val conversionTableFIDE: Map[Int, Float] = List(
3 -> 0.50f,
10 -> 0.51f,
17 -> 0.52f,
25 -> 0.53f,
32 -> 0.54f,
39 -> 0.55f,
46 -> 0.56f,
53 -> 0.57f,
61 -> 0.58f,
68 -> 0.59f,
76 -> 0.60f,
83 -> 0.61f,
91 -> 0.62f,
98 -> 0.63f,
106 -> 0.64f,
113 -> 0.65f,
121 -> 0.66f,
129 -> 0.67f,
137 -> 0.68f,
145 -> 0.69f,
153 -> 0.70f,
162 -> 0.71f,
170 -> 0.72f,
179 -> 0.73f,
188 -> 0.74f,
197 -> 0.75f,
206 -> 0.76f,
215 -> 0.77f,
225 -> 0.78f,
235 -> 0.79f,
245 -> 0.80f,
256 -> 0.81f,
267 -> 0.82f,
278 -> 0.83f,
290 -> 0.84f,
302 -> 0.85f,
315 -> 0.86f,
328 -> 0.87f,
344 -> 0.88f,
357 -> 0.89f,
374 -> 0.90f,
391 -> 0.91f
).foldLeft(0 -> Map.empty[Int, Float]):
case ((low, table), (up, value)) =>
val newTable = table ++
(low to up).view.map(_ -> value).toMap
(up + 1) -> newTable
._2
35 changes: 29 additions & 6 deletions test-kit/src/test/scala/EloTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,38 @@ class EloTest extends ChessTest:
ratingDiff(1500, 40, 1500, One, 20)
ratingDiff(1500, 40, 1500, Zero, -20)
ratingDiff(1500, 40, 1500, Half, 0)
ratingDiff(1500, 40, 1900, One, 36)
ratingDiff(1500, 40, 1900, Zero, -4)
ratingDiff(1500, 40, 1900, Half, 16)
ratingDiff(1500, 40, 2900, One, 36)
ratingDiff(1500, 40, 2900, Zero, -4)
ratingDiff(1500, 40, 2900, Half, 16)
ratingDiff(1500, 40, 1900, One, 37)
ratingDiff(1500, 40, 1900, Zero, -3)
ratingDiff(1500, 40, 1900, Half, 17)
ratingDiff(1500, 40, 2900, One, 37)
ratingDiff(1500, 40, 2900, Zero, -3)
ratingDiff(1500, 40, 2900, Half, 17)
ratingDiff(1500, 40, 1600, One, 26)
ratingDiff(1500, 40, 1600, Zero, -14)
ratingDiff(1500, 40, 1600, Half, 6)
ratingDiff(2000, 40, 1600, One, 3)
ratingDiff(2000, 40, 1600, Zero, -37)
ratingDiff(2000, 40, 1600, Half, -17)
ratingDiff(2000, 40, 1000, One, 3)
ratingDiff(2000, 40, 1000, Zero, -37)
ratingDiff(2000, 40, 1000, Half, -17)
ratingDiff(2000, 40, 1900, One, 14)
ratingDiff(2000, 40, 1900, Zero, -26)
ratingDiff(2000, 40, 1900, Half, -6)

private def expectedScore(ratingDiff: Int, expScore: Float)(using munit.Location) =
assertCloseTo(Elo.getExpectedScore(ratingDiff), expScore, 0.001f)

test("expected score"):
expectedScore(0, 0.5f)
expectedScore(100, 0.36f)
expectedScore(-100, 0.64f)
expectedScore(300, 0.15f)
expectedScore(-300, 0.85f)
expectedScore(400, 0.08f)
expectedScore(-400, 0.92f)
expectedScore(800, 0.08f)
expectedScore(-800, 0.92f)

private def perfRating(games: Seq[Elo.Game], expected: Int)(using munit.Location) =
assertEquals(Elo.computePerformanceRating(games), Some(Elo(expected)))
Expand Down

0 comments on commit 7daecfe

Please sign in to comment.