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 Oct 27, 2024
1 parent 49651af commit 8320559
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 130 deletions.
10 changes: 4 additions & 6 deletions modules/puzzle/src/main/PuzzleFinisher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -199,18 +199,16 @@ final private[puzzle] class PuzzleFinisher(
if player.clueless then glicko._1
else glicko._1.average(glicko._2, weightOf(angle, win))

private val VOLATILITY = lila.rating.Glicko.default.volatility
private val TAU = 0.75d
private val calculator = glicko2.RatingCalculator(VOLATILITY, TAU)
private val calculator = lila.rating.Glicko.system

def incPuzzlePlays(puzzleId: PuzzleId): Funit =
colls.puzzle.map(_.incFieldUnchecked($id(puzzleId), Puzzle.BSONFields.plays))

private def updateRatings(u1: glicko2.Rating, u2: glicko2.Rating, win: PuzzleWin): Unit =
val results = glicko2.GameRatingPeriodResults(
val results = glicko2.BinaryRatingPeriodResults(
List(
if win.yes then glicko2.GameResult(u1, u2, false)
else glicko2.GameResult(u2, u1, false)
if win.yes then glicko2.BinaryResult(u1, u2, false)
else glicko2.BinaryResult(u2, u1, false)
)
)
try calculator.updateRatings(results)
Expand Down
5 changes: 3 additions & 2 deletions modules/rating/src/main/Glicko.scala
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ object Glicko:
// rating that can be lost or gained with a single game
val maxRatingDelta = 700

val tau = 0.75d
val system = glicko2.RatingCalculator(tau, ratingPeriodsPerDay)
val tau = 0.75d
val system = glicko2.RatingCalculator(tau, ratingPeriodsPerDay)
def calculator(advantage: Double) = glicko2.RatingCalculator(advantage, tau, ratingPeriodsPerDay)

def liveDeviation(p: Perf, reverse: Boolean): Double = {
system.previewDeviation(p.toRating, nowInstant, reverse)
Expand Down
4 changes: 4 additions & 0 deletions modules/rating/src/main/glicko2/Rating.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ final class Rating(
*/
def getGlicko2Rating: Double = convertRatingToGlicko2Scale(this.rating)

def getGlicko2RatingWithAdvantage(advantage: Double): Double = convertRatingToGlicko2Scale(
this.rating + advantage
)

/** Set the average skill value, taking in a value in Glicko2 scale.
*/
def setGlicko2Rating(r: Double) =
Expand Down
20 changes: 10 additions & 10 deletions modules/rating/src/main/glicko2/RatingCalculator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ object RatingCalculator:
(ratingDeviation / MULTIPLIER)

final class RatingCalculator(
advantage: Double = 0.0d,
tau: Double = 0.75d,
ratingPeriodsPerDay: Double = 0
):
Expand All @@ -33,10 +34,9 @@ final class RatingCalculator(

val ratingPeriodsPerMilli: Double = ratingPeriodsPerDay * 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 @@ -166,13 +166,13 @@ final class RatingCalculator(
for result <- results do
v = v + ((Math.pow(g(result.getOpponent(player).getGlicko2RatingDeviation), 2))
* E(
player.getGlicko2Rating,
result.getOpponent(player).getGlicko2Rating,
player.getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player)),
result.getOpponent(player).getGlicko2RatingWithAdvantage(-result.getAdvantage(advantage, player)),
result.getOpponent(player).getGlicko2RatingDeviation
)
* (1.0 - E(
player.getGlicko2Rating,
result.getOpponent(player).getGlicko2Rating,
player.getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player)),
result.getOpponent(player).getGlicko2RatingWithAdvantage(-result.getAdvantage(advantage, player)),
result.getOpponent(player).getGlicko2RatingDeviation
)))
1 / v
Expand All @@ -193,8 +193,8 @@ final class RatingCalculator(
outcomeBasedRating = outcomeBasedRating
+ (g(result.getOpponent(player).getGlicko2RatingDeviation)
* (result.getScore(player) - E(
player.getGlicko2Rating,
result.getOpponent(player).getGlicko2Rating,
player.getGlicko2RatingWithAdvantage(result.getAdvantage(advantage, player)),
result.getOpponent(player).getGlicko2RatingWithAdvantage(-result.getAdvantage(advantage, player)),
result.getOpponent(player).getGlicko2RatingDeviation
)))
outcomeBasedRating
Expand Down
3 changes: 1 addition & 2 deletions modules/rating/src/main/glicko2/RatingPeriodResults.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ trait RatingPeriodResults[R <: Result]():

class GameRatingPeriodResults(val results: List[GameResult]) extends RatingPeriodResults[GameResult]

class FloatingRatingPeriodResults(val results: List[FloatingResult])
extends RatingPeriodResults[FloatingResult]
class BinaryRatingPeriodResults(val results: List[BinaryResult]) extends RatingPeriodResults[BinaryResult]
48 changes: 28 additions & 20 deletions modules/rating/src/main/glicko2/Result.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package lila.rating.glicko2

trait Result:

def getAdvantage(advantage: Double, player: Rating): Double

def getScore(player: Rating): Double

def getOpponent(player: Rating): Rating
Expand All @@ -10,42 +12,48 @@ trait Result:

def players: List[Rating]

// score from 0 (opponent wins) to 1 (player wins)
class FloatingResult(player: Rating, opponent: Rating, score: Float) extends Result:
class BinaryResult(first: Rating, second: Rating, score: Boolean) extends Result:
private val POINTS_FOR_WIN = 1.0d
private val POINTS_FOR_LOSS = 0.0d

def getAdvantage(advantage: Double, p: Rating) = 0.0d

def getScore(p: Rating) = if p == player then score else 1 - score
def getScore(p: Rating) = if p == first then if score then POINTS_FOR_WIN else POINTS_FOR_LOSS
else if score then POINTS_FOR_LOSS
else POINTS_FOR_WIN

def getOpponent(p: Rating) = if p == player then opponent else player
def getOpponent(p: Rating) = if p == first then second else first

def participated(p: Rating) = p == player || p == opponent
def participated(p: Rating) = p == first || p == second

def players = List(player, opponent)
def players = List(first, second)

final class GameResult(winner: Rating, loser: Rating, isDraw: Boolean) extends Result:
final class GameResult(first: Rating, second: Rating, outcome: Option[Boolean]) extends Result:
private val POINTS_FOR_WIN = 1.0d
private val POINTS_FOR_LOSS = 0.0d
private val POINTS_FOR_DRAW = 0.5d
private val POINTS_FOR_LOSS = 0.0d

def players = List(winner, loser)
def players = List(first, second)

def participated(player: Rating) = player == winner || player == loser
def participated(player: Rating) = player == first || player == second

def getAdvantage(advantage: Double, player: Rating) =
if player == first then advantage / 2.0d else -advantage / 2.0d

/** Returns the "score" for a match.
*
* @param player
* @return
* 1 for a win, 0.5 for a draw and 0 for a loss
* 1 for a first player win, 0.5 for a draw and 0 for a first player loss
* @throws IllegalArgumentException
*/
def getScore(player: Rating): Double =
if isDraw then POINTS_FOR_DRAW
else if winner == player then POINTS_FOR_WIN
else if loser == player then POINTS_FOR_LOSS
else throw new IllegalArgumentException("Player did not participate in match");
def getScore(player: Rating): Double = outcome match
case Some(true) => if player == first then POINTS_FOR_WIN else POINTS_FOR_LOSS
case Some(false) => if player == first then POINTS_FOR_LOSS else POINTS_FOR_WIN
case _ => POINTS_FOR_DRAW

def getOpponent(player: Rating) =
if winner == player then loser
else if loser == player then winner
if first == player then second
else if second == player then first
else throw new IllegalArgumentException("Player did not participate in match");

override def toString = s"$winner vs $loser = $isDraw"
override def toString = s"$first vs $second = $outcome"
109 changes: 54 additions & 55 deletions modules/rating/src/test/RatingCalculatorTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import lila.rating.PerfExt.*
import glicko2.*

class RatingCalculatorTest extends lila.common.LilaTest:

def updateRatings(wRating: Rating, bRating: Rating, winner: Option[Color]) =
val result = winner match
case Some(chess.White) => Glicko.Result.Win
Expand All @@ -17,43 +16,43 @@ class RatingCalculatorTest extends lila.common.LilaTest:
val results = GameRatingPeriodResults(
List(
result match
case Glicko.Result.Draw => GameResult(wRating, bRating, true)
case Glicko.Result.Win => GameResult(wRating, bRating, false)
case Glicko.Result.Loss => GameResult(bRating, wRating, false)
case Glicko.Result.Win => GameResult(wRating, bRating, Some(true))
case Glicko.Result.Loss => GameResult(wRating, bRating, Some(false))
case Glicko.Result.Draw => GameResult(wRating, bRating, None)
)
)
Glicko.system.updateRatings(results, true)
Glicko.calculator(35.0d).updateRatings(results, true)

test("default deviation: white wins") {
val wr = default.toRating
val br = default.toRating
updateRatings(wr, br, White.some)
assertCloseTo(wr.rating, 1741d, 1d)
assertCloseTo(br.rating, 1258d, 1d)
assertCloseTo(wr.ratingDeviation, 396d, 1d)
assertCloseTo(br.ratingDeviation, 396d, 1d)
assertCloseTo(wr.volatility, 0.0899983, 0.00000001d)
assertCloseTo(br.volatility, 0.0899983, 0.0000001d)
assertCloseTo(wr.rating, 1729d, 1d)
assertCloseTo(br.rating, 1271d, 1d)
assertCloseTo(wr.ratingDeviation, 396.9015d, 0.0001d)
assertCloseTo(br.ratingDeviation, 396.9015d, 0.0001d)
assertCloseTo(wr.volatility, 0.0899980, 0.0000001d)
assertCloseTo(br.volatility, 0.0899980, 0.0000001d)
}
test("default deviation: black wins") {
val wr = default.toRating
val br = default.toRating
updateRatings(wr, br, Black.some)
assertCloseTo(wr.rating, 1258d, 1d)
assertCloseTo(br.rating, 1741d, 1d)
assertCloseTo(wr.ratingDeviation, 396d, 1d)
assertCloseTo(br.ratingDeviation, 396d, 1d)
assertCloseTo(wr.volatility, 0.0899983, 0.00000001d)
assertCloseTo(br.volatility, 0.0899983, 0.0000001d)
assertCloseTo(wr.rating, 1245d, 1d)
assertCloseTo(br.rating, 1755d, 1d)
assertCloseTo(wr.ratingDeviation, 396.9015d, 0.0001d)
assertCloseTo(br.ratingDeviation, 396.9015d, 0.0001d)
assertCloseTo(wr.volatility, 0.0899986, 0.0000001d)
assertCloseTo(br.volatility, 0.0899986, 0.0000001d)
}
test("default deviation: draw") {
val wr = default.toRating
val br = default.toRating
updateRatings(wr, br, None)
assertCloseTo(wr.rating, 1500d, 1d)
assertCloseTo(br.rating, 1500d, 1d)
assertCloseTo(wr.ratingDeviation, 396d, 1d)
assertCloseTo(br.ratingDeviation, 396d, 1d)
assertCloseTo(wr.rating, 1487d, 1d)
assertCloseTo(br.rating, 1513d, 1d)
assertCloseTo(wr.ratingDeviation, 396.9015d, 0.0001d)
assertCloseTo(br.ratingDeviation, 396.9015d, 0.0001d)
assertCloseTo(wr.volatility, 0.0899954, 0.0000001d)
assertCloseTo(br.volatility, 0.0899954, 0.0000001d)
}
Expand All @@ -67,32 +66,32 @@ class RatingCalculatorTest extends lila.common.LilaTest:
val wr = perf.toRating
val br = perf.toRating
updateRatings(wr, br, White.some)
assertCloseTo(wr.rating, 1517d, 1d)
assertCloseTo(br.rating, 1482d, 1d)
assertCloseTo(wr.ratingDeviation, 78d, 1d)
assertCloseTo(br.ratingDeviation, 78d, 1d)
assertCloseTo(wr.rating, 1515d, 1d)
assertCloseTo(br.rating, 1485d, 1d)
assertCloseTo(wr.ratingDeviation, 78.0967d, 0.0001d)
assertCloseTo(br.ratingDeviation, 78.0967d, 0.0001d)
assertCloseTo(wr.volatility, 0.06, 0.00001d)
assertCloseTo(br.volatility, 0.06, 0.00001d)
}
test("low deviation: black wins") {
val wr = perf.toRating
val br = perf.toRating
updateRatings(wr, br, Black.some)
assertCloseTo(wr.rating, 1482d, 1d)
assertCloseTo(br.rating, 1517d, 1d)
assertCloseTo(wr.ratingDeviation, 78d, 1d)
assertCloseTo(br.ratingDeviation, 78d, 1d)
assertCloseTo(wr.rating, 1481d, 1d)
assertCloseTo(br.rating, 1519d, 1d)
assertCloseTo(wr.ratingDeviation, 78.0967d, 0.0001d)
assertCloseTo(br.ratingDeviation, 78.0967d, 0.0001d)
assertCloseTo(wr.volatility, 0.06, 0.00001d)
assertCloseTo(br.volatility, 0.06, 0.00001d)
}
test("low deviation: draw") {
val wr = perf.toRating
val br = perf.toRating
updateRatings(wr, br, None)
assertCloseTo(wr.rating, 1500d, 1d)
assertCloseTo(br.rating, 1500d, 1d)
assertCloseTo(wr.ratingDeviation, 78d, 1d)
assertCloseTo(br.ratingDeviation, 78d, 1d)
assertCloseTo(wr.rating, 1498d, 1d)
assertCloseTo(br.rating, 1502d, 1d)
assertCloseTo(wr.ratingDeviation, 78.0967d, 0.0001d)
assertCloseTo(br.ratingDeviation, 78.0967d, 0.0001d)
assertCloseTo(wr.volatility, 0.06, 0.00001d)
assertCloseTo(br.volatility, 0.06, 0.00001d)
}
Expand All @@ -116,9 +115,9 @@ class RatingCalculatorTest extends lila.common.LilaTest:
val br = bP.toRating
updateRatings(wr, br, White.some)
assertCloseTo(wr.rating, 1422d, 1d)
assertCloseTo(br.rating, 1506d, 1d)
assertCloseTo(wr.ratingDeviation, 77d, 1d)
assertCloseTo(br.ratingDeviation, 105d, 1d)
assertCloseTo(br.rating, 1509d, 1d)
assertCloseTo(wr.ratingDeviation, 77.3966, 0.0001d)
assertCloseTo(br.ratingDeviation, 105.5926, 0.0001d)
assertCloseTo(wr.volatility, 0.06, 0.00001d)
assertCloseTo(br.volatility, 0.065, 0.00001d)
}
Expand All @@ -127,20 +126,20 @@ class RatingCalculatorTest extends lila.common.LilaTest:
val br = bP.toRating
updateRatings(wr, br, Black.some)
assertCloseTo(wr.rating, 1389d, 1d)
assertCloseTo(br.rating, 1568d, 1d)
assertCloseTo(wr.ratingDeviation, 78d, 1d)
assertCloseTo(br.ratingDeviation, 105d, 1d)
assertCloseTo(br.rating, 1571d, 1d)
assertCloseTo(wr.ratingDeviation, 77.3966, 0.0001d)
assertCloseTo(br.ratingDeviation, 105.5926, 0.0001d)
assertCloseTo(wr.volatility, 0.06, 0.00001d)
assertCloseTo(br.volatility, 0.065, 0.00001d)
}
test("mixed ratings and deviations: draw") {
val wr = wP.toRating
val br = bP.toRating
updateRatings(wr, br, None)
assertCloseTo(wr.rating, 1406d, 1d)
assertCloseTo(br.rating, 1537d, 1d)
assertCloseTo(wr.ratingDeviation, 78d, 1d)
assertCloseTo(br.ratingDeviation, 105.87d, 0.01d)
assertCloseTo(wr.rating, 1405d, 1d)
assertCloseTo(br.rating, 1540d, 1d)
assertCloseTo(wr.ratingDeviation, 77.3966, 0.0001d)
assertCloseTo(br.ratingDeviation, 105.5926, 0.0001d)
assertCloseTo(wr.volatility, 0.06, 0.00001d)
assertCloseTo(br.volatility, 0.065, 0.00001d)
}
Expand All @@ -164,32 +163,32 @@ class RatingCalculatorTest extends lila.common.LilaTest:
val wr = wP.toRating
val br = bP.toRating
updateRatings(wr, br, White.some)
assertCloseTo(wr.rating, 1216.7d, 0.1d)
assertCloseTo(br.rating, 1636d, 0.1d)
assertCloseTo(wr.ratingDeviation, 59.9d, 0.1d)
assertCloseTo(br.ratingDeviation, 196.9d, 0.1d)
assertCloseTo(wr.rating, 1216.6d, 0.1d)
assertCloseTo(br.rating, 1638.4d, 0.1d)
assertCloseTo(wr.ratingDeviation, 59.8839d, 0.0001d)
assertCloseTo(br.ratingDeviation, 196.3840, 0.0001d)
assertCloseTo(wr.volatility, 0.053013, 0.000001d)
assertCloseTo(br.volatility, 0.062028, 0.000001d)
}
test("more mixed ratings and deviations: black wins") {
val wr = wP.toRating
val br = bP.toRating
updateRatings(wr, br, Black.some)
assertCloseTo(wr.rating, 1199.3d, 0.1d)
assertCloseTo(br.rating, 1855.4d, 0.1d)
assertCloseTo(wr.ratingDeviation, 59.9d, 0.1d)
assertCloseTo(br.ratingDeviation, 196.9d, 0.1d)
assertCloseTo(wr.rating, 1199.2d, 0.1d)
assertCloseTo(br.rating, 1856.5d, 0.1d)
assertCloseTo(wr.ratingDeviation, 59.8839d, 0.0001d)
assertCloseTo(br.ratingDeviation, 196.3840, 0.0001d)
assertCloseTo(wr.volatility, 0.052999, 0.000001d)
assertCloseTo(br.volatility, 0.061999, 0.000001d)
}
test("more mixed ratings and deviations: draw") {
val wr = wP.toRating
val br = bP.toRating
updateRatings(wr, br, None)
assertCloseTo(wr.rating, 1208.0, 0.1d)
assertCloseTo(br.rating, 1745.7, 0.1d)
assertCloseTo(wr.ratingDeviation, 59.90056, 0.1d)
assertCloseTo(br.ratingDeviation, 196.98729, 0.1d)
assertCloseTo(wr.rating, 1207.9, 0.1d)
assertCloseTo(br.rating, 1747.5, 0.1d)
assertCloseTo(wr.ratingDeviation, 59.8839, 0.0001d)
assertCloseTo(br.ratingDeviation, 196.3840, 0.0001d)
assertCloseTo(wr.volatility, 0.053002, 0.000001d)
assertCloseTo(br.volatility, 0.062006, 0.000001d)
}
Expand Down
Loading

0 comments on commit 8320559

Please sign in to comment.