BattleSim/server/glicko2.coffee

158 lines
4.9 KiB
CoffeeScript

# Formula from Glickman's paper:
# http://glicko.net/glicko/glicko2.pdf
config =
TAU: 0.5
DEFAULT_RATING: 1500
DEFAULT_DEVIATION: 350
DEFAULT_VOLATILITY: 0.06
CONVERGENCE_TOLERANCE: 0.000001
GLICKO_CONSTANT = 173.1718
PI_SQUARED = Math.PI * Math.PI
ratingFromGlicko = (rating) ->
(rating - 1500) / GLICKO_CONSTANT
deviationFromGlicko = (deviation) ->
deviation / GLICKO_CONSTANT
g = (deviation) ->
1 / Math.sqrt(1 + 3 * deviation * deviation / PI_SQUARED)
E = (mu, opponentMu, opponentPhi) ->
1 / (1 + Math.exp(-g(opponentPhi) * (mu - opponentMu)))
_extractPlayer = (player) ->
{rating, deviation, volatility} = player
createPlayer = ->
rating: config.DEFAULT_RATING
deviation: config.DEFAULT_DEVIATION
volatility: config.DEFAULT_VOLATILITY
calculate = (player, matches, options = {}) ->
# Step 1: Initialize tau. Players with no rating are assumed to have been
# handled already using the createPlayer function.
tau = options.systemConstant || config.TAU
extractPlayer = (options.extractPlayer || _extractPlayer)
glickoPlayer = extractPlayer(player)
playerRating = ratingFromGlicko(glickoPlayer.rating)
playerDeviation = deviationFromGlicko(glickoPlayer.deviation)
playerVolatility = glickoPlayer.volatility
# Step 2: Convert from Glicko to Glicko2
gPlayers = []
ePlayers = []
scores = []
for match in matches
{opponent, score} = match
glickoOpponent = extractPlayer(opponent)
opponentRating = ratingFromGlicko(opponent.rating)
opponentDeviation = deviationFromGlicko(opponent.deviation)
gPlayers.push g(opponentDeviation)
ePlayers.push E(playerRating, opponentRating, opponentDeviation)
scores.push score
# Step 3: Compute estimated variance (v)
estimatedVariance = calculateEstimatedVariance(gPlayers, ePlayers)
# Step 4: Compute estimated improvement in rating (delta)
improvementSum = calculateImprovementSum(gPlayers, ePlayers, scores)
# Step 5: Determine new volatility (delta prime)
newVolatility = calculateNewVolatility(improvementSum, playerDeviation,
estimatedVariance, playerVolatility, tau)
# Step 6: Update rating deviation to new pre-rating period value
periodValue = playerDeviation * playerDeviation + newVolatility * newVolatility
# Step 7: Determine new rating and rating deviation
newDeviation = 1 / Math.sqrt(1 / periodValue + 1 / estimatedVariance)
newRating = playerRating + newDeviation * newDeviation * improvementSum
# Step 8: Convert back to Glicko scale
rating = GLICKO_CONSTANT * newRating + config.DEFAULT_RATING
deviation = GLICKO_CONSTANT * newDeviation
volatility = newVolatility
# Return results
return {rating, deviation, volatility}
calculateImprovementSum = (gPlayers, ePlayers, scores) ->
improvementSum = 0.0
for gPlayer, i in gPlayers
improvementSum += gPlayer * (scores[i] - ePlayers[i])
improvementSum
calculateEstimatedVariance = (gPlayers, ePlayers) ->
estimatedVariance = 0.0
for gPlayer, i in gPlayers
gSquared = gPlayer * gPlayer
ePlayer = ePlayers[i]
estimatedVariance += gSquared * ePlayer * (1.0 - ePlayer)
estimatedVariance = 1.0 / estimatedVariance
estimatedVariance
calculateNewVolatility = (improvementSum, deviation, variance, volatility, tau) ->
# Step 5.1
A = a = Math.log(volatility * volatility)
improvement = variance * improvementSum
deviationSquared = deviation * deviation
varianceSquared = variance * variance
improvementSquared = improvement * improvement
tauSquared = tau * tau
convergenceTolerance = config.CONVERGENCE_TOLERANCE
fIter = (x) -> f(deviationSquared, improvementSquared, tauSquared, variance, a, x)
# Step 5.2
if improvementSquared > varianceSquared + variance
B = Math.log(improvementSquared - varianceSquared - variance)
else
absTau = Math.abs(tau)
k = 1
k += 1 while fIter(a - k * absTau) < 0
B = a - k * absTau
# Step 5.3
fA = fIter(A)
fB = fIter(B)
while Math.abs(B - A) > convergenceTolerance
# Step 5.4a
C = A + (A - B) * fA / (fB - fA)
fC = fIter(C)
# Step 5.4b
if fC * fB < 0 then [A, fA] = [B, fB] else fA /= 2
# Step 5.4c
[B, fB] = [C, fC]
# Step 5.5
return Math.exp(A / 2)
f = (deviationSquared, improvementSquared, tauSquared, variance, a, x) ->
expX = Math.exp(x)
denominator = deviationSquared + variance + expX
((expX * (improvementSquared - denominator) /
(2 * denominator * denominator)) - ((x - a) / tauSquared))
getRatingEstimate = (rating, deviation) ->
return 0 if deviation > 100
rds = deviation * deviation
sqr = sqrt(15.905694331435 * (rds + 221781.21786254))
inner = (1500.0 - rating) * Math.PI / sqr
return Math.floor(10000.0 / (1.0 + Math.pow(10.0, inner)) + 0.5) / 100.0
module.exports = {
calculate
calculateImprovementSum
calculateNewVolatility
calculateEstimatedVariance
createPlayer
ratingFromGlicko
deviationFromGlicko
getRatingEstimate
config
g
E
}