From d9d4b2a54112154fe46ffd7dca9595f9e2147c69 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 27 Oct 2024 16:41:11 -0500 Subject: [PATCH 01/44] Rate games with first move advantage (fix #6818) --- modules/puzzle/src/main/PuzzleFinisher.scala | 10 +- modules/rating/src/main/Glicko.scala | 5 +- modules/rating/src/main/glicko2/Rating.scala | 4 + .../src/main/glicko2/RatingCalculator.scala | 20 ++-- .../main/glicko2/RatingPeriodResults.scala | 3 +- modules/rating/src/main/glicko2/Result.scala | 48 ++++---- .../src/test/RatingCalculatorTest.scala | 109 +++++++++--------- modules/round/src/main/PerfsUpdater.scala | 44 ++++--- modules/tutor/src/main/TutorGlicko.scala | 20 ++-- modules/tutor/src/test/GlickoTest.scala | 10 +- 10 files changed, 143 insertions(+), 130 deletions(-) diff --git a/modules/puzzle/src/main/PuzzleFinisher.scala b/modules/puzzle/src/main/PuzzleFinisher.scala index fd4cf65e07025..5aecaa8b44315 100644 --- a/modules/puzzle/src/main/PuzzleFinisher.scala +++ b/modules/puzzle/src/main/PuzzleFinisher.scala @@ -199,18 +199,16 @@ final private[puzzle] class PuzzleFinisher( if player.clueless then glicko._1 else glicko._1.average(glicko._2, weightOf(angle, win)) - private val VOLATILITY = lila.rating.Glicko.default.volatility - private val TAU = 0.75d - private val calculator = glicko2.RatingCalculator(VOLATILITY, TAU) + private val calculator = lila.rating.Glicko.system def incPuzzlePlays(puzzleId: PuzzleId): Funit = colls.puzzle.map(_.incFieldUnchecked($id(puzzleId), Puzzle.BSONFields.plays)) private def updateRatings(u1: glicko2.Rating, u2: glicko2.Rating, win: PuzzleWin): Unit = - val results = glicko2.GameRatingPeriodResults( + val results = glicko2.BinaryRatingPeriodResults( List( - if win.yes then glicko2.GameResult(u1, u2, false) - else glicko2.GameResult(u2, u1, false) + if win.yes then glicko2.BinaryResult(u1, u2, false) + else glicko2.BinaryResult(u2, u1, false) ) ) try calculator.updateRatings(results) diff --git a/modules/rating/src/main/Glicko.scala b/modules/rating/src/main/Glicko.scala index cea3b5bb82ea6..f25d3a4f1626b 100644 --- a/modules/rating/src/main/Glicko.scala +++ b/modules/rating/src/main/Glicko.scala @@ -76,8 +76,9 @@ object Glicko: // rating that can be lost or gained with a single game val maxRatingDelta = 700 - val tau = 0.75d - val system = glicko2.RatingCalculator(tau, ratingPeriodsPerDay) + val tau = 0.75d + val system = glicko2.RatingCalculator(tau, ratingPeriodsPerDay) + def calculator(advantage: Double) = glicko2.RatingCalculator(advantage, tau, ratingPeriodsPerDay) def liveDeviation(p: Perf, reverse: Boolean): Double = { system.previewDeviation(p.toRating, nowInstant, reverse) diff --git a/modules/rating/src/main/glicko2/Rating.scala b/modules/rating/src/main/glicko2/Rating.scala index 4d363df832180..d9e45c8479a41 100644 --- a/modules/rating/src/main/glicko2/Rating.scala +++ b/modules/rating/src/main/glicko2/Rating.scala @@ -21,6 +21,10 @@ final class Rating( */ def getGlicko2Rating: Double = convertRatingToGlicko2Scale(this.rating) + def getGlicko2RatingWithAdvantage(advantage: Double): Double = convertRatingToGlicko2Scale( + this.rating + advantage + ) + /** Set the average skill value, taking in a value in Glicko2 scale. */ def setGlicko2Rating(r: Double) = diff --git a/modules/rating/src/main/glicko2/RatingCalculator.scala b/modules/rating/src/main/glicko2/RatingCalculator.scala index f9f49a9e8858b..696088838afdd 100644 --- a/modules/rating/src/main/glicko2/RatingCalculator.scala +++ b/modules/rating/src/main/glicko2/RatingCalculator.scala @@ -20,6 +20,7 @@ object RatingCalculator: (ratingDeviation / MULTIPLIER) final class RatingCalculator( + advantage: Double = 0.0d, tau: Double = 0.75d, ratingPeriodsPerDay: Double = 0 ): @@ -33,10 +34,9 @@ final class RatingCalculator( val ratingPeriodsPerMilli: Double = ratingPeriodsPerDay * DAYS_PER_MILLI - /**

Run through all players within a resultset and calculate their new ratings.

Players within the - * resultset who did not compete during the rating period will have see their deviation increase (in line - * with Prof Glickman's paper).

Note that this method will clear the results held in the association - * resultset.

+ /** Run through all players within a resultset and calculate their new ratings. Players within the resultset + * who did not compete during the rating period will have see their deviation increase (in line with Prof + * Glickman's paper). Note that this method will clear the results held in the association resultset. * * @param results */ @@ -166,13 +166,13 @@ final class RatingCalculator( for result <- results do v = v + ((Math.pow(g(result.getOpponent(player).getGlicko2RatingDeviation), 2)) * E( - player.getGlicko2Rating, - result.getOpponent(player).getGlicko2Rating, + player.getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player)), + result.getOpponent(player).getGlicko2RatingWithAdvantage(-result.getAdvantage(advantage, player)), result.getOpponent(player).getGlicko2RatingDeviation ) * (1.0 - E( - player.getGlicko2Rating, - result.getOpponent(player).getGlicko2Rating, + player.getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player)), + result.getOpponent(player).getGlicko2RatingWithAdvantage(-result.getAdvantage(advantage, player)), result.getOpponent(player).getGlicko2RatingDeviation ))) 1 / v @@ -193,8 +193,8 @@ final class RatingCalculator( outcomeBasedRating = outcomeBasedRating + (g(result.getOpponent(player).getGlicko2RatingDeviation) * (result.getScore(player) - E( - player.getGlicko2Rating, - result.getOpponent(player).getGlicko2Rating, + player.getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player)), + result.getOpponent(player).getGlicko2RatingWithAdvantage(-result.getAdvantage(advantage, player)), result.getOpponent(player).getGlicko2RatingDeviation ))) outcomeBasedRating diff --git a/modules/rating/src/main/glicko2/RatingPeriodResults.scala b/modules/rating/src/main/glicko2/RatingPeriodResults.scala index 04773b9518d92..8c08ca8585827 100644 --- a/modules/rating/src/main/glicko2/RatingPeriodResults.scala +++ b/modules/rating/src/main/glicko2/RatingPeriodResults.scala @@ -8,5 +8,4 @@ trait RatingPeriodResults[R <: Result](): class GameRatingPeriodResults(val results: List[GameResult]) extends RatingPeriodResults[GameResult] -class FloatingRatingPeriodResults(val results: List[FloatingResult]) - extends RatingPeriodResults[FloatingResult] +class BinaryRatingPeriodResults(val results: List[BinaryResult]) extends RatingPeriodResults[BinaryResult] diff --git a/modules/rating/src/main/glicko2/Result.scala b/modules/rating/src/main/glicko2/Result.scala index 7099c7c1b741d..32b5edc6a8813 100644 --- a/modules/rating/src/main/glicko2/Result.scala +++ b/modules/rating/src/main/glicko2/Result.scala @@ -2,6 +2,8 @@ package lila.rating.glicko2 trait Result: + def getAdvantage(advantage: Double, player: Rating): Double + def getScore(player: Rating): Double def getOpponent(player: Rating): Rating @@ -10,42 +12,48 @@ trait Result: def players: List[Rating] -// score from 0 (opponent wins) to 1 (player wins) -class FloatingResult(player: Rating, opponent: Rating, score: Float) extends Result: +class BinaryResult(first: Rating, second: Rating, score: Boolean) extends Result: + private val POINTS_FOR_WIN = 1.0d + private val POINTS_FOR_LOSS = 0.0d + + def getAdvantage(advantage: Double, p: Rating) = 0.0d - def getScore(p: Rating) = if p == player then score else 1 - score + def getScore(p: Rating) = if p == first then if score then POINTS_FOR_WIN else POINTS_FOR_LOSS + else if score then POINTS_FOR_LOSS + else POINTS_FOR_WIN - def getOpponent(p: Rating) = if p == player then opponent else player + def getOpponent(p: Rating) = if p == first then second else first - def participated(p: Rating) = p == player || p == opponent + def participated(p: Rating) = p == first || p == second - def players = List(player, opponent) + def players = List(first, second) -final class GameResult(winner: Rating, loser: Rating, isDraw: Boolean) extends Result: +final class GameResult(first: Rating, second: Rating, outcome: Option[Boolean]) extends Result: private val POINTS_FOR_WIN = 1.0d - private val POINTS_FOR_LOSS = 0.0d private val POINTS_FOR_DRAW = 0.5d + private val POINTS_FOR_LOSS = 0.0d - def players = List(winner, loser) + def players = List(first, second) - def participated(player: Rating) = player == winner || player == loser + def participated(player: Rating) = player == first || player == second + + def getAdvantage(advantage: Double, player: Rating) = + if player == first then advantage / 2.0d else -advantage / 2.0d /** Returns the "score" for a match. * - * @param player * @return - * 1 for a win, 0.5 for a draw and 0 for a loss + * 1 for a first player win, 0.5 for a draw and 0 for a first player loss * @throws IllegalArgumentException */ - def getScore(player: Rating): Double = - if isDraw then POINTS_FOR_DRAW - else if winner == player then POINTS_FOR_WIN - else if loser == player then POINTS_FOR_LOSS - else throw new IllegalArgumentException("Player did not participate in match"); + def getScore(player: Rating): Double = outcome match + case Some(true) => if player == first then POINTS_FOR_WIN else POINTS_FOR_LOSS + case Some(false) => if player == first then POINTS_FOR_LOSS else POINTS_FOR_WIN + case _ => POINTS_FOR_DRAW def getOpponent(player: Rating) = - if winner == player then loser - else if loser == player then winner + if first == player then second + else if second == player then first else throw new IllegalArgumentException("Player did not participate in match"); - override def toString = s"$winner vs $loser = $isDraw" + override def toString = s"$first vs $second = $outcome" diff --git a/modules/rating/src/test/RatingCalculatorTest.scala b/modules/rating/src/test/RatingCalculatorTest.scala index 8a43074414fa1..ecdb7a6d42778 100644 --- a/modules/rating/src/test/RatingCalculatorTest.scala +++ b/modules/rating/src/test/RatingCalculatorTest.scala @@ -8,7 +8,6 @@ import lila.rating.PerfExt.* import glicko2.* class RatingCalculatorTest extends lila.common.LilaTest: - def updateRatings(wRating: Rating, bRating: Rating, winner: Option[Color]) = val result = winner match case Some(chess.White) => Glicko.Result.Win @@ -17,43 +16,43 @@ class RatingCalculatorTest extends lila.common.LilaTest: val results = GameRatingPeriodResults( List( result match - case Glicko.Result.Draw => GameResult(wRating, bRating, true) - case Glicko.Result.Win => GameResult(wRating, bRating, false) - case Glicko.Result.Loss => GameResult(bRating, wRating, false) + case Glicko.Result.Win => GameResult(wRating, bRating, Some(true)) + case Glicko.Result.Loss => GameResult(wRating, bRating, Some(false)) + case Glicko.Result.Draw => GameResult(wRating, bRating, None) ) ) - Glicko.system.updateRatings(results, true) + Glicko.calculator(35.0d).updateRatings(results, true) test("default deviation: white wins") { val wr = default.toRating val br = default.toRating updateRatings(wr, br, White.some) - assertCloseTo(wr.rating, 1741d, 1d) - assertCloseTo(br.rating, 1258d, 1d) - assertCloseTo(wr.ratingDeviation, 396d, 1d) - assertCloseTo(br.ratingDeviation, 396d, 1d) - assertCloseTo(wr.volatility, 0.0899983, 0.00000001d) - assertCloseTo(br.volatility, 0.0899983, 0.0000001d) + assertCloseTo(wr.rating, 1729d, 1d) + assertCloseTo(br.rating, 1271d, 1d) + assertCloseTo(wr.ratingDeviation, 396.9015d, 0.0001d) + assertCloseTo(br.ratingDeviation, 396.9015d, 0.0001d) + assertCloseTo(wr.volatility, 0.0899980, 0.0000001d) + assertCloseTo(br.volatility, 0.0899980, 0.0000001d) } test("default deviation: black wins") { val wr = default.toRating val br = default.toRating updateRatings(wr, br, Black.some) - assertCloseTo(wr.rating, 1258d, 1d) - assertCloseTo(br.rating, 1741d, 1d) - assertCloseTo(wr.ratingDeviation, 396d, 1d) - assertCloseTo(br.ratingDeviation, 396d, 1d) - assertCloseTo(wr.volatility, 0.0899983, 0.00000001d) - assertCloseTo(br.volatility, 0.0899983, 0.0000001d) + assertCloseTo(wr.rating, 1245d, 1d) + assertCloseTo(br.rating, 1755d, 1d) + assertCloseTo(wr.ratingDeviation, 396.9015d, 0.0001d) + assertCloseTo(br.ratingDeviation, 396.9015d, 0.0001d) + assertCloseTo(wr.volatility, 0.0899986, 0.0000001d) + assertCloseTo(br.volatility, 0.0899986, 0.0000001d) } test("default deviation: draw") { val wr = default.toRating val br = default.toRating updateRatings(wr, br, None) - assertCloseTo(wr.rating, 1500d, 1d) - assertCloseTo(br.rating, 1500d, 1d) - assertCloseTo(wr.ratingDeviation, 396d, 1d) - assertCloseTo(br.ratingDeviation, 396d, 1d) + assertCloseTo(wr.rating, 1487d, 1d) + assertCloseTo(br.rating, 1513d, 1d) + assertCloseTo(wr.ratingDeviation, 396.9015d, 0.0001d) + assertCloseTo(br.ratingDeviation, 396.9015d, 0.0001d) assertCloseTo(wr.volatility, 0.0899954, 0.0000001d) assertCloseTo(br.volatility, 0.0899954, 0.0000001d) } @@ -67,10 +66,10 @@ class RatingCalculatorTest extends lila.common.LilaTest: val wr = perf.toRating val br = perf.toRating updateRatings(wr, br, White.some) - assertCloseTo(wr.rating, 1517d, 1d) - assertCloseTo(br.rating, 1482d, 1d) - assertCloseTo(wr.ratingDeviation, 78d, 1d) - assertCloseTo(br.ratingDeviation, 78d, 1d) + assertCloseTo(wr.rating, 1515d, 1d) + assertCloseTo(br.rating, 1485d, 1d) + assertCloseTo(wr.ratingDeviation, 78.0967d, 0.0001d) + assertCloseTo(br.ratingDeviation, 78.0967d, 0.0001d) assertCloseTo(wr.volatility, 0.06, 0.00001d) assertCloseTo(br.volatility, 0.06, 0.00001d) } @@ -78,10 +77,10 @@ class RatingCalculatorTest extends lila.common.LilaTest: val wr = perf.toRating val br = perf.toRating updateRatings(wr, br, Black.some) - assertCloseTo(wr.rating, 1482d, 1d) - assertCloseTo(br.rating, 1517d, 1d) - assertCloseTo(wr.ratingDeviation, 78d, 1d) - assertCloseTo(br.ratingDeviation, 78d, 1d) + assertCloseTo(wr.rating, 1481d, 1d) + assertCloseTo(br.rating, 1519d, 1d) + assertCloseTo(wr.ratingDeviation, 78.0967d, 0.0001d) + assertCloseTo(br.ratingDeviation, 78.0967d, 0.0001d) assertCloseTo(wr.volatility, 0.06, 0.00001d) assertCloseTo(br.volatility, 0.06, 0.00001d) } @@ -89,10 +88,10 @@ class RatingCalculatorTest extends lila.common.LilaTest: val wr = perf.toRating val br = perf.toRating updateRatings(wr, br, None) - assertCloseTo(wr.rating, 1500d, 1d) - assertCloseTo(br.rating, 1500d, 1d) - assertCloseTo(wr.ratingDeviation, 78d, 1d) - assertCloseTo(br.ratingDeviation, 78d, 1d) + assertCloseTo(wr.rating, 1498d, 1d) + assertCloseTo(br.rating, 1502d, 1d) + assertCloseTo(wr.ratingDeviation, 78.0967d, 0.0001d) + assertCloseTo(br.ratingDeviation, 78.0967d, 0.0001d) assertCloseTo(wr.volatility, 0.06, 0.00001d) assertCloseTo(br.volatility, 0.06, 0.00001d) } @@ -116,9 +115,9 @@ class RatingCalculatorTest extends lila.common.LilaTest: val br = bP.toRating updateRatings(wr, br, White.some) assertCloseTo(wr.rating, 1422d, 1d) - assertCloseTo(br.rating, 1506d, 1d) - assertCloseTo(wr.ratingDeviation, 77d, 1d) - assertCloseTo(br.ratingDeviation, 105d, 1d) + assertCloseTo(br.rating, 1509d, 1d) + assertCloseTo(wr.ratingDeviation, 77.3966, 0.0001d) + assertCloseTo(br.ratingDeviation, 105.5926, 0.0001d) assertCloseTo(wr.volatility, 0.06, 0.00001d) assertCloseTo(br.volatility, 0.065, 0.00001d) } @@ -127,9 +126,9 @@ class RatingCalculatorTest extends lila.common.LilaTest: val br = bP.toRating updateRatings(wr, br, Black.some) assertCloseTo(wr.rating, 1389d, 1d) - assertCloseTo(br.rating, 1568d, 1d) - assertCloseTo(wr.ratingDeviation, 78d, 1d) - assertCloseTo(br.ratingDeviation, 105d, 1d) + assertCloseTo(br.rating, 1571d, 1d) + assertCloseTo(wr.ratingDeviation, 77.3966, 0.0001d) + assertCloseTo(br.ratingDeviation, 105.5926, 0.0001d) assertCloseTo(wr.volatility, 0.06, 0.00001d) assertCloseTo(br.volatility, 0.065, 0.00001d) } @@ -137,10 +136,10 @@ class RatingCalculatorTest extends lila.common.LilaTest: val wr = wP.toRating val br = bP.toRating updateRatings(wr, br, None) - assertCloseTo(wr.rating, 1406d, 1d) - assertCloseTo(br.rating, 1537d, 1d) - assertCloseTo(wr.ratingDeviation, 78d, 1d) - assertCloseTo(br.ratingDeviation, 105.87d, 0.01d) + assertCloseTo(wr.rating, 1405d, 1d) + assertCloseTo(br.rating, 1540d, 1d) + assertCloseTo(wr.ratingDeviation, 77.3966, 0.0001d) + assertCloseTo(br.ratingDeviation, 105.5926, 0.0001d) assertCloseTo(wr.volatility, 0.06, 0.00001d) assertCloseTo(br.volatility, 0.065, 0.00001d) } @@ -164,10 +163,10 @@ class RatingCalculatorTest extends lila.common.LilaTest: val wr = wP.toRating val br = bP.toRating updateRatings(wr, br, White.some) - assertCloseTo(wr.rating, 1216.7d, 0.1d) - assertCloseTo(br.rating, 1636d, 0.1d) - assertCloseTo(wr.ratingDeviation, 59.9d, 0.1d) - assertCloseTo(br.ratingDeviation, 196.9d, 0.1d) + assertCloseTo(wr.rating, 1216.6d, 0.1d) + assertCloseTo(br.rating, 1638.4d, 0.1d) + assertCloseTo(wr.ratingDeviation, 59.8839d, 0.0001d) + assertCloseTo(br.ratingDeviation, 196.3840, 0.0001d) assertCloseTo(wr.volatility, 0.053013, 0.000001d) assertCloseTo(br.volatility, 0.062028, 0.000001d) } @@ -175,10 +174,10 @@ class RatingCalculatorTest extends lila.common.LilaTest: val wr = wP.toRating val br = bP.toRating updateRatings(wr, br, Black.some) - assertCloseTo(wr.rating, 1199.3d, 0.1d) - assertCloseTo(br.rating, 1855.4d, 0.1d) - assertCloseTo(wr.ratingDeviation, 59.9d, 0.1d) - assertCloseTo(br.ratingDeviation, 196.9d, 0.1d) + assertCloseTo(wr.rating, 1199.2d, 0.1d) + assertCloseTo(br.rating, 1856.5d, 0.1d) + assertCloseTo(wr.ratingDeviation, 59.8839d, 0.0001d) + assertCloseTo(br.ratingDeviation, 196.3840, 0.0001d) assertCloseTo(wr.volatility, 0.052999, 0.000001d) assertCloseTo(br.volatility, 0.061999, 0.000001d) } @@ -186,10 +185,10 @@ class RatingCalculatorTest extends lila.common.LilaTest: val wr = wP.toRating val br = bP.toRating updateRatings(wr, br, None) - assertCloseTo(wr.rating, 1208.0, 0.1d) - assertCloseTo(br.rating, 1745.7, 0.1d) - assertCloseTo(wr.ratingDeviation, 59.90056, 0.1d) - assertCloseTo(br.ratingDeviation, 196.98729, 0.1d) + assertCloseTo(wr.rating, 1207.9, 0.1d) + assertCloseTo(br.rating, 1747.5, 0.1d) + assertCloseTo(wr.ratingDeviation, 59.8839, 0.0001d) + assertCloseTo(br.ratingDeviation, 196.3840, 0.0001d) assertCloseTo(wr.volatility, 0.053002, 0.000001d) assertCloseTo(br.volatility, 0.062006, 0.000001d) } diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index ad49ff19feb18..2f5791039fb12 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -35,35 +35,35 @@ final class PerfsUpdater( val ratingsB = mkRatings(black.perfs) game.ratingVariant match case chess.variant.Chess960 => - updateRatings(ratingsW.chess960, ratingsB.chess960, game) + updateRatings(35.0d, ratingsW.chess960, ratingsB.chess960, game) case chess.variant.KingOfTheHill => - updateRatings(ratingsW.kingOfTheHill, ratingsB.kingOfTheHill, game) + updateRatings(35.0d, ratingsW.kingOfTheHill, ratingsB.kingOfTheHill, game) case chess.variant.ThreeCheck => - updateRatings(ratingsW.threeCheck, ratingsB.threeCheck, game) + updateRatings(35.0d, ratingsW.threeCheck, ratingsB.threeCheck, game) case chess.variant.Antichess => - updateRatings(ratingsW.antichess, ratingsB.antichess, game) + updateRatings(35.0d, ratingsW.antichess, ratingsB.antichess, game) case chess.variant.Atomic => - updateRatings(ratingsW.atomic, ratingsB.atomic, game) + updateRatings(35.0d, ratingsW.atomic, ratingsB.atomic, game) case chess.variant.Horde => - updateRatings(ratingsW.horde, ratingsB.horde, game) + updateRatings(0.0d, ratingsW.horde, ratingsB.horde, game) case chess.variant.RacingKings => - updateRatings(ratingsW.racingKings, ratingsB.racingKings, game) + updateRatings(0.0d, ratingsW.racingKings, ratingsB.racingKings, game) case chess.variant.Crazyhouse => - updateRatings(ratingsW.crazyhouse, ratingsB.crazyhouse, game) + updateRatings(35.0d, ratingsW.crazyhouse, ratingsB.crazyhouse, game) case chess.variant.Standard => game.speed match case Speed.Bullet => - updateRatings(ratingsW.bullet, ratingsB.bullet, game) + updateRatings(35.0d, ratingsW.bullet, ratingsB.bullet, game) case Speed.Blitz => - updateRatings(ratingsW.blitz, ratingsB.blitz, game) + updateRatings(35.0d, ratingsW.blitz, ratingsB.blitz, game) case Speed.Rapid => - updateRatings(ratingsW.rapid, ratingsB.rapid, game) + updateRatings(35.0d, ratingsW.rapid, ratingsB.rapid, game) case Speed.Classical => - updateRatings(ratingsW.classical, ratingsB.classical, game) + updateRatings(35.0d, ratingsW.classical, ratingsB.classical, game) case Speed.Correspondence => - updateRatings(ratingsW.correspondence, ratingsB.correspondence, game) + updateRatings(35.0d, ratingsW.correspondence, ratingsB.correspondence, game) case Speed.UltraBullet => - updateRatings(ratingsW.ultraBullet, ratingsB.ultraBullet, game) + updateRatings(35.0d, ratingsW.ultraBullet, ratingsB.ultraBullet, game) case _ => val perfsW = mkPerfs(ratingsW, white -> black, game) val perfsB = mkPerfs(ratingsB, black -> white, game) @@ -123,16 +123,22 @@ final class PerfsUpdater( correspondence = perfs.correspondence.toRating ) - private def updateRatings(white: glicko2.Rating, black: glicko2.Rating, game: Game): Unit = + private def updateRatings( + advantage: Double, + white: glicko2.Rating, + black: glicko2.Rating, + game: Game + ): Unit = val results = glicko2.GameRatingPeriodResults( List( game.winnerColor match - case None => glicko2.GameResult(white, black, true) - case Some(chess.White) => glicko2.GameResult(white, black, false) - case Some(chess.Black) => glicko2.GameResult(black, white, false) + case Some(chess.White) => glicko2.GameResult(white, black, Some(true)) + case Some(chess.Black) => glicko2.GameResult(black, white, Some(false)) + case None => glicko2.GameResult(white, black, None) ) ) - try Glicko.system.updateRatings(results, true) + // tuning TAU per game speed may improve accuracy + try Glicko.calculator(advantage).updateRatings(results, true) catch case e: Exception => logger.error(s"update ratings #${game.id}", e) private def mkPerfs(ratings: Ratings, users: PairOf[UserWithPerfs], game: Game): UserPerfs = diff --git a/modules/tutor/src/main/TutorGlicko.scala b/modules/tutor/src/main/TutorGlicko.scala index e99047c74e7c3..3352d48399ef2 100644 --- a/modules/tutor/src/main/TutorGlicko.scala +++ b/modules/tutor/src/main/TutorGlicko.scala @@ -5,22 +5,20 @@ import lila.rating.{ Glicko, glicko2 } object TutorGlicko: - private type Rating = Int - private type Score = Float + private type Rating = Int + private type Outcome = Boolean - private val VOLATILITY = Glicko.default.volatility - private val TAU = 0.75d + private val calculator = lila.rating.Glicko.system - def scoresRating(perf: Perf, scores: List[(Rating, Score)]): Rating = - val calculator = glicko2.RatingCalculator(VOLATILITY, TAU) - val player = perf.toRating - val results = glicko2.FloatingRatingPeriodResults( - scores.map { case (rating, score) => - glicko2.FloatingResult(player, glicko2.Rating(rating, 60, 0.06, 10), score) + def outcomesRating(perf: Perf, outcomes: List[(Rating, Outcome)]): Rating = + val player = perf.toRating + val results = glicko2.BinaryRatingPeriodResults( + outcomes.map { case (rating, outcome) => + glicko2.BinaryResult(player, glicko2.Rating(rating, 60, 0.06, 10), outcome) } ) try calculator.updateRatings(results, true) - catch case e: Exception => logger.error("TutorGlicko.scoresRating", e) + catch case e: Exception => logger.error("TutorGlicko.outcomesRating", e) player.rating.toInt diff --git a/modules/tutor/src/test/GlickoTest.scala b/modules/tutor/src/test/GlickoTest.scala index 3345a102fc020..2b6ca87796eb1 100644 --- a/modules/tutor/src/test/GlickoTest.scala +++ b/modules/tutor/src/test/GlickoTest.scala @@ -2,15 +2,15 @@ package lila.tutor class GlickoTest extends munit.FunSuite: - test("glicko for arbitrary outcomes") { + test("glicko for binary outcomes") { assertEquals( - TutorGlicko.scoresRating( + TutorGlicko.outcomesRating( lila.rating.Perf.default, List( - (1400, 0.8f), - (1700, 0.6f) + (1400, true), + (1600, false) ) ), - 1669 + 1500 ) } From aa180ce172087d53b426ad3855fcb6e7fcf02a7e Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 27 Oct 2024 16:57:38 -0500 Subject: [PATCH 02/44] Code cleanup (reorder parameters) --- modules/rating/src/main/Glicko.scala | 2 +- modules/rating/src/main/glicko2/RatingCalculator.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/rating/src/main/Glicko.scala b/modules/rating/src/main/Glicko.scala index f25d3a4f1626b..93aaee69f7717 100644 --- a/modules/rating/src/main/Glicko.scala +++ b/modules/rating/src/main/Glicko.scala @@ -78,7 +78,7 @@ object Glicko: val tau = 0.75d val system = glicko2.RatingCalculator(tau, ratingPeriodsPerDay) - def calculator(advantage: Double) = glicko2.RatingCalculator(advantage, tau, ratingPeriodsPerDay) + def calculator(advantage: Double) = glicko2.RatingCalculator(tau, ratingPeriodsPerDay, advantage) def liveDeviation(p: Perf, reverse: Boolean): Double = { system.previewDeviation(p.toRating, nowInstant, reverse) diff --git a/modules/rating/src/main/glicko2/RatingCalculator.scala b/modules/rating/src/main/glicko2/RatingCalculator.scala index 696088838afdd..44733a5f4dac1 100644 --- a/modules/rating/src/main/glicko2/RatingCalculator.scala +++ b/modules/rating/src/main/glicko2/RatingCalculator.scala @@ -20,9 +20,9 @@ object RatingCalculator: (ratingDeviation / MULTIPLIER) final class RatingCalculator( - advantage: Double = 0.0d, tau: Double = 0.75d, - ratingPeriodsPerDay: Double = 0 + ratingPeriodsPerDay: Double = 0, + advantage: Double = 0 ): import RatingCalculator.* From 6194f078c6ef1cc2980bf85407fc401ddeb0c1fc Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 27 Oct 2024 17:02:46 -0500 Subject: [PATCH 03/44] Allow advantage even for BinaryResult --- modules/rating/src/main/glicko2/Result.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/rating/src/main/glicko2/Result.scala b/modules/rating/src/main/glicko2/Result.scala index 32b5edc6a8813..4b722956f0c05 100644 --- a/modules/rating/src/main/glicko2/Result.scala +++ b/modules/rating/src/main/glicko2/Result.scala @@ -16,7 +16,8 @@ class BinaryResult(first: Rating, second: Rating, score: Boolean) extends Result private val POINTS_FOR_WIN = 1.0d private val POINTS_FOR_LOSS = 0.0d - def getAdvantage(advantage: Double, p: Rating) = 0.0d + def getAdvantage(advantage: Double, player: Rating) = + if player == first then advantage / 2.0d else -advantage / 2.0d def getScore(p: Rating) = if p == first then if score then POINTS_FOR_WIN else POINTS_FOR_LOSS else if score then POINTS_FOR_LOSS From 1f2aff785fbe7c8b11d07b1aa7949624c87ab9ca Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Mon, 28 Oct 2024 00:53:44 -0500 Subject: [PATCH 04/44] Code cleanup --- modules/rating/src/main/glicko2/Result.scala | 28 +++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/modules/rating/src/main/glicko2/Result.scala b/modules/rating/src/main/glicko2/Result.scala index 4b722956f0c05..c645a9a58a9af 100644 --- a/modules/rating/src/main/glicko2/Result.scala +++ b/modules/rating/src/main/glicko2/Result.scala @@ -2,45 +2,35 @@ package lila.rating.glicko2 trait Result: - def getAdvantage(advantage: Double, player: Rating): Double + val first: Rating + val second: Rating + + def getAdvantage(advantage: Double, player: Rating): Double = + if player == first then advantage / 2.0d else -advantage / 2.0d def getScore(player: Rating): Double def getOpponent(player: Rating): Rating - def participated(player: Rating): Boolean + def participated(player: Rating): Boolean = player == first || player == second - def players: List[Rating] + def players: List[Rating] = List(first, second) -class BinaryResult(first: Rating, second: Rating, score: Boolean) extends Result: +class BinaryResult(val first: Rating, val second: Rating, score: Boolean) extends Result: private val POINTS_FOR_WIN = 1.0d private val POINTS_FOR_LOSS = 0.0d - def getAdvantage(advantage: Double, player: Rating) = - if player == first then advantage / 2.0d else -advantage / 2.0d - def getScore(p: Rating) = if p == first then if score then POINTS_FOR_WIN else POINTS_FOR_LOSS else if score then POINTS_FOR_LOSS else POINTS_FOR_WIN def getOpponent(p: Rating) = if p == first then second else first - def participated(p: Rating) = p == first || p == second - - def players = List(first, second) - -final class GameResult(first: Rating, second: Rating, outcome: Option[Boolean]) extends Result: +final class GameResult(val first: Rating, val second: Rating, outcome: Option[Boolean]) extends Result: private val POINTS_FOR_WIN = 1.0d private val POINTS_FOR_DRAW = 0.5d private val POINTS_FOR_LOSS = 0.0d - def players = List(first, second) - - def participated(player: Rating) = player == first || player == second - - def getAdvantage(advantage: Double, player: Rating) = - if player == first then advantage / 2.0d else -advantage / 2.0d - /** Returns the "score" for a match. * * @return From 31ce96ccf071db7b68418205b19f89dab534bf83 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Mon, 28 Oct 2024 00:57:48 -0500 Subject: [PATCH 05/44] Code cleanup --- modules/rating/src/main/glicko2/RatingPeriodResults.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/rating/src/main/glicko2/RatingPeriodResults.scala b/modules/rating/src/main/glicko2/RatingPeriodResults.scala index 8c08ca8585827..d30f4ce9c87c8 100644 --- a/modules/rating/src/main/glicko2/RatingPeriodResults.scala +++ b/modules/rating/src/main/glicko2/RatingPeriodResults.scala @@ -6,6 +6,6 @@ trait RatingPeriodResults[R <: Result](): def getResults(player: Rating): List[R] = results.filter(_.participated(player)) def getParticipants: Set[Rating] = results.flatMap(_.players).toSet -class GameRatingPeriodResults(val results: List[GameResult]) extends RatingPeriodResults[GameResult] - class BinaryRatingPeriodResults(val results: List[BinaryResult]) extends RatingPeriodResults[BinaryResult] + +class GameRatingPeriodResults(val results: List[GameResult]) extends RatingPeriodResults[GameResult] From d22d638e96ef66d07ccbc0e341059201575a4f77 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Tue, 29 Oct 2024 20:42:51 -0500 Subject: [PATCH 06/44] Reduce first move advantage to 7.786 Elo --- .../src/test/RatingCalculatorTest.scala | 146 +++++++++--------- modules/round/src/main/PerfsUpdater.scala | 24 +-- 2 files changed, 85 insertions(+), 85 deletions(-) diff --git a/modules/rating/src/test/RatingCalculatorTest.scala b/modules/rating/src/test/RatingCalculatorTest.scala index ecdb7a6d42778..5483d8f6b644e 100644 --- a/modules/rating/src/test/RatingCalculatorTest.scala +++ b/modules/rating/src/test/RatingCalculatorTest.scala @@ -21,40 +21,40 @@ class RatingCalculatorTest extends lila.common.LilaTest: case Glicko.Result.Draw => GameResult(wRating, bRating, None) ) ) - Glicko.calculator(35.0d).updateRatings(results, true) + Glicko.calculator(7.786d).updateRatings(results, true) test("default deviation: white wins") { val wr = default.toRating val br = default.toRating updateRatings(wr, br, White.some) - assertCloseTo(wr.rating, 1729d, 1d) - assertCloseTo(br.rating, 1271d, 1d) - assertCloseTo(wr.ratingDeviation, 396.9015d, 0.0001d) - assertCloseTo(br.ratingDeviation, 396.9015d, 0.0001d) - assertCloseTo(wr.volatility, 0.0899980, 0.0000001d) - assertCloseTo(br.volatility, 0.0899980, 0.0000001d) + assertCloseTo(wr.rating, 1739d, 0.5d) + assertCloseTo(br.rating, 1261d, 0.5d) + assertCloseTo(wr.ratingDeviation, 396.7003d, 0.0001d) + assertCloseTo(br.ratingDeviation, 396.7003d, 0.0001d) + assertCloseTo(wr.volatility, 0.09000d, 0.00001d) + assertCloseTo(br.volatility, 0.09000d, 0.00001d) } test("default deviation: black wins") { val wr = default.toRating val br = default.toRating updateRatings(wr, br, Black.some) - assertCloseTo(wr.rating, 1245d, 1d) - assertCloseTo(br.rating, 1755d, 1d) - assertCloseTo(wr.ratingDeviation, 396.9015d, 0.0001d) - assertCloseTo(br.ratingDeviation, 396.9015d, 0.0001d) - assertCloseTo(wr.volatility, 0.0899986, 0.0000001d) - assertCloseTo(br.volatility, 0.0899986, 0.0000001d) + assertCloseTo(wr.rating, 1256d, 0.5d) + assertCloseTo(br.rating, 1744d, 0.5d) + assertCloseTo(wr.ratingDeviation, 396.7003d, 0.0001d) + assertCloseTo(br.ratingDeviation, 396.7003d, 0.0001d) + assertCloseTo(wr.volatility, 0.09000d, 0.00001d) + assertCloseTo(br.volatility, 0.09000d, 0.00001d) } test("default deviation: draw") { val wr = default.toRating val br = default.toRating updateRatings(wr, br, None) - assertCloseTo(wr.rating, 1487d, 1d) - assertCloseTo(br.rating, 1513d, 1d) - assertCloseTo(wr.ratingDeviation, 396.9015d, 0.0001d) - assertCloseTo(br.ratingDeviation, 396.9015d, 0.0001d) - assertCloseTo(wr.volatility, 0.0899954, 0.0000001d) - assertCloseTo(br.volatility, 0.0899954, 0.0000001d) + assertCloseTo(wr.rating, 1497d, 0.5d) + assertCloseTo(br.rating, 1503d, 0.5d) + assertCloseTo(wr.ratingDeviation, 396.7003d, 0.0001d) + assertCloseTo(br.ratingDeviation, 396.7003d, 0.0001d) + assertCloseTo(wr.volatility, 0.09000d, 0.00001d) + assertCloseTo(br.volatility, 0.09000d, 0.00001d) } val perf = Perf.default.copy(glicko = Glicko.default.copy( @@ -66,34 +66,34 @@ class RatingCalculatorTest extends lila.common.LilaTest: val wr = perf.toRating val br = perf.toRating updateRatings(wr, br, White.some) - assertCloseTo(wr.rating, 1515d, 1d) - assertCloseTo(br.rating, 1485d, 1d) - assertCloseTo(wr.ratingDeviation, 78.0967d, 0.0001d) - assertCloseTo(br.ratingDeviation, 78.0967d, 0.0001d) - assertCloseTo(wr.volatility, 0.06, 0.00001d) - assertCloseTo(br.volatility, 0.06, 0.00001d) + assertCloseTo(wr.rating, 1517d, 0.5d) + assertCloseTo(br.rating, 1483d, 0.5d) + assertCloseTo(wr.ratingDeviation, 78.0800d, 0.0001d) + assertCloseTo(br.ratingDeviation, 78.0800d, 0.0001d) + assertCloseTo(wr.volatility, 0.06000d, 0.00001d) + assertCloseTo(br.volatility, 0.06000d, 0.00001d) } test("low deviation: black wins") { val wr = perf.toRating val br = perf.toRating updateRatings(wr, br, Black.some) - assertCloseTo(wr.rating, 1481d, 1d) - assertCloseTo(br.rating, 1519d, 1d) - assertCloseTo(wr.ratingDeviation, 78.0967d, 0.0001d) - assertCloseTo(br.ratingDeviation, 78.0967d, 0.0001d) - assertCloseTo(wr.volatility, 0.06, 0.00001d) - assertCloseTo(br.volatility, 0.06, 0.00001d) + assertCloseTo(wr.rating, 1483d, 0.5d) + assertCloseTo(br.rating, 1517d, 0.5d) + assertCloseTo(wr.ratingDeviation, 78.0800d, 0.0001d) + assertCloseTo(br.ratingDeviation, 78.0800d, 0.0001d) + assertCloseTo(wr.volatility, 0.06000d, 0.00001d) + assertCloseTo(br.volatility, 0.06000d, 0.00001d) } test("low deviation: draw") { val wr = perf.toRating val br = perf.toRating updateRatings(wr, br, None) - assertCloseTo(wr.rating, 1498d, 1d) - assertCloseTo(br.rating, 1502d, 1d) - assertCloseTo(wr.ratingDeviation, 78.0967d, 0.0001d) - assertCloseTo(br.ratingDeviation, 78.0967d, 0.0001d) - assertCloseTo(wr.volatility, 0.06, 0.00001d) - assertCloseTo(br.volatility, 0.06, 0.00001d) + assertCloseTo(wr.rating, 1500d, 0.5d) + assertCloseTo(br.rating, 1500d, 0.5d) + assertCloseTo(wr.ratingDeviation, 78.0800d, 0.0001d) + assertCloseTo(br.ratingDeviation, 78.0800d, 0.0001d) + assertCloseTo(wr.volatility, 0.06000d, 0.00001d) + assertCloseTo(br.volatility, 0.06000d, 0.00001d) } { val wP = Perf.default.copy(glicko = @@ -114,34 +114,34 @@ class RatingCalculatorTest extends lila.common.LilaTest: val wr = wP.toRating val br = bP.toRating updateRatings(wr, br, White.some) - assertCloseTo(wr.rating, 1422d, 1d) - assertCloseTo(br.rating, 1509d, 1d) - assertCloseTo(wr.ratingDeviation, 77.3966, 0.0001d) - assertCloseTo(br.ratingDeviation, 105.5926, 0.0001d) - assertCloseTo(wr.volatility, 0.06, 0.00001d) - assertCloseTo(br.volatility, 0.065, 0.00001d) + assertCloseTo(wr.rating, 1422d, 0.5d) + assertCloseTo(br.rating, 1507d, 0.5d) + assertCloseTo(wr.ratingDeviation, 77.4720d, 0.0001d) + assertCloseTo(br.ratingDeviation, 105.8046d, 0.0001d) + assertCloseTo(wr.volatility, 0.06000d, 0.00001d) + assertCloseTo(br.volatility, 0.06500d, 0.00001d) } test("mixed ratings and deviations: black wins") { val wr = wP.toRating val br = bP.toRating updateRatings(wr, br, Black.some) - assertCloseTo(wr.rating, 1389d, 1d) - assertCloseTo(br.rating, 1571d, 1d) - assertCloseTo(wr.ratingDeviation, 77.3966, 0.0001d) - assertCloseTo(br.ratingDeviation, 105.5926, 0.0001d) - assertCloseTo(wr.volatility, 0.06, 0.00001d) - assertCloseTo(br.volatility, 0.065, 0.00001d) + assertCloseTo(wr.rating, 1390d, 0.5d) + assertCloseTo(br.rating, 1569d, 0.5d) + assertCloseTo(wr.ratingDeviation, 77.4720d, 0.0001d) + assertCloseTo(br.ratingDeviation, 105.8046d, 0.0001d) + assertCloseTo(wr.volatility, 0.06000d, 0.00001d) + assertCloseTo(br.volatility, 0.06500d, 0.00001d) } test("mixed ratings and deviations: draw") { val wr = wP.toRating val br = bP.toRating updateRatings(wr, br, None) - assertCloseTo(wr.rating, 1405d, 1d) - assertCloseTo(br.rating, 1540d, 1d) - assertCloseTo(wr.ratingDeviation, 77.3966, 0.0001d) - assertCloseTo(br.ratingDeviation, 105.5926, 0.0001d) - assertCloseTo(wr.volatility, 0.06, 0.00001d) - assertCloseTo(br.volatility, 0.065, 0.00001d) + assertCloseTo(wr.rating, 1406d, 0.5d) + assertCloseTo(br.rating, 1538d, 0.5d) + assertCloseTo(wr.ratingDeviation, 77.4720d, 0.0001d) + assertCloseTo(br.ratingDeviation, 105.8046d, 0.0001d) + assertCloseTo(wr.volatility, 0.06000d, 0.00001d) + assertCloseTo(br.volatility, 0.06500d, 0.00001d) } } { @@ -163,33 +163,33 @@ class RatingCalculatorTest extends lila.common.LilaTest: val wr = wP.toRating val br = bP.toRating updateRatings(wr, br, White.some) - assertCloseTo(wr.rating, 1216.6d, 0.1d) - assertCloseTo(br.rating, 1638.4d, 0.1d) - assertCloseTo(wr.ratingDeviation, 59.8839d, 0.0001d) - assertCloseTo(br.ratingDeviation, 196.3840, 0.0001d) - assertCloseTo(wr.volatility, 0.053013, 0.000001d) - assertCloseTo(br.volatility, 0.062028, 0.000001d) + assertCloseTo(wr.rating, 1217d, 0.5d) + assertCloseTo(br.rating, 1637d, 0.5d) + assertCloseTo(wr.ratingDeviation, 59.8971d, 0.0001d) + assertCloseTo(br.ratingDeviation, 196.8617d, 0.0001d) + assertCloseTo(wr.volatility, 0.053013d, 0.00001d) + assertCloseTo(br.volatility, 0.062028d, 0.00001d) } test("more mixed ratings and deviations: black wins") { val wr = wP.toRating val br = bP.toRating updateRatings(wr, br, Black.some) - assertCloseTo(wr.rating, 1199.2d, 0.1d) - assertCloseTo(br.rating, 1856.5d, 0.1d) - assertCloseTo(wr.ratingDeviation, 59.8839d, 0.0001d) - assertCloseTo(br.ratingDeviation, 196.3840, 0.0001d) - assertCloseTo(wr.volatility, 0.052999, 0.000001d) - assertCloseTo(br.volatility, 0.061999, 0.000001d) + assertCloseTo(wr.rating, 1199d, 0.5d) + assertCloseTo(br.rating, 1856d, 0.5d) + assertCloseTo(wr.ratingDeviation, 59.8971d, 0.0001d) + assertCloseTo(br.ratingDeviation, 196.8617d, 0.0001d) + assertCloseTo(wr.volatility, 0.052999d, 0.00001d) + assertCloseTo(br.volatility, 0.061999d, 0.00001d) } test("more mixed ratings and deviations: draw") { val wr = wP.toRating val br = bP.toRating updateRatings(wr, br, None) - assertCloseTo(wr.rating, 1207.9, 0.1d) - assertCloseTo(br.rating, 1747.5, 0.1d) - assertCloseTo(wr.ratingDeviation, 59.8839, 0.0001d) - assertCloseTo(br.ratingDeviation, 196.3840, 0.0001d) - assertCloseTo(wr.volatility, 0.053002, 0.000001d) - assertCloseTo(br.volatility, 0.062006, 0.000001d) + assertCloseTo(wr.rating, 1208d, 0.5d) + assertCloseTo(br.rating, 1746d, 0.5d) + assertCloseTo(wr.ratingDeviation, 59.8971d, 0.0001d) + assertCloseTo(br.ratingDeviation, 196.8617d, 0.0001d) + assertCloseTo(wr.volatility, 0.052999d, 0.00001d) + assertCloseTo(br.volatility, 0.062006d, 0.00001d) } } diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index 2f5791039fb12..220dc49920466 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -35,35 +35,35 @@ final class PerfsUpdater( val ratingsB = mkRatings(black.perfs) game.ratingVariant match case chess.variant.Chess960 => - updateRatings(35.0d, ratingsW.chess960, ratingsB.chess960, game) + updateRatings(7.786d, ratingsW.chess960, ratingsB.chess960, game) case chess.variant.KingOfTheHill => - updateRatings(35.0d, ratingsW.kingOfTheHill, ratingsB.kingOfTheHill, game) + updateRatings(7.786d, ratingsW.kingOfTheHill, ratingsB.kingOfTheHill, game) case chess.variant.ThreeCheck => - updateRatings(35.0d, ratingsW.threeCheck, ratingsB.threeCheck, game) + updateRatings(7.786d, ratingsW.threeCheck, ratingsB.threeCheck, game) case chess.variant.Antichess => - updateRatings(35.0d, ratingsW.antichess, ratingsB.antichess, game) + updateRatings(7.786d, ratingsW.antichess, ratingsB.antichess, game) case chess.variant.Atomic => - updateRatings(35.0d, ratingsW.atomic, ratingsB.atomic, game) + updateRatings(7.786d, ratingsW.atomic, ratingsB.atomic, game) case chess.variant.Horde => updateRatings(0.0d, ratingsW.horde, ratingsB.horde, game) case chess.variant.RacingKings => updateRatings(0.0d, ratingsW.racingKings, ratingsB.racingKings, game) case chess.variant.Crazyhouse => - updateRatings(35.0d, ratingsW.crazyhouse, ratingsB.crazyhouse, game) + updateRatings(7.786d, ratingsW.crazyhouse, ratingsB.crazyhouse, game) case chess.variant.Standard => game.speed match case Speed.Bullet => - updateRatings(35.0d, ratingsW.bullet, ratingsB.bullet, game) + updateRatings(7.786d, ratingsW.bullet, ratingsB.bullet, game) case Speed.Blitz => - updateRatings(35.0d, ratingsW.blitz, ratingsB.blitz, game) + updateRatings(7.786d, ratingsW.blitz, ratingsB.blitz, game) case Speed.Rapid => - updateRatings(35.0d, ratingsW.rapid, ratingsB.rapid, game) + updateRatings(7.786d, ratingsW.rapid, ratingsB.rapid, game) case Speed.Classical => - updateRatings(35.0d, ratingsW.classical, ratingsB.classical, game) + updateRatings(7.786d, ratingsW.classical, ratingsB.classical, game) case Speed.Correspondence => - updateRatings(35.0d, ratingsW.correspondence, ratingsB.correspondence, game) + updateRatings(7.786d, ratingsW.correspondence, ratingsB.correspondence, game) case Speed.UltraBullet => - updateRatings(35.0d, ratingsW.ultraBullet, ratingsB.ultraBullet, game) + updateRatings(7.786d, ratingsW.ultraBullet, ratingsB.ultraBullet, game) case _ => val perfsW = mkPerfs(ratingsW, white -> black, game) val perfsB = mkPerfs(ratingsB, black -> white, game) From 3f6bbb61dc8e2e6aafd86df94827c376a8baad9f Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 30 Oct 2024 20:43:52 +0100 Subject: [PATCH 07/44] sbt scalafmtAll --- modules/rating/src/main/Glicko.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/rating/src/main/Glicko.scala b/modules/rating/src/main/Glicko.scala index 270011bf0e995..ed71396fde9e5 100644 --- a/modules/rating/src/main/Glicko.scala +++ b/modules/rating/src/main/Glicko.scala @@ -77,7 +77,8 @@ object Glicko: val maxRatingDelta = 700 val system = glicko2.RatingCalculator(glicko2.Tau.default, ratingPeriodsPerDay) - def calculator(advantage: Double) = glicko2.RatingCalculator(glicko2.Tau.default, ratingPeriodsPerDay, advantage) + def calculator(advantage: Double) = + glicko2.RatingCalculator(glicko2.Tau.default, ratingPeriodsPerDay, advantage) def liveDeviation(p: Perf, reverse: Boolean): Double = { system.previewDeviation(p.toRating, nowInstant, reverse) From 09692bf62d67d02917dab376d51d2cbb8528568e Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Thu, 31 Oct 2024 04:00:07 -0500 Subject: [PATCH 08/44] Remove function getParticipants --- modules/puzzle/src/main/PuzzleFinisher.scala | 3 ++- .../rating/src/main/glicko2/RatingCalculator.scala | 3 +-- .../rating/src/main/glicko2/RatingPeriodResults.scala | 1 - modules/rating/src/main/glicko2/Result.scala | 11 ++--------- modules/rating/src/test/RatingCalculatorTest.scala | 3 ++- modules/round/src/main/PerfsUpdater.scala | 3 ++- modules/tutor/src/main/TutorGlicko.scala | 3 ++- 7 files changed, 11 insertions(+), 16 deletions(-) diff --git a/modules/puzzle/src/main/PuzzleFinisher.scala b/modules/puzzle/src/main/PuzzleFinisher.scala index 3139b7aba533c..693bb9da6be85 100644 --- a/modules/puzzle/src/main/PuzzleFinisher.scala +++ b/modules/puzzle/src/main/PuzzleFinisher.scala @@ -205,11 +205,12 @@ final private[puzzle] class PuzzleFinisher( colls.puzzle.map(_.incFieldUnchecked($id(puzzleId), Puzzle.BSONFields.plays)) private def updateRatings(u1: glicko2.Rating, u2: glicko2.Rating, win: PuzzleWin): Unit = + val ratings = Set(u1, u2) val results = glicko2.BinaryRatingPeriodResults( List( if win.yes then glicko2.BinaryResult(u1, u2, false) else glicko2.BinaryResult(u2, u1, false) ) ) - try calculator.updateRatings(results) + try calculator.updateRatings(ratings, results) catch case e: Exception => logger.error("finisher", e) diff --git a/modules/rating/src/main/glicko2/RatingCalculator.scala b/modules/rating/src/main/glicko2/RatingCalculator.scala index 7bbb8b6859a88..4b7f38017bb0f 100644 --- a/modules/rating/src/main/glicko2/RatingCalculator.scala +++ b/modules/rating/src/main/glicko2/RatingCalculator.scala @@ -48,8 +48,7 @@ final class RatingCalculator( * * @param results */ - def updateRatings(results: RatingPeriodResults[?], skipDeviationIncrease: Boolean = false) = - val players = results.getParticipants + def updateRatings(players: Iterable[Rating], results: RatingPeriodResults[?], skipDeviationIncrease: Boolean = false) = players.foreach { player => val elapsedRatingPeriods = if skipDeviationIncrease then 0 else 1 if results.getResults(player).sizeIs > 0 then diff --git a/modules/rating/src/main/glicko2/RatingPeriodResults.scala b/modules/rating/src/main/glicko2/RatingPeriodResults.scala index d30f4ce9c87c8..475984f09c830 100644 --- a/modules/rating/src/main/glicko2/RatingPeriodResults.scala +++ b/modules/rating/src/main/glicko2/RatingPeriodResults.scala @@ -4,7 +4,6 @@ package lila.rating.glicko2 trait RatingPeriodResults[R <: Result](): val results: List[R] def getResults(player: Rating): List[R] = results.filter(_.participated(player)) - def getParticipants: Set[Rating] = results.flatMap(_.players).toSet class BinaryRatingPeriodResults(val results: List[BinaryResult]) extends RatingPeriodResults[BinaryResult] diff --git a/modules/rating/src/main/glicko2/Result.scala b/modules/rating/src/main/glicko2/Result.scala index c645a9a58a9af..97e9951c58418 100644 --- a/modules/rating/src/main/glicko2/Result.scala +++ b/modules/rating/src/main/glicko2/Result.scala @@ -10,11 +10,11 @@ trait Result: def getScore(player: Rating): Double - def getOpponent(player: Rating): Rating + def getOpponent(player: Rating) = if player == first then second else first def participated(player: Rating): Boolean = player == first || player == second - def players: List[Rating] = List(first, second) + private def players: List[Rating] = List(first, second) class BinaryResult(val first: Rating, val second: Rating, score: Boolean) extends Result: private val POINTS_FOR_WIN = 1.0d @@ -24,8 +24,6 @@ class BinaryResult(val first: Rating, val second: Rating, score: Boolean) extend else if score then POINTS_FOR_LOSS else POINTS_FOR_WIN - def getOpponent(p: Rating) = if p == first then second else first - final class GameResult(val first: Rating, val second: Rating, outcome: Option[Boolean]) extends Result: private val POINTS_FOR_WIN = 1.0d private val POINTS_FOR_DRAW = 0.5d @@ -42,9 +40,4 @@ final class GameResult(val first: Rating, val second: Rating, outcome: Option[Bo case Some(false) => if player == first then POINTS_FOR_LOSS else POINTS_FOR_WIN case _ => POINTS_FOR_DRAW - def getOpponent(player: Rating) = - if first == player then second - else if second == player then first - else throw new IllegalArgumentException("Player did not participate in match"); - override def toString = s"$first vs $second = $outcome" diff --git a/modules/rating/src/test/RatingCalculatorTest.scala b/modules/rating/src/test/RatingCalculatorTest.scala index 5483d8f6b644e..c5ed67bcd45f1 100644 --- a/modules/rating/src/test/RatingCalculatorTest.scala +++ b/modules/rating/src/test/RatingCalculatorTest.scala @@ -9,6 +9,7 @@ import glicko2.* class RatingCalculatorTest extends lila.common.LilaTest: def updateRatings(wRating: Rating, bRating: Rating, winner: Option[Color]) = + val ratings = Set(wRating, bRating) val result = winner match case Some(chess.White) => Glicko.Result.Win case Some(chess.Black) => Glicko.Result.Loss @@ -21,7 +22,7 @@ class RatingCalculatorTest extends lila.common.LilaTest: case Glicko.Result.Draw => GameResult(wRating, bRating, None) ) ) - Glicko.calculator(7.786d).updateRatings(results, true) + Glicko.calculator(7.786d).updateRatings(ratings, results, true) test("default deviation: white wins") { val wr = default.toRating diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index 220dc49920466..ccb4a1a1d34ff 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -129,6 +129,7 @@ final class PerfsUpdater( black: glicko2.Rating, game: Game ): Unit = + val ratings = Set(white, black) val results = glicko2.GameRatingPeriodResults( List( game.winnerColor match @@ -138,7 +139,7 @@ final class PerfsUpdater( ) ) // tuning TAU per game speed may improve accuracy - try Glicko.calculator(advantage).updateRatings(results, true) + try Glicko.calculator(advantage).updateRatings(ratings, results, true) catch case e: Exception => logger.error(s"update ratings #${game.id}", e) private def mkPerfs(ratings: Ratings, users: PairOf[UserWithPerfs], game: Game): UserPerfs = diff --git a/modules/tutor/src/main/TutorGlicko.scala b/modules/tutor/src/main/TutorGlicko.scala index b9fb4a9dc5540..ebf09096f0cf4 100644 --- a/modules/tutor/src/main/TutorGlicko.scala +++ b/modules/tutor/src/main/TutorGlicko.scala @@ -18,7 +18,8 @@ object TutorGlicko: } ) - try calculator.updateRatings(results, true) + // for now (unlike puzzles) exercise ratings are fixed + try calculator.updateRatings(Set(player), results, true) catch case e: Exception => logger.error("TutorGlicko.outcomesRating", e) player.rating.toInt From 697a23b20dbc118e963bc4938a27a6223ff5c006 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Thu, 31 Oct 2024 04:01:25 -0500 Subject: [PATCH 09/44] sbt scalafmtAll --- modules/rating/src/main/glicko2/RatingCalculator.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/rating/src/main/glicko2/RatingCalculator.scala b/modules/rating/src/main/glicko2/RatingCalculator.scala index 4b7f38017bb0f..9e11f2d7e04c4 100644 --- a/modules/rating/src/main/glicko2/RatingCalculator.scala +++ b/modules/rating/src/main/glicko2/RatingCalculator.scala @@ -48,7 +48,11 @@ final class RatingCalculator( * * @param results */ - def updateRatings(players: Iterable[Rating], results: RatingPeriodResults[?], skipDeviationIncrease: Boolean = false) = + def updateRatings( + players: Iterable[Rating], + results: RatingPeriodResults[?], + skipDeviationIncrease: Boolean = false + ) = players.foreach { player => val elapsedRatingPeriods = if skipDeviationIncrease then 0 else 1 if results.getResults(player).sizeIs > 0 then From da2b5259819c2bb68c75b57b723a3c4c7cfbcd7f Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Thu, 31 Oct 2024 10:27:04 +0100 Subject: [PATCH 10/44] scala tweaks while reviewing --- .../rating/src/main/glicko2/RatingPeriodResults.scala | 5 +++-- modules/rating/src/main/glicko2/Result.scala | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/rating/src/main/glicko2/RatingPeriodResults.scala b/modules/rating/src/main/glicko2/RatingPeriodResults.scala index 475984f09c830..f24169e642619 100644 --- a/modules/rating/src/main/glicko2/RatingPeriodResults.scala +++ b/modules/rating/src/main/glicko2/RatingPeriodResults.scala @@ -5,6 +5,7 @@ trait RatingPeriodResults[R <: Result](): val results: List[R] def getResults(player: Rating): List[R] = results.filter(_.participated(player)) -class BinaryRatingPeriodResults(val results: List[BinaryResult]) extends RatingPeriodResults[BinaryResult] +final class BinaryRatingPeriodResults(val results: List[BinaryResult]) + extends RatingPeriodResults[BinaryResult] -class GameRatingPeriodResults(val results: List[GameResult]) extends RatingPeriodResults[GameResult] +final class GameRatingPeriodResults(val results: List[GameResult]) extends RatingPeriodResults[GameResult] diff --git a/modules/rating/src/main/glicko2/Result.scala b/modules/rating/src/main/glicko2/Result.scala index 97e9951c58418..0b6a5ad2fccbb 100644 --- a/modules/rating/src/main/glicko2/Result.scala +++ b/modules/rating/src/main/glicko2/Result.scala @@ -16,13 +16,14 @@ trait Result: private def players: List[Rating] = List(first, second) -class BinaryResult(val first: Rating, val second: Rating, score: Boolean) extends Result: +final class BinaryResult(val first: Rating, val second: Rating, score: Boolean) extends Result: private val POINTS_FOR_WIN = 1.0d private val POINTS_FOR_LOSS = 0.0d - def getScore(p: Rating) = if p == first then if score then POINTS_FOR_WIN else POINTS_FOR_LOSS - else if score then POINTS_FOR_LOSS - else POINTS_FOR_WIN + def getScore(p: Rating) = + if p == first then if score then POINTS_FOR_WIN else POINTS_FOR_LOSS + else if score then POINTS_FOR_LOSS + else POINTS_FOR_WIN final class GameResult(val first: Rating, val second: Rating, outcome: Option[Boolean]) extends Result: private val POINTS_FOR_WIN = 1.0d From ef022721fe3023c760ed3eab435175f39d7cfcc3 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Thu, 31 Oct 2024 10:44:57 +0100 Subject: [PATCH 11/44] type safety for glicko2.ColorAdvantage --- modules/rating/src/main/Glicko.scala | 3 ++- modules/rating/src/main/glicko2/Rating.scala | 4 ++-- .../src/main/glicko2/RatingCalculator.scala | 20 +++++++++++++++---- modules/rating/src/main/glicko2/Result.scala | 4 ++-- .../src/test/RatingCalculatorTest.scala | 2 +- modules/round/src/main/PerfsUpdater.scala | 2 +- 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/modules/rating/src/main/Glicko.scala b/modules/rating/src/main/Glicko.scala index ed71396fde9e5..32ed686b94744 100644 --- a/modules/rating/src/main/Glicko.scala +++ b/modules/rating/src/main/Glicko.scala @@ -6,6 +6,7 @@ import lila.core.perf.Perf import lila.core.rating.Glicko import lila.db.BSON import lila.rating.PerfExt.* +import lila.rating.glicko2.ColorAdvantage object GlickoExt: @@ -77,7 +78,7 @@ object Glicko: val maxRatingDelta = 700 val system = glicko2.RatingCalculator(glicko2.Tau.default, ratingPeriodsPerDay) - def calculator(advantage: Double) = + def calculator(advantage: ColorAdvantage) = glicko2.RatingCalculator(glicko2.Tau.default, ratingPeriodsPerDay, advantage) def liveDeviation(p: Perf, reverse: Boolean): Double = { diff --git a/modules/rating/src/main/glicko2/Rating.scala b/modules/rating/src/main/glicko2/Rating.scala index d9e45c8479a41..c992c6f0b7f47 100644 --- a/modules/rating/src/main/glicko2/Rating.scala +++ b/modules/rating/src/main/glicko2/Rating.scala @@ -21,8 +21,8 @@ final class Rating( */ def getGlicko2Rating: Double = convertRatingToGlicko2Scale(this.rating) - def getGlicko2RatingWithAdvantage(advantage: Double): Double = convertRatingToGlicko2Scale( - this.rating + advantage + def getGlicko2RatingWithAdvantage(advantage: ColorAdvantage): Double = convertRatingToGlicko2Scale( + this.rating + advantage.value ) /** Set the average skill value, taking in a value in Glicko2 scale. diff --git a/modules/rating/src/main/glicko2/RatingCalculator.scala b/modules/rating/src/main/glicko2/RatingCalculator.scala index 9e11f2d7e04c4..1dc1235bab1c2 100644 --- a/modules/rating/src/main/glicko2/RatingCalculator.scala +++ b/modules/rating/src/main/glicko2/RatingCalculator.scala @@ -5,6 +5,12 @@ opaque type Tau = Double object Tau extends OpaqueDouble[Tau]: val default: Tau = 0.75d +opaque type ColorAdvantage = Double +object ColorAdvantage extends OpaqueDouble[ColorAdvantage]: + val zero: ColorAdvantage = 0d + val standard: ColorAdvantage = 7.786d + extension (c: ColorAdvantage) def negate: ColorAdvantage = -c + opaque type RatingPeriodsPerDay = Double object RatingPeriodsPerDay extends OpaqueDouble[RatingPeriodsPerDay]: val default: RatingPeriodsPerDay = 0d @@ -30,7 +36,7 @@ object RatingCalculator: final class RatingCalculator( tau: Tau = Tau.default, ratingPeriodsPerDay: RatingPeriodsPerDay = RatingPeriodsPerDay.default, - advantage: Double = 0 + advantage: ColorAdvantage = ColorAdvantage.zero ): import RatingCalculator.* @@ -179,12 +185,16 @@ final class RatingCalculator( v = v + ((Math.pow(g(result.getOpponent(player).getGlicko2RatingDeviation), 2)) * E( player.getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player)), - result.getOpponent(player).getGlicko2RatingWithAdvantage(-result.getAdvantage(advantage, player)), + result + .getOpponent(player) + .getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player).negate), result.getOpponent(player).getGlicko2RatingDeviation ) * (1.0 - E( player.getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player)), - result.getOpponent(player).getGlicko2RatingWithAdvantage(-result.getAdvantage(advantage, player)), + result + .getOpponent(player) + .getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player).negate), result.getOpponent(player).getGlicko2RatingDeviation ))) 1 / v @@ -206,7 +216,9 @@ final class RatingCalculator( + (g(result.getOpponent(player).getGlicko2RatingDeviation) * (result.getScore(player) - E( player.getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player)), - result.getOpponent(player).getGlicko2RatingWithAdvantage(-result.getAdvantage(advantage, player)), + result + .getOpponent(player) + .getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player).negate), result.getOpponent(player).getGlicko2RatingDeviation ))) outcomeBasedRating diff --git a/modules/rating/src/main/glicko2/Result.scala b/modules/rating/src/main/glicko2/Result.scala index 0b6a5ad2fccbb..ce51a01f8678e 100644 --- a/modules/rating/src/main/glicko2/Result.scala +++ b/modules/rating/src/main/glicko2/Result.scala @@ -5,8 +5,8 @@ trait Result: val first: Rating val second: Rating - def getAdvantage(advantage: Double, player: Rating): Double = - if player == first then advantage / 2.0d else -advantage / 2.0d + def getAdvantage(advantage: ColorAdvantage, player: Rating): ColorAdvantage = + if player == first then advantage.map(_ / 2.0d) else advantage.map(_ / 2.0d).negate def getScore(player: Rating): Double diff --git a/modules/rating/src/test/RatingCalculatorTest.scala b/modules/rating/src/test/RatingCalculatorTest.scala index c5ed67bcd45f1..19fcda48005d0 100644 --- a/modules/rating/src/test/RatingCalculatorTest.scala +++ b/modules/rating/src/test/RatingCalculatorTest.scala @@ -22,7 +22,7 @@ class RatingCalculatorTest extends lila.common.LilaTest: case Glicko.Result.Draw => GameResult(wRating, bRating, None) ) ) - Glicko.calculator(7.786d).updateRatings(ratings, results, true) + Glicko.calculator(ColorAdvantage.standard).updateRatings(ratings, results, true) test("default deviation: white wins") { val wr = default.toRating diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index ccb4a1a1d34ff..ebdde80d0927b 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -139,7 +139,7 @@ final class PerfsUpdater( ) ) // tuning TAU per game speed may improve accuracy - try Glicko.calculator(advantage).updateRatings(ratings, results, true) + try Glicko.calculator(glicko2.ColorAdvantage.standard).updateRatings(ratings, results, true) catch case e: Exception => logger.error(s"update ratings #${game.id}", e) private def mkPerfs(ratings: Ratings, users: PairOf[UserWithPerfs], game: Game): UserPerfs = From d31bc4adcdcda1a139ac5215455f5f4344922def Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Thu, 31 Oct 2024 09:34:08 -0500 Subject: [PATCH 12/44] Rename GameResult to DuelResult --- modules/rating/src/main/glicko2/RatingPeriodResults.scala | 2 +- modules/rating/src/main/glicko2/Result.scala | 2 +- modules/rating/src/test/RatingCalculatorTest.scala | 6 +++--- modules/round/src/main/PerfsUpdater.scala | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/rating/src/main/glicko2/RatingPeriodResults.scala b/modules/rating/src/main/glicko2/RatingPeriodResults.scala index f24169e642619..e895246db0465 100644 --- a/modules/rating/src/main/glicko2/RatingPeriodResults.scala +++ b/modules/rating/src/main/glicko2/RatingPeriodResults.scala @@ -8,4 +8,4 @@ trait RatingPeriodResults[R <: Result](): final class BinaryRatingPeriodResults(val results: List[BinaryResult]) extends RatingPeriodResults[BinaryResult] -final class GameRatingPeriodResults(val results: List[GameResult]) extends RatingPeriodResults[GameResult] +final class GameRatingPeriodResults(val results: List[DuelResult]) extends RatingPeriodResults[DuelResult] diff --git a/modules/rating/src/main/glicko2/Result.scala b/modules/rating/src/main/glicko2/Result.scala index ce51a01f8678e..f5be76330064a 100644 --- a/modules/rating/src/main/glicko2/Result.scala +++ b/modules/rating/src/main/glicko2/Result.scala @@ -25,7 +25,7 @@ final class BinaryResult(val first: Rating, val second: Rating, score: Boolean) else if score then POINTS_FOR_LOSS else POINTS_FOR_WIN -final class GameResult(val first: Rating, val second: Rating, outcome: Option[Boolean]) extends Result: +final class DuelResult(val first: Rating, val second: Rating, outcome: Option[Boolean]) extends Result: private val POINTS_FOR_WIN = 1.0d private val POINTS_FOR_DRAW = 0.5d private val POINTS_FOR_LOSS = 0.0d diff --git a/modules/rating/src/test/RatingCalculatorTest.scala b/modules/rating/src/test/RatingCalculatorTest.scala index 19fcda48005d0..58d8a72cc90b2 100644 --- a/modules/rating/src/test/RatingCalculatorTest.scala +++ b/modules/rating/src/test/RatingCalculatorTest.scala @@ -17,9 +17,9 @@ class RatingCalculatorTest extends lila.common.LilaTest: val results = GameRatingPeriodResults( List( result match - case Glicko.Result.Win => GameResult(wRating, bRating, Some(true)) - case Glicko.Result.Loss => GameResult(wRating, bRating, Some(false)) - case Glicko.Result.Draw => GameResult(wRating, bRating, None) + case Glicko.Result.Win => DuelResult(wRating, bRating, Some(true)) + case Glicko.Result.Loss => DuelResult(wRating, bRating, Some(false)) + case Glicko.Result.Draw => DuelResult(wRating, bRating, None) ) ) Glicko.calculator(ColorAdvantage.standard).updateRatings(ratings, results, true) diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index ebdde80d0927b..90bafe31b3e63 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -133,9 +133,9 @@ final class PerfsUpdater( val results = glicko2.GameRatingPeriodResults( List( game.winnerColor match - case Some(chess.White) => glicko2.GameResult(white, black, Some(true)) - case Some(chess.Black) => glicko2.GameResult(black, white, Some(false)) - case None => glicko2.GameResult(white, black, None) + case Some(chess.White) => glicko2.DuelResult(white, black, Some(true)) + case Some(chess.Black) => glicko2.DuelResult(black, white, Some(false)) + case None => glicko2.DuelResult(white, black, None) ) ) // tuning TAU per game speed may improve accuracy From 2742c8c69ff3d7e2df93c81e778c7d6c5e8aba95 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sat, 2 Nov 2024 02:21:27 -0500 Subject: [PATCH 13/44] Define crazyhouse first move advantage as 15.171 Elo --- modules/round/src/main/PerfsUpdater.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index 90bafe31b3e63..f6fbb701a377e 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -49,7 +49,7 @@ final class PerfsUpdater( case chess.variant.RacingKings => updateRatings(0.0d, ratingsW.racingKings, ratingsB.racingKings, game) case chess.variant.Crazyhouse => - updateRatings(7.786d, ratingsW.crazyhouse, ratingsB.crazyhouse, game) + updateRatings(15.171d, ratingsW.crazyhouse, ratingsB.crazyhouse, game) case chess.variant.Standard => game.speed match case Speed.Bullet => From 3e254fdb04754d6b0292a686501f3b2fa7d74982 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sat, 2 Nov 2024 10:35:41 +0100 Subject: [PATCH 14/44] fix round perf updater doesn't use color advantages --- modules/round/src/main/PerfsUpdater.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index f6fbb701a377e..679371896a1ab 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -139,7 +139,7 @@ final class PerfsUpdater( ) ) // tuning TAU per game speed may improve accuracy - try Glicko.calculator(glicko2.ColorAdvantage.standard).updateRatings(ratings, results, true) + try Glicko.calculator(glicko2.ColorAdvantage(advantage)).updateRatings(ratings, results, true) catch case e: Exception => logger.error(s"update ratings #${game.id}", e) private def mkPerfs(ratings: Ratings, users: PairOf[UserWithPerfs], game: Game): UserPerfs = From 8e89323b65115e2e553abb288a03256e6dc4b972 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sat, 2 Nov 2024 10:39:20 +0100 Subject: [PATCH 15/44] tweak scala imports --- modules/round/src/main/PerfsUpdater.scala | 47 ++++++++++++----------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index 679371896a1ab..0f81eed4b6692 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -5,7 +5,8 @@ import chess.{ ByColor, Color, Speed } import lila.core.perf.{ UserPerfs, UserWithPerfs } import lila.rating.GlickoExt.average import lila.rating.PerfExt.{ addOrReset, toRating } -import lila.rating.{ Glicko, PerfType, RatingFactors, RatingRegulator, glicko2 } +import lila.rating.glicko2.{ DuelResult, GameRatingPeriodResults, Rating, ColorAdvantage } +import lila.rating.{ Glicko, PerfType, RatingFactors, RatingRegulator } import lila.user.{ RankingApi, UserApi } final class PerfsUpdater( @@ -89,20 +90,20 @@ final class PerfsUpdater( } private case class Ratings( - chess960: glicko2.Rating, - kingOfTheHill: glicko2.Rating, - threeCheck: glicko2.Rating, - antichess: glicko2.Rating, - atomic: glicko2.Rating, - horde: glicko2.Rating, - racingKings: glicko2.Rating, - crazyhouse: glicko2.Rating, - ultraBullet: glicko2.Rating, - bullet: glicko2.Rating, - blitz: glicko2.Rating, - rapid: glicko2.Rating, - classical: glicko2.Rating, - correspondence: glicko2.Rating + chess960: Rating, + kingOfTheHill: Rating, + threeCheck: Rating, + antichess: Rating, + atomic: Rating, + horde: Rating, + racingKings: Rating, + crazyhouse: Rating, + ultraBullet: Rating, + bullet: Rating, + blitz: Rating, + rapid: Rating, + classical: Rating, + correspondence: Rating ) private def mkRatings(perfs: UserPerfs) = @@ -125,21 +126,21 @@ final class PerfsUpdater( private def updateRatings( advantage: Double, - white: glicko2.Rating, - black: glicko2.Rating, + white: Rating, + black: Rating, game: Game ): Unit = val ratings = Set(white, black) - val results = glicko2.GameRatingPeriodResults( + val results = GameRatingPeriodResults( List( game.winnerColor match - case Some(chess.White) => glicko2.DuelResult(white, black, Some(true)) - case Some(chess.Black) => glicko2.DuelResult(black, white, Some(false)) - case None => glicko2.DuelResult(white, black, None) + case Some(chess.White) => DuelResult(white, black, Some(true)) + case Some(chess.Black) => DuelResult(black, white, Some(false)) + case None => DuelResult(white, black, None) ) ) // tuning TAU per game speed may improve accuracy - try Glicko.calculator(glicko2.ColorAdvantage(advantage)).updateRatings(ratings, results, true) + try Glicko.calculator(ColorAdvantage(advantage)).updateRatings(ratings, results, true) catch case e: Exception => logger.error(s"update ratings #${game.id}", e) private def mkPerfs(ratings: Ratings, users: PairOf[UserWithPerfs], game: Game): UserPerfs = @@ -148,7 +149,7 @@ final class PerfsUpdater( val speed = game.speed val isStd = game.ratingVariant.standard val isHumanVsMachine = player.noBot && opponent.isBot - def addRatingIf(cond: Boolean, perf: Perf, rating: glicko2.Rating) = + def addRatingIf(cond: Boolean, perf: Perf, rating: Rating) = if cond then val p = perf.addOrReset(_.round.error.glicko, s"game ${game.id}")(rating, game.movedAt) if isHumanVsMachine From 657c1e6a405d382b92f3fa49c9d59f36242b52c6 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sat, 2 Nov 2024 10:45:01 +0100 Subject: [PATCH 16/44] move color advantage constants --- .../src/main/glicko2/RatingCalculator.scala | 1 + modules/round/src/main/PerfsUpdater.scala | 35 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/modules/rating/src/main/glicko2/RatingCalculator.scala b/modules/rating/src/main/glicko2/RatingCalculator.scala index 1dc1235bab1c2..a9a4f13923a7c 100644 --- a/modules/rating/src/main/glicko2/RatingCalculator.scala +++ b/modules/rating/src/main/glicko2/RatingCalculator.scala @@ -9,6 +9,7 @@ opaque type ColorAdvantage = Double object ColorAdvantage extends OpaqueDouble[ColorAdvantage]: val zero: ColorAdvantage = 0d val standard: ColorAdvantage = 7.786d + val crazyhouse: ColorAdvantage = 15.171d extension (c: ColorAdvantage) def negate: ColorAdvantage = -c opaque type RatingPeriodsPerDay = Double diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index 0f81eed4b6692..28acb726aec09 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -1,5 +1,4 @@ package lila.round - import chess.{ ByColor, Color, Speed } import lila.core.perf.{ UserPerfs, UserWithPerfs } @@ -36,35 +35,35 @@ final class PerfsUpdater( val ratingsB = mkRatings(black.perfs) game.ratingVariant match case chess.variant.Chess960 => - updateRatings(7.786d, ratingsW.chess960, ratingsB.chess960, game) + updateRatings(ratingsW.chess960, ratingsB.chess960, game) case chess.variant.KingOfTheHill => - updateRatings(7.786d, ratingsW.kingOfTheHill, ratingsB.kingOfTheHill, game) + updateRatings(ratingsW.kingOfTheHill, ratingsB.kingOfTheHill, game) case chess.variant.ThreeCheck => - updateRatings(7.786d, ratingsW.threeCheck, ratingsB.threeCheck, game) + updateRatings(ratingsW.threeCheck, ratingsB.threeCheck, game) case chess.variant.Antichess => - updateRatings(7.786d, ratingsW.antichess, ratingsB.antichess, game) + updateRatings(ratingsW.antichess, ratingsB.antichess, game) case chess.variant.Atomic => - updateRatings(7.786d, ratingsW.atomic, ratingsB.atomic, game) + updateRatings(ratingsW.atomic, ratingsB.atomic, game) case chess.variant.Horde => - updateRatings(0.0d, ratingsW.horde, ratingsB.horde, game) + updateRatings(ratingsW.horde, ratingsB.horde, game, ColorAdvantage.zero) case chess.variant.RacingKings => - updateRatings(0.0d, ratingsW.racingKings, ratingsB.racingKings, game) + updateRatings(ratingsW.racingKings, ratingsB.racingKings, game, ColorAdvantage.zero) case chess.variant.Crazyhouse => - updateRatings(15.171d, ratingsW.crazyhouse, ratingsB.crazyhouse, game) + updateRatings(ratingsW.crazyhouse, ratingsB.crazyhouse, game, ColorAdvantage.crazyhouse) case chess.variant.Standard => game.speed match case Speed.Bullet => - updateRatings(7.786d, ratingsW.bullet, ratingsB.bullet, game) + updateRatings(ratingsW.bullet, ratingsB.bullet, game) case Speed.Blitz => - updateRatings(7.786d, ratingsW.blitz, ratingsB.blitz, game) + updateRatings(ratingsW.blitz, ratingsB.blitz, game) case Speed.Rapid => - updateRatings(7.786d, ratingsW.rapid, ratingsB.rapid, game) + updateRatings(ratingsW.rapid, ratingsB.rapid, game) case Speed.Classical => - updateRatings(7.786d, ratingsW.classical, ratingsB.classical, game) + updateRatings(ratingsW.classical, ratingsB.classical, game) case Speed.Correspondence => - updateRatings(7.786d, ratingsW.correspondence, ratingsB.correspondence, game) + updateRatings(ratingsW.correspondence, ratingsB.correspondence, game) case Speed.UltraBullet => - updateRatings(7.786d, ratingsW.ultraBullet, ratingsB.ultraBullet, game) + updateRatings(ratingsW.ultraBullet, ratingsB.ultraBullet, game) case _ => val perfsW = mkPerfs(ratingsW, white -> black, game) val perfsB = mkPerfs(ratingsB, black -> white, game) @@ -125,10 +124,10 @@ final class PerfsUpdater( ) private def updateRatings( - advantage: Double, white: Rating, black: Rating, - game: Game + game: Game, + advantage: ColorAdvantage = ColorAdvantage.standard ): Unit = val ratings = Set(white, black) val results = GameRatingPeriodResults( @@ -140,7 +139,7 @@ final class PerfsUpdater( ) ) // tuning TAU per game speed may improve accuracy - try Glicko.calculator(ColorAdvantage(advantage)).updateRatings(ratings, results, true) + try Glicko.calculator(advantage).updateRatings(ratings, results, true) catch case e: Exception => logger.error(s"update ratings #${game.id}", e) private def mkPerfs(ratings: Ratings, users: PairOf[UserWithPerfs], game: Game): UserPerfs = From 49a6d9e0d05fde0f7e0887415e525f42a877263a Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 17 Nov 2024 06:59:42 -0600 Subject: [PATCH 17/44] Revert tutor changes (WIP) --- modules/tutor/src/main/TutorGlicko.scala | 23 ++++++++++++----------- modules/tutor/src/test/GlickoTest.scala | 10 +++++----- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/modules/tutor/src/main/TutorGlicko.scala b/modules/tutor/src/main/TutorGlicko.scala index ebf09096f0cf4..e99047c74e7c3 100644 --- a/modules/tutor/src/main/TutorGlicko.scala +++ b/modules/tutor/src/main/TutorGlicko.scala @@ -5,21 +5,22 @@ import lila.rating.{ Glicko, glicko2 } object TutorGlicko: - private type Rating = Int - private type Outcome = Boolean + private type Rating = Int + private type Score = Float - private val calculator = glicko2.RatingCalculator() + private val VOLATILITY = Glicko.default.volatility + private val TAU = 0.75d - def outcomesRating(perf: Perf, outcomes: List[(Rating, Outcome)]): Rating = - val player = perf.toRating - val results = glicko2.BinaryRatingPeriodResults( - outcomes.map { case (rating, outcome) => - glicko2.BinaryResult(player, glicko2.Rating(rating, 60, 0.06, 10), outcome) + def scoresRating(perf: Perf, scores: List[(Rating, Score)]): Rating = + val calculator = glicko2.RatingCalculator(VOLATILITY, TAU) + val player = perf.toRating + val results = glicko2.FloatingRatingPeriodResults( + scores.map { case (rating, score) => + glicko2.FloatingResult(player, glicko2.Rating(rating, 60, 0.06, 10), score) } ) - // for now (unlike puzzles) exercise ratings are fixed - try calculator.updateRatings(Set(player), results, true) - catch case e: Exception => logger.error("TutorGlicko.outcomesRating", e) + try calculator.updateRatings(results, true) + catch case e: Exception => logger.error("TutorGlicko.scoresRating", e) player.rating.toInt diff --git a/modules/tutor/src/test/GlickoTest.scala b/modules/tutor/src/test/GlickoTest.scala index 2b6ca87796eb1..3345a102fc020 100644 --- a/modules/tutor/src/test/GlickoTest.scala +++ b/modules/tutor/src/test/GlickoTest.scala @@ -2,15 +2,15 @@ package lila.tutor class GlickoTest extends munit.FunSuite: - test("glicko for binary outcomes") { + test("glicko for arbitrary outcomes") { assertEquals( - TutorGlicko.outcomesRating( + TutorGlicko.scoresRating( lila.rating.Perf.default, List( - (1400, true), - (1600, false) + (1400, 0.8f), + (1700, 0.6f) ) ), - 1500 + 1669 ) } From 69b6e344766a9039e96970cd61c2883f250b57d1 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 17 Nov 2024 07:16:56 -0600 Subject: [PATCH 18/44] Reintroduce FloatingResult --- .../rating/src/main/glicko2/RatingPeriodResults.scala | 3 +++ modules/rating/src/main/glicko2/Result.scala | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/modules/rating/src/main/glicko2/RatingPeriodResults.scala b/modules/rating/src/main/glicko2/RatingPeriodResults.scala index e895246db0465..12bff956b8fd8 100644 --- a/modules/rating/src/main/glicko2/RatingPeriodResults.scala +++ b/modules/rating/src/main/glicko2/RatingPeriodResults.scala @@ -8,4 +8,7 @@ trait RatingPeriodResults[R <: Result](): final class BinaryRatingPeriodResults(val results: List[BinaryResult]) extends RatingPeriodResults[BinaryResult] +final class FloatingRatingPeriodResults(val results: List[FloatingResult]) + extends RatingPeriodResults[FloatingResult] + final class GameRatingPeriodResults(val results: List[DuelResult]) extends RatingPeriodResults[DuelResult] diff --git a/modules/rating/src/main/glicko2/Result.scala b/modules/rating/src/main/glicko2/Result.scala index f5be76330064a..19f2f2eead524 100644 --- a/modules/rating/src/main/glicko2/Result.scala +++ b/modules/rating/src/main/glicko2/Result.scala @@ -20,11 +20,18 @@ final class BinaryResult(val first: Rating, val second: Rating, score: Boolean) private val POINTS_FOR_WIN = 1.0d private val POINTS_FOR_LOSS = 0.0d - def getScore(p: Rating) = + def getScore(p: Rating): Double = if p == first then if score then POINTS_FOR_WIN else POINTS_FOR_LOSS else if score then POINTS_FOR_LOSS else POINTS_FOR_WIN + +final class FloatingResult(val first: Rating, val second: Rating, score: Double) extends Result: + private val ONE_HUNDRED_PCT = 1.0d + + def getScore(p: Rating): Double = + if p == first then score else ONE_HUNDRED_PCT - score + final class DuelResult(val first: Rating, val second: Rating, outcome: Option[Boolean]) extends Result: private val POINTS_FOR_WIN = 1.0d private val POINTS_FOR_DRAW = 0.5d From 41a260b1bccde6539aa005db9adf168fa3392927 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 17 Nov 2024 07:22:50 -0600 Subject: [PATCH 19/44] Fix compile errors --- modules/tutor/src/main/TutorGlicko.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/tutor/src/main/TutorGlicko.scala b/modules/tutor/src/main/TutorGlicko.scala index e99047c74e7c3..363c664a1506f 100644 --- a/modules/tutor/src/main/TutorGlicko.scala +++ b/modules/tutor/src/main/TutorGlicko.scala @@ -12,7 +12,7 @@ object TutorGlicko: private val TAU = 0.75d def scoresRating(perf: Perf, scores: List[(Rating, Score)]): Rating = - val calculator = glicko2.RatingCalculator(VOLATILITY, TAU) + val calculator = glicko2.RatingCalculator() val player = perf.toRating val results = glicko2.FloatingRatingPeriodResults( scores.map { case (rating, score) => @@ -20,7 +20,7 @@ object TutorGlicko: } ) - try calculator.updateRatings(results, true) + try calculator.updateRatings(Set(player), results, true) catch case e: Exception => logger.error("TutorGlicko.scoresRating", e) player.rating.toInt From 9e31c15da45fb28991c5fff070813446f7532cba Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 17 Nov 2024 07:33:20 -0600 Subject: [PATCH 20/44] Add code comment (lack of unit interval type) --- modules/rating/src/main/glicko2/Result.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/rating/src/main/glicko2/Result.scala b/modules/rating/src/main/glicko2/Result.scala index 19f2f2eead524..ce81be21dfd21 100644 --- a/modules/rating/src/main/glicko2/Result.scala +++ b/modules/rating/src/main/glicko2/Result.scala @@ -25,7 +25,7 @@ final class BinaryResult(val first: Rating, val second: Rating, score: Boolean) else if score then POINTS_FOR_LOSS else POINTS_FOR_WIN - +// ASSUME score is between unit interval [0.0d, 1.0d] final class FloatingResult(val first: Rating, val second: Rating, score: Double) extends Result: private val ONE_HUNDRED_PCT = 1.0d From 3e92964e94e4e7ada86f6fc14ccca9c59c219d76 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 17 Nov 2024 14:44:55 +0100 Subject: [PATCH 21/44] unused code and scala syntax --- modules/tutor/src/main/TutorGlicko.scala | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/modules/tutor/src/main/TutorGlicko.scala b/modules/tutor/src/main/TutorGlicko.scala index 363c664a1506f..7c773344a8b5d 100644 --- a/modules/tutor/src/main/TutorGlicko.scala +++ b/modules/tutor/src/main/TutorGlicko.scala @@ -8,16 +8,12 @@ object TutorGlicko: private type Rating = Int private type Score = Float - private val VOLATILITY = Glicko.default.volatility - private val TAU = 0.75d - def scoresRating(perf: Perf, scores: List[(Rating, Score)]): Rating = val calculator = glicko2.RatingCalculator() val player = perf.toRating val results = glicko2.FloatingRatingPeriodResults( - scores.map { case (rating, score) => + scores.map: (rating, score) => glicko2.FloatingResult(player, glicko2.Rating(rating, 60, 0.06, 10), score) - } ) try calculator.updateRatings(Set(player), results, true) From f6e8bb4303ddfdc5640054592526fded58f6c781 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 17 Nov 2024 14:57:03 +0100 Subject: [PATCH 22/44] better recover from erased users fixes clicking the reset password email link after obtaining a gdpr erasure --- modules/user/src/main/UserRepo.scala | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/modules/user/src/main/UserRepo.scala b/modules/user/src/main/UserRepo.scala index 51e2dc7a8edd2..b59cfe55ee622 100644 --- a/modules/user/src/main/UserRepo.scala +++ b/modules/user/src/main/UserRepo.scala @@ -20,6 +20,10 @@ final class UserRepo(c: Coll)(using Executor) extends lila.core.user.UserRepo(c) import lila.user.{ BSONFields as F } export lila.user.BSONHandlers.given + private def recoverErased(user: Fu[Option[User]]): Fu[Option[User]] = + user.recover: + case _: reactivemongo.api.bson.exceptions.BSONValueNotFoundException => none + def withColl[A](f: Coll => A): A = f(coll) def topNbGame(nb: Int): Fu[List[User]] = @@ -27,11 +31,8 @@ final class UserRepo(c: Coll)(using Executor) extends lila.core.user.UserRepo(c) def byId[U: UserIdOf](u: U): Fu[Option[User]] = u.id.noGhost.so: - coll - .byId[User](u.id) - .recover: - case _: reactivemongo.api.bson.exceptions.BSONValueNotFoundException => - none // probably GDPRed user + recoverErased: + coll.byId[User](u.id) def byIds[U: UserIdOf](us: Iterable[U]): Fu[List[User]] = val ids = us.map(_.id).filter(_.noGhost) @@ -41,7 +42,9 @@ final class UserRepo(c: Coll)(using Executor) extends lila.core.user.UserRepo(c) coll.byIds[User, UserId](ids, _.sec) def enabledById[U: UserIdOf](u: U): Fu[Option[User]] = - u.id.noGhost.so(coll.one[User](enabledSelect ++ $id(u.id))) + u.id.noGhost.so: + recoverErased: + coll.one[User](enabledSelect ++ $id(u.id)) def enabledByIds[U: UserIdOf](us: Iterable[U]): Fu[List[User]] = val ids = us.map(_.id).filter(_.noGhost) From 850f2f88682f6374a8a0b3eebd9f9a95677a3486 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 17 Nov 2024 14:59:01 +0100 Subject: [PATCH 23/44] rename Glicko.calculator --- modules/rating/src/main/Glicko.scala | 6 +++--- modules/rating/src/test/RatingCalculatorTest.scala | 2 +- modules/round/src/main/PerfsUpdater.scala | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/rating/src/main/Glicko.scala b/modules/rating/src/main/Glicko.scala index 5cc970cd0efde..40f03bb4a022b 100644 --- a/modules/rating/src/main/Glicko.scala +++ b/modules/rating/src/main/Glicko.scala @@ -81,12 +81,12 @@ object Glicko: // rating that can be lost or gained with a single game val maxRatingDelta = 700 - val system = glicko2.RatingCalculator(glicko2.Tau.default, ratingPeriodsPerDay) - def calculator(advantage: ColorAdvantage) = + val calculator = glicko2.RatingCalculator(glicko2.Tau.default, ratingPeriodsPerDay) + def calculatorWithAdvantage(advantage: ColorAdvantage) = glicko2.RatingCalculator(glicko2.Tau.default, ratingPeriodsPerDay, advantage) def liveDeviation(p: Perf, reverse: Boolean): Double = { - system.previewDeviation(p.toRating, nowInstant, reverse) + calculator.previewDeviation(p.toRating, nowInstant, reverse) }.atLeast(minDeviation).atMost(maxDeviation) given glickoHandler: BSONDocumentHandler[Glicko] = new BSON[Glicko]: diff --git a/modules/rating/src/test/RatingCalculatorTest.scala b/modules/rating/src/test/RatingCalculatorTest.scala index 58d8a72cc90b2..7dd061e854bcf 100644 --- a/modules/rating/src/test/RatingCalculatorTest.scala +++ b/modules/rating/src/test/RatingCalculatorTest.scala @@ -22,7 +22,7 @@ class RatingCalculatorTest extends lila.common.LilaTest: case Glicko.Result.Draw => DuelResult(wRating, bRating, None) ) ) - Glicko.calculator(ColorAdvantage.standard).updateRatings(ratings, results, true) + Glicko.calculatorWithAdvantage(ColorAdvantage.standard).updateRatings(ratings, results, true) test("default deviation: white wins") { val wr = default.toRating diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index 28acb726aec09..c73f2a7025fa9 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -139,7 +139,7 @@ final class PerfsUpdater( ) ) // tuning TAU per game speed may improve accuracy - try Glicko.calculator(advantage).updateRatings(ratings, results, true) + try Glicko.calculatorWithAdvantage(advantage).updateRatings(ratings, results, true) catch case e: Exception => logger.error(s"update ratings #${game.id}", e) private def mkPerfs(ratings: Ratings, users: PairOf[UserWithPerfs], game: Game): UserPerfs = From 2774b03e0673e5a0ed4fce3adfe53d0acd459993 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 17 Nov 2024 15:05:54 +0100 Subject: [PATCH 24/44] use chess.Outcome for glicko Result --- modules/rating/src/main/glicko2/Result.scala | 12 +++++++----- modules/rating/src/test/RatingCalculatorTest.scala | 8 ++++---- modules/round/src/main/PerfsUpdater.scala | 8 ++++---- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/modules/rating/src/main/glicko2/Result.scala b/modules/rating/src/main/glicko2/Result.scala index ce81be21dfd21..1b40c41908799 100644 --- a/modules/rating/src/main/glicko2/Result.scala +++ b/modules/rating/src/main/glicko2/Result.scala @@ -1,5 +1,7 @@ package lila.rating.glicko2 +import chess.Outcome + trait Result: val first: Rating @@ -32,7 +34,7 @@ final class FloatingResult(val first: Rating, val second: Rating, score: Double) def getScore(p: Rating): Double = if p == first then score else ONE_HUNDRED_PCT - score -final class DuelResult(val first: Rating, val second: Rating, outcome: Option[Boolean]) extends Result: +final class DuelResult(val first: Rating, val second: Rating, outcome: Outcome) extends Result: private val POINTS_FOR_WIN = 1.0d private val POINTS_FOR_DRAW = 0.5d private val POINTS_FOR_LOSS = 0.0d @@ -43,9 +45,9 @@ final class DuelResult(val first: Rating, val second: Rating, outcome: Option[Bo * 1 for a first player win, 0.5 for a draw and 0 for a first player loss * @throws IllegalArgumentException */ - def getScore(player: Rating): Double = outcome match - case Some(true) => if player == first then POINTS_FOR_WIN else POINTS_FOR_LOSS - case Some(false) => if player == first then POINTS_FOR_LOSS else POINTS_FOR_WIN - case _ => POINTS_FOR_DRAW + def getScore(player: Rating): Double = outcome.winner match + case Some(chess.White) => if player == first then POINTS_FOR_WIN else POINTS_FOR_LOSS + case Some(chess.Black) => if player == first then POINTS_FOR_LOSS else POINTS_FOR_WIN + case _ => POINTS_FOR_DRAW override def toString = s"$first vs $second = $outcome" diff --git a/modules/rating/src/test/RatingCalculatorTest.scala b/modules/rating/src/test/RatingCalculatorTest.scala index 7dd061e854bcf..5cd981d92892c 100644 --- a/modules/rating/src/test/RatingCalculatorTest.scala +++ b/modules/rating/src/test/RatingCalculatorTest.scala @@ -1,6 +1,6 @@ package lila.rating -import chess.{ Black, Color, White } +import chess.{ Black, Color, White, Outcome } import lila.rating.Perf.default import lila.rating.PerfExt.* @@ -17,9 +17,9 @@ class RatingCalculatorTest extends lila.common.LilaTest: val results = GameRatingPeriodResults( List( result match - case Glicko.Result.Win => DuelResult(wRating, bRating, Some(true)) - case Glicko.Result.Loss => DuelResult(wRating, bRating, Some(false)) - case Glicko.Result.Draw => DuelResult(wRating, bRating, None) + case Glicko.Result.Win => DuelResult(wRating, bRating, Outcome.white) + case Glicko.Result.Loss => DuelResult(wRating, bRating, Outcome.black) + case Glicko.Result.Draw => DuelResult(wRating, bRating, Outcome.draw) ) ) Glicko.calculatorWithAdvantage(ColorAdvantage.standard).updateRatings(ratings, results, true) diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index c73f2a7025fa9..746883ef5d6ec 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -1,5 +1,5 @@ package lila.round -import chess.{ ByColor, Color, Speed } +import chess.{ ByColor, Color, Speed, Outcome } import lila.core.perf.{ UserPerfs, UserWithPerfs } import lila.rating.GlickoExt.average @@ -133,9 +133,9 @@ final class PerfsUpdater( val results = GameRatingPeriodResults( List( game.winnerColor match - case Some(chess.White) => DuelResult(white, black, Some(true)) - case Some(chess.Black) => DuelResult(black, white, Some(false)) - case None => DuelResult(white, black, None) + case Some(chess.White) => DuelResult(white, black, Outcome.white) + case Some(chess.Black) => DuelResult(black, white, Outcome.black) + case None => DuelResult(white, black, Outcome.draw) ) ) // tuning TAU per game speed may improve accuracy From 685e069709612e0bdc3774155b5e9f55363e97d3 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 17 Nov 2024 12:49:53 -0600 Subject: [PATCH 25/44] Delete bug (WIP) --- .../src/main/glicko2/RatingCalculator.scala | 12 ++--- .../main/glicko2/RatingPeriodResults.scala | 5 +- modules/rating/src/main/glicko2/Result.scala | 54 ++++--------------- modules/tutor/src/main/TutorGlicko.scala | 2 +- 4 files changed, 19 insertions(+), 54 deletions(-) diff --git a/modules/rating/src/main/glicko2/RatingCalculator.scala b/modules/rating/src/main/glicko2/RatingCalculator.scala index a9a4f13923a7c..41d4514950885 100644 --- a/modules/rating/src/main/glicko2/RatingCalculator.scala +++ b/modules/rating/src/main/glicko2/RatingCalculator.scala @@ -185,17 +185,17 @@ final class RatingCalculator( for result <- results do v = v + ((Math.pow(g(result.getOpponent(player).getGlicko2RatingDeviation), 2)) * E( - player.getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player)), + player.getGlicko2RatingWithAdvantage(result.getAdvantage(advantage)), result .getOpponent(player) - .getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player).negate), + .getGlicko2RatingWithAdvantage(result.getAdvantage(advantage).negate), result.getOpponent(player).getGlicko2RatingDeviation ) * (1.0 - E( - player.getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player)), + player.getGlicko2RatingWithAdvantage(result.getAdvantage(advantage)), result .getOpponent(player) - .getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player).negate), + .getGlicko2RatingWithAdvantage(result.getAdvantage(advantage).negate), result.getOpponent(player).getGlicko2RatingDeviation ))) 1 / v @@ -216,10 +216,10 @@ final class RatingCalculator( outcomeBasedRating = outcomeBasedRating + (g(result.getOpponent(player).getGlicko2RatingDeviation) * (result.getScore(player) - E( - player.getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player)), + player.getGlicko2RatingWithAdvantage(result.getAdvantage(advantage)), result .getOpponent(player) - .getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player).negate), + .getGlicko2RatingWithAdvantage(result.getAdvantage(advantage).negate), result.getOpponent(player).getGlicko2RatingDeviation ))) outcomeBasedRating diff --git a/modules/rating/src/main/glicko2/RatingPeriodResults.scala b/modules/rating/src/main/glicko2/RatingPeriodResults.scala index 12bff956b8fd8..b0eface5ea024 100644 --- a/modules/rating/src/main/glicko2/RatingPeriodResults.scala +++ b/modules/rating/src/main/glicko2/RatingPeriodResults.scala @@ -6,9 +6,8 @@ trait RatingPeriodResults[R <: Result](): def getResults(player: Rating): List[R] = results.filter(_.participated(player)) final class BinaryRatingPeriodResults(val results: List[BinaryResult]) - extends RatingPeriodResults[BinaryResult] -final class FloatingRatingPeriodResults(val results: List[FloatingResult]) - extends RatingPeriodResults[FloatingResult] +final class FloatingRatingPeriodResults(val results: List[Result]) + extends RatingPeriodResults[Result] final class GameRatingPeriodResults(val results: List[DuelResult]) extends RatingPeriodResults[DuelResult] diff --git a/modules/rating/src/main/glicko2/Result.scala b/modules/rating/src/main/glicko2/Result.scala index 1b40c41908799..3cd58d96ff4a6 100644 --- a/modules/rating/src/main/glicko2/Result.scala +++ b/modules/rating/src/main/glicko2/Result.scala @@ -1,53 +1,19 @@ package lila.rating.glicko2 -import chess.Outcome - -trait Result: - - val first: Rating - val second: Rating - - def getAdvantage(advantage: ColorAdvantage, player: Rating): ColorAdvantage = - if player == first then advantage.map(_ / 2.0d) else advantage.map(_ / 2.0d).negate +// ASSUME score is between unit interval [0.0d, 1.0d] +class Result(opponent: Rating, score: Double): - def getScore(player: Rating): Double + def getAdvantage(advantage: ColorAdvantage): ColorAdvantage = advantage.map(_ / 2.0d) - def getOpponent(player: Rating) = if player == first then second else first + def getScore(player: Rating): Double = score - def participated(player: Rating): Boolean = player == first || player == second + def getOpponent(player: Rating): Rating = opponent - private def players: List[Rating] = List(first, second) + def participated(player: Rating): Boolean = player == opponent -final class BinaryResult(val first: Rating, val second: Rating, score: Boolean) extends Result: - private val POINTS_FOR_WIN = 1.0d - private val POINTS_FOR_LOSS = 0.0d +class BinaryResult(val opponent: Rating, val success: Boolean) extends Result(opponent, if (success) 1.0d else 0.0d) - def getScore(p: Rating): Double = - if p == first then if score then POINTS_FOR_WIN else POINTS_FOR_LOSS - else if score then POINTS_FOR_LOSS - else POINTS_FOR_WIN +class DuelResult(val opponent: Rating, val score: Double, first: Boolean) extends Result(opponent, score): -// ASSUME score is between unit interval [0.0d, 1.0d] -final class FloatingResult(val first: Rating, val second: Rating, score: Double) extends Result: - private val ONE_HUNDRED_PCT = 1.0d - - def getScore(p: Rating): Double = - if p == first then score else ONE_HUNDRED_PCT - score - -final class DuelResult(val first: Rating, val second: Rating, outcome: Outcome) extends Result: - private val POINTS_FOR_WIN = 1.0d - private val POINTS_FOR_DRAW = 0.5d - private val POINTS_FOR_LOSS = 0.0d - - /** Returns the "score" for a match. - * - * @return - * 1 for a first player win, 0.5 for a draw and 0 for a first player loss - * @throws IllegalArgumentException - */ - def getScore(player: Rating): Double = outcome.winner match - case Some(chess.White) => if player == first then POINTS_FOR_WIN else POINTS_FOR_LOSS - case Some(chess.Black) => if player == first then POINTS_FOR_LOSS else POINTS_FOR_WIN - case _ => POINTS_FOR_DRAW - - override def toString = s"$first vs $second = $outcome" + override def getAdvantage(advantage: ColorAdvantage): ColorAdvantage = + if first then advantage.map(_ / 2.0d) else advantage.map(_ / 2.0d).negate diff --git a/modules/tutor/src/main/TutorGlicko.scala b/modules/tutor/src/main/TutorGlicko.scala index 7c773344a8b5d..c8dec5d45deb7 100644 --- a/modules/tutor/src/main/TutorGlicko.scala +++ b/modules/tutor/src/main/TutorGlicko.scala @@ -13,7 +13,7 @@ object TutorGlicko: val player = perf.toRating val results = glicko2.FloatingRatingPeriodResults( scores.map: (rating, score) => - glicko2.FloatingResult(player, glicko2.Rating(rating, 60, 0.06, 10), score) + glicko2.Result(player, glicko2.Rating(rating, 60, 0.06, 10), score) ) try calculator.updateRatings(Set(player), results, true) From 2f0db66202e6791b81b1078eca49b8c3604ab737 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 17 Nov 2024 12:55:35 -0600 Subject: [PATCH 26/44] Delete bug (WIP) --- modules/rating/src/main/glicko2/RatingPeriodResults.scala | 4 ++-- modules/rating/src/main/glicko2/Result.scala | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/rating/src/main/glicko2/RatingPeriodResults.scala b/modules/rating/src/main/glicko2/RatingPeriodResults.scala index b0eface5ea024..02f8989d8f7dd 100644 --- a/modules/rating/src/main/glicko2/RatingPeriodResults.scala +++ b/modules/rating/src/main/glicko2/RatingPeriodResults.scala @@ -2,8 +2,8 @@ package lila.rating.glicko2 // rewrite from java https://github.com/goochjs/glicko2 trait RatingPeriodResults[R <: Result](): - val results: List[R] - def getResults(player: Rating): List[R] = results.filter(_.participated(player)) + val results: Map[Rating, List[R]] + def getResults(player: Rating): List[R] = results.get(player) final class BinaryRatingPeriodResults(val results: List[BinaryResult]) diff --git a/modules/rating/src/main/glicko2/Result.scala b/modules/rating/src/main/glicko2/Result.scala index 3cd58d96ff4a6..b97191b0f5cc9 100644 --- a/modules/rating/src/main/glicko2/Result.scala +++ b/modules/rating/src/main/glicko2/Result.scala @@ -9,8 +9,6 @@ class Result(opponent: Rating, score: Double): def getOpponent(player: Rating): Rating = opponent - def participated(player: Rating): Boolean = player == opponent - class BinaryResult(val opponent: Rating, val success: Boolean) extends Result(opponent, if (success) 1.0d else 0.0d) class DuelResult(val opponent: Rating, val score: Double, first: Boolean) extends Result(opponent, score): From fad133dfb4b4304b705177242c7d397dae25d83c Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 17 Nov 2024 13:01:07 -0600 Subject: [PATCH 27/44] Delete bug (WIP) --- .../rating/src/main/glicko2/RatingCalculator.scala | 4 ++-- .../rating/src/main/glicko2/RatingPeriodResults.scala | 11 +---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/modules/rating/src/main/glicko2/RatingCalculator.scala b/modules/rating/src/main/glicko2/RatingCalculator.scala index 41d4514950885..ded88aa3d864f 100644 --- a/modules/rating/src/main/glicko2/RatingCalculator.scala +++ b/modules/rating/src/main/glicko2/RatingCalculator.scala @@ -62,8 +62,8 @@ final class RatingCalculator( ) = players.foreach { player => val elapsedRatingPeriods = if skipDeviationIncrease then 0 else 1 - if results.getResults(player).sizeIs > 0 then - calculateNewRating(player, results.getResults(player), elapsedRatingPeriods) + if results.get(player).sizeIs > 0 then + calculateNewRating(player, results.get(player), elapsedRatingPeriods) else // if a player does not compete during the rating period, then only Step 6 applies. // the player's rating and volatility parameters remain the same but deviation increases diff --git a/modules/rating/src/main/glicko2/RatingPeriodResults.scala b/modules/rating/src/main/glicko2/RatingPeriodResults.scala index 02f8989d8f7dd..417c0357dff37 100644 --- a/modules/rating/src/main/glicko2/RatingPeriodResults.scala +++ b/modules/rating/src/main/glicko2/RatingPeriodResults.scala @@ -1,13 +1,4 @@ package lila.rating.glicko2 // rewrite from java https://github.com/goochjs/glicko2 -trait RatingPeriodResults[R <: Result](): - val results: Map[Rating, List[R]] - def getResults(player: Rating): List[R] = results.get(player) - -final class BinaryRatingPeriodResults(val results: List[BinaryResult]) - -final class FloatingRatingPeriodResults(val results: List[Result]) - extends RatingPeriodResults[Result] - -final class GameRatingPeriodResults(val results: List[DuelResult]) extends RatingPeriodResults[DuelResult] +type RatingPeriodResults[R <: Result] = Map[Rating, List[R]] From 627098244de57a5eeb1ffbc884dc09fd32d29bcd Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 17 Nov 2024 13:05:37 -0600 Subject: [PATCH 28/44] Delete bug --- modules/rating/src/main/glicko2/RatingCalculator.scala | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/rating/src/main/glicko2/RatingCalculator.scala b/modules/rating/src/main/glicko2/RatingCalculator.scala index ded88aa3d864f..03370dca01a1b 100644 --- a/modules/rating/src/main/glicko2/RatingCalculator.scala +++ b/modules/rating/src/main/glicko2/RatingCalculator.scala @@ -56,14 +56,13 @@ final class RatingCalculator( * @param results */ def updateRatings( - players: Iterable[Rating], results: RatingPeriodResults[?], skipDeviationIncrease: Boolean = false ) = - players.foreach { player => + results.foreach { case (player, results) => val elapsedRatingPeriods = if skipDeviationIncrease then 0 else 1 - if results.get(player).sizeIs > 0 then - calculateNewRating(player, results.get(player), elapsedRatingPeriods) + if results.sizeIs > 0 then + calculateNewRating(player, results, elapsedRatingPeriods) else // if a player does not compete during the rating period, then only Step 6 applies. // the player's rating and volatility parameters remain the same but deviation increases @@ -74,7 +73,7 @@ final class RatingCalculator( } // now iterate through the participants and confirm their new ratings - players.foreach { _.finaliseRating() } + results.keySet.foreach { _.finaliseRating() } /** This is the formula defined in step 6. It is also used for players who have not competed during the * rating period. From 3083ac31c800cedac5514dd35901070b68c5932f Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 17 Nov 2024 17:12:39 -0600 Subject: [PATCH 29/44] Attempt to fix type errors --- modules/puzzle/src/main/PuzzleFinisher.scala | 14 +++++++++----- .../src/main/glicko2/RatingCalculator.scala | 2 +- modules/rating/src/main/glicko2/Result.scala | 2 +- .../rating/src/test/RatingCalculatorTest.scala | 2 +- modules/round/src/main/PerfsUpdater.scala | 17 +++++++++++------ modules/tutor/src/main/TutorGlicko.scala | 4 ++-- 6 files changed, 25 insertions(+), 16 deletions(-) diff --git a/modules/puzzle/src/main/PuzzleFinisher.scala b/modules/puzzle/src/main/PuzzleFinisher.scala index ccca7eda55458..41ba37f1ca207 100644 --- a/modules/puzzle/src/main/PuzzleFinisher.scala +++ b/modules/puzzle/src/main/PuzzleFinisher.scala @@ -206,11 +206,15 @@ final private[puzzle] class PuzzleFinisher( private def updateRatings(u1: glicko2.Rating, u2: glicko2.Rating, win: PuzzleWin): Unit = val ratings = Set(u1, u2) - val results = glicko2.BinaryRatingPeriodResults( - List( - if win.yes then glicko2.BinaryResult(u1, u2, false) - else glicko2.BinaryResult(u2, u1, false) + val results = glicko2.RatingPeriodResults[BinaryResult]( + u1 -> List( + if win.yes then glicko2.BinaryResult(u2, true) + else glicko2.BinaryResult(u2, false) + ), + u2 -> List( + if win.yes then glicko2.BinaryResult(u1, false) + else glicko2.BinaryResult(u1, true) ) ) - try calculator.updateRatings(ratings, results) + try calculator.updateRatings(results) catch case e: Exception => logger.error("finisher", e) diff --git a/modules/rating/src/main/glicko2/RatingCalculator.scala b/modules/rating/src/main/glicko2/RatingCalculator.scala index 03370dca01a1b..20104498cd319 100644 --- a/modules/rating/src/main/glicko2/RatingCalculator.scala +++ b/modules/rating/src/main/glicko2/RatingCalculator.scala @@ -56,7 +56,7 @@ final class RatingCalculator( * @param results */ def updateRatings( - results: RatingPeriodResults[?], + results: RatingPeriodResults[Result], skipDeviationIncrease: Boolean = false ) = results.foreach { case (player, results) => diff --git a/modules/rating/src/main/glicko2/Result.scala b/modules/rating/src/main/glicko2/Result.scala index b97191b0f5cc9..7824cca579869 100644 --- a/modules/rating/src/main/glicko2/Result.scala +++ b/modules/rating/src/main/glicko2/Result.scala @@ -9,7 +9,7 @@ class Result(opponent: Rating, score: Double): def getOpponent(player: Rating): Rating = opponent -class BinaryResult(val opponent: Rating, val success: Boolean) extends Result(opponent, if (success) 1.0d else 0.0d) +class BinaryResult(val opponent: Rating, val win: Boolean) extends Result(opponent, if (win) 1.0d else 0.0d) class DuelResult(val opponent: Rating, val score: Double, first: Boolean) extends Result(opponent, score): diff --git a/modules/rating/src/test/RatingCalculatorTest.scala b/modules/rating/src/test/RatingCalculatorTest.scala index 5cd981d92892c..f96bcf2794371 100644 --- a/modules/rating/src/test/RatingCalculatorTest.scala +++ b/modules/rating/src/test/RatingCalculatorTest.scala @@ -14,7 +14,7 @@ class RatingCalculatorTest extends lila.common.LilaTest: case Some(chess.White) => Glicko.Result.Win case Some(chess.Black) => Glicko.Result.Loss case None => Glicko.Result.Draw - val results = GameRatingPeriodResults( + val results = RatingPeriodResults[DuelResult]( List( result match case Glicko.Result.Win => DuelResult(wRating, bRating, Outcome.white) diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index 746883ef5d6ec..0e00abe855079 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -129,17 +129,22 @@ final class PerfsUpdater( game: Game, advantage: ColorAdvantage = ColorAdvantage.standard ): Unit = - val ratings = Set(white, black) val results = GameRatingPeriodResults( - List( + white -> List( game.winnerColor match - case Some(chess.White) => DuelResult(white, black, Outcome.white) - case Some(chess.Black) => DuelResult(black, white, Outcome.black) - case None => DuelResult(white, black, Outcome.draw) + case Some(chess.White) => DuelResult(black, 1.0d) + case Some(chess.Black) => DuelResult(black, 0.0d) + case None => DuelResult(black, 0.5d) + ), + black -> List( + game.winnerColor match + case Some(chess.White) => DuelResult(white, 0.0d) + case Some(chess.Black) => DuelResult(white, 1.0d) + case None => DuelResult(white, 0.5d) ) ) // tuning TAU per game speed may improve accuracy - try Glicko.calculatorWithAdvantage(advantage).updateRatings(ratings, results, true) + try Glicko.calculatorWithAdvantage(advantage).updateRatings(results, true) catch case e: Exception => logger.error(s"update ratings #${game.id}", e) private def mkPerfs(ratings: Ratings, users: PairOf[UserWithPerfs], game: Game): UserPerfs = diff --git a/modules/tutor/src/main/TutorGlicko.scala b/modules/tutor/src/main/TutorGlicko.scala index c8dec5d45deb7..a3eb06ef12835 100644 --- a/modules/tutor/src/main/TutorGlicko.scala +++ b/modules/tutor/src/main/TutorGlicko.scala @@ -11,12 +11,12 @@ object TutorGlicko: def scoresRating(perf: Perf, scores: List[(Rating, Score)]): Rating = val calculator = glicko2.RatingCalculator() val player = perf.toRating - val results = glicko2.FloatingRatingPeriodResults( + val results = glicko2.RatingPeriodResults[Result](player -> scores.map: (rating, score) => glicko2.Result(player, glicko2.Rating(rating, 60, 0.06, 10), score) ) - try calculator.updateRatings(Set(player), results, true) + try calculator.updateRatings(results, true) catch case e: Exception => logger.error("TutorGlicko.scoresRating", e) player.rating.toInt From 82a7c74a789ebdff348551ab485bf42bbd6f9d57 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 17 Nov 2024 17:14:46 -0600 Subject: [PATCH 30/44] Attempt to fix type errors --- modules/tutor/src/main/TutorGlicko.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/tutor/src/main/TutorGlicko.scala b/modules/tutor/src/main/TutorGlicko.scala index a3eb06ef12835..ee1ee9301fbd0 100644 --- a/modules/tutor/src/main/TutorGlicko.scala +++ b/modules/tutor/src/main/TutorGlicko.scala @@ -13,7 +13,7 @@ object TutorGlicko: val player = perf.toRating val results = glicko2.RatingPeriodResults[Result](player -> scores.map: (rating, score) => - glicko2.Result(player, glicko2.Rating(rating, 60, 0.06, 10), score) + glicko2.Result(glicko2.Rating(rating, 60, 0.06, 10), score) ) try calculator.updateRatings(results, true) From 31543bd077a83f7a0434fbc9bcbc5930be412350 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 17 Nov 2024 17:16:50 -0600 Subject: [PATCH 31/44] Attempt to fix type errors --- modules/round/src/main/PerfsUpdater.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index 0e00abe855079..2112def5bb8d0 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -132,15 +132,15 @@ final class PerfsUpdater( val results = GameRatingPeriodResults( white -> List( game.winnerColor match - case Some(chess.White) => DuelResult(black, 1.0d) - case Some(chess.Black) => DuelResult(black, 0.0d) - case None => DuelResult(black, 0.5d) + case Some(chess.White) => DuelResult(black, 1.0d, true) + case Some(chess.Black) => DuelResult(black, 0.0d, true) + case None => DuelResult(black, 0.5d, true) ), black -> List( game.winnerColor match - case Some(chess.White) => DuelResult(white, 0.0d) - case Some(chess.Black) => DuelResult(white, 1.0d) - case None => DuelResult(white, 0.5d) + case Some(chess.White) => DuelResult(white, 0.0d, false) + case Some(chess.Black) => DuelResult(white, 1.0d, false) + case None => DuelResult(white, 0.5d, false) ) ) // tuning TAU per game speed may improve accuracy From 32e91cf21e4ca86c6e76442d187b8206592ca34a Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 17 Nov 2024 17:20:56 -0600 Subject: [PATCH 32/44] Attempt to fix type errors --- modules/round/src/main/PerfsUpdater.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index 2112def5bb8d0..a7a319e044723 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -4,7 +4,7 @@ import chess.{ ByColor, Color, Speed, Outcome } import lila.core.perf.{ UserPerfs, UserWithPerfs } import lila.rating.GlickoExt.average import lila.rating.PerfExt.{ addOrReset, toRating } -import lila.rating.glicko2.{ DuelResult, GameRatingPeriodResults, Rating, ColorAdvantage } +import lila.rating.glicko2.{ DuelResult, RatingPeriodResults, Rating, ColorAdvantage } import lila.rating.{ Glicko, PerfType, RatingFactors, RatingRegulator } import lila.user.{ RankingApi, UserApi } @@ -129,7 +129,7 @@ final class PerfsUpdater( game: Game, advantage: ColorAdvantage = ColorAdvantage.standard ): Unit = - val results = GameRatingPeriodResults( + val results = RatingPeriodResults( white -> List( game.winnerColor match case Some(chess.White) => DuelResult(black, 1.0d, true) From 3f71c58182450e825076193dc6af886cace4a0db Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 17 Nov 2024 21:22:29 -0600 Subject: [PATCH 33/44] Attempt to fix type errors --- modules/puzzle/src/main/PuzzleFinisher.scala | 3 +-- modules/tutor/src/main/TutorGlicko.scala | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/puzzle/src/main/PuzzleFinisher.scala b/modules/puzzle/src/main/PuzzleFinisher.scala index 41ba37f1ca207..3c694b04dd946 100644 --- a/modules/puzzle/src/main/PuzzleFinisher.scala +++ b/modules/puzzle/src/main/PuzzleFinisher.scala @@ -205,8 +205,7 @@ final private[puzzle] class PuzzleFinisher( colls.puzzle.map(_.incFieldUnchecked($id(puzzleId), Puzzle.BSONFields.plays)) private def updateRatings(u1: glicko2.Rating, u2: glicko2.Rating, win: PuzzleWin): Unit = - val ratings = Set(u1, u2) - val results = glicko2.RatingPeriodResults[BinaryResult]( + val results = glicko2.RatingPeriodResults[glicko2.BinaryResult]( u1 -> List( if win.yes then glicko2.BinaryResult(u2, true) else glicko2.BinaryResult(u2, false) diff --git a/modules/tutor/src/main/TutorGlicko.scala b/modules/tutor/src/main/TutorGlicko.scala index ee1ee9301fbd0..01230d05c4f71 100644 --- a/modules/tutor/src/main/TutorGlicko.scala +++ b/modules/tutor/src/main/TutorGlicko.scala @@ -11,7 +11,7 @@ object TutorGlicko: def scoresRating(perf: Perf, scores: List[(Rating, Score)]): Rating = val calculator = glicko2.RatingCalculator() val player = perf.toRating - val results = glicko2.RatingPeriodResults[Result](player -> + val results = glicko2.RatingPeriodResults[glicko2.Result](player -> scores.map: (rating, score) => glicko2.Result(glicko2.Rating(rating, 60, 0.06, 10), score) ) From 85ecdac5d8a4efa824cb120d021e69552cea8466 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Sun, 17 Nov 2024 21:24:08 -0600 Subject: [PATCH 34/44] Attempt to fix type errors --- modules/round/src/main/PerfsUpdater.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index a7a319e044723..9c85bb9e74e14 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -129,7 +129,7 @@ final class PerfsUpdater( game: Game, advantage: ColorAdvantage = ColorAdvantage.standard ): Unit = - val results = RatingPeriodResults( + val results = RatingPeriodResults[DuelResult]( white -> List( game.winnerColor match case Some(chess.White) => DuelResult(black, 1.0d, true) From f9e34be195bcaa46b2aecd97a6bad0460f49ad1c Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Mon, 18 Nov 2024 05:20:26 -0600 Subject: [PATCH 35/44] Attempt to fix type errors --- modules/rating/src/main/glicko2/RatingPeriodResults.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/rating/src/main/glicko2/RatingPeriodResults.scala b/modules/rating/src/main/glicko2/RatingPeriodResults.scala index 417c0357dff37..435c654e4036e 100644 --- a/modules/rating/src/main/glicko2/RatingPeriodResults.scala +++ b/modules/rating/src/main/glicko2/RatingPeriodResults.scala @@ -2,3 +2,6 @@ package lila.rating.glicko2 // rewrite from java https://github.com/goochjs/glicko2 type RatingPeriodResults[R <: Result] = Map[Rating, List[R]] + +object RatingPeriodResults: + def apply[R <: Result]() = Map[Rating, List[R]]() From d10fc58971388f6af80e5dd7dabc72b75d007588 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Mon, 18 Nov 2024 05:33:04 -0600 Subject: [PATCH 36/44] Attempt to fix type errors --- modules/rating/src/main/glicko2/RatingPeriodResults.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/rating/src/main/glicko2/RatingPeriodResults.scala b/modules/rating/src/main/glicko2/RatingPeriodResults.scala index 435c654e4036e..7ff093bda4eae 100644 --- a/modules/rating/src/main/glicko2/RatingPeriodResults.scala +++ b/modules/rating/src/main/glicko2/RatingPeriodResults.scala @@ -4,4 +4,5 @@ package lila.rating.glicko2 type RatingPeriodResults[R <: Result] = Map[Rating, List[R]] object RatingPeriodResults: - def apply[R <: Result]() = Map[Rating, List[R]]() + def apply[R <: Result](a: (Rating, List[R])) = Map[Rating, List[R]](a) + def apply[R <: Result](a: (Rating, List[R]), b: (Rating, List[R])) = Map[Rating, List[R]](a, b) From 0b4948e4928d8aa587113b35ff5da0b17d4ece5b Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Mon, 18 Nov 2024 05:34:37 -0600 Subject: [PATCH 37/44] scalafmtAll --- modules/rating/src/main/glicko2/RatingCalculator.scala | 3 +-- modules/rating/src/main/glicko2/RatingPeriodResults.scala | 2 +- modules/rating/src/main/glicko2/Result.scala | 3 ++- modules/tutor/src/main/TutorGlicko.scala | 7 ++++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/modules/rating/src/main/glicko2/RatingCalculator.scala b/modules/rating/src/main/glicko2/RatingCalculator.scala index 20104498cd319..72a411b88ea9f 100644 --- a/modules/rating/src/main/glicko2/RatingCalculator.scala +++ b/modules/rating/src/main/glicko2/RatingCalculator.scala @@ -61,8 +61,7 @@ final class RatingCalculator( ) = results.foreach { case (player, results) => val elapsedRatingPeriods = if skipDeviationIncrease then 0 else 1 - if results.sizeIs > 0 then - calculateNewRating(player, results, elapsedRatingPeriods) + if results.sizeIs > 0 then calculateNewRating(player, results, elapsedRatingPeriods) else // if a player does not compete during the rating period, then only Step 6 applies. // the player's rating and volatility parameters remain the same but deviation increases diff --git a/modules/rating/src/main/glicko2/RatingPeriodResults.scala b/modules/rating/src/main/glicko2/RatingPeriodResults.scala index 7ff093bda4eae..2627c7aabe067 100644 --- a/modules/rating/src/main/glicko2/RatingPeriodResults.scala +++ b/modules/rating/src/main/glicko2/RatingPeriodResults.scala @@ -4,5 +4,5 @@ package lila.rating.glicko2 type RatingPeriodResults[R <: Result] = Map[Rating, List[R]] object RatingPeriodResults: - def apply[R <: Result](a: (Rating, List[R])) = Map[Rating, List[R]](a) + def apply[R <: Result](a: (Rating, List[R])) = Map[Rating, List[R]](a) def apply[R <: Result](a: (Rating, List[R]), b: (Rating, List[R])) = Map[Rating, List[R]](a, b) diff --git a/modules/rating/src/main/glicko2/Result.scala b/modules/rating/src/main/glicko2/Result.scala index 7824cca579869..d96b651bedd8e 100644 --- a/modules/rating/src/main/glicko2/Result.scala +++ b/modules/rating/src/main/glicko2/Result.scala @@ -9,7 +9,8 @@ class Result(opponent: Rating, score: Double): def getOpponent(player: Rating): Rating = opponent -class BinaryResult(val opponent: Rating, val win: Boolean) extends Result(opponent, if (win) 1.0d else 0.0d) +class BinaryResult(val opponent: Rating, val win: Boolean) + extends Result(opponent, if win then 1.0d else 0.0d) class DuelResult(val opponent: Rating, val score: Double, first: Boolean) extends Result(opponent, score): diff --git a/modules/tutor/src/main/TutorGlicko.scala b/modules/tutor/src/main/TutorGlicko.scala index 01230d05c4f71..ac37be1936f21 100644 --- a/modules/tutor/src/main/TutorGlicko.scala +++ b/modules/tutor/src/main/TutorGlicko.scala @@ -11,9 +11,10 @@ object TutorGlicko: def scoresRating(perf: Perf, scores: List[(Rating, Score)]): Rating = val calculator = glicko2.RatingCalculator() val player = perf.toRating - val results = glicko2.RatingPeriodResults[glicko2.Result](player -> - scores.map: (rating, score) => - glicko2.Result(glicko2.Rating(rating, 60, 0.06, 10), score) + val results = glicko2.RatingPeriodResults[glicko2.Result]( + player -> + scores.map: (rating, score) => + glicko2.Result(glicko2.Rating(rating, 60, 0.06, 10), score) ) try calculator.updateRatings(results, true) From 1bc6e0469e0e093712da9a17c4f017db63f95e04 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Mon, 18 Nov 2024 05:42:57 -0600 Subject: [PATCH 38/44] Attempt to fix type errors --- .../rating/src/test/RatingCalculatorTest.scala | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/modules/rating/src/test/RatingCalculatorTest.scala b/modules/rating/src/test/RatingCalculatorTest.scala index f96bcf2794371..4f866bd46383b 100644 --- a/modules/rating/src/test/RatingCalculatorTest.scala +++ b/modules/rating/src/test/RatingCalculatorTest.scala @@ -9,20 +9,25 @@ import glicko2.* class RatingCalculatorTest extends lila.common.LilaTest: def updateRatings(wRating: Rating, bRating: Rating, winner: Option[Color]) = - val ratings = Set(wRating, bRating) val result = winner match case Some(chess.White) => Glicko.Result.Win case Some(chess.Black) => Glicko.Result.Loss case None => Glicko.Result.Draw val results = RatingPeriodResults[DuelResult]( - List( + wRating -> List( result match - case Glicko.Result.Win => DuelResult(wRating, bRating, Outcome.white) - case Glicko.Result.Loss => DuelResult(wRating, bRating, Outcome.black) - case Glicko.Result.Draw => DuelResult(wRating, bRating, Outcome.draw) + case Glicko.Result.Win => DuelResult(bRating, 1.0d, true) + case Glicko.Result.Loss => DuelResult(bRating, 0.0d, true) + case Glicko.Result.Draw => DuelResult(bRating, 0.5d, true) + ), + bRating -> List( + result match + case Glicko.Result.Win => DuelResult(wRating, 0.0d, false) + case Glicko.Result.Loss => DuelResult(wRating, 1.0d, false) + case Glicko.Result.Draw => DuelResult(wRating, 0.5d, false) ) ) - Glicko.calculatorWithAdvantage(ColorAdvantage.standard).updateRatings(ratings, results, true) + Glicko.calculatorWithAdvantage(ColorAdvantage.standard).updateRatings(results, true) test("default deviation: white wins") { val wr = default.toRating From aa374d380aee67f60c28d743594b8b01cce9a4a1 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Mon, 18 Nov 2024 06:26:06 -0600 Subject: [PATCH 39/44] Strongly type result color --- modules/rating/src/main/glicko2/Result.scala | 6 ++++-- modules/rating/src/test/RatingCalculatorTest.scala | 12 ++++++------ modules/round/src/main/PerfsUpdater.scala | 12 ++++++------ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/modules/rating/src/main/glicko2/Result.scala b/modules/rating/src/main/glicko2/Result.scala index d96b651bedd8e..82ae21515805d 100644 --- a/modules/rating/src/main/glicko2/Result.scala +++ b/modules/rating/src/main/glicko2/Result.scala @@ -1,5 +1,7 @@ package lila.rating.glicko2 +import chess.{ Color, White } + // ASSUME score is between unit interval [0.0d, 1.0d] class Result(opponent: Rating, score: Double): @@ -12,7 +14,7 @@ class Result(opponent: Rating, score: Double): class BinaryResult(val opponent: Rating, val win: Boolean) extends Result(opponent, if win then 1.0d else 0.0d) -class DuelResult(val opponent: Rating, val score: Double, first: Boolean) extends Result(opponent, score): +class DuelResult(val opponent: Rating, val score: Double, color: Color) extends Result(opponent, score): override def getAdvantage(advantage: ColorAdvantage): ColorAdvantage = - if first then advantage.map(_ / 2.0d) else advantage.map(_ / 2.0d).negate + if color == White then advantage.map(_ / 2.0d) else advantage.map(_ / 2.0d).negate diff --git a/modules/rating/src/test/RatingCalculatorTest.scala b/modules/rating/src/test/RatingCalculatorTest.scala index 4f866bd46383b..b73e1ae459d3d 100644 --- a/modules/rating/src/test/RatingCalculatorTest.scala +++ b/modules/rating/src/test/RatingCalculatorTest.scala @@ -16,15 +16,15 @@ class RatingCalculatorTest extends lila.common.LilaTest: val results = RatingPeriodResults[DuelResult]( wRating -> List( result match - case Glicko.Result.Win => DuelResult(bRating, 1.0d, true) - case Glicko.Result.Loss => DuelResult(bRating, 0.0d, true) - case Glicko.Result.Draw => DuelResult(bRating, 0.5d, true) + case Glicko.Result.Win => DuelResult(bRating, 1.0d, White) + case Glicko.Result.Loss => DuelResult(bRating, 0.0d, White) + case Glicko.Result.Draw => DuelResult(bRating, 0.5d, White) ), bRating -> List( result match - case Glicko.Result.Win => DuelResult(wRating, 0.0d, false) - case Glicko.Result.Loss => DuelResult(wRating, 1.0d, false) - case Glicko.Result.Draw => DuelResult(wRating, 0.5d, false) + case Glicko.Result.Win => DuelResult(wRating, 0.0d, Black) + case Glicko.Result.Loss => DuelResult(wRating, 1.0d, Black) + case Glicko.Result.Draw => DuelResult(wRating, 0.5d, Black) ) ) Glicko.calculatorWithAdvantage(ColorAdvantage.standard).updateRatings(results, true) diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index 9c85bb9e74e14..8021e50185948 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -132,15 +132,15 @@ final class PerfsUpdater( val results = RatingPeriodResults[DuelResult]( white -> List( game.winnerColor match - case Some(chess.White) => DuelResult(black, 1.0d, true) - case Some(chess.Black) => DuelResult(black, 0.0d, true) - case None => DuelResult(black, 0.5d, true) + case Some(chess.White) => DuelResult(black, 1.0d, chess.White) + case Some(chess.Black) => DuelResult(black, 0.0d, chess.White) + case None => DuelResult(black, 0.5d, chess.White) ), black -> List( game.winnerColor match - case Some(chess.White) => DuelResult(white, 0.0d, false) - case Some(chess.Black) => DuelResult(white, 1.0d, false) - case None => DuelResult(white, 0.5d, false) + case Some(chess.White) => DuelResult(white, 0.0d, chess.Black) + case Some(chess.Black) => DuelResult(white, 1.0d, chess.Black) + case None => DuelResult(white, 0.5d, chess.Black) ) ) // tuning TAU per game speed may improve accuracy From 22a850a9e0e1ab77a359b6a4d43c505146d4b49f Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Mon, 18 Nov 2024 06:40:35 -0600 Subject: [PATCH 40/44] Make RatingPeriodResults immutable --- modules/rating/src/main/glicko2/RatingPeriodResults.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/rating/src/main/glicko2/RatingPeriodResults.scala b/modules/rating/src/main/glicko2/RatingPeriodResults.scala index 2627c7aabe067..2189c8a901630 100644 --- a/modules/rating/src/main/glicko2/RatingPeriodResults.scala +++ b/modules/rating/src/main/glicko2/RatingPeriodResults.scala @@ -1,5 +1,7 @@ package lila.rating.glicko2 +import scala.collection.immutable.Map + // rewrite from java https://github.com/goochjs/glicko2 type RatingPeriodResults[R <: Result] = Map[Rating, List[R]] From e36615856e237dc7b00f448fafdd38402a79a1d1 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Mon, 18 Nov 2024 06:44:45 -0600 Subject: [PATCH 41/44] Revert "Make RatingPeriodResults immutable" This reverts commit 22a850a9e0e1ab77a359b6a4d43c505146d4b49f. --- modules/rating/src/main/glicko2/RatingPeriodResults.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/rating/src/main/glicko2/RatingPeriodResults.scala b/modules/rating/src/main/glicko2/RatingPeriodResults.scala index 2189c8a901630..2627c7aabe067 100644 --- a/modules/rating/src/main/glicko2/RatingPeriodResults.scala +++ b/modules/rating/src/main/glicko2/RatingPeriodResults.scala @@ -1,7 +1,5 @@ package lila.rating.glicko2 -import scala.collection.immutable.Map - // rewrite from java https://github.com/goochjs/glicko2 type RatingPeriodResults[R <: Result] = Map[Rating, List[R]] From 485d45988fe807f26d567179c3068ec6a8c90c6a Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Mon, 18 Nov 2024 07:03:10 -0600 Subject: [PATCH 42/44] Make Rating immutable --- modules/rating/src/main/glicko2/Rating.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/rating/src/main/glicko2/Rating.scala b/modules/rating/src/main/glicko2/Rating.scala index c992c6f0b7f47..79b6c1aa25e97 100644 --- a/modules/rating/src/main/glicko2/Rating.scala +++ b/modules/rating/src/main/glicko2/Rating.scala @@ -2,11 +2,11 @@ package lila.rating.glicko2 // rewrite from java https://github.com/goochjs/glicko2 final class Rating( - var rating: Double, - var ratingDeviation: Double, - var volatility: Double, - var numberOfResults: Int, - var lastRatingPeriodEnd: Option[java.time.Instant] = None + val rating: Double, + val ratingDeviation: Double, + val volatility: Double, + val numberOfResults: Int, + val lastRatingPeriodEnd: Option[java.time.Instant] = None ): import RatingCalculator.* From 9aecdde45b50b91dc82f4db5990d695bbade4c9c Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Mon, 18 Nov 2024 07:19:47 -0600 Subject: [PATCH 43/44] Rating facade WIP --- modules/rating/src/main/glicko2/Rating.scala | 8 ++++---- modules/rating/src/main/glicko2/RatingCalculator.scala | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/rating/src/main/glicko2/Rating.scala b/modules/rating/src/main/glicko2/Rating.scala index 79b6c1aa25e97..d857db94bfd2d 100644 --- a/modules/rating/src/main/glicko2/Rating.scala +++ b/modules/rating/src/main/glicko2/Rating.scala @@ -2,9 +2,9 @@ package lila.rating.glicko2 // rewrite from java https://github.com/goochjs/glicko2 final class Rating( - val rating: Double, - val ratingDeviation: Double, - val volatility: Double, + var rating: Double, + var ratingDeviation: Double, + var volatility: Double, val numberOfResults: Int, val lastRatingPeriodEnd: Option[java.time.Instant] = None ): @@ -53,4 +53,4 @@ final class Rating( override def toString = s"$rating / $ratingDeviation / $volatility / $numberOfResults" def incrementNumberOfResults(increment: Int) = - numberOfResults = numberOfResults + increment + Rating(rating, ratingDeviation, volatility, numberOfResults + increment, lastRatingPeriodEnd) diff --git a/modules/rating/src/main/glicko2/RatingCalculator.scala b/modules/rating/src/main/glicko2/RatingCalculator.scala index 72a411b88ea9f..836503ad3bd2c 100644 --- a/modules/rating/src/main/glicko2/RatingCalculator.scala +++ b/modules/rating/src/main/glicko2/RatingCalculator.scala @@ -59,7 +59,7 @@ final class RatingCalculator( results: RatingPeriodResults[Result], skipDeviationIncrease: Boolean = false ) = - results.foreach { case (player, results) => + results.map { case (player, results) => val elapsedRatingPeriods = if skipDeviationIncrease then 0 else 1 if results.sizeIs > 0 then calculateNewRating(player, results, elapsedRatingPeriods) else @@ -99,7 +99,7 @@ final class RatingCalculator( * @param results * @param elapsedRatingPeriods */ - def calculateNewRating(player: Rating, results: List[Result], elapsedRatingPeriods: Double): Unit = + def calculateNewRating(player: Rating, results: List[Result], elapsedRatingPeriods: Double): Rating = val phi = player.getGlicko2RatingDeviation val sigma = player.volatility val a = Math.log(Math.pow(sigma, 2)) From 8b752af00a34c89618876507bd01d0ad836c6783 Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Mon, 18 Nov 2024 08:02:50 -0600 Subject: [PATCH 44/44] Rating facade --- modules/puzzle/src/main/PuzzleFinisher.scala | 10 ++-- modules/rating/src/main/glicko2/Rating.scala | 34 ++++++----- .../src/main/glicko2/RatingCalculator.scala | 6 +- .../src/test/RatingCalculatorTest.scala | 60 ++++++------------- modules/round/src/main/PerfsUpdater.scala | 34 +++++------ modules/tutor/src/main/TutorGlicko.scala | 7 +-- 6 files changed, 63 insertions(+), 88 deletions(-) diff --git a/modules/puzzle/src/main/PuzzleFinisher.scala b/modules/puzzle/src/main/PuzzleFinisher.scala index 3c694b04dd946..d2ab5e815dcbf 100644 --- a/modules/puzzle/src/main/PuzzleFinisher.scala +++ b/modules/puzzle/src/main/PuzzleFinisher.scala @@ -78,15 +78,15 @@ final private[puzzle] class PuzzleFinisher( ) (round, none, perf) case None => - val userRating = perf.toRating - val puzzleRating = glicko2.Rating( + val userOldRating = perf.toRating + val puzzleOldRating = glicko2.Rating( puzzle.glicko.rating.atLeast(lila.rating.Glicko.minRating.value), puzzle.glicko.deviation, puzzle.glicko.volatility, puzzle.plays, none ) - updateRatings(userRating, puzzleRating, win) + val (userRating, puzzleRating) = computeRatings(userOldRating, puzzleOldRating, win) userApi .dubiousPuzzle(me.userId, perf) .map: dubiousPuzzleRating => @@ -204,7 +204,7 @@ final private[puzzle] class PuzzleFinisher( def incPuzzlePlays(puzzleId: PuzzleId): Funit = colls.puzzle.map(_.incFieldUnchecked($id(puzzleId), Puzzle.BSONFields.plays)) - private def updateRatings(u1: glicko2.Rating, u2: glicko2.Rating, win: PuzzleWin): Unit = + private def computeRatings(u1: glicko2.Rating, u2: glicko2.Rating, win: PuzzleWin): Set[glicko2.Rating] = val results = glicko2.RatingPeriodResults[glicko2.BinaryResult]( u1 -> List( if win.yes then glicko2.BinaryResult(u2, true) @@ -215,5 +215,5 @@ final private[puzzle] class PuzzleFinisher( else glicko2.BinaryResult(u1, true) ) ) - try calculator.updateRatings(results) + try calculator.computeRatings(results) catch case e: Exception => logger.error("finisher", e) diff --git a/modules/rating/src/main/glicko2/Rating.scala b/modules/rating/src/main/glicko2/Rating.scala index d857db94bfd2d..c87782384305a 100644 --- a/modules/rating/src/main/glicko2/Rating.scala +++ b/modules/rating/src/main/glicko2/Rating.scala @@ -2,9 +2,9 @@ package lila.rating.glicko2 // rewrite from java https://github.com/goochjs/glicko2 final class Rating( - var rating: Double, - var ratingDeviation: Double, - var volatility: Double, + val rating: Double, + val ratingDeviation: Double, + val volatility: Double, val numberOfResults: Int, val lastRatingPeriodEnd: Option[java.time.Instant] = None ): @@ -12,9 +12,9 @@ final class Rating( import RatingCalculator.* // the following variables are used to hold values temporarily whilst running calculations - private[glicko2] var workingRating: Double = scala.compiletime.uninitialized - private[glicko2] var workingRatingDeviation: Double = scala.compiletime.uninitialized - private[glicko2] var workingVolatility: Double = scala.compiletime.uninitialized + private[glicko2] var workingRating: Double = 0d + private[glicko2] var workingRatingDeviation: Double = 0d + private[glicko2] var workingVolatility: Double = 0d /** Return the average skill value of the player scaled down to the scale used by the algorithm's internal * workings. @@ -27,8 +27,8 @@ final class Rating( /** Set the average skill value, taking in a value in Glicko2 scale. */ - def setGlicko2Rating(r: Double) = - rating = convertRatingToOriginalGlickoScale(r) + private def setGlicko2Rating(r: Double) = + convertRatingToOriginalGlickoScale(r) /** Return the rating deviation of the player scaled down to the scale used by the algorithm's internal * workings. @@ -37,18 +37,20 @@ final class Rating( /** Set the rating deviation, taking in a value in Glicko2 scale. */ - def setGlicko2RatingDeviation(rd: Double) = - ratingDeviation = convertRatingDeviationToOriginalGlickoScale(rd) + private def setGlicko2RatingDeviation(rd: Double) = + convertRatingDeviationToOriginalGlickoScale(rd) /** Used by the calculation engine, to move interim calculations into their "proper" places. */ def finaliseRating() = - setGlicko2Rating(workingRating) - setGlicko2RatingDeviation(workingRatingDeviation) - volatility = workingVolatility - workingRatingDeviation = 0d - workingRating = 0d - workingVolatility = 0d + Rating( + convertRatingToOriginalGlickoScale(workingRating), + convertRatingDeviationToOriginalGlickoScale(workingRatingDeviation), + workingVolatility, + numberOfResults, + lastRatingPeriodEnd + ) + override def toString = s"$rating / $ratingDeviation / $volatility / $numberOfResults" diff --git a/modules/rating/src/main/glicko2/RatingCalculator.scala b/modules/rating/src/main/glicko2/RatingCalculator.scala index 836503ad3bd2c..bf146f84f8a5c 100644 --- a/modules/rating/src/main/glicko2/RatingCalculator.scala +++ b/modules/rating/src/main/glicko2/RatingCalculator.scala @@ -55,10 +55,10 @@ final class RatingCalculator( * * @param results */ - def updateRatings( + def computeRatings( results: RatingPeriodResults[Result], skipDeviationIncrease: Boolean = false - ) = + ): Set[Rating] = results.map { case (player, results) => val elapsedRatingPeriods = if skipDeviationIncrease then 0 else 1 if results.sizeIs > 0 then calculateNewRating(player, results, elapsedRatingPeriods) @@ -72,7 +72,7 @@ final class RatingCalculator( } // now iterate through the participants and confirm their new ratings - results.keySet.foreach { _.finaliseRating() } + results.keySet.map { _.finaliseRating() } /** This is the formula defined in step 6. It is also used for players who have not competed during the * rating period. diff --git a/modules/rating/src/test/RatingCalculatorTest.scala b/modules/rating/src/test/RatingCalculatorTest.scala index b73e1ae459d3d..6ab1653237838 100644 --- a/modules/rating/src/test/RatingCalculatorTest.scala +++ b/modules/rating/src/test/RatingCalculatorTest.scala @@ -8,7 +8,7 @@ import lila.rating.PerfExt.* import glicko2.* class RatingCalculatorTest extends lila.common.LilaTest: - def updateRatings(wRating: Rating, bRating: Rating, winner: Option[Color]) = + def computeRatings(wRating: Rating, bRating: Rating, winner: Option[Color]) = val result = winner match case Some(chess.White) => Glicko.Result.Win case Some(chess.Black) => Glicko.Result.Loss @@ -27,12 +27,10 @@ class RatingCalculatorTest extends lila.common.LilaTest: case Glicko.Result.Draw => DuelResult(wRating, 0.5d, Black) ) ) - Glicko.calculatorWithAdvantage(ColorAdvantage.standard).updateRatings(results, true) + Glicko.calculatorWithAdvantage(ColorAdvantage.standard).var (wr, br) = computeRatings(results, true) test("default deviation: white wins") { - val wr = default.toRating - val br = default.toRating - updateRatings(wr, br, White.some) + var (wr, br) = computeRatings(wp.toRating, bp.toRating, White.some) assertCloseTo(wr.rating, 1739d, 0.5d) assertCloseTo(br.rating, 1261d, 0.5d) assertCloseTo(wr.ratingDeviation, 396.7003d, 0.0001d) @@ -41,9 +39,7 @@ class RatingCalculatorTest extends lila.common.LilaTest: assertCloseTo(br.volatility, 0.09000d, 0.00001d) } test("default deviation: black wins") { - val wr = default.toRating - val br = default.toRating - updateRatings(wr, br, Black.some) + var (wr, br) = computeRatings(wp.toRating, bp.toRating, Black.some) assertCloseTo(wr.rating, 1256d, 0.5d) assertCloseTo(br.rating, 1744d, 0.5d) assertCloseTo(wr.ratingDeviation, 396.7003d, 0.0001d) @@ -52,9 +48,7 @@ class RatingCalculatorTest extends lila.common.LilaTest: assertCloseTo(br.volatility, 0.09000d, 0.00001d) } test("default deviation: draw") { - val wr = default.toRating - val br = default.toRating - updateRatings(wr, br, None) + var (wr, br) = computeRatings(wp.toRating, bp.toRating, None) assertCloseTo(wr.rating, 1497d, 0.5d) assertCloseTo(br.rating, 1503d, 0.5d) assertCloseTo(wr.ratingDeviation, 396.7003d, 0.0001d) @@ -69,9 +63,7 @@ class RatingCalculatorTest extends lila.common.LilaTest: ) ) test("low deviation: white wins") { - val wr = perf.toRating - val br = perf.toRating - updateRatings(wr, br, White.some) + var (wr, br) = computeRatings(wp.toRating, bp.toRating, White.some) assertCloseTo(wr.rating, 1517d, 0.5d) assertCloseTo(br.rating, 1483d, 0.5d) assertCloseTo(wr.ratingDeviation, 78.0800d, 0.0001d) @@ -80,9 +72,7 @@ class RatingCalculatorTest extends lila.common.LilaTest: assertCloseTo(br.volatility, 0.06000d, 0.00001d) } test("low deviation: black wins") { - val wr = perf.toRating - val br = perf.toRating - updateRatings(wr, br, Black.some) + var (wr, br) = computeRatings(wp.toRating, bp.toRating, Black.some) assertCloseTo(wr.rating, 1483d, 0.5d) assertCloseTo(br.rating, 1517d, 0.5d) assertCloseTo(wr.ratingDeviation, 78.0800d, 0.0001d) @@ -91,9 +81,7 @@ class RatingCalculatorTest extends lila.common.LilaTest: assertCloseTo(br.volatility, 0.06000d, 0.00001d) } test("low deviation: draw") { - val wr = perf.toRating - val br = perf.toRating - updateRatings(wr, br, None) + var (wr, br) = computeRatings(wp.toRating, bp.toRating, None) assertCloseTo(wr.rating, 1500d, 0.5d) assertCloseTo(br.rating, 1500d, 0.5d) assertCloseTo(wr.ratingDeviation, 78.0800d, 0.0001d) @@ -102,14 +90,14 @@ class RatingCalculatorTest extends lila.common.LilaTest: assertCloseTo(br.volatility, 0.06000d, 0.00001d) } { - val wP = Perf.default.copy(glicko = + val wp.toRating = Perf.default.copy(glicko = Glicko.default.copy( rating = 1400, deviation = 79, volatility = 0.06 ) ) - val bP = Perf.default.copy(glicko = + val bp.toRating = Perf.default.copy(glicko = Glicko.default.copy( rating = 1550, deviation = 110, @@ -117,9 +105,7 @@ class RatingCalculatorTest extends lila.common.LilaTest: ) ) test("mixed ratings and deviations: white wins") { - val wr = wP.toRating - val br = bP.toRating - updateRatings(wr, br, White.some) + var (wr, br) = computeRatings(wp.toRating, bp.toRating, White.some) assertCloseTo(wr.rating, 1422d, 0.5d) assertCloseTo(br.rating, 1507d, 0.5d) assertCloseTo(wr.ratingDeviation, 77.4720d, 0.0001d) @@ -128,9 +114,7 @@ class RatingCalculatorTest extends lila.common.LilaTest: assertCloseTo(br.volatility, 0.06500d, 0.00001d) } test("mixed ratings and deviations: black wins") { - val wr = wP.toRating - val br = bP.toRating - updateRatings(wr, br, Black.some) + var (wr, br) = computeRatings(wp.toRating, bp.toRating, Black.some) assertCloseTo(wr.rating, 1390d, 0.5d) assertCloseTo(br.rating, 1569d, 0.5d) assertCloseTo(wr.ratingDeviation, 77.4720d, 0.0001d) @@ -139,9 +123,7 @@ class RatingCalculatorTest extends lila.common.LilaTest: assertCloseTo(br.volatility, 0.06500d, 0.00001d) } test("mixed ratings and deviations: draw") { - val wr = wP.toRating - val br = bP.toRating - updateRatings(wr, br, None) + var (wr, br) = computeRatings(wp.toRating, bp.toRating, None) assertCloseTo(wr.rating, 1406d, 0.5d) assertCloseTo(br.rating, 1538d, 0.5d) assertCloseTo(wr.ratingDeviation, 77.4720d, 0.0001d) @@ -151,14 +133,14 @@ class RatingCalculatorTest extends lila.common.LilaTest: } } { - val wP = Perf.default.copy(glicko = + val wp.toRating = Perf.default.copy(glicko = Glicko.default.copy( rating = 1200, deviation = 60, volatility = 0.053 ) ) - val bP = Perf.default.copy(glicko = + val bp.toRating = Perf.default.copy(glicko = Glicko.default.copy( rating = 1850, deviation = 200, @@ -166,9 +148,7 @@ class RatingCalculatorTest extends lila.common.LilaTest: ) ) test("more mixed ratings and deviations: white wins") { - val wr = wP.toRating - val br = bP.toRating - updateRatings(wr, br, White.some) + var (wr, br) = computeRatings(wp.toRating, bp.toRating, White.some) assertCloseTo(wr.rating, 1217d, 0.5d) assertCloseTo(br.rating, 1637d, 0.5d) assertCloseTo(wr.ratingDeviation, 59.8971d, 0.0001d) @@ -177,9 +157,7 @@ class RatingCalculatorTest extends lila.common.LilaTest: assertCloseTo(br.volatility, 0.062028d, 0.00001d) } test("more mixed ratings and deviations: black wins") { - val wr = wP.toRating - val br = bP.toRating - updateRatings(wr, br, Black.some) + var (wr, br) = computeRatings(wp.toRating, bp.toRating, Black.some) assertCloseTo(wr.rating, 1199d, 0.5d) assertCloseTo(br.rating, 1856d, 0.5d) assertCloseTo(wr.ratingDeviation, 59.8971d, 0.0001d) @@ -188,9 +166,7 @@ class RatingCalculatorTest extends lila.common.LilaTest: assertCloseTo(br.volatility, 0.061999d, 0.00001d) } test("more mixed ratings and deviations: draw") { - val wr = wP.toRating - val br = bP.toRating - updateRatings(wr, br, None) + var (wr, br) = computeRatings(wp.toRating, bp.toRating, None) assertCloseTo(wr.rating, 1208d, 0.5d) assertCloseTo(br.rating, 1746d, 0.5d) assertCloseTo(wr.ratingDeviation, 59.8971d, 0.0001d) diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index 8021e50185948..9d2a1b051af43 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -35,35 +35,35 @@ final class PerfsUpdater( val ratingsB = mkRatings(black.perfs) game.ratingVariant match case chess.variant.Chess960 => - updateRatings(ratingsW.chess960, ratingsB.chess960, game) + computeRatings(ratingsW.chess960, ratingsB.chess960, game) case chess.variant.KingOfTheHill => - updateRatings(ratingsW.kingOfTheHill, ratingsB.kingOfTheHill, game) + computeRatings(ratingsW.kingOfTheHill, ratingsB.kingOfTheHill, game) case chess.variant.ThreeCheck => - updateRatings(ratingsW.threeCheck, ratingsB.threeCheck, game) + computeRatings(ratingsW.threeCheck, ratingsB.threeCheck, game) case chess.variant.Antichess => - updateRatings(ratingsW.antichess, ratingsB.antichess, game) + computeRatings(ratingsW.antichess, ratingsB.antichess, game) case chess.variant.Atomic => - updateRatings(ratingsW.atomic, ratingsB.atomic, game) + computeRatings(ratingsW.atomic, ratingsB.atomic, game) case chess.variant.Horde => - updateRatings(ratingsW.horde, ratingsB.horde, game, ColorAdvantage.zero) + computeRatings(ratingsW.horde, ratingsB.horde, game, ColorAdvantage.zero) case chess.variant.RacingKings => - updateRatings(ratingsW.racingKings, ratingsB.racingKings, game, ColorAdvantage.zero) + computeRatings(ratingsW.racingKings, ratingsB.racingKings, game, ColorAdvantage.zero) case chess.variant.Crazyhouse => - updateRatings(ratingsW.crazyhouse, ratingsB.crazyhouse, game, ColorAdvantage.crazyhouse) + computeRatings(ratingsW.crazyhouse, ratingsB.crazyhouse, game, ColorAdvantage.crazyhouse) case chess.variant.Standard => game.speed match case Speed.Bullet => - updateRatings(ratingsW.bullet, ratingsB.bullet, game) + computeRatings(ratingsW.bullet, ratingsB.bullet, game) case Speed.Blitz => - updateRatings(ratingsW.blitz, ratingsB.blitz, game) + computeRatings(ratingsW.blitz, ratingsB.blitz, game) case Speed.Rapid => - updateRatings(ratingsW.rapid, ratingsB.rapid, game) + computeRatings(ratingsW.rapid, ratingsB.rapid, game) case Speed.Classical => - updateRatings(ratingsW.classical, ratingsB.classical, game) + computeRatings(ratingsW.classical, ratingsB.classical, game) case Speed.Correspondence => - updateRatings(ratingsW.correspondence, ratingsB.correspondence, game) + computeRatings(ratingsW.correspondence, ratingsB.correspondence, game) case Speed.UltraBullet => - updateRatings(ratingsW.ultraBullet, ratingsB.ultraBullet, game) + computeRatings(ratingsW.ultraBullet, ratingsB.ultraBullet, game) case _ => val perfsW = mkPerfs(ratingsW, white -> black, game) val perfsB = mkPerfs(ratingsB, black -> white, game) @@ -123,12 +123,12 @@ final class PerfsUpdater( correspondence = perfs.correspondence.toRating ) - private def updateRatings( + private def computeRatings( white: Rating, black: Rating, game: Game, advantage: ColorAdvantage = ColorAdvantage.standard - ): Unit = + ): Set[Rating] = val results = RatingPeriodResults[DuelResult]( white -> List( game.winnerColor match @@ -144,7 +144,7 @@ final class PerfsUpdater( ) ) // tuning TAU per game speed may improve accuracy - try Glicko.calculatorWithAdvantage(advantage).updateRatings(results, true) + try Glicko.calculatorWithAdvantage(advantage).computeRatings(results, true) catch case e: Exception => logger.error(s"update ratings #${game.id}", e) private def mkPerfs(ratings: Ratings, users: PairOf[UserWithPerfs], game: Game): UserPerfs = diff --git a/modules/tutor/src/main/TutorGlicko.scala b/modules/tutor/src/main/TutorGlicko.scala index ac37be1936f21..a2dfdcc79ee82 100644 --- a/modules/tutor/src/main/TutorGlicko.scala +++ b/modules/tutor/src/main/TutorGlicko.scala @@ -10,14 +10,11 @@ object TutorGlicko: def scoresRating(perf: Perf, scores: List[(Rating, Score)]): Rating = val calculator = glicko2.RatingCalculator() - val player = perf.toRating val results = glicko2.RatingPeriodResults[glicko2.Result]( - player -> + perf.toRating -> scores.map: (rating, score) => glicko2.Result(glicko2.Rating(rating, 60, 0.06, 10), score) ) - try calculator.updateRatings(results, true) + try calculator.computeRatings(results, true) catch case e: Exception => logger.error("TutorGlicko.scoresRating", e) - - player.rating.toInt