glicko engine and migration WIP

This commit is contained in:
Thibault Duplessis 2013-12-15 22:06:11 +01:00
parent 0537a94f77
commit c2dabcf3fb
8 changed files with 235 additions and 5 deletions

View file

@ -18,6 +18,10 @@ private[api] final class Cli(bus: lila.common.Bus, renderer: ActorSelection) ext
def process = {
case "deploy" :: "pre" :: Nil remindDeploy(lila.hub.actorApi.RemindDeployPre)
case "deploy" :: "post" :: Nil remindDeploy(lila.hub.actorApi.RemindDeployPost)
case "glicko" :: "migration" :: Nil => GlickoMigration(
lila.db.Env.current,
lila.game.Env.current,
lila.user.Env.current)
}
private def remindDeploy(event: RemindDeploy): Fu[String] = {

View file

@ -0,0 +1,35 @@
package lila.api
import scala.concurrent.duration._
import scala.concurrent.Future
import scala.util.{ Try, Success, Failure }
import play.api.libs.iteratee._
import play.api.libs.json.Json
import reactivemongo.bson._
import lila.db.api._
import lila.db.Implicits._
import lila.game.Game.{ BSONFields G }
import lila.user.{ User, UserRepo }
object GlickoMigration {
def apply(
db: lila.db.Env,
gameEnv: lila.game.Env,
userEnv: lila.user.Env) = {
val oldUserColl = db("user3")
val repo = UserRepo
val enumerator: Enumerator[BSONDocument] = lila.game.tube.gameTube |> { implicit gameTube
val query = $query(lila.game.Query.rated)
.projection(BSONDocument(G.playerUids -> true))
.sort($sort asc G.createdAt)
query.cursor[BSONDocument].enumerate(1000, true)
}
fuccess("done")
}
}

View file

@ -38,6 +38,7 @@ object BSON {
def int(k: String) = get[Int](k)
def intO(k: String) = getO[Int](k)
def intD(k: String) = intO(k) getOrElse 0
def double(k: String) = get[Double](k)
def bool(k: String) = get[Boolean](k)
def boolO(k: String) = getO[Boolean](k)
def boolD(k: String) = boolO(k) getOrElse false
@ -70,6 +71,7 @@ object BSON {
case full Some(full)
}
def docO(o: BSONDocument): Option[BSONDocument] = if (o.isEmpty) None else Some(o)
def double(i: Double): BSONDouble = BSONDouble(i)
import scalaz.Functor
def map[M[_]: Functor, A, B <: BSONValue](a: M[A])(implicit writer: BSONWriter[A, B]): M[B] =

View file

@ -21,7 +21,6 @@ final class Env(
val CaptcherName = config getString "captcher.name"
val CaptcherDuration = config duration "captcher.duration"
val CollectionGame = config getString "collection.game"
val CollectionPgn = config getString "collection.pgn"
val JsPathRaw = config getString "js_path.raw"
val JsPathCompiled = config getString "js_path.compiled"
val ActorName = config getString "actor.name"
@ -33,8 +32,6 @@ final class Env(
private[game] lazy val gameColl = db(CollectionGame)
private[game] lazy val pgnColl = db(CollectionPgn)
lazy val cached = new Cached(ttl = CachedNbTtl)
lazy val paginator = new PaginatorBuilder(

View file

@ -8,8 +8,6 @@ package object game extends PackageObject with WithPlay {
object tube {
val pgnColl = Env.current.pgnColl
implicit lazy val gameTube = Game.tube inColl Env.current.gameColl
}
}

View file

@ -0,0 +1,30 @@
package lila.user
import reactivemongo.bson.BSONDocument
import lila.db.BSON
case class Glicko(
rating: Double,
rd: Double,
volatility: Double)
case object Glicko {
val default = Glicko(1500d, 350d, 0.6d)
private def GlickoBSONHandler = new BSON[Glicko] {
def reads(r: BSON.Reader): Glicko = Glicko(
rating = r double "r",
rd = r double "rd",
volatility = r double "v")
def writes(w: BSON.Writer, o: Glicko) = BSONDocument(
"r" -> w.double(o.rating),
"rd" -> w.double(o.rd),
"v" -> w.double(o.volatility))
}
private[user] lazy val tube = lila.db.BsTube(GlickoBSONHandler)
}

View file

@ -0,0 +1,159 @@
package lila.user
import math._
object GlickoEngine {
private val Glicko2Conversion: Double = 173.7178
private val Tau: Double = 0.3
// factory method to create glicko2 rating objects from glicko1 ratings and RDs
def apply(glicko: Glicko): GlickoEngine = new GlickoEngine(
glicko.rating / Glicko2Conversion,
glicko.rd / Glicko2Conversion,
glicko.volatility)
}
final class GlickoEngine private (
val rating: Double = 1500.0,
val rd: Double = 350.0,
val volatility: Double = 0.06) {
import GlickoEngine._
/**
* This function accepts a list of tuples of opponent ratings:
* Glicko2, and result:Double (result: 0.0=loss, 0.5=draw, 1.0=win)
*
* @param opponents List of Tuples[Glicko2, Double]
* @return a new Glicko2 object with the new rating
*/
def calculateNewRating(opponents: List[(Glicko, Double)]): Glicko = {
// step1 - set tau, system volatility constraint
// tau set by default to 0.3
// step2 - convert to clicko2 scale
// already in glicko2 scale
// step3 - compute the variance
// helper function g
def g(phi: Double): Double = {
1.0 / sqrt(1.0 + 3 * pow2(phi) / pow2(math.Pi))
}
// helper function E
def E(rating: Double, oppRating: Double, oppRD: Double): Double = {
1.0 / (1.0 + exp(-g(oppRD) * (rating - oppRating)))
}
// run through opponents to calculate the variance v
def v: Double = {
var sum: Double = 0.0
opponents.foreach { opp
sum += pow2(g(opp._1.rd)) * E(this.rating, opp._1.rating, opp._1.rd) * (1 - E(this.rating, opp._1.rating, opp._1.rd))
}
1.0 / sum
}
// step4 - compute the delta
def Δ = {
var sum: Double = 0.0
opponents.foreach { opp
sum += g(opp._1.rd) * (opp._2 - E(this.rating, opp._1.rating, opp._1.rd))
}
v * sum
}
// step5 - calculate new volatility
val ε = 0.000001 // convergence tolerance
def newVolatility: Double = {
def a: Double = {
log(pow2(this.volatility))
}
def f(x: Double): Double = {
(exp(x) * (pow2(Δ) - pow2(this.rd) - v - exp(x))) / (2.0 * pow2(pow2(this.rd) + v + exp(x))) - (x - a) / pow2(Tau)
}
var A: Double = a
var B: Double = if (pow2(Δ) > pow2(this.rd)) {
log(pow2(Δ) - pow2(this.rd) - v)
}
else {
var k = 1
while (f(a - k * sqrt(pow2(Tau))) < 0) {
k += 1
}
a - k * sqrt(pow2(Tau))
}
var fA = f(A)
var fB = f(B)
while (abs(B - A) > ε) {
var C: Double = A + (A - B) * fA / (fB - fA)
var fC = f(C)
if (fC * fB < 0) {
A = B
fA = fB
}
else {
fA = fA / 2
}
B = C
fB = fC
}
exp(A / 2)
}
// step6 - update rating deviation to new pre-rating period value (decay RD)
def preRatingRD: Double = {
sqrt(pow2(this.rd) + pow2(newVolatility))
}
// step7a - calculate new RD
def newRD: Double = {
1.0 / sqrt(1.0 / pow2(preRatingRD) + 1.0 / v)
}
// step7b - calculate new rating
def newRating: Double = {
var sum: Double = 0.0
opponents.foreach { opp
sum += g(opp._1.rd) * (opp._2 - E(this.rating, opp._1.rating, opp._1.rd))
}
this.rating + pow2(newRD) * sum
}
// step8 isn't needed. we store things in Glicko2 scale
Glicko(
rating * Glicko2Conversion,
rd * Glicko2Conversion,
volatility)
}
/**
* This function is used to decay the RD when no games
* are played during a "rating period".
*
* @param n the number of rating periods to decay
* @return a new Glicko2 object with the decayed RD
*/
def decayRD(n: Int): GlickoEngine = {
var preRatingRD: Double = this.rd
for (i 0 until n) {
// step6 - update rating deviation to new pre-rating period value (decay RD)
preRatingRD = sqrt(pow2(preRatingRD) + pow2(this.volatility))
}
new GlickoEngine(this.rating, preRatingRD, this.volatility)
}
private def pow2(op: Double): Double = op * op
// end friendlier looking math functions
override def toString: String = {
"rating: %1.0f, rd: %1.2f, volatility: %1.6f".format(rating, rd, volatility)
}
}

View file

@ -9,6 +9,7 @@ case class User(
id: String,
username: String,
elo: Int,
glicko: Glicko,
speedElos: SpeedElos,
variantElos: VariantElos,
count: Count,
@ -68,6 +69,7 @@ object User {
val id = "_id"
val username = "username"
val elo = "elo"
val glicko = "glicko"
val speedElos = "speedElos"
val variantElos = "variantElos"
val count = "count"
@ -94,11 +96,13 @@ object User {
implicit def speedElosHandler = SpeedElos.tube.handler
implicit def variantElosHandler = VariantElos.tube.handler
implicit def profileHandler = Profile.tube.handler
implicit def glickoHandler = Glicko.tube.handler
def reads(r: BSON.Reader): User = User(
id = r str id,
username = r str username,
elo = r nInt elo,
glicko = r.getO[Glicko](glicko) | Glicko.default,
speedElos = r.getO[SpeedElos](speedElos) | SpeedElos.default,
variantElos = r.getO[VariantElos](variantElos) | VariantElos.default,
count = r.get[Count](count),
@ -117,6 +121,7 @@ object User {
id -> o.id,
username -> o.username,
elo -> w.int(o.elo),
glicko -> o.glicko,
speedElos -> o.speedElos,
variantElos -> o.variantElos,
count -> o.count,