glicko engine and migration WIP
This commit is contained in:
parent
0537a94f77
commit
c2dabcf3fb
|
@ -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] = {
|
||||
|
|
35
modules/api/src/main/GlickoMigration.scala
Normal file
35
modules/api/src/main/GlickoMigration.scala
Normal 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")
|
||||
}
|
||||
}
|
|
@ -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] =
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
30
modules/user/src/main/Glicko.scala
Normal file
30
modules/user/src/main/Glicko.scala
Normal 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)
|
||||
}
|
159
modules/user/src/main/GlickoEngine.scala
Normal file
159
modules/user/src/main/GlickoEngine.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue