From cdbcda03fec1851e08ab1565b599c7d4b57367ae Mon Sep 17 00:00:00 2001 From: Daniel Dugovic Date: Wed, 1 Jan 2025 16:09:56 -0600 Subject: [PATCH] Rate games accounting for first move advantage --- .../main/scala/glicko/GlickoCalculator.scala | 7 ++--- .../scala/glicko/impl/RatingCalculator.scala | 18 +++++++---- .../src/main/scala/glicko/impl/results.scala | 30 ++++++++++++------- rating/src/main/scala/glicko/model.scala | 1 + ...ickoCalculatorWithColorAdvantageTest.scala | 2 +- .../glicko/impl/RatingCalculatorTest.scala | 7 ++--- 6 files changed, 37 insertions(+), 28 deletions(-) diff --git a/rating/src/main/scala/glicko/GlickoCalculator.scala b/rating/src/main/scala/glicko/GlickoCalculator.scala index 4ae48ee81..1d642816d 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.{ ByColor, Outcome } import java.time.Instant import scala.util.Try @@ -39,10 +39,7 @@ final class GlickoCalculator( 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) + GameResult(ratings.white, ratings.black, outcome) def toRating(player: Player) = impl.Rating( rating = player.rating, diff --git a/rating/src/main/scala/glicko/impl/RatingCalculator.scala b/rating/src/main/scala/glicko/impl/RatingCalculator.scala index 18c43b4bb..3192ad921 100644 --- a/rating/src/main/scala/glicko/impl/RatingCalculator.scala +++ b/rating/src/main/scala/glicko/impl/RatingCalculator.scala @@ -170,13 +170,17 @@ final private[glicko] 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(colorAdvantage, player)), + result + .getOpponent(player) + .getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, result.getOpponent(player))), result.getOpponent(player).getGlicko2RatingDeviation ) * (1.0 - E( - player.getGlicko2Rating, - result.getOpponent(player).getGlicko2Rating, + player.getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, player)), + result + .getOpponent(player) + .getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, result.getOpponent(player))), result.getOpponent(player).getGlicko2RatingDeviation ))) 1 / v @@ -197,8 +201,10 @@ final private[glicko] class RatingCalculator( outcomeBasedRating = outcomeBasedRating + (g(result.getOpponent(player).getGlicko2RatingDeviation) * (result.getScore(player) - E( - player.getGlicko2Rating, - result.getOpponent(player).getGlicko2Rating, + player.getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, player)), + result + .getOpponent(player) + .getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, result.getOpponent(player))), result.getOpponent(player).getGlicko2RatingDeviation ))) outcomeBasedRating diff --git a/rating/src/main/scala/glicko/impl/results.scala b/rating/src/main/scala/glicko/impl/results.scala index 22551d7bb..a15804934 100644 --- a/rating/src/main/scala/glicko/impl/results.scala +++ b/rating/src/main/scala/glicko/impl/results.scala @@ -3,6 +3,8 @@ package impl private[glicko] trait Result: + def getAdvantage(advantage: ColorAdvantage, p: Rating): ColorAdvantage + def getScore(player: Rating): Double def getOpponent(player: Rating): Rating @@ -14,6 +16,8 @@ private[glicko] trait Result: // score from 0 (opponent wins) to 1 (player wins) final private[glicko] class FloatingResult(player: Rating, opponent: Rating, score: Float) extends Result: + def getAdvantage(advantage: ColorAdvantage, p: Rating): ColorAdvantage = ColorAdvantage.zero + def getScore(p: Rating) = if p == player then score else 1 - score def getOpponent(p: Rating) = if p == player then opponent else player @@ -22,14 +26,17 @@ final private[glicko] class FloatingResult(player: Rating, opponent: Rating, sco def players = List(player, opponent) -final private[glicko] class GameResult(winner: Rating, loser: Rating, isDraw: Boolean) extends Result: +final private[glicko] class GameResult(first: Rating, second: Rating, outcome: chess.Outcome) 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 players = List(first, second) - def participated(player: Rating) = player == winner || player == loser + def participated(player: Rating) = player == first || player == second + + def getAdvantage(advantage: ColorAdvantage, player: Rating): ColorAdvantage = + if player == first then advantage.half else advantage.negate.half /** Returns the "score" for a match. * @@ -38,18 +45,19 @@ final private[glicko] class GameResult(winner: Rating, loser: Rating, isDraw: Bo * 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 getScore(player: Rating): Double = outcome.winner match + case Some(chess.Color.White) => if player == first then POINTS_FOR_WIN else POINTS_FOR_LOSS + case Some(chess.Color.Black) => if player == first then POINTS_FOR_LOSS else POINTS_FOR_WIN + case _ => + if participated(player) then POINTS_FOR_DRAW + 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 + 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" private[glicko] trait RatingPeriodResults[R <: Result](): val results: List[R] diff --git a/rating/src/main/scala/glicko/model.scala b/rating/src/main/scala/glicko/model.scala index 34f7e2707..b64165e89 100644 --- a/rating/src/main/scala/glicko/model.scala +++ b/rating/src/main/scala/glicko/model.scala @@ -54,4 +54,5 @@ object ColorAdvantage extends OpaqueDouble[ColorAdvantage]: val zero: ColorAdvantage = 0d val standard: ColorAdvantage = 7.786d val crazyhouse: ColorAdvantage = 15.171d + extension (c: ColorAdvantage) def half: ColorAdvantage = c / 2.0d extension (c: ColorAdvantage) def negate: ColorAdvantage = -c diff --git a/test-kit/src/test/scala/rating/glicko/GlickoCalculatorWithColorAdvantageTest.scala b/test-kit/src/test/scala/rating/glicko/GlickoCalculatorWithColorAdvantageTest.scala index 30eacb241..9b2cc6a55 100644 --- a/test-kit/src/test/scala/rating/glicko/GlickoCalculatorWithColorAdvantageTest.scala +++ b/test-kit/src/test/scala/rating/glicko/GlickoCalculatorWithColorAdvantageTest.scala @@ -24,7 +24,7 @@ class GlickoCalculatorWithColorAdvantageTest extends ScalaCheckSuite with chess. calc.computeGame(Game(players, outcome), skipDeviationIncrease = true).get.toPair def computeGameWithAdvantage(players: ByColor[Player], outcome: Outcome) = - calc.computeGame(Game(players, outcome), skipDeviationIncrease = true).get.toPair + calcWithAdvantage.computeGame(Game(players, outcome), skipDeviationIncrease = true).get.toPair { val players = ByColor.fill: 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 0b44cad41..1076e7731 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.Outcome import munit.ScalaCheckSuite class RatingCalculatorTest extends ScalaCheckSuite with chess.MunitExtensions: @@ -15,10 +15,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with chess.MunitExtensions: 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) + GameResult(wRating, bRating, outcome) calculator.updateRatings(results, true) def defaultRating = Rating(