Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rating color advantage merge into PR #598 #599

Open
wants to merge 13 commits into
base: rating-color-advantage
Choose a base branch
from
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
32 changes: 17 additions & 15 deletions rating/src/main/scala/glicko/impl/RatingCalculator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ final private[glicko] class RatingCalculator(

private val ratingPeriodsPerMilli: Double = ratingPeriodsPerDay.value * DAYS_PER_MILLI

/** <p>Run through all players within a resultset and calculate their new ratings.</p> <p>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).</p> <p>Note that this method will clear the results held in the association
* resultset.</p>
/** 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
*/
Expand Down Expand Up @@ -168,16 +168,17 @@ final private[glicko] class RatingCalculator(
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))
val opponent = result.getOpponent(player)
v = v + ((Math.pow(g(opponent.getGlicko2RatingDeviation), 2))
* E(
player.getGlicko2Rating,
result.getOpponent(player).getGlicko2Rating,
result.getOpponent(player).getGlicko2RatingDeviation
player.getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, player)),
opponent.getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, opponent)),
opponent.getGlicko2RatingDeviation
)
* (1.0 - E(
player.getGlicko2Rating,
result.getOpponent(player).getGlicko2Rating,
result.getOpponent(player).getGlicko2RatingDeviation
player.getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, player)),
opponent.getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, opponent)),
opponent.getGlicko2RatingDeviation
)))
1 / v

Expand All @@ -194,12 +195,13 @@ final private[glicko] class RatingCalculator(
private def outcomeBasedRating(player: Rating, results: List[Result]) =
var outcomeBasedRating = 0d
for result <- results do
val opponent = result.getOpponent(player)
outcomeBasedRating = outcomeBasedRating
+ (g(result.getOpponent(player).getGlicko2RatingDeviation)
+ (g(opponent.getGlicko2RatingDeviation)
* (result.getScore(player) - E(
player.getGlicko2Rating,
result.getOpponent(player).getGlicko2Rating,
result.getOpponent(player).getGlicko2RatingDeviation
player.getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, player)),
opponent.getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, opponent)),
opponent.getGlicko2RatingDeviation
)))
outcomeBasedRating

Expand Down
30 changes: 19 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,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]
Expand Down
3 changes: 2 additions & 1 deletion rating/src/main/scala/glicko/model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ val cluelessDeviation = 230

case class Player(
glicko: Glicko,
numberOfResults: Int,
numberOfResults: Int = 0,
lastRatingPeriodEnd: Option[Instant] = None
):
export glicko.*
Expand All @@ -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 @@ -4,6 +4,11 @@ import chess.{ ByColor, Outcome }
import munit.ScalaCheckSuite

class GlickoCalculatorTest extends ScalaCheckSuite with chess.MunitExtensions:
// Validate results with reference implementations
// http://www.glicko.net/glicko/glicko2.pdf
val R: Double = 1500d
val RD: Double = 350d
val V: Double = 0.06d

