lila/modules/swiss/src/main/SwissJson.scala

332 lines
10 KiB
Scala

package lila.swiss
import org.joda.time.DateTime
import org.joda.time.format.ISODateTimeFormat
import play.api.i18n.Lang
import play.api.libs.json._
import scala.concurrent.ExecutionContext
import lila.common.{ GreatPlayer, LightUser }
import lila.db.dsl._
import lila.game.Game
import lila.quote.Quote.quoteWriter
import lila.socket.Socket.SocketVersion
import lila.user.{ User, UserRepo }
final class SwissJson(
colls: SwissColls,
standingApi: SwissStandingApi,
rankingApi: SwissRankingApi,
boardApi: SwissBoardApi,
statsApi: SwissStatsApi,
userRepo: UserRepo,
lightUserApi: lila.user.LightUserApi
)(implicit ec: ExecutionContext) {
import SwissJson._
import BsonHandlers._
def api(swiss: Swiss) =
swissJsonBase(swiss) ++ Json.obj(
"rated" -> swiss.settings.rated
)
def apply(
swiss: Swiss,
me: Option[User],
isInTeam: Boolean,
verdicts: SwissCondition.All.WithVerdicts,
reqPage: Option[Int] = None, // None = focus on me
socketVersion: Option[SocketVersion] = None,
playerInfo: Option[SwissPlayer.ViewExt] = None
)(implicit lang: Lang): Fu[JsObject] = {
for {
myInfo <- me.?? { fetchMyInfo(swiss, _) }
page = reqPage orElse myInfo.map(_.page) getOrElse 1
standing <- standingApi(swiss, page)
podium <- podiumJson(swiss)
boards <- boardApi(swiss.id)
stats <- statsApi(swiss)
} yield swissJsonBase(swiss) ++ Json
.obj(
"canJoin" -> {
{
(swiss.isNotFinished && myInfo.exists(_.player.absent)) ||
(myInfo.isEmpty && swiss.isEnterable)
} && verdicts.accepted && isInTeam
},
"standing" -> standing,
"boards" -> boards.map(boardJson)
)
.add("me" -> myInfo.map(myInfoJson))
.add("joinTeam" -> (!isInTeam).option(swiss.teamId))
.add("socketVersion" -> socketVersion.map(_.value))
.add("playerInfo" -> playerInfo.map { playerJsonExt(swiss, _) })
.add("podium" -> podium)
.add("isRecentlyFinished" -> swiss.isRecentlyFinished)
.add("password" -> swiss.settings.password.isDefined)
.add("stats" -> stats)
.add("greatPlayer" -> GreatPlayer.wikiUrl(swiss.name).map { url =>
Json.obj("name" -> swiss.name, "url" -> url)
})
}.monSuccess(_.swiss.json)
def fetchMyInfo(swiss: Swiss, me: User): Fu[Option[MyInfo]] =
colls.player.byId[SwissPlayer](SwissPlayer.makeId(swiss.id, me.id).value) flatMap {
_ ?? { player =>
updatePlayerRating(swiss, player, me) >>
SwissPairing.fields { f =>
(swiss.nbOngoing > 0)
.?? {
colls.pairing
.find(
$doc(f.swissId -> swiss.id, f.players -> player.userId, f.status -> SwissPairing.ongoing),
$doc(f.id -> true).some
)
.one[Bdoc]
.dmap { _.flatMap(_.getAsOpt[Game.ID](f.id)) }
}
.flatMap { gameId =>
rankingApi(swiss).dmap(_ get player.userId) map2 { rank =>
MyInfo(rank, gameId, me, player)
}
}
}
}
}
private def updatePlayerRating(swiss: Swiss, player: SwissPlayer, user: User): Funit =
swiss.settings.rated
.option(user perfs swiss.perfType)
.filter(_.intRating != player.rating)
.?? { perf =>
SwissPlayer.fields { f =>
colls.player.update
.one(
$id(SwissPlayer.makeId(swiss.id, user.id)),
$set(f.rating -> perf.intRating)
)
.void
}
}
private def podiumJson(swiss: Swiss): Fu[Option[JsArray]] =
swiss.isFinished ?? {
SwissPlayer.fields { f =>
colls.player
.find($doc(f.swissId -> swiss.id))
.sort($sort desc f.score)
.cursor[SwissPlayer]()
.list(3) flatMap { top3 =>
// check that the winner is still correctly denormalized
top3.headOption
.map(_.userId)
.filter(w => swiss.winnerId.fold(true)(w !=))
.foreach {
colls.swiss.updateField($id(swiss.id), "winnerId", _).void
}
.unit
userRepo.filterLame(top3.map(_.userId)) map { lame =>
JsArray(
top3.map { player =>
playerJsonBase(
player,
lightUserApi.sync(player.userId) | LightUser.fallback(player.userId),
performance = true
).add("lame", lame(player.userId))
}
).some
}
}
}
}
def playerResult(p: SwissPlayer.WithUserAndRank): JsObject = p match {
case SwissPlayer.WithUserAndRank(player, user, rank) =>
Json
.obj(
"rank" -> rank,
"points" -> player.points.value,
"tieBreak" -> player.tieBreak.value,
"rating" -> player.rating,
"username" -> user.name
)
.add("title" -> user.title)
.add("performance" -> player.performance)
.add("absent" -> player.absent)
}
}
object SwissJson {
private def formatDate(date: DateTime) = ISODateTimeFormat.dateTime print date
private def swissJsonBase(swiss: Swiss) =
Json
.obj(
"id" -> swiss.id.value,
"createdBy" -> swiss.createdBy,
"startsAt" -> formatDate(swiss.startsAt),
"name" -> swiss.name,
"clock" -> swiss.clock,
"variant" -> swiss.variant.key,
"round" -> swiss.round,
"nbRounds" -> swiss.settings.nbRounds,
"nbPlayers" -> swiss.nbPlayers,
"nbOngoing" -> swiss.nbOngoing,
"status" -> {
if (swiss.isStarted) "started"
else if (swiss.isFinished) "finished"
else "created"
}
)
.add("quote" -> swiss.isCreated.option(lila.quote.Quote.one(swiss.id.value)))
.add("nextRound" -> swiss.nextRoundAt.map { next =>
Json.obj(
"at" -> formatDate(next),
"in" -> (next.getSeconds - nowSeconds).toInt.atLeast(0)
)
})
private[swiss] def playerJson(swiss: Swiss, view: SwissPlayer.View): JsObject =
playerJsonBase(view, performance = false) ++ Json
.obj(
"sheetMin" -> swiss.allRounds
.map(view.pairings.get)
.zip(view.sheet.outcomes)
.map {
pairingJsonOrOutcome(view.player)
}
.mkString("|")
)
def playerJsonExt(swiss: Swiss, view: SwissPlayer.ViewExt): JsObject =
playerJsonBase(view, performance = true) ++ Json.obj(
"sheet" -> swiss.allRounds
.zip(view.sheet.outcomes)
.reverse
.map { case (round, outcome) =>
view.pairings.get(round).fold[JsValue](JsString(outcomeJson(outcome))) { p =>
pairingJson(view.player, p.pairing) ++
Json.obj(
"user" -> p.player.user,
"rating" -> p.player.player.rating
)
}
}
)
private def playerJsonBase(
view: SwissPlayer.Viewish,
performance: Boolean
): JsObject =
playerJsonBase(view.player, view.user, performance) ++
Json.obj("rank" -> view.rank)
private def playerJsonBase(
p: SwissPlayer,
user: LightUser,
performance: Boolean
): JsObject =
Json
.obj(
"user" -> user,
"rating" -> p.rating,
"points" -> p.points,
"tieBreak" -> p.tieBreak
)
.add("performance" -> (performance ?? p.performance))
.add("provisional" -> p.provisional)
.add("absent" -> p.absent)
private def outcomeJson(outcome: SwissSheet.Outcome): String =
outcome match {
case SwissSheet.Absent => "absent"
case SwissSheet.Late => "late"
case SwissSheet.Bye => "bye"
case _ => ""
}
private def pairingJsonMin(player: SwissPlayer, pairing: SwissPairing): String = {
val status =
if (pairing.isOngoing) "o"
else pairing.resultFor(player.userId).fold("d") { r => if (r) "w" else "l" }
s"${pairing.gameId}$status"
}
private def pairingJson(player: SwissPlayer, pairing: SwissPairing) =
Json
.obj(
"g" -> pairing.gameId
)
.add("o" -> pairing.isOngoing)
.add("w" -> pairing.resultFor(player.userId))
.add("c" -> (pairing.white == player.userId))
private def pairingJsonOrOutcome(
player: SwissPlayer
): ((Option[SwissPairing], SwissSheet.Outcome)) => String = {
case (Some(pairing), _) => pairingJsonMin(player, pairing)
case (_, outcome) => outcomeJson(outcome)
}
private def myInfoJson(i: MyInfo) =
Json
.obj(
"rank" -> i.rank,
"gameId" -> i.gameId,
"id" -> i.user.id,
"name" -> i.user.username,
"absent" -> i.player.absent
)
private[swiss] def boardJson(b: SwissBoard.WithGame) =
Json
.obj(
"id" -> b.game.id,
"fen" -> chess.format.Forsyth.boardAndColor(b.game.situation),
"lastMove" -> ~b.game.lastMoveKeys,
"orientation" -> b.game.naturalOrientation.name,
"white" -> boardPlayerJson(b.board.white),
"black" -> boardPlayerJson(b.board.black)
)
.add(
"clock" -> b.game.clock.ifTrue(b.game.isBeingPlayed).map { c =>
Json.obj(
"white" -> c.remainingTime(chess.White).roundSeconds,
"black" -> c.remainingTime(chess.Black).roundSeconds
)
}
)
.add("winner" -> b.game.winnerColor.map(_.name))
private def boardPlayerJson(player: SwissBoard.Player) =
Json.obj(
"rank" -> player.rank,
"rating" -> player.rating,
"user" -> player.user
)
implicit private val roundNumberWriter: Writes[SwissRound.Number] = Writes[SwissRound.Number] { n =>
JsNumber(n.value)
}
implicit private val pointsWriter: Writes[Swiss.Points] = Writes[Swiss.Points] { p =>
JsNumber(p.value)
}
implicit private val tieBreakWriter: Writes[Swiss.TieBreak] = Writes[Swiss.TieBreak] { t =>
JsNumber(t.value)
}
implicit private val performanceWriter: Writes[Swiss.Performance] = Writes[Swiss.Performance] { t =>
JsNumber(t.value.toInt)
}
implicit private val clockWrites: OWrites[chess.Clock.Config] = OWrites { clock =>
Json.obj(
"limit" -> clock.limitSeconds,
"increment" -> clock.incrementSeconds
)
}
implicit private val statsWrites: Writes[SwissStats] = Json.writes[SwissStats]
}