more perf stat wip

perfStats
Thibault Duplessis 2015-12-24 10:41:28 +07:00
parent 51f93f3896
commit b9f62b3321
13 changed files with 136 additions and 24 deletions

View File

@ -80,7 +80,8 @@ final class Env(
Env.shutup, // required to load the actor
Env.insight, // required to load the actor
Env.worldMap, // required to load the actor
Env.push // required to load the actor
Env.push, // required to load the actor
Env.perfStat // required to load the actor
)
play.api.Logger("boot").info("Preloading complete")
}
@ -144,4 +145,5 @@ object Env {
def shutup = lila.shutup.Env.current
def insight = lila.insight.Env.current
def push = lila.push.Env.current
def perfStat = lila.perfStat.Env.current
}

View File

@ -222,6 +222,16 @@ object User extends LilaController {
}
}
def perfStat(username: String, perfKey: String) = Open { implicit ctx =>
OptionFuResult(UserRepo named username) { user =>
lila.rating.PerfType(perfKey).fold(notFound) { perfType =>
Env.perfStat.get(user, perfType).map { perfStat =>
Ok(html.user.perfStat(user, perfStat))
}
}
}
}
def autocomplete = Open { implicit ctx =>
get("term", ctx.req).filter(_.nonEmpty).fold(BadRequest("No search term provided").fuccess: Fu[Result]) { term =>
JsonOk(UserRepo usernamesLike term)

View File

@ -0,0 +1,7 @@
@(u: User, perfStat: lila.perfStat.PerfStat)(implicit ctx: Context)
@perfStat
<a href="@routes.User.showFilter(u.username, "search")?perf=@perfStat.perfType.id">
View all @perfStat.perfType.name games
</a>

View File

@ -4,8 +4,6 @@
@title = @{ s"${u.username} : ${userGameFilterTitleNoTag(info, filters.current)}${if(games.currentPage == 1) "" else " - page " + games.currentPage}" }
@searchUrl = @{ routes.User.showFilter(u.username, "search") }
@evenMoreJs = {
@if(!u.lame || ctx.is(u) || isGranted(_.UserSpy)) {
@if(filters.current.name == "search") {
@ -32,7 +30,7 @@ if (lichess.once('user-tournaments-tour')) setTimeout(lichess.startTournamentSta
@if(lila.rating.PerfType.nonGame.contains(perfType)) {
@name.getOrElse(perfType.name).toUpperCase
} else {
<a href="@searchUrl?perf=@perfType.id">
<a href="@routes.User.perfStat(u.username, perfType.key)">
@name.getOrElse(perfType.name).toUpperCase
</a>
}

View File

@ -269,6 +269,9 @@ playban {
worldMap {
geoip = ${geoip}
}
perfStat {
collection.perf_stat = "perf_stat"
}
push {
collection.device = push_device
google {

View File

@ -58,6 +58,7 @@ GET /@/:username/mod controllers.User.mod(username: String)
POST /@/:username/note controllers.User.writeNote(username: String)
GET /@/:username/mini controllers.User.showMini(username: String)
GET /@/:username/tv controllers.User.tv(username: String)
GET /@/:username/perf/:perfKey controllers.User.perfStat(username: String, perfKey: String)
GET /@/:username/:filterName controllers.User.showFilter(username: String, filterName: String, page: Int ?= 1)
GET /@/:username controllers.User.show(username: String)
GET /player controllers.User.list

View File

@ -90,6 +90,8 @@ case class Player(
def ratingAfter = rating map (_ + ~ratingDiff)
def stableRating = rating ifFalse provisional
def stableRatingAfter = stableRating map (_ + ~ratingDiff)
}
object Player {

View File

@ -18,7 +18,7 @@ import lila.user.User
private final class Indexer(storage: Storage, sequencer: ActorRef) {
private implicit val timeout = makeTimeout.minutes(5)
private implicit val timeout = makeTimeout minutes 5
def all(user: User): Funit = {
val p = scala.concurrent.Promise[Unit]()

View File

@ -9,6 +9,7 @@ import lila.common.PimpedConfig._
final class Env(
config: Config,
system: ActorSystem,
db: lila.db.Env) {
private val settings = new {
@ -16,9 +17,18 @@ final class Env(
}
import settings._
lazy val api = new PerfStatApi(
lazy val storage = new PerfStatStorage(
coll = db(CollectionPerfStat))
lazy val indexer = new PerfStatIndexer(
storage = storage,
sequencer = system.actorOf(Props(classOf[lila.hub.Sequencer], None, None)))
def get(user: lila.user.User, perfType: lila.rating.PerfType) =
storage.find(user.id, perfType) orElse {
indexer.userPerf(user, perfType) >> storage.find(user.id, perfType)
} map (_ | PerfStat.init(user.id, perfType))
// system.actorOf(Props(new Actor {
// system.lilaBus.subscribe(self, 'analysisReady)
// def receive = {
@ -31,5 +41,6 @@ object Env {
lazy val current: Env = "perfStat" boot new Env(
config = lila.common.PlayApp loadConfig "perfStat",
system = lila.common.PlayApp.system,
db = lila.db.Env.current)
}

View File

@ -17,11 +17,13 @@ case class PerfStat(
resultStreak: ResultStreak,
playStreak: PlayStreak) {
def +(pov: Pov) = if (!pov.game.finished) this else copy(
def id = _id
def agg(pov: Pov) = if (!pov.game.finished) this else copy(
highest = RatingAt.agg(highest, pov, 1),
lowest = RatingAt.agg(lowest, pov, -1),
bestWin = Result.agg(bestWin, pov, 1),
worstLoss = Result.agg(worstLoss, pov, -1),
bestWin = (~pov.win).fold(Result.agg(bestWin, pov, 1), bestWin),
worstLoss = (~pov.loss).fold(Result.agg(worstLoss, pov, -1), worstLoss),
count = count(pov),
resultStreak = resultStreak agg pov,
playStreak = playStreak agg pov
@ -40,17 +42,23 @@ object PerfStat {
lowest = none,
bestWin = none,
worstLoss = none,
count = Count(all = 0, rated = 0, win = 0, loss = 0, draw = 0, opAvg = 0),
resultStreak = ResultStreak(win = 0, loss = 0, lastRes = none),
count = Count(all = 0, rated = 0, win = 0, loss = 0, draw = 0, opAvg = Avg(0, 0)),
resultStreak = ResultStreak(curWin = 0, curLoss = 0, maxWin = 0, maxLoss = 0),
playStreak = PlayStreak(curNb = 0, curSeconds = 0, maxNb = 0, maxSeconds = 0, lastDate = none)
)
}
case class ResultStreak(win: Int, loss: Int, lastRes: Option[Boolean]) {
case class ResultStreak(
curWin: Int,
curLoss: Int,
maxWin: Int,
maxLoss: Int) {
def agg(pov: Pov) = copy(
win = (~pov.win && lastRes.contains(true)).fold(win + 1, win),
loss = (~pov.loss && lastRes.contains(false)).fold(loss + 1, loss),
lastRes = pov.win)
curWin = (~pov.win).fold(curWin + 1, 0),
curLoss = (~pov.loss).fold(curLoss + 1, 0)).setMax
private def setMax = copy(
maxWin = curWin max maxWin,
maxLoss = curLoss max maxLoss)
}
case class PlayStreak(
@ -79,22 +87,26 @@ case class Count(
win: Int,
loss: Int,
draw: Int,
opAvg: Double) {
opAvg: Avg) {
def apply(pov: Pov) = copy(
all = all + 1,
rated = rated + pov.game.rated.fold(1, 0),
win = win + pov.win.contains(true).fold(1, 0),
loss = loss + pov.win.contains(false).fold(1, 0),
draw = draw + pov.win.isEmpty.fold(1, 0),
opAvg = pov.opponent.stableRating.fold(opAvg) { r =>
(opAvg * all / (all + 1)) + (r * 1 / (all + 1))
})
opAvg = pov.opponent.stableRating.fold(opAvg)(opAvg.agg))
}
case class Avg(avg: Double, pop: Int) {
def agg(v: Int) = copy(
avg = ((avg * pop) + v) / (pop + 1),
pop = pop + 1)
}
case class RatingAt(int: Int, at: DateTime, gameId: String)
object RatingAt {
def agg(cur: Option[RatingAt], pov: Pov, comp: Int) =
pov.player.ratingAfter.filter { r =>
pov.player.stableRatingAfter.filter { r =>
cur.fold(true) { c => r.compare(c.int) == comp }
}.map {
RatingAt(_, pov.game.updatedAtOrCreatedAt, pov.game.id)

View File

@ -1,9 +1,55 @@
package lila.perfStat
import lila.user.User
import akka.actor.ActorRef
import play.api.libs.iteratee._
import lila.db.api._
import lila.db.Implicits._
import lila.game.{ Game, Pov, Query }
import lila.hub.Sequencer
import lila.rating.PerfType
import lila.user.User
private final class PerfStatIndexer(api: PerfStatApi) {
final class PerfStatIndexer(storage: PerfStatStorage, sequencer: ActorRef) {
def userPerf(user: User, perfType: PerfType): Funit = ???
private implicit val timeout = makeTimeout minutes 2
def userPerf(user: User, perfType: PerfType): Funit = {
val p = scala.concurrent.Promise[Unit]()
sequencer ! Sequencer.work(compute(user, perfType), p.some)
p.future
}
private def compute(user: User, perfType: PerfType): Funit = {
import lila.game.tube.gameTube
import lila.game.BSONHandlers.gameBSONHandler
pimpQB($query {
Query.user(user.id) ++
Query.finished ++
Query.turnsMoreThan(2) ++
Query.variant(PerfType variantOf perfType)
}).sort(Query.sortChronological)
.cursor[Game]()
.enumerate(Int.MaxValue, stopOnError = true) |>>>
Iteratee.fold[Game, PerfStat](PerfStat.init(user.id, perfType)) {
case (perfStat, game) if game.perfType.contains(perfType) =>
Pov.ofUserId(game, user.id).fold(perfStat)(perfStat.agg)
case (perfStat, _) => perfStat
}
} flatMap storage.insert
def addGame(game: Game): Funit = game.players.flatMap { player =>
player.userId.map { userId =>
addPov(Pov(game, player), userId)
}
}.sequenceFu.void
private def addPov(pov: Pov, userId: String): Funit = pov.game.perfType ?? { perfType =>
storage.find(userId, perfType) flatMap {
_ ?? { perfStat =>
storage.update(perfStat agg pov)
}
}
}
}

View File

@ -9,7 +9,7 @@ import lila.db.BSON._
import lila.db.Types.Coll
import lila.rating.PerfType
final class PerfStatApi(coll: Coll) {
final class PerfStatStorage(coll: Coll) {
import lila.db.BSON.BSONJodaDateTimeHandler
import reactivemongo.bson.Macros
@ -21,6 +21,16 @@ final class PerfStatApi(coll: Coll) {
private implicit val ResultBSONHandler = Macros.handler[Result]
private implicit val PlayStreakBSONHandler = Macros.handler[PlayStreak]
private implicit val ResultStreakBSONHandler = Macros.handler[ResultStreak]
private implicit val AvgBSONHandler = Macros.handler[Avg]
private implicit val CountBSONHandler = Macros.handler[Count]
private implicit val PerfStatBSONHandler = Macros.handler[PerfStat]
def find(userId: String, perfType: PerfType): Fu[Option[PerfStat]] =
coll.find(BSONDocument("_id" -> PerfStat.makeId(userId, perfType))).one[PerfStat]
def update(perfStat: PerfStat): Funit =
coll.update(BSONDocument("_id" -> perfStat.id), perfStat).void
def insert(perfStat: PerfStat): Funit =
coll.insert(perfStat).void
}

View File

@ -113,4 +113,14 @@ object PerfType {
val nonPuzzleIconByName = nonPuzzle.map { pt =>
pt.name -> pt.iconString
} toMap
def variantOf(pt: PerfType): chess.variant.Variant = pt match {
case Chess960 => chess.variant.Chess960
case KingOfTheHill => chess.variant.KingOfTheHill
case ThreeCheck => chess.variant.ThreeCheck
case Antichess => chess.variant.Antichess
case Atomic => chess.variant.Atomic
case Horde => chess.variant.Horde
case _ => chess.variant.Standard
}
}