val calc = GlickoCalculator(
ratingPeriodsPerDay = RatingPeriodsPerDay(0.21436d)
Expand All @@ -15,34 +20,32 @@ class GlickoCalculatorTest extends ScalaCheckSuite with chess.MunitExtensions:
{
val players = ByColor.fill:
Player(
Glicko(rating = 1500d, deviation = 500d, volatility = 0.09d),
numberOfResults = 0,
lastRatingPeriodEnd = None
Glicko(rating = R, deviation = RD, volatility = V)
)
test("default deviation: white wins"):
val (w, b) = computeGame(players, Outcome.white)
assertCloseTo(w.rating, 1741d, 1d)
assertCloseTo(b.rating, 1258d, 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)
assertCloseTo(w.rating, 1662.21d, 0.005d)
assertCloseTo(b.rating, 1337.79d, 0.005d)
assertCloseTo(w.deviation, 290.23d, 0.005d)
assertCloseTo(b.deviation, 290.23d, 0.005d)
assertCloseTo(w.volatility, 0.0599993d, 0.0000001d)
assertCloseTo(b.volatility, 0.0599993d, 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.deviation, 396d, 1d)
assertCloseTo(b.deviation, 396d, 1d)
assertCloseTo(w.volatility, 0.0899983, 0.00000001d)
assertCloseTo(b.volatility, 0.0899983, 0.0000001d)
assertCloseTo(w.rating, 1337.79d, 0.005d)
assertCloseTo(b.rating, 1662.21d, 0.005d)
assertCloseTo(w.deviation, 290.23d, 0.005d)
assertCloseTo(b.deviation, 290.23d, 0.005d)
assertCloseTo(w.volatility, 0.0599993d, 0.0000001d)
assertCloseTo(b.volatility, 0.0599993d, 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.deviation, 396d, 1d)
assertCloseTo(b.deviation, 396d, 1d)
assertCloseTo(w.volatility, 0.0899954, 0.0000001d)
assertCloseTo(b.volatility, 0.0899954, 0.0000001d)
assertCloseTo(w.rating, 1500d, 0.005d)
assertCloseTo(b.rating, 1500d, 0.005d)
assertCloseTo(w.deviation, 290.23d, 0.005d)
assertCloseTo(b.deviation, 290.23d, 0.005d)
assertCloseTo(w.volatility, 0.0599977d, 0.0000001d)
assertCloseTo(b.volatility, 0.0599977d, 0.0000001d)
}

{
Expand All @@ -60,26 +63,26 @@ class GlickoCalculatorTest extends ScalaCheckSuite with chess.MunitExtensions:
)
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.deviation, 77d, 1d)
assertCloseTo(b.deviation, 105d, 1d)
assertCloseTo(w.rating, 1422.63d, 0.005d)
assertCloseTo(b.rating, 1506.32d, 0.005d)
assertCloseTo(w.deviation, 77.50d, 0.005d)
assertCloseTo(b.deviation, 105.87d, 0.005d)
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.deviation, 78d, 1d)
assertCloseTo(b.deviation, 105d, 1d)
assertCloseTo(w.rating, 1389.99d, 0.005d)
assertCloseTo(b.rating, 1568.90d, 0.005d)
assertCloseTo(w.deviation, 77.50d, 0.005d)
assertCloseTo(b.deviation, 105.87d, 0.005d)
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.deviation, 78d, 1d)
assertCloseTo(b.deviation, 105.87d, 0.01d)
assertCloseTo(w.rating, 1406.31d, 0.005d)
assertCloseTo(b.rating, 1537.61d, 0.005d)
assertCloseTo(w.deviation, 77.50d, 0.005d)
assertCloseTo(b.deviation, 105.87d, 0.005d)
assertCloseTo(w.volatility, 0.06, 0.00001d)
assertCloseTo(b.volatility, 0.065, 0.00001d)
}
Expand All @@ -99,26 +102,26 @@ class GlickoCalculatorTest extends ScalaCheckSuite with chess.MunitExtensions:
)
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.deviation, 59.9d, 0.1d)
assertCloseTo(b.deviation, 196.9d, 0.1d)
assertCloseTo(w.rating, 1216.73d, 0.005d)
assertCloseTo(b.rating, 1635.99d, 0.005d)
assertCloseTo(w.deviation, 59.90d, 0.005d)
assertCloseTo(b.deviation, 196.99d, 0.005d)
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.deviation, 59.9d, 0.1d)
assertCloseTo(b.deviation, 196.9d, 0.1d)
assertCloseTo(w.rating, 1199.29d, 0.005d)
assertCloseTo(b.rating, 1855.42d, 0.005d)
assertCloseTo(w.deviation, 59.90d, 0.005d)
assertCloseTo(b.deviation, 196.99d, 0.005d)
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.deviation, 59.90056, 0.1d)
assertCloseTo(b.deviation, 196.98729, 0.1d)
assertCloseTo(w.rating, 1208.01d, 0.005d)
assertCloseTo(b.rating, 1745.71d, 0.005d)
assertCloseTo(w.deviation, 59.90056, 0.005d)
assertCloseTo(b.deviation, 196.98729, 0.005d)
assertCloseTo(w.volatility, 0.053002, 0.000001d)
assertCloseTo(b.volatility, 0.062006, 0.000001d)
}
Loading
Loading