From 6e62e76b090a69aea3353d0d4e1b21a3ce0492a5 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Tue, 19 Nov 2024 08:59:13 +0100 Subject: [PATCH 01/13] import glicko implementation from lila along with its tests --- core/src/main/scala/glicko/glicko.scala | 11 + core/src/main/scala/glicko/impl/README.md | 4 + core/src/main/scala/glicko/impl/Rating.scala | 51 ++++ .../scala/glicko/impl/RatingCalculator.scala | 217 ++++++++++++++++++ core/src/main/scala/glicko/impl/results.scala | 61 +++++ .../scala/glicko/RatingCalculatorTest.scala | 202 ++++++++++++++++ 6 files changed, 546 insertions(+) create mode 100644 core/src/main/scala/glicko/glicko.scala create mode 100644 core/src/main/scala/glicko/impl/README.md create mode 100644 core/src/main/scala/glicko/impl/Rating.scala create mode 100644 core/src/main/scala/glicko/impl/RatingCalculator.scala create mode 100644 core/src/main/scala/glicko/impl/results.scala create mode 100644 test-kit/src/test/scala/glicko/RatingCalculatorTest.scala diff --git a/core/src/main/scala/glicko/glicko.scala b/core/src/main/scala/glicko/glicko.scala new file mode 100644 index 000000000..ee47637d4 --- /dev/null +++ b/core/src/main/scala/glicko/glicko.scala @@ -0,0 +1,11 @@ +package chess.glicko + +import java.time.Instant + +case class Player( + rating: Double, + ratingDeviation: Double, + volatility: Double, + numberOfResults: Int, + lastRatingPeriodEnd: Option[Instant] = None +) diff --git a/core/src/main/scala/glicko/impl/README.md b/core/src/main/scala/glicko/impl/README.md new file mode 100644 index 000000000..bc97ba577 --- /dev/null +++ b/core/src/main/scala/glicko/impl/README.md @@ -0,0 +1,4 @@ +Loosely ported from java: https://github.com/goochjs/glicko2 + +The implementation is not idiomatic scala and should not be used. +Use the public API instead. diff --git a/core/src/main/scala/glicko/impl/Rating.scala b/core/src/main/scala/glicko/impl/Rating.scala new file mode 100644 index 000000000..9ca796fd0 --- /dev/null +++ b/core/src/main/scala/glicko/impl/Rating.scala @@ -0,0 +1,51 @@ +package chess.glicko.impl + +final class Rating( + var rating: Double, + var ratingDeviation: Double, + var volatility: Double, + var numberOfResults: Int, + var lastRatingPeriodEnd: Option[java.time.Instant] = None +): + + import RatingCalculator.* + + // the following variables are used to hold values temporarily whilst running calculations + private[glicko] var workingRating: Double = scala.compiletime.uninitialized + private[glicko] var workingRatingDeviation: Double = scala.compiletime.uninitialized + private[glicko] var workingVolatility: Double = scala.compiletime.uninitialized + + /** Return the average skill value of the player scaled down to the scale used by the algorithm's internal + * workings. + */ + def getGlicko2Rating: Double = convertRatingToGlicko2Scale(this.rating) + + /** Set the average skill value, taking in a value in Glicko2 scale. + */ + def setGlicko2Rating(r: Double) = + rating = convertRatingToOriginalGlickoScale(r) + + /** Return the rating deviation of the player scaled down to the scale used by the algorithm's internal + * workings. + */ + def getGlicko2RatingDeviation: Double = convertRatingDeviationToGlicko2Scale(ratingDeviation) + + /** Set the rating deviation, taking in a value in Glicko2 scale. + */ + def setGlicko2RatingDeviation(rd: Double) = + ratingDeviation = 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 + + override def toString = s"$rating / $ratingDeviation / $volatility / $numberOfResults" + + def incrementNumberOfResults(increment: Int) = + numberOfResults = numberOfResults + increment diff --git a/core/src/main/scala/glicko/impl/RatingCalculator.scala b/core/src/main/scala/glicko/impl/RatingCalculator.scala new file mode 100644 index 000000000..73a56b567 --- /dev/null +++ b/core/src/main/scala/glicko/impl/RatingCalculator.scala @@ -0,0 +1,217 @@ +package chess.glicko.impl + +import scalalib.newtypes.OpaqueDouble +import scalalib.extensions.ifTrue +import java.time.Instant + +opaque type Tau = Double +object Tau extends OpaqueDouble[Tau]: + val default: Tau = 0.75d + +opaque type RatingPeriodsPerDay = Double +object RatingPeriodsPerDay extends OpaqueDouble[RatingPeriodsPerDay]: + val default: RatingPeriodsPerDay = 0d + +// rewrite from java https://github.com/goochjs/glicko2 +object RatingCalculator: + + private val MULTIPLIER: Double = 173.7178 + val DEFAULT_RATING: Double = 1500.0 + + def convertRatingToOriginalGlickoScale(rating: Double): Double = + ((rating * MULTIPLIER) + DEFAULT_RATING) + + def convertRatingToGlicko2Scale(rating: Double): Double = + ((rating - DEFAULT_RATING) / MULTIPLIER) + + def convertRatingDeviationToOriginalGlickoScale(ratingDeviation: Double): Double = + (ratingDeviation * MULTIPLIER) + + def convertRatingDeviationToGlicko2Scale(ratingDeviation: Double): Double = + (ratingDeviation / MULTIPLIER) + +final class RatingCalculator( + tau: Tau = Tau.default, + ratingPeriodsPerDay: RatingPeriodsPerDay = RatingPeriodsPerDay.default +): + + import RatingCalculator.* + + private val CONVERGENCE_TOLERANCE: Double = 0.000001 + private val ITERATION_MAX: Int = 1000 + private val DAYS_PER_MILLI: Double = 1.0 / (1000 * 60 * 60 * 24) + + private val ratingPeriodsPerMilli: Double = ratingPeriodsPerDay.value * 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.

