coach stats wip
parent
7a4b3b57a1
commit
2f72b1cb1d
|
@ -3,11 +3,120 @@
|
|||
@user.layout(title = s"${u.username} stats") {
|
||||
<div class="content_box no_padding">
|
||||
<h1>@userLink(u, withOnline = false) stats</h1>
|
||||
@stat
|
||||
@if(stat.fold(true)(!_.isFresh)) {
|
||||
<form method="post" action="@routes.Coach.refresh(u.username)">
|
||||
<button type="submit">Refresh @u.username stats</button>
|
||||
</form>
|
||||
}
|
||||
@stat.map { s =>
|
||||
<table class="slist">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan=2>Favourite openings</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
@chess.Color.all.map { color =>
|
||||
<td>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan=2>As @color.name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@s.openings(color).toList.sortBy(-_._2).take(10).map {
|
||||
case (code, nb) => {
|
||||
<tr>
|
||||
<td>@code</td>
|
||||
<td>@nb.localize</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@defining(s.results) { r =>
|
||||
<table class="slist">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan=2>Results over all rated games</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Games</th>
|
||||
<td>@r.nbGames.localize</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Analysed games</th>
|
||||
<td>@r.nbAnalysis.localize</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Wins</th>
|
||||
<td>@r.nbWin.localize</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Losses</th>
|
||||
<td>@r.nbLoss.localize</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Draws</th>
|
||||
<td>@r.nbDraw.localize</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Rating diff</th>
|
||||
<td>@showProgress(r.ratingDiff)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Best Rating</th>
|
||||
<td>@r.bestRating.map { br =>
|
||||
<strong>@br.rating</strong> after <a href="@routes.Round.watcher(br.id, "white")">playing</a> @userIdLink(br.userId.some)
|
||||
}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Best Win</th>
|
||||
<td>@r.bestWin.map { bw =>
|
||||
<a href="@routes.Round.watcher(bw.id, "white")">@userIdSpanMini(bw.userId) <strong>@bw.rating</strong></a>
|
||||
}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Opponent average rating</th>
|
||||
<td>@r.opponentRatingAvg</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Win streak</th>
|
||||
<td>@r.winStreak.best (current: @r.winStreak.cur)</td>
|
||||
</tr>
|
||||
@List("Win" -> r.outcomeStatuses.win, "Loss" -> r.outcomeStatuses.loss).map {
|
||||
case (name, statuses) => {
|
||||
<tr>
|
||||
<th>@name statuses</th>
|
||||
<td>
|
||||
<table>
|
||||
<tbody>
|
||||
@statuses.m.toList.sortBy(-_._2).map {
|
||||
case (status, nb) => {
|
||||
<tr>
|
||||
<th>@status.name</th>
|
||||
<td>@nb.localize</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package lila.coach
|
||||
|
||||
import reactivemongo.bson._
|
||||
import reactivemongo.bson.Macros
|
||||
|
||||
import lila.db.BSON._
|
||||
import lila.db.Implicits._
|
||||
import lila.rating.PerfType
|
||||
|
||||
private[coach] object BSONHandlers {
|
||||
|
||||
import UserStat.PerfResults
|
||||
import Results.{ OutcomeStatuses, StatusScores, BestWin, BestRating, Streak }
|
||||
|
||||
private implicit val intMapHandler = MapValue.MapHandler[Int]
|
||||
|
||||
private implicit val StatusScoresBSONHandler = new BSONHandler[BSONDocument, StatusScores] {
|
||||
def read(doc: BSONDocument): StatusScores = StatusScores {
|
||||
intMapHandler read doc mapKeys { k =>
|
||||
parseIntOption(k) flatMap chess.Status.apply
|
||||
} collect { case (Some(k), v) => k -> v }
|
||||
}
|
||||
def write(x: StatusScores) = intMapHandler write x.m.mapKeys(_.id.toString)
|
||||
}
|
||||
implicit val ResultsStreakBSONHandler = Macros.handler[Streak]
|
||||
implicit val ResultsOutcomeStatusesBSONHandler = Macros.handler[OutcomeStatuses]
|
||||
implicit val OpeningsBSONHandler = Macros.handler[Openings]
|
||||
implicit val ResultsBestWinBSONHandler = Macros.handler[BestWin]
|
||||
implicit val ResultsBestRatingBSONHandler = Macros.handler[BestRating]
|
||||
implicit val ResultsBSONHandler = Macros.handler[Results]
|
||||
|
||||
private implicit val resultsMapHandler = Map.MapHandler[Results]
|
||||
private implicit val PerfResultsBSONHandler = new BSONHandler[BSONDocument, PerfResults] {
|
||||
def read(doc: BSONDocument): PerfResults = PerfResults {
|
||||
resultsMapHandler read doc mapKeys PerfType.apply collect { case (Some(k), v) => k -> v }
|
||||
}
|
||||
def write(x: PerfResults) = resultsMapHandler write x.m.mapKeys(_.key)
|
||||
}
|
||||
implicit val UserStatBSONHandler = Macros.handler[UserStat]
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package lila.coach
|
||||
|
||||
case class Openings(
|
||||
white: Map[String, Int],
|
||||
black: Map[String, Int]) {
|
||||
|
||||
def apply(c: chess.Color) = c.fold(white, black)
|
||||
|
||||
def aggregate(p: lila.game.Pov) = p.game.opening.map(_.code).fold(this) { code =>
|
||||
copy(
|
||||
white = if (p.color.white) openingWithCode(white, code) else white,
|
||||
black = if (p.color.black) openingWithCode(black, code) else black)
|
||||
}
|
||||
|
||||
private def openingWithCode(opening: Map[String, Int], code: String) =
|
||||
opening + (code -> opening.get(code).fold(1)(1+))
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package lila.coach
|
||||
|
||||
import chess.Status
|
||||
import lila.analyse.Analysis
|
||||
import lila.game.Pov
|
||||
import org.joda.time.DateTime
|
||||
|
||||
case class Results(
|
||||
nbGames: Int,
|
||||
nbAnalysis: Int,
|
||||
nbWin: Int,
|
||||
nbLoss: Int,
|
||||
nbDraw: Int,
|
||||
ratingDiff: Int,
|
||||
plySum: Int,
|
||||
acplSum: Int,
|
||||
bestWin: Option[Results.BestWin],
|
||||
bestRating: Option[Results.BestRating],
|
||||
opponentRatingSum: Int,
|
||||
winStreak: Results.Streak, // nb games won in a row
|
||||
awakeMinutesStreak: Results.Streak, // minutes played without sleeping
|
||||
dayStreak: Results.Streak, // days played in a row
|
||||
outcomeStatuses: Results.OutcomeStatuses) {
|
||||
|
||||
def plyAvg = plySum / nbGames
|
||||
def acplAvg = acplSum / nbAnalysis
|
||||
def opponentRatingAvg = opponentRatingSum / nbGames
|
||||
|
||||
def aggregate(pov: Pov, analysis: Option[Analysis]) = copy(
|
||||
nbGames = nbGames + 1,
|
||||
nbAnalysis = nbAnalysis + analysis.isDefined.fold(1, 0),
|
||||
nbWin = nbWin + (~pov.win).fold(1, 0),
|
||||
nbLoss = nbLoss + (~pov.loss).fold(1, 0),
|
||||
nbDraw = nbDraw + pov.game.draw.fold(1, 0),
|
||||
ratingDiff = ratingDiff + ~pov.player.ratingDiff,
|
||||
plySum = plySum + pov.game.turns,
|
||||
acplSum = acplSum + ~analysis.flatMap { lila.analyse.Accuracy(pov, _) },
|
||||
bestWin = if (~pov.win) {
|
||||
Results.makeBestWin(pov).fold(bestWin) { newBest =>
|
||||
bestWin.fold(newBest) { prev =>
|
||||
if (newBest.rating > prev.rating) newBest else prev
|
||||
}.some
|
||||
}
|
||||
}
|
||||
else bestWin,
|
||||
bestRating = if (~pov.win) {
|
||||
Results.makeBestRating(pov).fold(bestRating) { newBest =>
|
||||
bestRating.fold(newBest) { prev =>
|
||||
if (newBest.rating > prev.rating) newBest else prev
|
||||
}.some
|
||||
}
|
||||
}
|
||||
else bestRating,
|
||||
opponentRatingSum = opponentRatingSum + ~pov.opponent.rating,
|
||||
outcomeStatuses = outcomeStatuses.aggregate(pov))
|
||||
}
|
||||
|
||||
object Results {
|
||||
|
||||
val emptyStreak = Streak(0, 0)
|
||||
val emptyOutcomeStatuses = OutcomeStatuses(StatusScores(Map.empty), StatusScores(Map.empty))
|
||||
val empty = Results(0, 0, 0, 0, 0, 0, 0, 0, none, none, 0, emptyStreak, emptyStreak, emptyStreak, emptyOutcomeStatuses)
|
||||
|
||||
case class BestWin(id: String, userId: String, rating: Int)
|
||||
def makeBestWin(pov: Pov): Option[BestWin] = pov.opponent.userId |@| pov.opponent.rating apply {
|
||||
case (opId, opRating) => BestWin(pov.gameId, opId, opRating)
|
||||
}
|
||||
|
||||
case class BestRating(id: String, userId: String, rating: Int)
|
||||
def makeBestRating(pov: Pov): Option[BestRating] = pov.opponent.userId |@| pov.player.ratingAfter apply {
|
||||
case (opId, myRating) => BestRating(pov.gameId, opId, myRating)
|
||||
}
|
||||
|
||||
case class OutcomeStatuses(win: StatusScores, loss: StatusScores) {
|
||||
def aggregate(pov: Pov) = copy(
|
||||
win = if (~pov.win) win add pov.game.status else win,
|
||||
loss = if (~pov.loss) loss add pov.game.status else loss)
|
||||
}
|
||||
|
||||
case class StatusScores(m: Map[Status, Int]) {
|
||||
def add(s: Status) = copy(m = m + (s -> m.get(s).fold(1)(1+)))
|
||||
}
|
||||
|
||||
case class Streak(cur: Int, best: Int) {
|
||||
def add(v: Int) = copy(cur = cur + v, best = best max (cur + v))
|
||||
def reset = copy(cur = 0)
|
||||
def set(v: Int) = copy(cur = v)
|
||||
}
|
||||
|
||||
case class Computation(
|
||||
results: Results,
|
||||
previousWin: Boolean = false,
|
||||
previousEndDate: Option[DateTime] = None) {
|
||||
|
||||
def aggregate(pov: Pov, analysis: Option[Analysis]) = copy(
|
||||
results = results.aggregate(pov, analysis).copy(
|
||||
winStreak = if (~pov.win) {
|
||||
if (previousWin) results.winStreak.add(1)
|
||||
else results.winStreak.set(1)
|
||||
}
|
||||
else results.winStreak.reset,
|
||||
awakeMinutesStreak = results.awakeMinutesStreak,
|
||||
dayStreak = (previousEndDate |@| pov.game.updatedAt) apply {
|
||||
case (prev, next) if prev.getDayOfYear == next.getDayOfYear => results.dayStreak
|
||||
case (prev, next) if next.minusDays(1).isBefore(prev) => results.dayStreak.add(1)
|
||||
} getOrElse results.dayStreak.reset
|
||||
),
|
||||
previousWin = ~pov.win,
|
||||
previousEndDate = pov.game.updatedAt)
|
||||
|
||||
def run = results
|
||||
}
|
||||
val emptyComputation = Computation(empty)
|
||||
}
|
|
@ -14,33 +14,32 @@ import lila.user.UserRepo
|
|||
|
||||
final class StatApi(coll: Coll) {
|
||||
|
||||
private implicit val openingsHandler = MapValue.MapHandler[Int]
|
||||
import UserStat.Openings
|
||||
private implicit val UserStatOpeningsBSONHandler = Macros.handler[Openings]
|
||||
private implicit val UserStatBSONHandler = Macros.handler[UserStat]
|
||||
import BSONHandlers._
|
||||
|
||||
private def selectId(id: String) = BSONDocument("_id" -> id)
|
||||
|
||||
def fetch(id: String): Fu[Option[UserStat]] = coll.find(selectId(id)).one[UserStat]
|
||||
|
||||
def computeIfOld(id: String): Fu[Option[UserStat]] = fetch(id) flatMap {
|
||||
case Some(stat) if stat.isFresh => fuccess(stat.some)
|
||||
def computeIfOld(id: String): Fu[UserStat] = fetch(id) flatMap {
|
||||
case Some(stat) if stat.isFresh => fuccess(stat)
|
||||
case _ => compute(id)
|
||||
}
|
||||
|
||||
private def compute(id: String): Fu[Option[UserStat]] = {
|
||||
private def compute(id: String): Fu[UserStat] = {
|
||||
import lila.game.tube.gameTube
|
||||
import lila.game.BSONHandlers.gameBSONHandler
|
||||
import lila.game.Query
|
||||
pimpQB($query(Query.user(id) ++ Query.rated))
|
||||
pimpQB($query(Query.user(id) ++ Query.rated ++ Query.finished))
|
||||
.sort(Query.sortCreated)
|
||||
.cursor[lila.game.Game]().enumerate(10 * 1000, stopOnError = false) &>
|
||||
StatApi.withAnalysis |>>>
|
||||
Iteratee.fold(UserStat(id)) {
|
||||
case (stat, a) => lila.game.Pov.ofUserId(a.game, id).fold(stat) { stat.withGame(_, a.analysis) }
|
||||
Iteratee.fold(UserStat.makeComputation(id)) {
|
||||
case (comp, a) => lila.game.Pov.ofUserId(a.game, id).fold(comp) {
|
||||
comp.aggregate(_, a.analysis)
|
||||
}
|
||||
}
|
||||
} flatMap { stat =>
|
||||
(stat.nbGames > 0) ?? (coll.update(selectId(id), stat, upsert = true) inject stat.some)
|
||||
} map (_.run) flatMap { stat =>
|
||||
coll.update(selectId(id), stat, upsert = true) inject stat
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,48 +2,58 @@ package lila.coach
|
|||
|
||||
import org.joda.time.DateTime
|
||||
|
||||
import lila.analyse.Analysis
|
||||
import lila.game.Pov
|
||||
import lila.rating.PerfType
|
||||
|
||||
case class UserStat(
|
||||
_id: String, // user ID
|
||||
openings: UserStat.Openings,
|
||||
nbGames: Int,
|
||||
nbAnalysis: Int,
|
||||
openings: Openings,
|
||||
results: Results,
|
||||
perfResults: UserStat.PerfResults,
|
||||
date: DateTime) {
|
||||
|
||||
def id = _id
|
||||
|
||||
def withGame(pov: lila.game.Pov, analysis: Option[lila.analyse.Analysis]) = copy(
|
||||
nbGames = nbGames + 1,
|
||||
nbAnalysis = nbAnalysis + analysis.isDefined.fold(1, 0),
|
||||
openings = openings withGame pov
|
||||
)
|
||||
def aggregate(pov: Pov, analysis: Option[lila.analyse.Analysis]) = copy(
|
||||
openings = openings aggregate pov)
|
||||
|
||||
def isFresh = nbGames < 100 || {
|
||||
def isFresh = results.nbGames < 100 || {
|
||||
DateTime.now minusDays 1 isBefore date
|
||||
}
|
||||
}
|
||||
|
||||
object UserStat {
|
||||
|
||||
type OpeningMap = Map[String, Int]
|
||||
case class PerfResults(m: Map[PerfType, Results])
|
||||
val emptyPerfResults = PerfResults(Map.empty)
|
||||
|
||||
case class Openings(
|
||||
white: OpeningMap,
|
||||
black: OpeningMap) {
|
||||
case class Computation(
|
||||
stat: UserStat,
|
||||
resultsComp: Results.Computation = Results.emptyComputation,
|
||||
perfResultsComp: Map[PerfType, Results.Computation] = Map.empty) {
|
||||
|
||||
def withGame(p: lila.game.Pov) = p.game.opening.map(_.code).fold(this) { code =>
|
||||
copy(
|
||||
white = if (p.color.white) openingWithCode(white, code) else white,
|
||||
black = if (p.color.black) openingWithCode(black, code) else black)
|
||||
}
|
||||
def aggregate(pov: Pov, analysis: Option[Analysis]) = copy(
|
||||
stat = stat.aggregate(pov, analysis),
|
||||
resultsComp = resultsComp.aggregate(pov, analysis),
|
||||
perfResultsComp = pov.game.perfType.fold(perfResultsComp) { perfType =>
|
||||
perfResultsComp + (
|
||||
perfType ->
|
||||
perfResultsComp.get(perfType).|(Results.emptyComputation).aggregate(pov, analysis)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
private def openingWithCode(opening: OpeningMap, code: String) =
|
||||
opening + (code -> opening.get(code).fold(1)(1+))
|
||||
def run = stat.copy(
|
||||
results = resultsComp.run,
|
||||
perfResults = PerfResults(perfResultsComp.mapValues(_.run)))
|
||||
}
|
||||
def makeComputation(id: String) = Computation(apply(id))
|
||||
|
||||
def apply(id: String): UserStat = UserStat(
|
||||
_id = id,
|
||||
openings = Openings(Map.empty, Map.empty),
|
||||
nbGames = 0,
|
||||
nbAnalysis = 0,
|
||||
results = Results.empty,
|
||||
perfResults = emptyPerfResults,
|
||||
date = DateTime.now)
|
||||
}
|
||||
|
|
|
@ -43,6 +43,13 @@ object BSON {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicit def MapHandler[V](implicit vr: BSONDocumentReader[V], vw: BSONDocumentWriter[V]): BSONHandler[BSONDocument, Map[String, V]] = new BSONHandler[BSONDocument, Map[String, V]] {
|
||||
private val reader = MapReader[V]
|
||||
private val writer = MapWriter[V]
|
||||
def read(bson: BSONDocument): Map[String, V] = reader read bson
|
||||
def write(map: Map[String, V]): BSONDocument = writer write map
|
||||
}
|
||||
}
|
||||
|
||||
object MapValue {
|
||||
|
@ -70,6 +77,37 @@ object BSON {
|
|||
}
|
||||
}
|
||||
|
||||
// object MapKeyValue {
|
||||
|
||||
// type K = String
|
||||
|
||||
// implicit def MapReader[V](
|
||||
// implicit kr: BSONReader[_ <: BSONValue, K],
|
||||
// vr: BSONReader[_ <: BSONValue, V]): BSONDocumentReader[Map[K, V]] = new BSONDocumentReader[Map[K, V]] {
|
||||
// def read(bson: BSONDocument): Map[K, V] =
|
||||
// bson.elements.map { tuple =>
|
||||
// kr.asInstanceOf[BSONReader[BSONValue, K]].read(tuple._1) -> vr.asInstanceOf[BSONReader[BSONValue, V]].read(tuple._2)
|
||||
// }.toMap
|
||||
// }
|
||||
|
||||
// implicit def MapWriter[V](
|
||||
// implicit kw: BSONWriter[K, _ <: BSONValue],
|
||||
// vw: BSONWriter[V, _ <: BSONValue]): BSONDocumentWriter[Map[K, V]] = new BSONDocumentWriter[Map[K, V]] {
|
||||
// def write(map: Map[K, V]): BSONDocument = BSONDocument {
|
||||
// map.toStream.map { tuple =>
|
||||
// kw.write(tuple._1) -> vw.write(tuple._2)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// implicit def MapHandler[V](implicit kr: BSONReader[_ <: BSONValue, K], kw: BSONWriter[K, _ <: BSONValue], vr: BSONReader[_ <: BSONValue, V], vw: BSONWriter[V, _ <: BSONValue]): BSONHandler[BSONDocument, Map[K, V]] = new BSONHandler[BSONDocument, Map[K, V]] {
|
||||
// private val reader = MapReader[K, V]
|
||||
// private val writer = MapWriter[K, V]
|
||||
// def read(bson: BSONDocument): Map[K, V] = reader read bson
|
||||
// def write(map: Map[K, V]): BSONDocument = writer write map
|
||||
// }
|
||||
// }
|
||||
|
||||
// List Handler
|
||||
final class ListHandler[T](implicit reader: BSONReader[_ <: BSONValue, T], writer: BSONWriter[T, _ <: BSONValue]) extends BSONHandler[BSONArray, List[T]] {
|
||||
def read(array: BSONArray) = array.stream.filter(_.isSuccess).map { v =>
|
||||
|
|
|
@ -13,6 +13,11 @@ object BSONHandlers {
|
|||
def write(cc: CheckCount) = BSONArray(cc.white, cc.black)
|
||||
}
|
||||
|
||||
implicit val StatusBSONHandler = new BSONHandler[BSONInteger, Status] {
|
||||
def read(bsonInt: BSONInteger): Status = Status(bsonInt.value) err s"No such status: ${bsonInt.value}"
|
||||
def write(x: Status) = BSONInteger(x.id)
|
||||
}
|
||||
|
||||
implicit val gameBSONHandler = new BSON[Game] {
|
||||
|
||||
import Game.BSONFields._
|
||||
|
@ -40,7 +45,7 @@ object BSONHandlers {
|
|||
blackPlayer = player(blackPlayer, Black, blackId, blackUid),
|
||||
binaryPieces = r bytes binaryPieces,
|
||||
binaryPgn = r bytesD binaryPgn,
|
||||
status = Status(r int status) err "game invalid status",
|
||||
status = r.get[Status](status),
|
||||
turns = nbTurns,
|
||||
startedAtTurn = r intD startedAtTurn,
|
||||
clock = r.getO[Color => Clock](clock)(clockBSONHandler(createdAtValue)) map (_(Color(0 == nbTurns % 2))),
|
||||
|
@ -77,7 +82,7 @@ object BSONHandlers {
|
|||
blackPlayer -> w.docO(playerBSONHandler write ((_: Color) => (_: Player.Id) => (_: Player.UserId) => (_: Player.Win) => o.blackPlayer)),
|
||||
binaryPieces -> o.binaryPieces,
|
||||
binaryPgn -> w.byteArrayO(o.binaryPgn),
|
||||
status -> o.status.id,
|
||||
status -> o.status,
|
||||
turns -> o.turns,
|
||||
startedAtTurn -> w.intO(o.startedAtTurn),
|
||||
clock -> (o.clock map { c => clockBSONHandler(o.createdAt).write(_ => c) }),
|
||||
|
|
|
@ -346,6 +346,10 @@ case class Game(
|
|||
|
||||
def wonBy(c: Color): Option[Boolean] = winnerColor map (_ == c)
|
||||
|
||||
def lostBy(c: Color): Option[Boolean] = winnerColor map (_ != c)
|
||||
|
||||
def draw = status == Status.Draw
|
||||
|
||||
def outoftimePlayer: Option[Player] =
|
||||
outoftimePlayerClock orElse outoftimePlayerCorrespondence
|
||||
|
||||
|
@ -372,9 +376,10 @@ case class Game(
|
|||
|
||||
def withClock(c: Clock) = Progress(this, copy(clock = Some(c)))
|
||||
|
||||
def estimateTotalTime =
|
||||
clock.map(_.estimateTotalTime) orElse
|
||||
correspondenceClock.map(_.estimateTotalTime) getOrElse 1200
|
||||
def estimateClockTotalTime = clock.map(_.estimateTotalTime)
|
||||
|
||||
def estimateTotalTime = estimateClockTotalTime orElse
|
||||
correspondenceClock.map(_.estimateTotalTime) getOrElse 1200
|
||||
|
||||
def playerWhoDidNotMove: Option[Player] = playedTurns match {
|
||||
case 0 => player(White).some
|
||||
|
|
|
@ -83,6 +83,8 @@ case class Player(
|
|||
case ((None, _), (Some(_), _)) => false
|
||||
case ((_, a), (_, b)) => a < b
|
||||
}
|
||||
|
||||
def ratingAfter = rating map (_ + ~ratingDiff)
|
||||
}
|
||||
|
||||
object Player {
|
||||
|
|
|
@ -32,6 +32,10 @@ case class Pov(game: Game, color: Color) {
|
|||
|
||||
def hasMoved = game playerHasMoved color
|
||||
|
||||
def win = game wonBy color
|
||||
|
||||
def loss = game lostBy color
|
||||
|
||||
override def toString = ref.toString
|
||||
}
|
||||
|
||||
|
|
|
@ -18,10 +18,7 @@ private[simul] final class SimulRepo(simulColl: Coll) {
|
|||
def read(bsonInt: BSONInteger): SimulStatus = SimulStatus(bsonInt.value) err s"No such simul status: ${bsonInt.value}"
|
||||
def write(x: SimulStatus) = BSONInteger(x.id)
|
||||
}
|
||||
private implicit val ChessStatusBSONHandler = new BSONHandler[BSONInteger, Status] {
|
||||
def read(bsonInt: BSONInteger): Status = Status(bsonInt.value) err s"No such chess status: ${bsonInt.value}"
|
||||
def write(x: Status) = BSONInteger(x.id)
|
||||
}
|
||||
private implicit val ChessStatusBSONHandler = lila.game.BSONHandlers.StatusBSONHandler
|
||||
private implicit val VariantBSONHandler = new BSONHandler[BSONInteger, Variant] {
|
||||
def read(bsonInt: BSONInteger): Variant = Variant(bsonInt.value) err s"No such variant: ${bsonInt.value}"
|
||||
def write(x: Variant) = BSONInteger(x.id)
|
||||
|
|
Loading…
Reference in New Issue