more perf stat wip
parent
51f93f3896
commit
b9f62b3321
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -269,6 +269,9 @@ playban {
|
|||
worldMap {
|
||||
geoip = ${geoip}
|
||||
}
|
||||
perfStat {
|
||||
collection.perf_stat = "perf_stat"
|
||||
}
|
||||
push {
|
||||
collection.device = push_device
|
||||
google {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -90,6 +90,8 @@ case class Player(
|
|||
def ratingAfter = rating map (_ + ~ratingDiff)
|
||||
|
||||
def stableRating = rating ifFalse provisional
|
||||
|
||||
def stableRatingAfter = stableRating map (_ + ~ratingDiff)
|
||||
}
|
||||
|
||||
object Player {
|
||||
|
|
|
@ -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]()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue