Skip to content

Commit 8e3af29

Browse files
committed
Rate games accounting for first move advantage
1 parent 1b89080 commit 8e3af29

File tree

6 files changed

+29
-28
lines changed

6 files changed

+29
-28
lines changed

rating/src/main/scala/glicko/GlickoCalculator.scala

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package chess.rating
22
package glicko
33

4-
import chess.{ Black, ByColor, Outcome, White }
4+
import chess.{ ByColor, Outcome }
55

66
import java.time.Instant
77
import scala.util.Try
@@ -39,10 +39,7 @@ final class GlickoCalculator(
3939
import impl.*
4040

4141
def toGameResult(ratings: ByColor[Rating], outcome: Outcome): GameResult =
42-
outcome.winner match
43-
case None => GameResult(ratings.white, ratings.black, true)
44-
case Some(White) => GameResult(ratings.white, ratings.black, false)
45-
case Some(Black) => GameResult(ratings.black, ratings.white, false)
42+
GameResult(ratings.white, ratings.black, outcome)
4643

4744
def toRating(player: Player) = impl.Rating(
4845
rating = player.rating,

rating/src/main/scala/glicko/impl/RatingCalculator.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,13 @@ final private[glicko] class RatingCalculator(
170170
for result <- results do
171171
v = v + ((Math.pow(g(result.getOpponent(player).getGlicko2RatingDeviation), 2))
172172
* E(
173-
player.getGlicko2Rating,
174-
result.getOpponent(player).getGlicko2Rating,
173+
player.getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, player)),
174+
result.getOpponent(player).getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, player)),
175175
result.getOpponent(player).getGlicko2RatingDeviation
176176
)
177177
* (1.0 - E(
178-
player.getGlicko2Rating,
179-
result.getOpponent(player).getGlicko2Rating,
178+
player.getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, player)),
179+
result.getOpponent(player).getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, player)),
180180
result.getOpponent(player).getGlicko2RatingDeviation
181181
)))
182182
1 / v
@@ -197,8 +197,8 @@ final private[glicko] class RatingCalculator(
197197
outcomeBasedRating = outcomeBasedRating
198198
+ (g(result.getOpponent(player).getGlicko2RatingDeviation)
199199
* (result.getScore(player) - E(
200-
player.getGlicko2Rating,
201-
result.getOpponent(player).getGlicko2Rating,
200+
player.getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, player)),
201+
result.getOpponent(player).getGlicko2RatingWithAdvantage(result.getAdvantage(colorAdvantage, player)),
202202
result.getOpponent(player).getGlicko2RatingDeviation
203203
)))
204204
outcomeBasedRating

rating/src/main/scala/glicko/impl/results.scala

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package impl
33

44
private[glicko] trait Result:
55

6+
def getAdvantage(advantage: ColorAdvantage, p: Rating): ColorAdvantage
7+
68
def getScore(player: Rating): Double
79

810
def getOpponent(player: Rating): Rating
@@ -14,6 +16,8 @@ private[glicko] trait Result:
1416
// score from 0 (opponent wins) to 1 (player wins)
1517
final private[glicko] class FloatingResult(player: Rating, opponent: Rating, score: Float) extends Result:
1618

19+
def getAdvantage(advantage: ColorAdvantage, p: Rating): ColorAdvantage = ColorAdvantage.zero
20+
1721
def getScore(p: Rating) = if p == player then score else 1 - score
1822

1923
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
2226

2327
def players = List(player, opponent)
2428

25-
final private[glicko] class GameResult(winner: Rating, loser: Rating, isDraw: Boolean) extends Result:
29+
final private[glicko] class GameResult(first: Rating, second: Rating, outcome: chess.Outcome) extends Result:
2630
private val POINTS_FOR_WIN = 1.0d
2731
private val POINTS_FOR_LOSS = 0.0d
2832
private val POINTS_FOR_DRAW = 0.5d
2933

30-
def players = List(winner, loser)
34+
def players = List(first, second)
3135

