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 ) }