GlickoRatingAlgorithm.kt
package delta.codecharacter.server.logic.rating
import java.time.Instant
import java.util.concurrent.TimeUnit
import kotlin.math.PI
import kotlin.math.ln
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
/*
Refer: http://www.glicko.net/glicko/glicko.pdf
*/
class GlickoRatingAlgorithm : RatingAlgorithm {
// Constant
private val q: Double = ln(10.0) / 400
// " One practical problem with the Glicko system is that when a player competes very frequently,
// his/her rating stops changing appreciably which reflects that the RD is very small. This may
// sometimes prevent a player’s rating from changing substantially when the player is truly
// improving. I would therefore recommend that an RD never drop below a threshold value,
// such as 30, so that ratings can change appreciably even in a relatively short time."
// - Mark E Glickman
private val minRatingDeviation: Double = 30.0
// Time for one rating period in minutes
private val timePeriod: Double = 15.0
// Amount which decides how much a player's rating deviation changes every time period
// Using timePeriod 15 mins and time to reduce the rating to min rating as 48 hrs,
// we get the number of time periods to reduce the rating to min rating as 192.
// Using these we calculate c = sqrt((350**2 - 50**2)/192) = 25, where 50 is
// the rating deviation of a typical player.
private val c: Double = 25.0
// Wont let RD be more than this value
private val unratedPlayerRD: Double = 350.0
private fun g(rd: Double): Double = 1.0 / (sqrt(1.0 + ((3.0 * q * q * rd * rd)) / (PI * PI)))
private fun e(r0: Double, rI: Double, rdI: Double): Double {
val grdI = g(rdI)
val ratingDiff = r0 - rI
return 1.0 / (1.0 + ((10.0).pow((grdI * ratingDiff) / (-400.0))))
}
private fun dSquared(
r0: Double,
opponentRatings: List<GlickoRating>,
): Double {
var sm = 0.0
for (oppRating in opponentRatings) {
val rI = oppRating.rating
val rdI = oppRating.ratingDeviation
val grd = g(rdI)
val expectation = e(r0, rI, rdI)
sm += (grd * grd * expectation * (1.0 - expectation))
}
return 1.0 / (q * q * sm)
}
override fun calculateNewRating(
rating: GlickoRating,
opponentRatings: List<GlickoRating>,
opponentOutcomes: List<Double>,
): GlickoRating {
val r0 = rating.rating
var sm = 0.0
for ((i, opponentRating) in opponentRatings.withIndex()) {
val rI = opponentRating.rating
val rdI = opponentRating.ratingDeviation
val grd = g(rdI)
val expectation = e(r0, rI, rdI)
val sI = opponentOutcomes[i]
sm += (grd * (sI - expectation))
}
val rd = rating.ratingDeviation
val d2 = dSquared(r0, opponentRatings)
val numerator = q * sm
val denominator = (1.0 / (rd * rd)) + (1.0 / d2)
val newR = r0 + (numerator / denominator)
val newRd = max(minRatingDeviation, sqrt(1.0 / ((1.0 / (rd * rd)) + (1.0 / d2))))
return GlickoRating(newR, newRd)
}
override fun getWeightedRatingDeviationSinceLastCompetition(
lastRD: Double,
lastMatchDate: Instant,
): Double {
val currentInstant = Instant.now()
val offset = currentInstant.toEpochMilli() - lastMatchDate.toEpochMilli()
val noOfTimePeriodsElapsed = TimeUnit.MILLISECONDS.toMinutes(offset).toDouble() / timePeriod
return min(unratedPlayerRD, sqrt((lastRD * lastRD) + (c * c * noOfTimePeriodsElapsed)))
}
}