32-
def participated(player: Rating) = player == winner || player == loser
36+
def participated(player: Rating) = player == first || player == second
37+
38+
def getAdvantage(advantage: ColorAdvantage, player: Rating): ColorAdvantage =
39+
if player == first then advantage.half else advantage.negate.half
3340

3441
/** Returns the "score" for a match.
3542
*
@@ -38,18 +45,17 @@ final private[glicko] class GameResult(winner: Rating, loser: Rating, isDraw: Bo
3845
* 1 for a win, 0.5 for a draw and 0 for a loss
3946
* @throws IllegalArgumentException
4047
*/
41-
def getScore(player: Rating): Double =
42-
if isDraw then POINTS_FOR_DRAW
43-
else if winner == player then POINTS_FOR_WIN
44-
else if loser == player then POINTS_FOR_LOSS
45-
else throw new IllegalArgumentException("Player did not participate in match");
48+
def getScore(player: Rating): Double = outcome.winner match
49+
case Some(chess.Color.White) => if player == first then POINTS_FOR_WIN else POINTS_FOR_LOSS
50+
case Some(chess.Color.Black) => if player == first then POINTS_FOR_LOSS else POINTS_FOR_WIN
51+
case _ => if participated(player) then POINTS_FOR_DRAW else throw new IllegalArgumentException("Player did not participate in match");
4652

4753
def getOpponent(player: Rating) =
48-
if winner == player then loser
49-
else if loser == player then winner
54+
if first == player then second
55+
else if second == player then first
5056
else throw new IllegalArgumentException("Player did not participate in match");
5157

52-
override def toString = s"$winner vs $loser = $isDraw"
58+
override def toString = s"$first vs $second = $outcome"
5359

5460
private[glicko] trait RatingPeriodResults[R <: Result]():
5561
val results: List[R]

rating/src/main/scala/glicko/model.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@ object ColorAdvantage extends OpaqueDouble[ColorAdvantage]:
5454
val zero: ColorAdvantage = 0d
5555
val standard: ColorAdvantage = 7.786d
5656
val crazyhouse: ColorAdvantage = 15.171d
57+
extension (c: ColorAdvantage) def half: ColorAdvantage = c/2.0d
5758
extension (c: ColorAdvantage) def negate: ColorAdvantage = -c

test-kit/src/test/scala/rating/glicko/GlickoCalculatorWithColorAdvantageTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class GlickoCalculatorWithColorAdvantageTest extends ScalaCheckSuite with chess.
2424
calc.computeGame(Game(players, outcome), skipDeviationIncrease = true).get.toPair
2525

2626
def computeGameWithAdvantage(players: ByColor[Player], outcome: Outcome) =
27-
calc.computeGame(Game(players, outcome), skipDeviationIncrease = true).get.toPair
27+
calcWithAdvantage.computeGame(Game(players, outcome), skipDeviationIncrease = true).get.toPair
2828

2929
{
3030
val players = ByColor.fill:

test-kit/src/test/scala/rating/glicko/impl/RatingCalculatorTest.scala

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package chess.rating.glicko
22
package impl
33

44
import cats.syntax.all.*
5-
import chess.{ Black, Outcome, White }
5+
import chess.Outcome
66
import munit.ScalaCheckSuite
77

88
class RatingCalculatorTest extends ScalaCheckSuite with chess.MunitExtensions:
@@ -15,10 +15,7 @@ class RatingCalculatorTest extends ScalaCheckSuite with chess.MunitExtensions:
1515
def updateRatings(wRating: Rating, bRating: Rating, outcome: Outcome) =
1616
val results = GameRatingPeriodResults:
1717
List:
18-
outcome.winner match
19-
case None => GameResult(wRating, bRating, true)
20-
case Some(White) => GameResult(wRating, bRating, false)
21-
case Some(Black) => GameResult(bRating, wRating, false)
18+
GameResult(wRating, bRating, outcome)
2219
calculator.updateRatings(results, true)
2320

2421
def defaultRating = Rating(

0 commit comments

Comments
 (0)