+ * + * @param results + */ + def updateRatings(results: RatingPeriodResults[?], skipDeviationIncrease: Boolean = false) = + val players = results.getParticipants + players.foreach { player => + val elapsedRatingPeriods = if skipDeviationIncrease then 0 else 1 + if results.getResults(player).sizeIs > 0 then + calculateNewRating(player, results.getResults(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 + player.workingRating = player.getGlicko2Rating + player.workingRatingDeviation = + calculateNewRD(player.getGlicko2RatingDeviation, player.volatility, elapsedRatingPeriods) + player.workingVolatility = player.volatility + } + + // now iterate through the participants and confirm their new ratings + players.foreach { _.finaliseRating() } + + /** This is the formula defined in step 6. It is also used for players who have not competed during the + * rating period. + * + * @param player + * @param ratingPeriodEndDate + * @param reverse + * @return + * new rating deviation + */ + def previewDeviation(player: Rating, ratingPeriodEndDate: Instant, reverse: Boolean): Double = + var elapsedRatingPeriods = 0d + player.lastRatingPeriodEnd.ifTrue(ratingPeriodsPerMilli > 0).foreach { periodEnd => + val interval = java.time.Duration.between(periodEnd, ratingPeriodEndDate) + elapsedRatingPeriods = interval.toMillis * ratingPeriodsPerMilli + } + if reverse then elapsedRatingPeriods = -elapsedRatingPeriods + val newRD = calculateNewRD(player.getGlicko2RatingDeviation, player.volatility, elapsedRatingPeriods) + convertRatingDeviationToOriginalGlickoScale(newRD) + + /** This is the function processing described in step 5 of Glickman's paper. + * + * @param player + * @param results + * @param elapsedRatingPeriods + */ + def calculateNewRating(player: Rating, results: List[Result], elapsedRatingPeriods: Double): Unit = + val phi = player.getGlicko2RatingDeviation + val sigma = player.volatility + val a = Math.log(Math.pow(sigma, 2)) + val delta = deltaOf(player, results) + val v = vOf(player, results) + val tau = this.tau.value + + // step 5.2 - set the initial values of the iterative algorithm to come in step 5.4 + var A: Double = a + var B: Double = 0 + if Math.pow(delta, 2) > Math.pow(phi, 2) + v then B = Math.log(Math.pow(delta, 2) - Math.pow(phi, 2) - v) + else + var k = 1d + B = a - (k * Math.abs(tau)) + + while f(B, delta, phi, v, a, tau) < 0 do + k = k + 1 + B = a - (k * Math.abs(tau)) + + // step 5.3 + var fA = f(A, delta, phi, v, a, tau) + var fB = f(B, delta, phi, v, a, tau) + + // step 5.4 + var iterations = 0 + while Math.abs(B - A) > CONVERGENCE_TOLERANCE && iterations < ITERATION_MAX do + iterations = iterations + 1 + // println(String.format("%f - %f (%f) > %f", B, A, Math.abs(B - A), CONVERGENCE_TOLERANCE)) + val C = A + (((A - B) * fA) / (fB - fA)) + val fC = f(C, delta, phi, v, a, tau) + + if fC * fB <= 0 then + A = B + fA = fB + else fA = fA / 2.0 + + B = C + fB = fC + if iterations == ITERATION_MAX then + println(String.format("Convergence fail at %d iterations", iterations)) + println(player.toString()) + results.foreach(println) + throw new RuntimeException("Convergence fail") + + val newSigma = Math.exp(A / 2.0) + + player.workingVolatility = newSigma + + // Step 6 + val phiStar = calculateNewRD(phi, newSigma, elapsedRatingPeriods) + + // Step 7 + val newPhi = 1.0 / Math.sqrt((1.0 / Math.pow(phiStar, 2)) + (1.0 / v)) + + // note that the newly calculated rating values are stored in a "working" area in the Rating object + // this avoids us attempting to calculate subsequent participants' ratings against a moving target + player.workingRating = + player.getGlicko2Rating + (Math.pow(newPhi, 2) * outcomeBasedRating(player, results)) + player.workingRatingDeviation = newPhi + player.incrementNumberOfResults(results.size) + + private def f(x: Double, delta: Double, phi: Double, v: Double, a: Double, tau: Double) = + (Math.exp(x) * (Math.pow(delta, 2) - Math.pow(phi, 2) - v - Math.exp(x)) / + (2.0 * Math.pow(Math.pow(phi, 2) + v + Math.exp(x), 2))) - + ((x - a) / Math.pow(tau, 2)) + + /** This is the first sub-function of step 3 of Glickman's paper. + */ + private def g(deviation: Double) = + 1.0 / (Math.sqrt(1.0 + (3.0 * Math.pow(deviation, 2) / Math.pow(Math.PI, 2)))) + + /** This is the second sub-function of step 3 of Glickman's paper. + */ + private def E(playerRating: Double, opponentRating: Double, opponentDeviation: Double) = + 1.0 / (1.0 + Math.exp(-1.0 * g(opponentDeviation) * (playerRating - opponentRating))) + + /** This is the main function in step 3 of Glickman's paper. + */ + private def vOf(player: Rating, results: List[Result]) = + var v = 0.0d + for result <- results do + v = v + ((Math.pow(g(result.getOpponent(player).getGlicko2RatingDeviation), 2)) + * E( + player.getGlicko2Rating, + result.getOpponent(player).getGlicko2Rating, + result.getOpponent(player).getGlicko2RatingDeviation + ) + * (1.0 - E( + player.getGlicko2Rating, + result.getOpponent(player).getGlicko2Rating, + result.getOpponent(player).getGlicko2RatingDeviation + ))) + 1 / v + + /** This is a formula as per step 4 of Glickman's paper. + */ + private def deltaOf(player: Rating, results: List[Result]): Double = + vOf(player, results) * outcomeBasedRating(player, results) + + /** This is a formula as per step 4 of Glickman's paper. + * + * @return + * expected rating based on game outcomes + */ + private def outcomeBasedRating(player: Rating, results: List[Result]) = + var outcomeBasedRating = 0d + for result <- results do + outcomeBasedRating = outcomeBasedRating + + (g(result.getOpponent(player).getGlicko2RatingDeviation) + * (result.getScore(player) - E( + player.getGlicko2Rating, + result.getOpponent(player).getGlicko2Rating, + result.getOpponent(player).getGlicko2RatingDeviation + ))) + outcomeBasedRating + + /** This is the formula defined in step 6. It is also used for players who have not competed during the + * rating period. + */ + private def calculateNewRD(phi: Double, sigma: Double, elapsedRatingPeriods: Double) = + Math.sqrt(Math.pow(phi, 2) + elapsedRatingPeriods * Math.pow(sigma, 2)) diff --git a/core/src/main/scala/glicko/impl/results.scala b/core/src/main/scala/glicko/impl/results.scala new file mode 100644 index 000000000..7e4b6528b --- /dev/null +++ b/core/src/main/scala/glicko/impl/results.scala @@ -0,0 +1,61 @@ +package chess.glicko.impl + +trait Result: + + def getScore(player: Rating): Double + + def getOpponent(player: Rating): Rating + + def participated(player: Rating): Boolean + + def players: List[Rating] + +// score from 0 (opponent wins) to 1 (player wins) +class FloatingResult(player: Rating, opponent: Rating, score: Float) extends Result: + + def getScore(p: Rating) = if p == player then score else 1 - score + + def getOpponent(p: Rating) = if p == player then opponent else player + + def participated(p: Rating) = p == player || p == opponent + + def players = List(player, opponent) + +final class GameResult(winner: Rating, loser: Rating, isDraw: Boolean) extends Result: + private val POINTS_FOR_WIN = 1.0d + private val POINTS_FOR_LOSS = 0.0d + private val POINTS_FOR_DRAW = 0.5d + + def players = List(winner, loser) + + def participated(player: Rating) = player == winner || player == loser + + /** Returns the "score" for a match. + * + * @param player + * @return + * 1 for a win, 0.5 for a draw and 0 for a 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 getOpponent(player: Rating) = + if winner == player then loser + else if loser == player then winner + else throw new IllegalArgumentException("Player did not participate in match"); + + override def toString = s"$winner vs $loser = $isDraw" + +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 GameRatingPeriodResults(val results: List[GameResult]) extends RatingPeriodResults[GameResult] + +class FloatingRatingPeriodResults(val results: List[FloatingResult]) + extends RatingPeriodResults[FloatingResult] diff --git a/test-kit/src/test/scala/glicko/RatingCalculatorTest.scala b/test-kit/src/test/scala/glicko/RatingCalculatorTest.scala new file mode 100644 index 000000000..ee194d326 --- /dev/null +++ b/test-kit/src/test/scala/glicko/RatingCalculatorTest.scala @@ -0,0 +1,202 @@ +package chess +package glicko.impl + +import munit.ScalaCheckSuite +import cats.syntax.all.* + +class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: + + // Chosen so a typical player's RD goes from 60 -> 110 in 1 year + val ratingPeriodsPerDay = RatingPeriodsPerDay(0.21436d) + + val system = RatingCalculator(Tau.default, ratingPeriodsPerDay) + + def updateRatings(wRating: Rating, bRating: Rating, outcome: Outcome) = + val results = GameRatingPeriodResults: + List: + outcome.winner match + case None => GameResult(wRating, bRating, true) + case Some(White) => GameResult(wRating, bRating, false) + case Some(Black) => GameResult(bRating, wRating, false) + system.updateRatings(results, true) + + def defaultRating = Rating( + rating = 1500d, + ratingDeviation = 500d, + volatility = 0.09d, + numberOfResults = 0, + lastRatingPeriodEnd = None + ) + + test("default deviation: white wins") { + val wr = defaultRating + val br = defaultRating + updateRatings(wr, br, Outcome.white) + 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) + } + test("default deviation: black wins") { + val wr = defaultRating + val br = defaultRating + updateRatings(wr, br, Outcome.black) + 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) + } + test("default deviation: draw") { + val wr = defaultRating + val br = defaultRating + updateRatings(wr, br, Outcome.draw) + assertCloseTo(wr.rating, 1500d, 1d) + assertCloseTo(br.rating, 1500d, 1d) + assertCloseTo(wr.ratingDeviation, 396d, 1d) + assertCloseTo(br.ratingDeviation, 396d, 1d) + assertCloseTo(wr.volatility, 0.0899954, 0.0000001d) + assertCloseTo(br.volatility, 0.0899954, 0.0000001d) + } + + def oldRating = Rating( + rating = 1500d, + ratingDeviation = 80d, + volatility = 0.06d, + numberOfResults = 0, + lastRatingPeriodEnd = None + ) + test("low deviation: white wins") { + val wr = oldRating + val br = oldRating + updateRatings(wr, br, Outcome.white) + assertCloseTo(wr.rating, 1517d, 1d) + assertCloseTo(br.rating, 1482d, 1d) + assertCloseTo(wr.ratingDeviation, 78d, 1d) + assertCloseTo(br.ratingDeviation, 78d, 1d) + assertCloseTo(wr.volatility, 0.06, 0.00001d) + assertCloseTo(br.volatility, 0.06, 0.00001d) + } + test("low deviation: black wins") { + val wr = oldRating + val br = oldRating + updateRatings(wr, br, Outcome.black) + assertCloseTo(wr.rating, 1482d, 1d) + assertCloseTo(br.rating, 1517d, 1d) + assertCloseTo(wr.ratingDeviation, 78d, 1d) + assertCloseTo(br.ratingDeviation, 78d, 1d) + assertCloseTo(wr.volatility, 0.06, 0.00001d) + assertCloseTo(br.volatility, 0.06, 0.00001d) + } + test("low deviation: draw") { + val wr = oldRating + val br = oldRating + updateRatings(wr, br, Outcome.draw) + assertCloseTo(wr.rating, 1500d, 1d) + assertCloseTo(br.rating, 1500d, 1d) + assertCloseTo(wr.ratingDeviation, 78d, 1d) + assertCloseTo(br.ratingDeviation, 78d, 1d) + assertCloseTo(wr.volatility, 0.06, 0.00001d) + assertCloseTo(br.volatility, 0.06, 0.00001d) + } + { + def whiteRating = Rating( + rating = 1400d, + ratingDeviation = 79d, + volatility = 0.06d, + numberOfResults = 0, + lastRatingPeriodEnd = None + ) + def blackRating = Rating( + rating = 1550d, + ratingDeviation = 110d, + volatility = 0.065d, + numberOfResults = 0, + lastRatingPeriodEnd = None + ) + test("mixed ratings and deviations: white wins") { + val wr = whiteRating + val br = blackRating + updateRatings(wr, br, Outcome.white) + assertCloseTo(wr.rating, 1422d, 1d) + assertCloseTo(br.rating, 1506d, 1d) + assertCloseTo(wr.ratingDeviation, 77d, 1d) + assertCloseTo(br.ratingDeviation, 105d, 1d) + assertCloseTo(wr.volatility, 0.06, 0.00001d) + assertCloseTo(br.volatility, 0.065, 0.00001d) + } + test("mixed ratings and deviations: black wins") { + val wr = whiteRating + val br = blackRating + updateRatings(wr, br, Outcome.black) + assertCloseTo(wr.rating, 1389d, 1d) + assertCloseTo(br.rating, 1568d, 1d) + assertCloseTo(wr.ratingDeviation, 78d, 1d) + assertCloseTo(br.ratingDeviation, 105d, 1d) + assertCloseTo(wr.volatility, 0.06, 0.00001d) + assertCloseTo(br.volatility, 0.065, 0.00001d) + } + test("mixed ratings and deviations: draw") { + val wr = whiteRating + val br = blackRating + updateRatings(wr, br, Outcome.draw) + assertCloseTo(wr.rating, 1406d, 1d) + assertCloseTo(br.rating, 1537d, 1d) + assertCloseTo(wr.ratingDeviation, 78d, 1d) + assertCloseTo(br.ratingDeviation, 105.87d, 0.01d) + assertCloseTo(wr.volatility, 0.06, 0.00001d) + assertCloseTo(br.volatility, 0.065, 0.00001d) + } + } + { + def whiteRating = Rating( + rating = 1200d, + ratingDeviation = 60d, + volatility = 0.053d, + numberOfResults = 0, + lastRatingPeriodEnd = None + ) + def blackRating = Rating( + rating = 1850d, + ratingDeviation = 200d, + volatility = 0.062d, + numberOfResults = 0, + lastRatingPeriodEnd = None + ) + test("more mixed ratings and deviations: white wins") { + val wr = whiteRating + val br = blackRating + updateRatings(wr, br, Outcome.white) + 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.volatility, 0.053013, 0.000001d) + assertCloseTo(br.volatility, 0.062028, 0.000001d) + } + test("more mixed ratings and deviations: black wins") { + val wr = whiteRating + val br = blackRating + updateRatings(wr, br, Outcome.black) + 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.volatility, 0.052999, 0.000001d) + assertCloseTo(br.volatility, 0.061999, 0.000001d) + } + test("more mixed ratings and deviations: draw") { + val wr = whiteRating + val br = blackRating + updateRatings(wr, br, Outcome.draw) + 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.volatility, 0.053002, 0.000001d) + assertCloseTo(br.volatility, 0.062006, 0.000001d) + } + } From cc5b9f51c9e76f289a2ec557dd77cc767af37af4 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Tue, 19 Nov 2024 09:05:03 +0100 Subject: [PATCH 02/13] sbt scalafix --- core/src/main/scala/glicko/impl/RatingCalculator.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/scala/glicko/impl/RatingCalculator.scala b/core/src/main/scala/glicko/impl/RatingCalculator.scala index 73a56b567..ca16a3435 100644 --- a/core/src/main/scala/glicko/impl/RatingCalculator.scala +++ b/core/src/main/scala/glicko/impl/RatingCalculator.scala @@ -1,7 +1,8 @@ package chess.glicko.impl -import scalalib.newtypes.OpaqueDouble import scalalib.extensions.ifTrue +import scalalib.newtypes.OpaqueDouble + import java.time.Instant opaque type Tau = Double From bef2941e0375c3a98dab988734fc3071a23c4ba8 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Tue, 19 Nov 2024 10:25:27 +0100 Subject: [PATCH 03/13] implement and test pure API to glicko rating computation for a single game only. multiple games require some way to uniquely identify players. --- core/src/main/scala/glicko/glicko.scala | 71 ++++++++- core/src/main/scala/glicko/impl/Rating.scala | 2 +- .../scala/glicko/impl/RatingCalculator.scala | 1 - .../test/scala/glicko/GlickoCalculator.scala | 135 ++++++++++++++++++ .../{ => impl}/RatingCalculatorTest.scala | 40 ++---- 5 files changed, 220 insertions(+), 29 deletions(-) create mode 100644 test-kit/src/test/scala/glicko/GlickoCalculator.scala rename test-kit/src/test/scala/glicko/{ => impl}/RatingCalculatorTest.scala (89%) diff --git a/core/src/main/scala/glicko/glicko.scala b/core/src/main/scala/glicko/glicko.scala index ee47637d4..43fd550aa 100644 --- a/core/src/main/scala/glicko/glicko.scala +++ b/core/src/main/scala/glicko/glicko.scala @@ -1,4 +1,5 @@ -package chess.glicko +package chess +package glicko import java.time.Instant @@ -9,3 +10,71 @@ case class Player( numberOfResults: Int, lastRatingPeriodEnd: Option[Instant] = None ) + +case class Game(players: ByColor[Player], outcome: Outcome) + +case class Config( + tau: impl.Tau = impl.Tau.default, + ratingPeriodsPerDay: impl.RatingPeriodsPerDay = impl.RatingPeriodsPerDay.default +) + +/* Purely functional interface hiding the mutable implementation */ +trait GlickoCalculatorApi: + + /** Apply rating calculations and return updated players. + * Note that players who did not compete during the rating period will have see their deviation increase. + * This requires players to have some sort of unique identifier. + */ + // def computeGames( + // games: List[Game], + // skipDeviationIncrease: Boolean = false + // ): List[Player] + + // Simpler use case: a single game + def computeGame(game: Game, skipDeviationIncrease: Boolean = false): ByColor[Player] + + /** This is the formula defined in step 6. It is also used for players who have not competed during the rating period. + */ + // def previewDeviation(player: Player, ratingPeriodEndDate: Instant, reverse: Boolean): Double + +final class GlickoCalculator(config: Config) extends GlickoCalculatorApi: + + private val calculator = chess.glicko.impl.RatingCalculator(config.tau, config.ratingPeriodsPerDay) + + // Simpler use case: a single game + def computeGame(game: Game, skipDeviationIncrease: Boolean = false): ByColor[Player] = + val ratings = game.players.map(conversions.toRating) + val gameResult = conversions.toGameResult(ratings, game.outcome) + val periodResults = impl.GameRatingPeriodResults(List(gameResult)) + calculator.updateRatings(periodResults, skipDeviationIncrease) + ratings.map(conversions.toPlayer) + + private object conversions: + + import impl.* + + def toGameResult(ratings: ByColor[Rating], outcome: Outcome): GameResult = + outcome.winner match + case None => GameResult(ratings.white, ratings.black, true) + case Some(White) => GameResult(ratings.white, ratings.black, false) + case Some(Black) => GameResult(ratings.black, ratings.white, false) + + def gamesToPeriodResults(games: List[Game]) = GameRatingPeriodResults: + games.map: game => + toGameResult(game.players.map(toRating), game.outcome) + + def toRating(player: Player) = impl.Rating( + rating = player.rating, + ratingDeviation = player.ratingDeviation, + volatility = player.volatility, + numberOfResults = player.numberOfResults, + lastRatingPeriodEnd = player.lastRatingPeriodEnd + ) + + def toPlayer(rating: Rating) = Player( + rating = rating.rating, + ratingDeviation = rating.ratingDeviation, + volatility = rating.volatility, + numberOfResults = rating.numberOfResults, + lastRatingPeriodEnd = rating.lastRatingPeriodEnd + ) diff --git a/core/src/main/scala/glicko/impl/Rating.scala b/core/src/main/scala/glicko/impl/Rating.scala index 9ca796fd0..34901127f 100644 --- a/core/src/main/scala/glicko/impl/Rating.scala +++ b/core/src/main/scala/glicko/impl/Rating.scala @@ -45,7 +45,7 @@ final class Rating( workingRating = 0d workingVolatility = 0d - override def toString = s"$rating / $ratingDeviation / $volatility / $numberOfResults" + override def toString = f"Rating($rating%1.2f, $ratingDeviation%1.2f, $volatility%1.2f, $numberOfResults)" def incrementNumberOfResults(increment: Int) = numberOfResults = numberOfResults + increment diff --git a/core/src/main/scala/glicko/impl/RatingCalculator.scala b/core/src/main/scala/glicko/impl/RatingCalculator.scala index ca16a3435..03c2c909b 100644 --- a/core/src/main/scala/glicko/impl/RatingCalculator.scala +++ b/core/src/main/scala/glicko/impl/RatingCalculator.scala @@ -13,7 +13,6 @@ opaque type RatingPeriodsPerDay = Double object RatingPeriodsPerDay extends OpaqueDouble[RatingPeriodsPerDay]: val default: RatingPeriodsPerDay = 0d -// rewrite from java https://github.com/goochjs/glicko2 object RatingCalculator: private val MULTIPLIER: Double = 173.7178 diff --git a/test-kit/src/test/scala/glicko/GlickoCalculator.scala b/test-kit/src/test/scala/glicko/GlickoCalculator.scala new file mode 100644 index 000000000..91899c4cb --- /dev/null +++ b/test-kit/src/test/scala/glicko/GlickoCalculator.scala @@ -0,0 +1,135 @@ +package chess +package glicko + +import munit.ScalaCheckSuite + +class GlickoCalculatorTest extends ScalaCheckSuite with MunitExtensions: + + val calc = GlickoCalculator: + Config( + ratingPeriodsPerDay = impl.RatingPeriodsPerDay(0.21436d) + ) + + def computeGame(players: ByColor[Player], outcome: Outcome) = + calc.computeGame(Game(players, outcome), skipDeviationIncrease = true).toPair + + { + val players = ByColor.fill: + Player( + rating = 1500d, + ratingDeviation = 500d, + volatility = 0.09d, + numberOfResults = 0, + lastRatingPeriodEnd = None + ) + test("default deviation: white wins"): + val (w, b) = computeGame(players, Outcome.white) + assertCloseTo(w.rating, 1741d, 1d) + assertCloseTo(b.rating, 1258d, 1d) + assertCloseTo(w.ratingDeviation, 396d, 1d) + assertCloseTo(b.ratingDeviation, 396d, 1d) + assertCloseTo(w.volatility, 0.0899983, 0.00000001d) + assertCloseTo(b.volatility, 0.0899983, 0.0000001d) + test("default deviation: black wins"): + val (w, b) = computeGame(players, Outcome.black) + assertCloseTo(w.rating, 1258d, 1d) + assertCloseTo(b.rating, 1741d, 1d) + assertCloseTo(w.ratingDeviation, 396d, 1d) + assertCloseTo(b.ratingDeviation, 396d, 1d) + assertCloseTo(w.volatility, 0.0899983, 0.00000001d) + assertCloseTo(b.volatility, 0.0899983, 0.0000001d) + test("default deviation: draw"): + val (w, b) = computeGame(players, Outcome.draw) + assertCloseTo(w.rating, 1500d, 1d) + assertCloseTo(b.rating, 1500d, 1d) + assertCloseTo(w.ratingDeviation, 396d, 1d) + assertCloseTo(b.ratingDeviation, 396d, 1d) + assertCloseTo(w.volatility, 0.0899954, 0.0000001d) + assertCloseTo(b.volatility, 0.0899954, 0.0000001d) + } + + { + val players = ByColor( + Player( + rating = 1400d, + ratingDeviation = 79d, + volatility = 0.06d, + numberOfResults = 0, + lastRatingPeriodEnd = None + ), + Player( + rating = 1550d, + ratingDeviation = 110d, + volatility = 0.065d, + numberOfResults = 0, + lastRatingPeriodEnd = None + ) + ) + test("mixed ratings and deviations: white wins"): + val (w, b) = computeGame(players, Outcome.white) + assertCloseTo(w.rating, 1422d, 1d) + assertCloseTo(b.rating, 1506d, 1d) + assertCloseTo(w.ratingDeviation, 77d, 1d) + assertCloseTo(b.ratingDeviation, 105d, 1d) + assertCloseTo(w.volatility, 0.06, 0.00001d) + assertCloseTo(b.volatility, 0.065, 0.00001d) + test("mixed ratings and deviations: black wins"): + val (w, b) = computeGame(players, Outcome.black) + assertCloseTo(w.rating, 1389d, 1d) + assertCloseTo(b.rating, 1568d, 1d) + assertCloseTo(w.ratingDeviation, 78d, 1d) + assertCloseTo(b.ratingDeviation, 105d, 1d) + assertCloseTo(w.volatility, 0.06, 0.00001d) + assertCloseTo(b.volatility, 0.065, 0.00001d) + test("mixed ratings and deviations: draw"): + val (w, b) = computeGame(players, Outcome.draw) + assertCloseTo(w.rating, 1406d, 1d) + assertCloseTo(b.rating, 1537d, 1d) + assertCloseTo(w.ratingDeviation, 78d, 1d) + assertCloseTo(b.ratingDeviation, 105.87d, 0.01d) + assertCloseTo(w.volatility, 0.06, 0.00001d) + assertCloseTo(b.volatility, 0.065, 0.00001d) + } + + { + val players = ByColor( + Player( + rating = 1200d, + ratingDeviation = 60d, + volatility = 0.053d, + numberOfResults = 0, + lastRatingPeriodEnd = None + ), + Player( + rating = 1850d, + ratingDeviation = 200d, + volatility = 0.062d, + numberOfResults = 0, + lastRatingPeriodEnd = None + ) + ) + test("more mixed ratings and deviations: white wins"): + val (w, b) = computeGame(players, Outcome.white) + assertCloseTo(w.rating, 1216.7d, 0.1d) + assertCloseTo(b.rating, 1636d, 0.1d) + assertCloseTo(w.ratingDeviation, 59.9d, 0.1d) + assertCloseTo(b.ratingDeviation, 196.9d, 0.1d) + assertCloseTo(w.volatility, 0.053013, 0.000001d) + assertCloseTo(b.volatility, 0.062028, 0.000001d) + test("more mixed ratings and deviations: black wins"): + val (w, b) = computeGame(players, Outcome.black) + assertCloseTo(w.rating, 1199.3d, 0.1d) + assertCloseTo(b.rating, 1855.4d, 0.1d) + assertCloseTo(w.ratingDeviation, 59.9d, 0.1d) + assertCloseTo(b.ratingDeviation, 196.9d, 0.1d) + assertCloseTo(w.volatility, 0.052999, 0.000001d) + assertCloseTo(b.volatility, 0.061999, 0.000001d) + test("more mixed ratings and deviations: draw"): + val (w, b) = computeGame(players, Outcome.draw) + assertCloseTo(w.rating, 1208.0, 0.1d) + assertCloseTo(b.rating, 1745.7, 0.1d) + assertCloseTo(w.ratingDeviation, 59.90056, 0.1d) + assertCloseTo(b.ratingDeviation, 196.98729, 0.1d) + assertCloseTo(w.volatility, 0.053002, 0.000001d) + assertCloseTo(b.volatility, 0.062006, 0.000001d) + } diff --git a/test-kit/src/test/scala/glicko/RatingCalculatorTest.scala b/test-kit/src/test/scala/glicko/impl/RatingCalculatorTest.scala similarity index 89% rename from test-kit/src/test/scala/glicko/RatingCalculatorTest.scala rename to test-kit/src/test/scala/glicko/impl/RatingCalculatorTest.scala index ee194d326..4de82fde8 100644 --- a/test-kit/src/test/scala/glicko/RatingCalculatorTest.scala +++ b/test-kit/src/test/scala/glicko/impl/RatingCalculatorTest.scala @@ -9,7 +9,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: // Chosen so a typical player's RD goes from 60 -> 110 in 1 year val ratingPeriodsPerDay = RatingPeriodsPerDay(0.21436d) - val system = RatingCalculator(Tau.default, ratingPeriodsPerDay) + val calculator = RatingCalculator(Tau.default, ratingPeriodsPerDay) def updateRatings(wRating: Rating, bRating: Rating, outcome: Outcome) = val results = GameRatingPeriodResults: @@ -18,7 +18,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: case None => GameResult(wRating, bRating, true) case Some(White) => GameResult(wRating, bRating, false) case Some(Black) => GameResult(bRating, wRating, false) - system.updateRatings(results, true) + calculator.updateRatings(results, true) def defaultRating = Rating( rating = 1500d, @@ -28,7 +28,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: lastRatingPeriodEnd = None ) - test("default deviation: white wins") { + test("default deviation: white wins"): val wr = defaultRating val br = defaultRating updateRatings(wr, br, Outcome.white) @@ -38,8 +38,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: assertCloseTo(br.ratingDeviation, 396d, 1d) assertCloseTo(wr.volatility, 0.0899983, 0.00000001d) assertCloseTo(br.volatility, 0.0899983, 0.0000001d) - } - test("default deviation: black wins") { + test("default deviation: black wins"): val wr = defaultRating val br = defaultRating updateRatings(wr, br, Outcome.black) @@ -49,8 +48,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: assertCloseTo(br.ratingDeviation, 396d, 1d) assertCloseTo(wr.volatility, 0.0899983, 0.00000001d) assertCloseTo(br.volatility, 0.0899983, 0.0000001d) - } - test("default deviation: draw") { + test("default deviation: draw"): val wr = defaultRating val br = defaultRating updateRatings(wr, br, Outcome.draw) @@ -60,7 +58,6 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: assertCloseTo(br.ratingDeviation, 396d, 1d) assertCloseTo(wr.volatility, 0.0899954, 0.0000001d) assertCloseTo(br.volatility, 0.0899954, 0.0000001d) - } def oldRating = Rating( rating = 1500d, @@ -69,7 +66,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: numberOfResults = 0, lastRatingPeriodEnd = None ) - test("low deviation: white wins") { + test("low deviation: white wins"): val wr = oldRating val br = oldRating updateRatings(wr, br, Outcome.white) @@ -79,8 +76,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: assertCloseTo(br.ratingDeviation, 78d, 1d) assertCloseTo(wr.volatility, 0.06, 0.00001d) assertCloseTo(br.volatility, 0.06, 0.00001d) - } - test("low deviation: black wins") { + test("low deviation: black wins"): val wr = oldRating val br = oldRating updateRatings(wr, br, Outcome.black) @@ -90,8 +86,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: assertCloseTo(br.ratingDeviation, 78d, 1d) assertCloseTo(wr.volatility, 0.06, 0.00001d) assertCloseTo(br.volatility, 0.06, 0.00001d) - } - test("low deviation: draw") { + test("low deviation: draw"): val wr = oldRating val br = oldRating updateRatings(wr, br, Outcome.draw) @@ -101,7 +96,6 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: assertCloseTo(br.ratingDeviation, 78d, 1d) assertCloseTo(wr.volatility, 0.06, 0.00001d) assertCloseTo(br.volatility, 0.06, 0.00001d) - } { def whiteRating = Rating( rating = 1400d, @@ -117,7 +111,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: numberOfResults = 0, lastRatingPeriodEnd = None ) - test("mixed ratings and deviations: white wins") { + test("mixed ratings and deviations: white wins"): val wr = whiteRating val br = blackRating updateRatings(wr, br, Outcome.white) @@ -127,8 +121,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: assertCloseTo(br.ratingDeviation, 105d, 1d) assertCloseTo(wr.volatility, 0.06, 0.00001d) assertCloseTo(br.volatility, 0.065, 0.00001d) - } - test("mixed ratings and deviations: black wins") { + test("mixed ratings and deviations: black wins"): val wr = whiteRating val br = blackRating updateRatings(wr, br, Outcome.black) @@ -138,8 +131,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: assertCloseTo(br.ratingDeviation, 105d, 1d) assertCloseTo(wr.volatility, 0.06, 0.00001d) assertCloseTo(br.volatility, 0.065, 0.00001d) - } - test("mixed ratings and deviations: draw") { + test("mixed ratings and deviations: draw"): val wr = whiteRating val br = blackRating updateRatings(wr, br, Outcome.draw) @@ -149,7 +141,6 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: assertCloseTo(br.ratingDeviation, 105.87d, 0.01d) assertCloseTo(wr.volatility, 0.06, 0.00001d) assertCloseTo(br.volatility, 0.065, 0.00001d) - } } { def whiteRating = Rating( @@ -166,7 +157,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: numberOfResults = 0, lastRatingPeriodEnd = None ) - test("more mixed ratings and deviations: white wins") { + test("more mixed ratings and deviations: white wins"): val wr = whiteRating val br = blackRating updateRatings(wr, br, Outcome.white) @@ -176,8 +167,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: assertCloseTo(br.ratingDeviation, 196.9d, 0.1d) assertCloseTo(wr.volatility, 0.053013, 0.000001d) assertCloseTo(br.volatility, 0.062028, 0.000001d) - } - test("more mixed ratings and deviations: black wins") { + test("more mixed ratings and deviations: black wins"): val wr = whiteRating val br = blackRating updateRatings(wr, br, Outcome.black) @@ -187,8 +177,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: assertCloseTo(br.ratingDeviation, 196.9d, 0.1d) assertCloseTo(wr.volatility, 0.052999, 0.000001d) assertCloseTo(br.volatility, 0.061999, 0.000001d) - } - test("more mixed ratings and deviations: draw") { + test("more mixed ratings and deviations: draw"): val wr = whiteRating val br = blackRating updateRatings(wr, br, Outcome.draw) @@ -198,5 +187,4 @@ class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: assertCloseTo(br.ratingDeviation, 196.98729, 0.1d) assertCloseTo(wr.volatility, 0.053002, 0.000001d) assertCloseTo(br.volatility, 0.062006, 0.000001d) - } } From a23e060f625cd74d25a63a172b4df82f78023daf Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Tue, 19 Nov 2024 10:58:05 +0100 Subject: [PATCH 04/13] glicko calculations can fail somehow it happens --- core/src/main/scala/glicko/glicko.scala | 15 ++++++++++----- .../src/test/scala/glicko/GlickoCalculator.scala | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/src/main/scala/glicko/glicko.scala b/core/src/main/scala/glicko/glicko.scala index 43fd550aa..beb494bd1 100644 --- a/core/src/main/scala/glicko/glicko.scala +++ b/core/src/main/scala/glicko/glicko.scala @@ -2,6 +2,7 @@ package chess package glicko import java.time.Instant +import scala.util.Try case class Player( rating: Double, @@ -31,23 +32,27 @@ trait GlickoCalculatorApi: // ): List[Player] // Simpler use case: a single game - def computeGame(game: Game, skipDeviationIncrease: Boolean = false): ByColor[Player] + def computeGame(game: Game, skipDeviationIncrease: Boolean = false): Try[ByColor[Player]] /** This is the formula defined in step 6. It is also used for players who have not competed during the rating period. */ - // def previewDeviation(player: Player, ratingPeriodEndDate: Instant, reverse: Boolean): Double + def previewDeviation(player: Player, ratingPeriodEndDate: Instant, reverse: Boolean): Double final class GlickoCalculator(config: Config) extends GlickoCalculatorApi: private val calculator = chess.glicko.impl.RatingCalculator(config.tau, config.ratingPeriodsPerDay) // Simpler use case: a single game - def computeGame(game: Game, skipDeviationIncrease: Boolean = false): ByColor[Player] = + def computeGame(game: Game, skipDeviationIncrease: Boolean = false): Try[ByColor[Player]] = val ratings = game.players.map(conversions.toRating) val gameResult = conversions.toGameResult(ratings, game.outcome) val periodResults = impl.GameRatingPeriodResults(List(gameResult)) - calculator.updateRatings(periodResults, skipDeviationIncrease) - ratings.map(conversions.toPlayer) + Try: + calculator.updateRatings(periodResults, skipDeviationIncrease) + ratings.map(conversions.toPlayer) + + def previewDeviation(player: Player, ratingPeriodEndDate: Instant, reverse: Boolean): Double = + calculator.previewDeviation(conversions.toRating(player), ratingPeriodEndDate, reverse) private object conversions: diff --git a/test-kit/src/test/scala/glicko/GlickoCalculator.scala b/test-kit/src/test/scala/glicko/GlickoCalculator.scala index 91899c4cb..870b29b91 100644 --- a/test-kit/src/test/scala/glicko/GlickoCalculator.scala +++ b/test-kit/src/test/scala/glicko/GlickoCalculator.scala @@ -11,7 +11,7 @@ class GlickoCalculatorTest extends ScalaCheckSuite with MunitExtensions: ) def computeGame(players: ByColor[Player], outcome: Outcome) = - calc.computeGame(Game(players, outcome), skipDeviationIncrease = true).toPair + calc.computeGame(Game(players, outcome), skipDeviationIncrease = true).get.toPair { val players = ByColor.fill: From 3ff94c13318825fed3837fd5c1812f87ee51bef6 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Tue, 19 Nov 2024 10:58:07 +0100 Subject: [PATCH 05/13] 16.4.0 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 5e65c0ff1..7037b417a 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ inThisBuild( Seq( scalaVersion := "3.5.2", - version := "16.3.4", + version := "16.4.0", organization := "org.lichess", licenses += ("MIT" -> url("https://opensource.org/licenses/MIT")), publishTo := Option(Resolver.file("file", new File(sys.props.getOrElse("publishTo", "")))), From 18adc9748ae02f63171ea3937bf0b0681adfbee2 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Tue, 19 Nov 2024 15:12:31 +0100 Subject: [PATCH 06/13] add Glicko data type --- core/src/main/scala/glicko/glicko.scala | 33 ++++++++--- .../test/scala/glicko/GlickoCalculator.scala | 56 ++++++++----------- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/core/src/main/scala/glicko/glicko.scala b/core/src/main/scala/glicko/glicko.scala index beb494bd1..277ed6646 100644 --- a/core/src/main/scala/glicko/glicko.scala +++ b/core/src/main/scala/glicko/glicko.scala @@ -4,13 +4,28 @@ package glicko import java.time.Instant import scala.util.Try -case class Player( +case class Glicko( rating: Double, - ratingDeviation: Double, - volatility: Double, + deviation: Double, + volatility: Double +): + def average(other: Glicko, weight: Float = 0.5f): Glicko = + if weight >= 1 then other + else if weight <= 0 then this + else + Glicko( + rating = rating * (1 - weight) + other.rating * weight, + deviation = deviation * (1 - weight) + other.deviation * weight, + volatility = volatility * (1 - weight) + other.volatility * weight + ) + override def toString = f"${rating.toInt}/${deviation.toInt}/${volatility}%.3f" + +case class Player( + glicko: Glicko, numberOfResults: Int, lastRatingPeriodEnd: Option[Instant] = None -) +): + export glicko.* case class Game(players: ByColor[Player], outcome: Outcome) @@ -70,16 +85,18 @@ final class GlickoCalculator(config: Config) extends GlickoCalculatorApi: def toRating(player: Player) = impl.Rating( rating = player.rating, - ratingDeviation = player.ratingDeviation, + ratingDeviation = player.deviation, volatility = player.volatility, numberOfResults = player.numberOfResults, lastRatingPeriodEnd = player.lastRatingPeriodEnd ) def toPlayer(rating: Rating) = Player( - rating = rating.rating, - ratingDeviation = rating.ratingDeviation, - volatility = rating.volatility, + glicko = Glicko( + rating = rating.rating, + deviation = rating.ratingDeviation, + volatility = rating.volatility + ), numberOfResults = rating.numberOfResults, lastRatingPeriodEnd = rating.lastRatingPeriodEnd ) diff --git a/test-kit/src/test/scala/glicko/GlickoCalculator.scala b/test-kit/src/test/scala/glicko/GlickoCalculator.scala index 870b29b91..bf0a28f8f 100644 --- a/test-kit/src/test/scala/glicko/GlickoCalculator.scala +++ b/test-kit/src/test/scala/glicko/GlickoCalculator.scala @@ -16,9 +16,7 @@ class GlickoCalculatorTest extends ScalaCheckSuite with MunitExtensions: { val players = ByColor.fill: Player( - rating = 1500d, - ratingDeviation = 500d, - volatility = 0.09d, + Glicko(rating = 1500d, deviation = 500d, volatility = 0.09d), numberOfResults = 0, lastRatingPeriodEnd = None ) @@ -26,24 +24,24 @@ class GlickoCalculatorTest extends ScalaCheckSuite with MunitExtensions: val (w, b) = computeGame(players, Outcome.white) assertCloseTo(w.rating, 1741d, 1d) assertCloseTo(b.rating, 1258d, 1d) - assertCloseTo(w.ratingDeviation, 396d, 1d) - assertCloseTo(b.ratingDeviation, 396d, 1d) + assertCloseTo(w.deviation, 396d, 1d) + assertCloseTo(b.deviation, 396d, 1d) assertCloseTo(w.volatility, 0.0899983, 0.00000001d) assertCloseTo(b.volatility, 0.0899983, 0.0000001d) test("default deviation: black wins"): val (w, b) = computeGame(players, Outcome.black) assertCloseTo(w.rating, 1258d, 1d) assertCloseTo(b.rating, 1741d, 1d) - assertCloseTo(w.ratingDeviation, 396d, 1d) - assertCloseTo(b.ratingDeviation, 396d, 1d) + assertCloseTo(w.deviation, 396d, 1d) + assertCloseTo(b.deviation, 396d, 1d) assertCloseTo(w.volatility, 0.0899983, 0.00000001d) assertCloseTo(b.volatility, 0.0899983, 0.0000001d) test("default deviation: draw"): val (w, b) = computeGame(players, Outcome.draw) assertCloseTo(w.rating, 1500d, 1d) assertCloseTo(b.rating, 1500d, 1d) - assertCloseTo(w.ratingDeviation, 396d, 1d) - assertCloseTo(b.ratingDeviation, 396d, 1d) + assertCloseTo(w.deviation, 396d, 1d) + assertCloseTo(b.deviation, 396d, 1d) assertCloseTo(w.volatility, 0.0899954, 0.0000001d) assertCloseTo(b.volatility, 0.0899954, 0.0000001d) } @@ -51,16 +49,12 @@ class GlickoCalculatorTest extends ScalaCheckSuite with MunitExtensions: { val players = ByColor( Player( - rating = 1400d, - ratingDeviation = 79d, - volatility = 0.06d, + Glicko(rating = 1400d, deviation = 79d, volatility = 0.06d), numberOfResults = 0, lastRatingPeriodEnd = None ), Player( - rating = 1550d, - ratingDeviation = 110d, - volatility = 0.065d, + Glicko(rating = 1550d, deviation = 110d, volatility = 0.065d), numberOfResults = 0, lastRatingPeriodEnd = None ) @@ -69,24 +63,24 @@ class GlickoCalculatorTest extends ScalaCheckSuite with MunitExtensions: val (w, b) = computeGame(players, Outcome.white) assertCloseTo(w.rating, 1422d, 1d) assertCloseTo(b.rating, 1506d, 1d) - assertCloseTo(w.ratingDeviation, 77d, 1d) - assertCloseTo(b.ratingDeviation, 105d, 1d) + assertCloseTo(w.deviation, 77d, 1d) + assertCloseTo(b.deviation, 105d, 1d) assertCloseTo(w.volatility, 0.06, 0.00001d) assertCloseTo(b.volatility, 0.065, 0.00001d) test("mixed ratings and deviations: black wins"): val (w, b) = computeGame(players, Outcome.black) assertCloseTo(w.rating, 1389d, 1d) assertCloseTo(b.rating, 1568d, 1d) - assertCloseTo(w.ratingDeviation, 78d, 1d) - assertCloseTo(b.ratingDeviation, 105d, 1d) + assertCloseTo(w.deviation, 78d, 1d) + assertCloseTo(b.deviation, 105d, 1d) assertCloseTo(w.volatility, 0.06, 0.00001d) assertCloseTo(b.volatility, 0.065, 0.00001d) test("mixed ratings and deviations: draw"): val (w, b) = computeGame(players, Outcome.draw) assertCloseTo(w.rating, 1406d, 1d) assertCloseTo(b.rating, 1537d, 1d) - assertCloseTo(w.ratingDeviation, 78d, 1d) - assertCloseTo(b.ratingDeviation, 105.87d, 0.01d) + assertCloseTo(w.deviation, 78d, 1d) + assertCloseTo(b.deviation, 105.87d, 0.01d) assertCloseTo(w.volatility, 0.06, 0.00001d) assertCloseTo(b.volatility, 0.065, 0.00001d) } @@ -94,16 +88,12 @@ class GlickoCalculatorTest extends ScalaCheckSuite with MunitExtensions: { val players = ByColor( Player( - rating = 1200d, - ratingDeviation = 60d, - volatility = 0.053d, + Glicko(rating = 1200d, deviation = 60d, volatility = 0.053d), numberOfResults = 0, lastRatingPeriodEnd = None ), Player( - rating = 1850d, - ratingDeviation = 200d, - volatility = 0.062d, + Glicko(rating = 1850d, deviation = 200d, volatility = 0.062d), numberOfResults = 0, lastRatingPeriodEnd = None ) @@ -112,24 +102,24 @@ class GlickoCalculatorTest extends ScalaCheckSuite with MunitExtensions: val (w, b) = computeGame(players, Outcome.white) assertCloseTo(w.rating, 1216.7d, 0.1d) assertCloseTo(b.rating, 1636d, 0.1d) - assertCloseTo(w.ratingDeviation, 59.9d, 0.1d) - assertCloseTo(b.ratingDeviation, 196.9d, 0.1d) + assertCloseTo(w.deviation, 59.9d, 0.1d) + assertCloseTo(b.deviation, 196.9d, 0.1d) assertCloseTo(w.volatility, 0.053013, 0.000001d) assertCloseTo(b.volatility, 0.062028, 0.000001d) test("more mixed ratings and deviations: black wins"): val (w, b) = computeGame(players, Outcome.black) assertCloseTo(w.rating, 1199.3d, 0.1d) assertCloseTo(b.rating, 1855.4d, 0.1d) - assertCloseTo(w.ratingDeviation, 59.9d, 0.1d) - assertCloseTo(b.ratingDeviation, 196.9d, 0.1d) + assertCloseTo(w.deviation, 59.9d, 0.1d) + assertCloseTo(b.deviation, 196.9d, 0.1d) assertCloseTo(w.volatility, 0.052999, 0.000001d) assertCloseTo(b.volatility, 0.061999, 0.000001d) test("more mixed ratings and deviations: draw"): val (w, b) = computeGame(players, Outcome.draw) assertCloseTo(w.rating, 1208.0, 0.1d) assertCloseTo(b.rating, 1745.7, 0.1d) - assertCloseTo(w.ratingDeviation, 59.90056, 0.1d) - assertCloseTo(b.ratingDeviation, 196.98729, 0.1d) + assertCloseTo(w.deviation, 59.90056, 0.1d) + assertCloseTo(b.deviation, 196.98729, 0.1d) assertCloseTo(w.volatility, 0.053002, 0.000001d) assertCloseTo(b.volatility, 0.062006, 0.000001d) } From 3f5d4fa0272d54c633380a9983d96e4303c0eed5 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Tue, 19 Nov 2024 16:52:54 +0100 Subject: [PATCH 07/13] more rating types and functions --- build.sbt | 2 +- core/src/main/scala/glicko/glicko.scala | 13 ++++++++++++- core/src/main/scala/glicko/rating.scala | 22 ++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 core/src/main/scala/glicko/rating.scala diff --git a/build.sbt b/build.sbt index 7037b417a..3f5b2a3de 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ inThisBuild( Seq( scalaVersion := "3.5.2", - version := "16.4.0", + version := "16.4.0.1", organization := "org.lichess", licenses += ("MIT" -> url("https://opensource.org/licenses/MIT")), publishTo := Option(Resolver.file("file", new File(sys.props.getOrElse("publishTo", "")))), diff --git a/core/src/main/scala/glicko/glicko.scala b/core/src/main/scala/glicko/glicko.scala index 277ed6646..e1e378680 100644 --- a/core/src/main/scala/glicko/glicko.scala +++ b/core/src/main/scala/glicko/glicko.scala @@ -9,6 +9,13 @@ case class Glicko( deviation: Double, volatility: Double ): + def intRating: IntRating = IntRating(rating.toInt) + def intDeviation = deviation.toInt + def provisional = RatingProvisional(deviation >= Glicko.provisionalDeviation) + def established = provisional.no + def establishedIntRating = Option.when(established)(intRating) + def clueless = deviation >= Glicko.cluelessDeviation + def display = s"$intRating${if provisional.yes then "?" else ""}" def average(other: Glicko, weight: Float = 0.5f): Glicko = if weight >= 1 then other else if weight <= 0 then this @@ -18,7 +25,11 @@ case class Glicko( deviation = deviation * (1 - weight) + other.deviation * weight, volatility = volatility * (1 - weight) + other.volatility * weight ) - override def toString = f"${rating.toInt}/${deviation.toInt}/${volatility}%.3f" + override def toString = f"$intRating/$intDeviation/${volatility}%.3f" + +object Glicko: + val provisionalDeviation = 110 + val cluelessDeviation = 230 case class Player( glicko: Glicko, diff --git a/core/src/main/scala/glicko/rating.scala b/core/src/main/scala/glicko/rating.scala new file mode 100644 index 000000000..5e1eb9e8e --- /dev/null +++ b/core/src/main/scala/glicko/rating.scala @@ -0,0 +1,22 @@ +package chess +package glicko // todo move to chess.rating? + +import alleycats.Zero + +opaque type IntRating = Int +object IntRating extends RichOpaqueInt[IntRating]: + extension (r: IntRating) def applyDiff(diff: IntRatingDiff): IntRating = r + diff.value + +opaque type IntRatingDiff = Int +object IntRatingDiff extends RichOpaqueInt[IntRatingDiff]: + extension (diff: IntRatingDiff) + def positive: Boolean = diff > 0 + def negative: Boolean = diff < 0 + def zero: Boolean = diff == 0 + given Zero[IntRatingDiff] = Zero(0) + +opaque type Rating = Double +object Rating extends OpaqueDouble[Rating] + +opaque type RatingProvisional = Boolean +object RatingProvisional extends YesNo[RatingProvisional] From 166e93eb7f0982feb836f2586bbe596bf76a5b8c Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Tue, 19 Nov 2024 16:53:53 +0100 Subject: [PATCH 08/13] 16.4.1 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 3f5b2a3de..3dc915180 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ inThisBuild( Seq( scalaVersion := "3.5.2", - version := "16.4.0.1", + version := "16.4.1", organization := "org.lichess", licenses += ("MIT" -> url("https://opensource.org/licenses/MIT")), publishTo := Option(Resolver.file("file", new File(sys.props.getOrElse("publishTo", "")))), From f83ce3636ab3d059fe4a90e4f05b92885fe60468 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 20 Nov 2024 09:07:05 +0100 Subject: [PATCH 09/13] move Elo and Glicko to a new rating module, v16.5.0 --- build.sbt | 13 +++- core/src/main/scala/format/pgn/Tag.scala | 5 +- core/src/main/scala/model.scala | 3 + {core => rating}/src/main/scala/Elo.scala | 12 +-- .../main/scala/glicko/GlickoCalculator.scala | 78 ++++--------------- .../src/main/scala/glicko/impl/README.md | 4 +- .../src/main/scala/glicko/impl/Rating.scala | 2 +- .../scala/glicko/impl/RatingCalculator.scala | 2 +- .../src/main/scala/glicko/impl/results.scala | 2 +- rating/src/main/scala/glicko/model.scala | 41 ++++++++++ .../src/main/scala/model.scala | 8 +- .../src/test/scala/{ => rating}/EloTest.scala | 1 + .../glicko/GlickoCalculator.scala | 14 ++-- .../glicko/impl/RatingCalculatorTest.scala | 7 +- 14 files changed, 96 insertions(+), 96 deletions(-) rename {core => rating}/src/main/scala/Elo.scala (93%) rename core/src/main/scala/glicko/glicko.scala => rating/src/main/scala/glicko/GlickoCalculator.scala (51%) rename {core => rating}/src/main/scala/glicko/impl/README.md (61%) rename {core => rating}/src/main/scala/glicko/impl/Rating.scala (98%) rename {core => rating}/src/main/scala/glicko/impl/RatingCalculator.scala (99%) rename {core => rating}/src/main/scala/glicko/impl/results.scala (98%) create mode 100644 rating/src/main/scala/glicko/model.scala rename core/src/main/scala/glicko/rating.scala => rating/src/main/scala/model.scala (67%) rename test-kit/src/test/scala/{ => rating}/EloTest.scala (99%) rename test-kit/src/test/scala/{ => rating}/glicko/GlickoCalculator.scala (95%) rename test-kit/src/test/scala/{ => rating}/glicko/impl/RatingCalculatorTest.scala (97%) diff --git a/build.sbt b/build.sbt index 3dc915180..eb2fc32ea 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ inThisBuild( Seq( scalaVersion := "3.5.2", - version := "16.4.1", + version := "16.5.0", organization := "org.lichess", licenses += ("MIT" -> url("https://opensource.org/licenses/MIT")), publishTo := Option(Resolver.file("file", new File(sys.props.getOrElse("publishTo", "")))), @@ -54,6 +54,13 @@ lazy val playJson: Project = Project("playJson", file("playJson")) ) .dependsOn(scalachess) +lazy val rating: Project = Project("rating", file("rating")) + .settings( + commonSettings, + name := "scalachess-rating" + ) + .dependsOn(scalachess) + lazy val bench = project .enablePlugins(JmhPlugin) .settings(commonSettings, scalacOptions -= "-Wunused:all", name := "bench") @@ -79,12 +86,12 @@ lazy val testKit = project "org.typelevel" %% "cats-laws" % "2.12.0" % Test ) ) - .dependsOn(scalachess % "compile->compile") + .dependsOn(scalachess % "compile->compile", rating % "compile->compile") lazy val root = project .in(file(".")) .settings(publish := {}, publish / skip := true) - .aggregate(scalachess, playJson, testKit, bench) + .aggregate(scalachess, rating, playJson, testKit, bench) addCommandAlias("prepare", "scalafixAll; scalafmtAll") addCommandAlias("check", "; scalafixAll --check; scalafmtCheckAll") diff --git a/core/src/main/scala/format/pgn/Tag.scala b/core/src/main/scala/format/pgn/Tag.scala index 6f708ad4c..6497e900c 100644 --- a/core/src/main/scala/format/pgn/Tag.scala +++ b/core/src/main/scala/format/pgn/Tag.scala @@ -1,4 +1,5 @@ package chess + package format.pgn import cats.Eq @@ -87,8 +88,8 @@ case class Tags(value: List[Tag]) extends AnyVal: .flatMap(_.toIntOption) def names: ByColor[Option[PlayerName]] = ByColor(apply(_.White), apply(_.Black)).map(PlayerName.from(_)) - def elos: ByColor[Option[Elo]] = ByColor(apply(_.WhiteElo), apply(_.BlackElo)).map: elo => - Elo.from(elo.flatMap(_.toIntOption)) + def ratings: ByColor[Option[IntRating]] = ByColor(apply(_.WhiteElo), apply(_.BlackElo)).map: r => + IntRating.from(r.flatMap(_.toIntOption)) def titles: ByColor[Option[PlayerTitle]] = ByColor(apply(_.WhiteTitle), apply(_.BlackTitle)).map(_.flatMap(PlayerTitle.get)) def fideIds: ByColor[Option[FideId]] = ByColor(apply(_.WhiteFideId), apply(_.BlackFideId)).map: id => diff --git a/core/src/main/scala/model.scala b/core/src/main/scala/model.scala index 92fae5b53..db33cdb53 100644 --- a/core/src/main/scala/model.scala +++ b/core/src/main/scala/model.scala @@ -40,3 +40,6 @@ object FideId extends OpaqueInt[FideId] opaque type PlayerName = String object PlayerName extends OpaqueString[PlayerName] + +opaque type IntRating = Int +object IntRating extends RichOpaqueInt[IntRating] diff --git a/core/src/main/scala/Elo.scala b/rating/src/main/scala/Elo.scala similarity index 93% rename from core/src/main/scala/Elo.scala rename to rating/src/main/scala/Elo.scala index e01c315f4..1f0f1a2a7 100644 --- a/core/src/main/scala/Elo.scala +++ b/rating/src/main/scala/Elo.scala @@ -1,6 +1,8 @@ -package chess +package chess.rating import cats.syntax.all.* +import scalalib.newtypes.* +import scalalib.extensions.* opaque type Elo = Int @@ -12,10 +14,10 @@ object KFactor extends OpaqueInt[KFactor]: * https://handbook.fide.com/chapter/B022022 * https://ratings.fide.com/calc.phtml * */ -object Elo extends RelaxedOpaqueInt[Elo]: +object Elo extends RichOpaqueInt[Elo]: - def computeRatingDiff(player: Player, games: Seq[Game]): Int = - computeNewRating(player, games) - player.rating + def computeRatingDiff(player: Player, games: Seq[Game]): IntRatingDiff = + IntRatingDiff(computeNewRating(player, games) - player.rating) def computeNewRating(player: Player, games: Seq[Game]): Elo = val expectedScore = games.foldMap: game => @@ -45,7 +47,7 @@ object Elo extends RelaxedOpaqueInt[Elo]: final class Player(val rating: Elo, val kFactor: KFactor) - final class Game(val points: Outcome.Points, val opponentRating: Elo) + final class Game(val points: chess.Outcome.Points, val opponentRating: Elo) // 8.1.2 FIDE table val conversionTableFIDE: Map[Int, Float] = List( diff --git a/core/src/main/scala/glicko/glicko.scala b/rating/src/main/scala/glicko/GlickoCalculator.scala similarity index 51% rename from core/src/main/scala/glicko/glicko.scala rename to rating/src/main/scala/glicko/GlickoCalculator.scala index e1e378680..a0b7aa60e 100644 --- a/core/src/main/scala/glicko/glicko.scala +++ b/rating/src/main/scala/glicko/GlickoCalculator.scala @@ -1,72 +1,17 @@ -package chess +package chess.rating package glicko import java.time.Instant +import chess.{ ByColor, Outcome, White, Black } import scala.util.Try -case class Glicko( - rating: Double, - deviation: Double, - volatility: Double -): - def intRating: IntRating = IntRating(rating.toInt) - def intDeviation = deviation.toInt - def provisional = RatingProvisional(deviation >= Glicko.provisionalDeviation) - def established = provisional.no - def establishedIntRating = Option.when(established)(intRating) - def clueless = deviation >= Glicko.cluelessDeviation - def display = s"$intRating${if provisional.yes then "?" else ""}" - def average(other: Glicko, weight: Float = 0.5f): Glicko = - if weight >= 1 then other - else if weight <= 0 then this - else - Glicko( - rating = rating * (1 - weight) + other.rating * weight, - deviation = deviation * (1 - weight) + other.deviation * weight, - volatility = volatility * (1 - weight) + other.volatility * weight - ) - override def toString = f"$intRating/$intDeviation/${volatility}%.3f" - -object Glicko: - val provisionalDeviation = 110 - val cluelessDeviation = 230 - -case class Player( - glicko: Glicko, - numberOfResults: Int, - lastRatingPeriodEnd: Option[Instant] = None -): - export glicko.* - -case class Game(players: ByColor[Player], outcome: Outcome) - -case class Config( +/* Purely functional interface hiding the mutable implementation */ +final class GlickoCalculator( tau: impl.Tau = impl.Tau.default, ratingPeriodsPerDay: impl.RatingPeriodsPerDay = impl.RatingPeriodsPerDay.default -) - -/* Purely functional interface hiding the mutable implementation */ -trait GlickoCalculatorApi: - - /** Apply rating calculations and return updated players. - * Note that players who did not compete during the rating period will have see their deviation increase. - * This requires players to have some sort of unique identifier. - */ - // def computeGames( - // games: List[Game], - // skipDeviationIncrease: Boolean = false - // ): List[Player] - - // Simpler use case: a single game - def computeGame(game: Game, skipDeviationIncrease: Boolean = false): Try[ByColor[Player]] - - /** This is the formula defined in step 6. It is also used for players who have not competed during the rating period. - */ - def previewDeviation(player: Player, ratingPeriodEndDate: Instant, reverse: Boolean): Double - -final class GlickoCalculator(config: Config) extends GlickoCalculatorApi: +): - private val calculator = chess.glicko.impl.RatingCalculator(config.tau, config.ratingPeriodsPerDay) + private val calculator = impl.RatingCalculator(tau, ratingPeriodsPerDay) // Simpler use case: a single game def computeGame(game: Game, skipDeviationIncrease: Boolean = false): Try[ByColor[Player]] = @@ -77,9 +22,16 @@ final class GlickoCalculator(config: Config) extends GlickoCalculatorApi: calculator.updateRatings(periodResults, skipDeviationIncrease) ratings.map(conversions.toPlayer) + /** This is the formula defined in step 6. It is also used for players who have not competed during the rating period. */ def previewDeviation(player: Player, ratingPeriodEndDate: Instant, reverse: Boolean): Double = calculator.previewDeviation(conversions.toRating(player), ratingPeriodEndDate, reverse) + /** Apply rating calculations and return updated players. + * Note that players who did not compete during the rating period will have see their deviation increase. + * This requires players to have some sort of unique identifier. + */ + // def computeGames( games: List[Game], skipDeviationIncrease: Boolean = false): List[Player] + private object conversions: import impl.* @@ -90,10 +42,6 @@ final class GlickoCalculator(config: Config) extends GlickoCalculatorApi: case Some(White) => GameResult(ratings.white, ratings.black, false) case Some(Black) => GameResult(ratings.black, ratings.white, false) - def gamesToPeriodResults(games: List[Game]) = GameRatingPeriodResults: - games.map: game => - toGameResult(game.players.map(toRating), game.outcome) - def toRating(player: Player) = impl.Rating( rating = player.rating, ratingDeviation = player.deviation, diff --git a/core/src/main/scala/glicko/impl/README.md b/rating/src/main/scala/glicko/impl/README.md similarity index 61% rename from core/src/main/scala/glicko/impl/README.md rename to rating/src/main/scala/glicko/impl/README.md index bc97ba577..ef319d102 100644 --- a/core/src/main/scala/glicko/impl/README.md +++ b/rating/src/main/scala/glicko/impl/README.md @@ -1,4 +1,4 @@ Loosely ported from java: https://github.com/goochjs/glicko2 -The implementation is not idiomatic scala and should not be used. -Use the public API instead. +The implementation is not idiomatic scala and should not be used directly. +Use the public API `chess.rating.glicko.GlickoCalculator` instead. diff --git a/core/src/main/scala/glicko/impl/Rating.scala b/rating/src/main/scala/glicko/impl/Rating.scala similarity index 98% rename from core/src/main/scala/glicko/impl/Rating.scala rename to rating/src/main/scala/glicko/impl/Rating.scala index 34901127f..5bebaeac7 100644 --- a/core/src/main/scala/glicko/impl/Rating.scala +++ b/rating/src/main/scala/glicko/impl/Rating.scala @@ -1,4 +1,4 @@ -package chess.glicko.impl +package chess.rating.glicko.impl final class Rating( var rating: Double, diff --git a/core/src/main/scala/glicko/impl/RatingCalculator.scala b/rating/src/main/scala/glicko/impl/RatingCalculator.scala similarity index 99% rename from core/src/main/scala/glicko/impl/RatingCalculator.scala rename to rating/src/main/scala/glicko/impl/RatingCalculator.scala index 03c2c909b..b0f651be9 100644 --- a/core/src/main/scala/glicko/impl/RatingCalculator.scala +++ b/rating/src/main/scala/glicko/impl/RatingCalculator.scala @@ -1,4 +1,4 @@ -package chess.glicko.impl +package chess.rating.glicko.impl import scalalib.extensions.ifTrue import scalalib.newtypes.OpaqueDouble diff --git a/core/src/main/scala/glicko/impl/results.scala b/rating/src/main/scala/glicko/impl/results.scala similarity index 98% rename from core/src/main/scala/glicko/impl/results.scala rename to rating/src/main/scala/glicko/impl/results.scala index 7e4b6528b..59d972be4 100644 --- a/core/src/main/scala/glicko/impl/results.scala +++ b/rating/src/main/scala/glicko/impl/results.scala @@ -1,4 +1,4 @@ -package chess.glicko.impl +package chess.rating.glicko.impl trait Result: diff --git a/rating/src/main/scala/glicko/model.scala b/rating/src/main/scala/glicko/model.scala new file mode 100644 index 000000000..fbe8ef909 --- /dev/null +++ b/rating/src/main/scala/glicko/model.scala @@ -0,0 +1,41 @@ +package chess.rating +package glicko + +import chess.{ IntRating, ByColor, Outcome } +import java.time.Instant + +case class Glicko( + rating: Double, + deviation: Double, + volatility: Double +): + def intRating: IntRating = IntRating(rating.toInt) + def intDeviation = deviation.toInt + def provisional = RatingProvisional(deviation >= Glicko.provisionalDeviation) + def established = provisional.no + def establishedIntRating = Option.when(established)(intRating) + def clueless = deviation >= Glicko.cluelessDeviation + def display = s"$intRating${if provisional.yes then "?" else ""}" + def average(other: Glicko, weight: Float = 0.5f): Glicko = + if weight >= 1 then other + else if weight <= 0 then this + else + Glicko( + rating = rating * (1 - weight) + other.rating * weight, + deviation = deviation * (1 - weight) + other.deviation * weight, + volatility = volatility * (1 - weight) + other.volatility * weight + ) + override def toString = f"$intRating/$intDeviation/${volatility}%.3f" + +object Glicko: + val provisionalDeviation = 110 + val cluelessDeviation = 230 + +case class Player( + glicko: Glicko, + numberOfResults: Int, + lastRatingPeriodEnd: Option[Instant] = None +): + export glicko.* + +case class Game(players: ByColor[Player], outcome: Outcome) diff --git a/core/src/main/scala/glicko/rating.scala b/rating/src/main/scala/model.scala similarity index 67% rename from core/src/main/scala/glicko/rating.scala rename to rating/src/main/scala/model.scala index 5e1eb9e8e..8fae3c5d5 100644 --- a/core/src/main/scala/glicko/rating.scala +++ b/rating/src/main/scala/model.scala @@ -1,11 +1,7 @@ -package chess -package glicko // todo move to chess.rating? +package chess.rating import alleycats.Zero - -opaque type IntRating = Int -object IntRating extends RichOpaqueInt[IntRating]: - extension (r: IntRating) def applyDiff(diff: IntRatingDiff): IntRating = r + diff.value +import scalalib.newtypes.* opaque type IntRatingDiff = Int object IntRatingDiff extends RichOpaqueInt[IntRatingDiff]: diff --git a/test-kit/src/test/scala/EloTest.scala b/test-kit/src/test/scala/rating/EloTest.scala similarity index 99% rename from test-kit/src/test/scala/EloTest.scala rename to test-kit/src/test/scala/rating/EloTest.scala index 9984b5bae..c2c7db357 100644 --- a/test-kit/src/test/scala/EloTest.scala +++ b/test-kit/src/test/scala/rating/EloTest.scala @@ -1,4 +1,5 @@ package chess +package rating class EloTest extends ChessTest: diff --git a/test-kit/src/test/scala/glicko/GlickoCalculator.scala b/test-kit/src/test/scala/rating/glicko/GlickoCalculator.scala similarity index 95% rename from test-kit/src/test/scala/glicko/GlickoCalculator.scala rename to test-kit/src/test/scala/rating/glicko/GlickoCalculator.scala index bf0a28f8f..225193343 100644 --- a/test-kit/src/test/scala/glicko/GlickoCalculator.scala +++ b/test-kit/src/test/scala/rating/glicko/GlickoCalculator.scala @@ -1,14 +1,14 @@ -package chess -package glicko +package chess.rating.glicko import munit.ScalaCheckSuite -class GlickoCalculatorTest extends ScalaCheckSuite with MunitExtensions: +import chess.{ ByColor, Outcome } - val calc = GlickoCalculator: - Config( - ratingPeriodsPerDay = impl.RatingPeriodsPerDay(0.21436d) - ) +class GlickoCalculatorTest extends ScalaCheckSuite with chess.MunitExtensions: + + val calc = GlickoCalculator( + ratingPeriodsPerDay = impl.RatingPeriodsPerDay(0.21436d) + ) def computeGame(players: ByColor[Player], outcome: Outcome) = calc.computeGame(Game(players, outcome), skipDeviationIncrease = true).get.toPair diff --git a/test-kit/src/test/scala/glicko/impl/RatingCalculatorTest.scala b/test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala similarity index 97% rename from test-kit/src/test/scala/glicko/impl/RatingCalculatorTest.scala rename to test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala index 4de82fde8..afa7c8cb4 100644 --- a/test-kit/src/test/scala/glicko/impl/RatingCalculatorTest.scala +++ b/test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala @@ -1,10 +1,11 @@ -package chess -package glicko.impl +package chess.rating.glicko.impl import munit.ScalaCheckSuite import cats.syntax.all.* -class RatingCalculatorTest extends ScalaCheckSuite with MunitExtensions: +import chess.{ Outcome, White, Black } + +class RatingCalculatorTest extends ScalaCheckSuite with chess.MunitExtensions: // Chosen so a typical player's RD goes from 60 -> 110 in 1 year val ratingPeriodsPerDay = RatingPeriodsPerDay(0.21436d) From 394df62c46ee60e33f5df37c506d50fd24c96306 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 20 Nov 2024 11:19:44 +0100 Subject: [PATCH 10/13] refine glicko code --- .../main/scala/glicko/GlickoCalculator.scala | 6 +++--- .../src/main/scala/glicko/impl/Rating.scala | 5 +++-- .../scala/glicko/impl/RatingCalculator.scala | 19 +++++-------------- .../src/main/scala/glicko/impl/results.scala | 16 +++++++++------- rating/src/main/scala/glicko/model.scala | 18 +++++++++++++----- .../rating/glicko/GlickoCalculator.scala | 2 +- .../glicko/impl/RatingCalculatorTest.scala | 3 ++- 7 files changed, 36 insertions(+), 33 deletions(-) diff --git a/rating/src/main/scala/glicko/GlickoCalculator.scala b/rating/src/main/scala/glicko/GlickoCalculator.scala index a0b7aa60e..64507ccb3 100644 --- a/rating/src/main/scala/glicko/GlickoCalculator.scala +++ b/rating/src/main/scala/glicko/GlickoCalculator.scala @@ -7,11 +7,11 @@ import scala.util.Try /* Purely functional interface hiding the mutable implementation */ final class GlickoCalculator( - tau: impl.Tau = impl.Tau.default, - ratingPeriodsPerDay: impl.RatingPeriodsPerDay = impl.RatingPeriodsPerDay.default + tau: Tau = Tau.default, + ratingPeriodsPerDay: RatingPeriodsPerDay = RatingPeriodsPerDay.default ): - private val calculator = impl.RatingCalculator(tau, ratingPeriodsPerDay) + private val calculator = new impl.RatingCalculator(tau, ratingPeriodsPerDay) // Simpler use case: a single game def computeGame(game: Game, skipDeviationIncrease: Boolean = false): Try[ByColor[Player]] = diff --git a/rating/src/main/scala/glicko/impl/Rating.scala b/rating/src/main/scala/glicko/impl/Rating.scala index 5bebaeac7..71d278e99 100644 --- a/rating/src/main/scala/glicko/impl/Rating.scala +++ b/rating/src/main/scala/glicko/impl/Rating.scala @@ -1,6 +1,7 @@ -package chess.rating.glicko.impl +package chess.rating.glicko +package impl -final class Rating( +final private[glicko] class Rating( var rating: Double, var ratingDeviation: Double, var volatility: Double, diff --git a/rating/src/main/scala/glicko/impl/RatingCalculator.scala b/rating/src/main/scala/glicko/impl/RatingCalculator.scala index b0f651be9..94ea96ccc 100644 --- a/rating/src/main/scala/glicko/impl/RatingCalculator.scala +++ b/rating/src/main/scala/glicko/impl/RatingCalculator.scala @@ -1,19 +1,10 @@ -package chess.rating.glicko.impl - -import scalalib.extensions.ifTrue -import scalalib.newtypes.OpaqueDouble +package chess.rating.glicko +package impl import java.time.Instant +import scalalib.extensions.ifTrue -opaque type Tau = Double -object Tau extends OpaqueDouble[Tau]: - val default: Tau = 0.75d - -opaque type RatingPeriodsPerDay = Double -object RatingPeriodsPerDay extends OpaqueDouble[RatingPeriodsPerDay]: - val default: RatingPeriodsPerDay = 0d - -object RatingCalculator: +private object RatingCalculator: private val MULTIPLIER: Double = 173.7178 val DEFAULT_RATING: Double = 1500.0 @@ -30,7 +21,7 @@ object RatingCalculator: def convertRatingDeviationToGlicko2Scale(ratingDeviation: Double): Double = (ratingDeviation / MULTIPLIER) -final class RatingCalculator( +final private[glicko] class RatingCalculator( tau: Tau = Tau.default, ratingPeriodsPerDay: RatingPeriodsPerDay = RatingPeriodsPerDay.default ): diff --git a/rating/src/main/scala/glicko/impl/results.scala b/rating/src/main/scala/glicko/impl/results.scala index 59d972be4..22551d7bb 100644 --- a/rating/src/main/scala/glicko/impl/results.scala +++ b/rating/src/main/scala/glicko/impl/results.scala @@ -1,6 +1,7 @@ -package chess.rating.glicko.impl +package chess.rating.glicko +package impl -trait Result: +private[glicko] trait Result: def getScore(player: Rating): Double @@ -11,7 +12,7 @@ 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: +final private[glicko] class FloatingResult(player: Rating, opponent: Rating, score: Float) extends Result: def getScore(p: Rating) = if p == player then score else 1 - score @@ -21,7 +22,7 @@ class FloatingResult(player: Rating, opponent: Rating, score: Float) extends Res def players = List(player, opponent) -final class GameResult(winner: Rating, loser: Rating, isDraw: Boolean) extends Result: +final private[glicko] class GameResult(winner: Rating, loser: Rating, isDraw: Boolean) extends Result: private val POINTS_FOR_WIN = 1.0d private val POINTS_FOR_LOSS = 0.0d private val POINTS_FOR_DRAW = 0.5d @@ -50,12 +51,13 @@ final class GameResult(winner: Rating, loser: Rating, isDraw: Boolean) extends R override def toString = s"$winner vs $loser = $isDraw" -trait RatingPeriodResults[R <: Result](): +private[glicko] 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 GameRatingPeriodResults(val results: List[GameResult]) extends RatingPeriodResults[GameResult] +final private[glicko] class GameRatingPeriodResults(val results: List[GameResult]) + extends RatingPeriodResults[GameResult] -class FloatingRatingPeriodResults(val results: List[FloatingResult]) +final private[glicko] class FloatingRatingPeriodResults(val results: List[FloatingResult]) extends RatingPeriodResults[FloatingResult] diff --git a/rating/src/main/scala/glicko/model.scala b/rating/src/main/scala/glicko/model.scala index fbe8ef909..e9855ce09 100644 --- a/rating/src/main/scala/glicko/model.scala +++ b/rating/src/main/scala/glicko/model.scala @@ -3,6 +3,7 @@ package glicko import chess.{ IntRating, ByColor, Outcome } import java.time.Instant +import scalalib.newtypes.OpaqueDouble case class Glicko( rating: Double, @@ -11,10 +12,10 @@ case class Glicko( ): def intRating: IntRating = IntRating(rating.toInt) def intDeviation = deviation.toInt - def provisional = RatingProvisional(deviation >= Glicko.provisionalDeviation) + def provisional = RatingProvisional(deviation >= provisionalDeviation) def established = provisional.no def establishedIntRating = Option.when(established)(intRating) - def clueless = deviation >= Glicko.cluelessDeviation + def clueless = deviation >= cluelessDeviation def display = s"$intRating${if provisional.yes then "?" else ""}" def average(other: Glicko, weight: Float = 0.5f): Glicko = if weight >= 1 then other @@ -27,9 +28,8 @@ case class Glicko( ) override def toString = f"$intRating/$intDeviation/${volatility}%.3f" -object Glicko: - val provisionalDeviation = 110 - val cluelessDeviation = 230 +val provisionalDeviation = 110 +val cluelessDeviation = 230 case class Player( glicko: Glicko, @@ -39,3 +39,11 @@ case class Player( export glicko.* case class Game(players: ByColor[Player], outcome: Outcome) + +opaque type Tau = Double +object Tau extends OpaqueDouble[Tau]: + val default: Tau = 0.75d + +opaque type RatingPeriodsPerDay = Double +object RatingPeriodsPerDay extends OpaqueDouble[RatingPeriodsPerDay]: + val default: RatingPeriodsPerDay = 0d diff --git a/test-kit/src/test/scala/rating/glicko/GlickoCalculator.scala b/test-kit/src/test/scala/rating/glicko/GlickoCalculator.scala index 225193343..344f30f71 100644 --- a/test-kit/src/test/scala/rating/glicko/GlickoCalculator.scala +++ b/test-kit/src/test/scala/rating/glicko/GlickoCalculator.scala @@ -7,7 +7,7 @@ import chess.{ ByColor, Outcome } class GlickoCalculatorTest extends ScalaCheckSuite with chess.MunitExtensions: val calc = GlickoCalculator( - ratingPeriodsPerDay = impl.RatingPeriodsPerDay(0.21436d) + ratingPeriodsPerDay = RatingPeriodsPerDay(0.21436d) ) def computeGame(players: ByColor[Player], outcome: Outcome) = diff --git a/test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala b/test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala index afa7c8cb4..5e72431b7 100644 --- a/test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala +++ b/test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala @@ -1,4 +1,5 @@ -package chess.rating.glicko.impl +package chess.rating.glicko +package impl import munit.ScalaCheckSuite import cats.syntax.all.* From 44c4b765bfc6e38e35a213eabe495fcc1703e8a5 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 20 Nov 2024 11:29:07 +0100 Subject: [PATCH 11/13] sbt scalafixAll --- rating/src/main/scala/Elo.scala | 2 +- rating/src/main/scala/glicko/GlickoCalculator.scala | 3 ++- rating/src/main/scala/glicko/impl/RatingCalculator.scala | 3 ++- rating/src/main/scala/glicko/model.scala | 5 +++-- test-kit/src/test/scala/rating/glicko/GlickoCalculator.scala | 3 +-- .../test/scala/rating/glicko/impl/RatingCalculatorTest.scala | 5 ++--- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/rating/src/main/scala/Elo.scala b/rating/src/main/scala/Elo.scala index 1f0f1a2a7..06614e914 100644 --- a/rating/src/main/scala/Elo.scala +++ b/rating/src/main/scala/Elo.scala @@ -1,8 +1,8 @@ package chess.rating import cats.syntax.all.* -import scalalib.newtypes.* import scalalib.extensions.* +import scalalib.newtypes.* opaque type Elo = Int diff --git a/rating/src/main/scala/glicko/GlickoCalculator.scala b/rating/src/main/scala/glicko/GlickoCalculator.scala index 64507ccb3..f1cc426c7 100644 --- a/rating/src/main/scala/glicko/GlickoCalculator.scala +++ b/rating/src/main/scala/glicko/GlickoCalculator.scala @@ -1,8 +1,9 @@ package chess.rating package glicko +import chess.{Black, ByColor, Outcome, White} + import java.time.Instant -import chess.{ ByColor, Outcome, White, Black } import scala.util.Try /* Purely functional interface hiding the mutable implementation */ diff --git a/rating/src/main/scala/glicko/impl/RatingCalculator.scala b/rating/src/main/scala/glicko/impl/RatingCalculator.scala index 94ea96ccc..7326f0788 100644 --- a/rating/src/main/scala/glicko/impl/RatingCalculator.scala +++ b/rating/src/main/scala/glicko/impl/RatingCalculator.scala @@ -1,9 +1,10 @@ package chess.rating.glicko package impl -import java.time.Instant import scalalib.extensions.ifTrue +import java.time.Instant + private object RatingCalculator: private val MULTIPLIER: Double = 173.7178 diff --git a/rating/src/main/scala/glicko/model.scala b/rating/src/main/scala/glicko/model.scala index e9855ce09..42df0bf94 100644 --- a/rating/src/main/scala/glicko/model.scala +++ b/rating/src/main/scala/glicko/model.scala @@ -1,10 +1,11 @@ package chess.rating package glicko -import chess.{ IntRating, ByColor, Outcome } -import java.time.Instant +import chess.{ByColor, IntRating, Outcome} import scalalib.newtypes.OpaqueDouble +import java.time.Instant + case class Glicko( rating: Double, deviation: Double, diff --git a/test-kit/src/test/scala/rating/glicko/GlickoCalculator.scala b/test-kit/src/test/scala/rating/glicko/GlickoCalculator.scala index 344f30f71..bd9f14afd 100644 --- a/test-kit/src/test/scala/rating/glicko/GlickoCalculator.scala +++ b/test-kit/src/test/scala/rating/glicko/GlickoCalculator.scala @@ -1,8 +1,7 @@ package chess.rating.glicko -import munit.ScalaCheckSuite - import chess.{ ByColor, Outcome } +import munit.ScalaCheckSuite class GlickoCalculatorTest extends ScalaCheckSuite with chess.MunitExtensions: diff --git a/test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala b/test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala index 5e72431b7..a9b643183 100644 --- a/test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala +++ b/test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala @@ -1,10 +1,9 @@ package chess.rating.glicko package impl -import munit.ScalaCheckSuite import cats.syntax.all.* - -import chess.{ Outcome, White, Black } +import chess.{Black, Outcome, White} +import munit.ScalaCheckSuite class RatingCalculatorTest extends ScalaCheckSuite with chess.MunitExtensions: From 1216db02a695fc616a29a33fa99005af9a25c378 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 20 Nov 2024 11:32:29 +0100 Subject: [PATCH 12/13] refine visibility --- .../src/main/scala/glicko/impl/Rating.scala | 22 +++++++++---------- .../scala/glicko/impl/RatingCalculator.scala | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/rating/src/main/scala/glicko/impl/Rating.scala b/rating/src/main/scala/glicko/impl/Rating.scala index 71d278e99..66c82e826 100644 --- a/rating/src/main/scala/glicko/impl/Rating.scala +++ b/rating/src/main/scala/glicko/impl/Rating.scala @@ -12,33 +12,33 @@ final private[glicko] class Rating( import RatingCalculator.* // the following variables are used to hold values temporarily whilst running calculations - private[glicko] var workingRating: Double = scala.compiletime.uninitialized - private[glicko] var workingRatingDeviation: Double = scala.compiletime.uninitialized - private[glicko] var workingVolatility: Double = scala.compiletime.uninitialized + private[impl] var workingRating: Double = scala.compiletime.uninitialized + private[impl] var workingRatingDeviation: Double = scala.compiletime.uninitialized + private[impl] var workingVolatility: Double = scala.compiletime.uninitialized /** Return the average skill value of the player scaled down to the scale used by the algorithm's internal * workings. */ - def getGlicko2Rating: Double = convertRatingToGlicko2Scale(this.rating) + private[impl] def getGlicko2Rating: Double = convertRatingToGlicko2Scale(this.rating) /** Set the average skill value, taking in a value in Glicko2 scale. */ - def setGlicko2Rating(r: Double) = + private[impl] def setGlicko2Rating(r: Double) = rating = convertRatingToOriginalGlickoScale(r) /** Return the rating deviation of the player scaled down to the scale used by the algorithm's internal * workings. */ - def getGlicko2RatingDeviation: Double = convertRatingDeviationToGlicko2Scale(ratingDeviation) + private[impl] def getGlicko2RatingDeviation: Double = convertRatingDeviationToGlicko2Scale(ratingDeviation) /** Set the rating deviation, taking in a value in Glicko2 scale. */ - def setGlicko2RatingDeviation(rd: Double) = + private[impl] def setGlicko2RatingDeviation(rd: Double) = ratingDeviation = convertRatingDeviationToOriginalGlickoScale(rd) /** Used by the calculation engine, to move interim calculations into their "proper" places. */ - def finaliseRating() = + private[impl] def finaliseRating() = setGlicko2Rating(workingRating) setGlicko2RatingDeviation(workingRatingDeviation) volatility = workingVolatility @@ -46,7 +46,7 @@ final private[glicko] class Rating( workingRating = 0d workingVolatility = 0d - override def toString = f"Rating($rating%1.2f, $ratingDeviation%1.2f, $volatility%1.2f, $numberOfResults)" - - def incrementNumberOfResults(increment: Int) = + private[impl] def incrementNumberOfResults(increment: Int) = numberOfResults = numberOfResults + increment + + override def toString = f"Rating($rating%1.2f, $ratingDeviation%1.2f, $volatility%1.2f, $numberOfResults)" diff --git a/rating/src/main/scala/glicko/impl/RatingCalculator.scala b/rating/src/main/scala/glicko/impl/RatingCalculator.scala index 7326f0788..331cfb494 100644 --- a/rating/src/main/scala/glicko/impl/RatingCalculator.scala +++ b/rating/src/main/scala/glicko/impl/RatingCalculator.scala @@ -85,7 +85,7 @@ final private[glicko] class RatingCalculator( * @param results * @param elapsedRatingPeriods */ - def calculateNewRating(player: Rating, results: List[Result], elapsedRatingPeriods: Double): Unit = + private def calculateNewRating(player: Rating, results: List[Result], elapsedRatingPeriods: Double): Unit = val phi = player.getGlicko2RatingDeviation val sigma = player.volatility val a = Math.log(Math.pow(sigma, 2)) From 5b314a305285077d7cf4f008a7262d184976a62b Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 20 Nov 2024 11:33:37 +0100 Subject: [PATCH 13/13] sbt scalafmtAll --- rating/src/main/scala/glicko/GlickoCalculator.scala | 2 +- rating/src/main/scala/glicko/model.scala | 2 +- .../test/scala/rating/glicko/impl/RatingCalculatorTest.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rating/src/main/scala/glicko/GlickoCalculator.scala b/rating/src/main/scala/glicko/GlickoCalculator.scala index f1cc426c7..d7075683b 100644 --- a/rating/src/main/scala/glicko/GlickoCalculator.scala +++ b/rating/src/main/scala/glicko/GlickoCalculator.scala @@ -1,7 +1,7 @@ package chess.rating package glicko -import chess.{Black, ByColor, Outcome, White} +import chess.{ Black, ByColor, Outcome, White } import java.time.Instant import scala.util.Try diff --git a/rating/src/main/scala/glicko/model.scala b/rating/src/main/scala/glicko/model.scala index 42df0bf94..154942154 100644 --- a/rating/src/main/scala/glicko/model.scala +++ b/rating/src/main/scala/glicko/model.scala @@ -1,7 +1,7 @@ package chess.rating package glicko -import chess.{ByColor, IntRating, Outcome} +import chess.{ ByColor, IntRating, Outcome } import scalalib.newtypes.OpaqueDouble import java.time.Instant diff --git a/test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala b/test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala index a9b643183..0b44cad41 100644 --- a/test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala +++ b/test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala @@ -2,7 +2,7 @@ package chess.rating.glicko package impl import cats.syntax.all.* -import chess.{Black, Outcome, White} +import chess.{ Black, Outcome, White } import munit.ScalaCheckSuite class RatingCalculatorTest extends ScalaCheckSuite with chess.MunitExtensions: