diff --git a/core/src/main/scala/Elo.scala b/core/src/main/scala/Elo.scala index 06de8bb9..9ff0f9be 100644 --- a/core/src/main/scala/Elo.scala +++ b/core/src/main/scala/Elo.scala @@ -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 @@ -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 diff --git a/test-kit/src/test/scala/EloTest.scala b/test-kit/src/test/scala/EloTest.scala index 1a9e3935..3aa0a78e 100644 --- a/test-kit/src/test/scala/EloTest.scala +++ b/test-kit/src/test/scala/EloTest.scala @@ -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)))