229 lines
6.2 KiB
Scala
229 lines
6.2 KiB
Scala
package lila.perfStat
|
|
|
|
import chess.Color
|
|
import org.joda.time.{ DateTime, Period }
|
|
|
|
import lila.common.Heapsort
|
|
import lila.game.Pov
|
|
import lila.rating.PerfType
|
|
|
|
case class PerfStat(
|
|
_id: String, // userId/perfId
|
|
userId: UserId,
|
|
perfType: PerfType,
|
|
highest: Option[RatingAt],
|
|
lowest: Option[RatingAt],
|
|
bestWins: Results,
|
|
worstLosses: Results,
|
|
count: Count,
|
|
resultStreak: ResultStreak,
|
|
playStreak: PlayStreak
|
|
) {
|
|
|
|
def id = _id
|
|
|
|
def agg(pov: Pov) =
|
|
if (!pov.game.finished) this
|
|
else {
|
|
val thisYear = pov.game.createdAt isAfter DateTime.now.minusYears(1)
|
|
copy(
|
|
highest = RatingAt.agg(highest, pov, 1),
|
|
lowest = if (thisYear) RatingAt.agg(lowest, pov, -1) else lowest,
|
|
bestWins = if (~pov.win) bestWins.agg(pov, 1) else bestWins,
|
|
worstLosses = if (thisYear && ~pov.loss) worstLosses.agg(pov, -1) else worstLosses,
|
|
count = count(pov),
|
|
resultStreak = resultStreak agg pov,
|
|
playStreak = playStreak agg pov
|
|
)
|
|
}
|
|
|
|
def userIds = bestWins.userIds ::: worstLosses.userIds
|
|
}
|
|
|
|
object PerfStat {
|
|
|
|
type Getter = (lila.user.User, PerfType) => Fu[PerfStat]
|
|
|
|
def makeId(userId: String, perfType: PerfType) = s"$userId/${perfType.id}"
|
|
|
|
def init(userId: String, perfType: PerfType) =
|
|
PerfStat(
|
|
_id = makeId(userId, perfType),
|
|
userId = UserId(userId),
|
|
perfType = perfType,
|
|
highest = none,
|
|
lowest = none,
|
|
bestWins = Results(Nil),
|
|
worstLosses = Results(Nil),
|
|
count = Count.init,
|
|
resultStreak = ResultStreak(win = Streaks.init, loss = Streaks.init),
|
|
playStreak = PlayStreak(nb = Streaks.init, time = Streaks.init, lastDate = none)
|
|
)
|
|
}
|
|
|
|
case class ResultStreak(win: Streaks, loss: Streaks) {
|
|
def agg(pov: Pov) =
|
|
copy(
|
|
win = win.continueOrReset(~pov.win, pov)(1),
|
|
loss = loss.continueOrReset(~pov.loss, pov)(1)
|
|
)
|
|
}
|
|
|
|
case class PlayStreak(nb: Streaks, time: Streaks, lastDate: Option[DateTime]) {
|
|
def agg(pov: Pov) =
|
|
pov.game.durationSeconds.fold(this) { seconds =>
|
|
val cont = seconds < 3 * 60 * 60 && isContinued(pov.game.createdAt)
|
|
copy(
|
|
nb = nb.continueOrStart(cont, pov)(1),
|
|
time = time.continueOrStart(cont, pov)(seconds),
|
|
lastDate = pov.game.movedAt.some
|
|
)
|
|
}
|
|
def checkCurrent =
|
|
if (isContinued(DateTime.now)) this
|
|
else copy(nb = nb.reset, time = time.reset)
|
|
private def isContinued(at: DateTime) =
|
|
lastDate.fold(true) { ld =>
|
|
at.isBefore(ld plusMinutes PlayStreak.expirationMinutes)
|
|
}
|
|
}
|
|
object PlayStreak {
|
|
val expirationMinutes = 60
|
|
}
|
|
|
|
case class Streaks(cur: Streak, max: Streak) {
|
|
def continueOrReset(cont: Boolean, pov: Pov)(v: Int) =
|
|
copy(cur = cur.continueOrReset(cont, pov)(v)).setMax
|
|
def continueOrStart(cont: Boolean, pov: Pov)(v: Int) =
|
|
copy(cur = cur.continueOrStart(cont, pov)(v)).setMax
|
|
def reset = copy(cur = Streak.init)
|
|
private def setMax = copy(max = if (cur.v >= max.v) cur else max)
|
|
}
|
|
object Streaks {
|
|
val init = Streaks(Streak.init, Streak.init)
|
|
}
|
|
case class Streak(v: Int, from: Option[GameAt], to: Option[GameAt]) {
|
|
def continueOrReset(cont: Boolean, pov: Pov)(v: Int) =
|
|
if (cont) inc(pov, v) else Streak.init
|
|
def continueOrStart(cont: Boolean, pov: Pov)(v: Int) =
|
|
if (cont) inc(pov, v)
|
|
else {
|
|
val at = GameAt(pov.game.createdAt, pov.gameId).some
|
|
val end = GameAt(pov.game.movedAt, pov.gameId).some
|
|
Streak(v, at, end)
|
|
}
|
|
private def inc(pov: Pov, by: Int) = {
|
|
val at = GameAt(pov.game.createdAt, pov.gameId).some
|
|
val end = GameAt(pov.game.movedAt, pov.gameId).some
|
|
Streak(v + by, from orElse at, end)
|
|
}
|
|
def period = new Period(v * 1000L)
|
|
}
|
|
object Streak {
|
|
val init = Streak(0, none, none)
|
|
}
|
|
|
|
case class Count(
|
|
all: Int,
|
|
rated: Int,
|
|
win: Int,
|
|
loss: Int,
|
|
draw: Int,
|
|
tour: Int,
|
|
berserk: Int,
|
|
opAvg: Avg,
|
|
seconds: Int,
|
|
disconnects: Int
|
|
) {
|
|
def apply(pov: Pov) =
|
|
copy(
|
|
all = all + 1,
|
|
rated = rated + (if (pov.game.rated) 1 else 0),
|
|
win = win + (if (pov.win.contains(true)) 1 else 0),
|
|
loss = loss + (if (pov.win.contains(false)) 1 else 0),
|
|
draw = draw + (if (pov.win.isEmpty) 1 else 0),
|
|
tour = tour + (if (pov.game.isTournament) 1 else 0),
|
|
berserk = berserk + (if (pov.player.berserk) 1 else 0),
|
|
opAvg = pov.opponent.stableRating.fold(opAvg)(opAvg.agg),
|
|
seconds = seconds + (pov.game.durationSeconds match {
|
|
case Some(s) if s <= 3 * 60 * 60 => s
|
|
case _ => 0
|
|
}),
|
|
disconnects = disconnects + {
|
|
if (~pov.loss && pov.game.status == chess.Status.Timeout) 1 else 0
|
|
}
|
|
)
|
|
def period = new Period(seconds * 1000L)
|
|
}
|
|
object Count {
|
|
val init = Count(
|
|
all = 0,
|
|
rated = 0,
|
|
win = 0,
|
|
loss = 0,
|
|
draw = 0,
|
|
tour = 0,
|
|
berserk = 0,
|
|
opAvg = Avg(0, 0),
|
|
seconds = 0,
|
|
disconnects = 0
|
|
)
|
|
}
|
|
|
|
case class Avg(avg: Double, pop: Int) {
|
|
def agg(v: Int) =
|
|
copy(
|
|
avg = ((avg * pop) + v) / (pop + 1),
|
|
pop = pop + 1
|
|
)
|
|
}
|
|
|
|
case class GameAt(at: DateTime, gameId: String)
|
|
object GameAt {
|
|
def agg(pov: Pov) = GameAt(pov.game.movedAt, pov.gameId)
|
|
}
|
|
|
|
case class RatingAt(int: Int, at: DateTime, gameId: String)
|
|
object RatingAt {
|
|
def agg(cur: Option[RatingAt], pov: Pov, comp: Int) =
|
|
pov.player.stableRatingAfter
|
|
.filter { r =>
|
|
cur.fold(true) { c =>
|
|
r.compare(c.int) == comp
|
|
}
|
|
}
|
|
.map {
|
|
RatingAt(_, pov.game.movedAt, pov.gameId)
|
|
} orElse cur
|
|
}
|
|
|
|
case class Result(opInt: Int, opId: UserId, at: DateTime, gameId: String, color: Color)
|
|
|
|
case class Results(results: List[Result]) extends AnyVal {
|
|
def agg(pov: Pov, comp: Int) =
|
|
pov.opponent.stableRating
|
|
.ifTrue(pov.game.rated)
|
|
.ifTrue(pov.game.bothPlayersHaveMoved)
|
|
.fold(this) { opInt =>
|
|
Results(
|
|
Heapsort.topN(
|
|
Result(
|
|
opInt,
|
|
UserId(~pov.opponent.userId),
|
|
pov.game.movedAt,
|
|
pov.gameId,
|
|
pov.player.color
|
|
) :: results,
|
|
Results.nb,
|
|
Ordering.by[Result, Int](_.opInt * comp)
|
|
)
|
|
)
|
|
}
|
|
def userIds = results.map(_.opId)
|
|
}
|
|
object Results {
|
|
val nb = 5
|
|
}
|
|
|
|
case class UserId(value: String) extends AnyVal
|