Skip to content

Commit

Permalink
Rate games accounting for first move advantage
Browse files Browse the repository at this point in the history
  • Loading branch information
ddugovic committed Jan 1, 2025
1 parent 1b89080 commit 8e3af29
Show file tree
Hide file tree
Showing 6 changed files with 29 additions and 28 deletions.
7 changes: 2 additions & 5 deletions rating/src/main/scala/glicko/GlickoCalculator.scala
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 6 additions & 6 deletions rating/src/main/scala/glicko/impl/RatingCalculator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,13 @@ 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, 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, player)),
result.getOpponent(player).getGlicko2RatingDeviation
)))
1 / v
Expand All @@ -197,8 +197,8 @@ 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, player)),
result.getOpponent(player).getGlicko2RatingDeviation
)))
outcomeBasedRating
Expand Down
28 changes: 17 additions & 11 deletions rating/src/main/scala/glicko/impl/results.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.
*
Expand All @@ -38,18 +45,17 @@ 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]
Expand Down
1 change: 1 addition & 0 deletions rating/src/main/scala/glicko/model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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(
Expand Down

0 comments on commit 8e3af29

Please sign in to comment.