lila/modules/tournament/src/main/JsonView.scala

374 lines
12 KiB
Scala

package lila.tournament
import org.joda.time.DateTime
import org.joda.time.format.ISODateTimeFormat
import play.api.libs.json._
import scala.concurrent.duration._
import lila.common.LightUser
import lila.common.PimpedJson._
import lila.game.{ GameRepo, Pov }
import lila.quote.Quote.quoteWriter
import lila.rating.PerfType
import lila.user.User
final class JsonView(
lightUserApi: lila.user.LightUserApi,
cached: Cached,
performance: Performance,
statsApi: TournamentStatsApi,
asyncCache: lila.memo.AsyncCache.Builder,
verify: Condition.Verify
) {
import JsonView._
import Condition.JSONHandlers._
private case class CachableData(
pairings: JsArray,
featured: Option[JsObject],
podium: Option[JsArray],
next: Option[JsObject]
)
def apply(
tour: Tournament,
page: Option[Int],
me: Option[User],
playerInfoExt: Option[PlayerInfoExt],
socketVersion: Option[Int]
): Fu[JsObject] = for {
data <- cachableData get tour.id
myInfo <- me ?? { u => PlayerRepo.playerInfo(tour.id, u.id) }
stand <- (myInfo, page) match {
case (_, Some(p)) => standing(tour, p)
case (Some(i), _) => standing(tour, i.page)
case _ => standing(tour, 1)
}
playerInfoJson <- playerInfoExt ?? { pie =>
playerInfo(pie).map(_.some)
}
verdicts <- me match {
case None => fuccess(tour.conditions.accepted)
case Some(user) if myInfo.isDefined => fuccess(tour.conditions.accepted)
case Some(user) => verify(tour.conditions, user)
}
stats <- statsApi(tour)
} yield Json.obj(
"id" -> tour.id,
"createdBy" -> tour.createdBy,
"system" -> tour.system.toString.toLowerCase,
"fullName" -> tour.fullName,
"perf" -> tour.perfType,
"nbPlayers" -> tour.nbPlayers,
"minutes" -> tour.minutes,
"clock" -> tour.clock,
"verdicts" -> verdicts,
"variant" -> tour.variant.key,
"isStarted" -> tour.isStarted,
"isFinished" -> tour.isFinished,
"startsAt" -> formatDate(tour.startsAt),
"pairings" -> data.pairings,
"standing" -> stand,
"socketVersion" -> socketVersion
).add("greatPlayer" -> GreatPlayer.wikiUrl(tour.name).map { url =>
Json.obj("name" -> tour.name, "url" -> url)
}).add("position" -> tour.position.some.filterNot(_.initial).map(positionJson))
.add("schedule" -> tour.schedule.map(scheduleJson))
.add("private" -> tour.`private`)
.add("isRecentlyFinished" -> tour.isRecentlyFinished)
.add("secondsToFinish" -> tour.isStarted.option(tour.secondsToFinish))
.add("secondsToStart" -> tour.isCreated.option(tour.secondsToStart))
.add("me" -> myInfo.map(myInfoJson(me)))
.add("featured" -> data.featured)
.add("podium" -> data.podium)
.add("playerInfo" -> playerInfoJson)
.add("quote" -> tour.isCreated.option(lila.quote.Quote.one(tour.id)))
.add("spotlight" -> tour.spotlight)
.add("pairingsClosed" -> tour.pairingsClosed)
.add("stats" -> stats)
.add("next" -> data.next)
def standing(tour: Tournament, page: Int): Fu[JsObject] =
if (page == 1) firstPageCache get tour.id
else computeStanding(tour, page)
def clearCache(id: String) = {
firstPageCache invalidate id
cachableData invalidate id
}
def playerInfo(info: PlayerInfoExt): Fu[JsObject] = for {
ranking <- cached ranking info.tour
sheet <- cached.sheet(info.tour, info.user.id)
tpr <- performance(info.tour, info.player, sheet)
} yield info match {
case PlayerInfoExt(tour, user, player, povs) =>
val isPlaying = povs.headOption.??(_.game.playable)
val povScores: List[(Pov, Option[Score])] = povs zip {
(isPlaying ?? List(none[Score])) ::: sheet.scores.map(some)
}
Json.obj(
"player" -> Json.obj(
"id" -> user.id,
"name" -> user.username,
"rating" -> player.rating,
"score" -> player.score,
"ratingDiff" -> player.ratingDiff,
"fire" -> player.fire,
"nb" -> sheetNbs(user.id, sheet)
).add("title" -> user.title)
.add("rank" -> ranking.get(user.id).map(1+))
.add("provisional" -> player.provisional)
.add("withdraw" -> player.withdraw)
.add("performance" -> tpr),
"pairings" -> povScores.map {
case (pov, score) => Json.obj(
"id" -> pov.gameId,
"color" -> pov.color.name,
"op" -> gameUserJson(pov.opponent.userId, pov.opponent.rating),
"win" -> pov.win,
"status" -> pov.game.status.id,
"score" -> score.map(sheetScoreJson)
).add("berserk" -> pov.player.berserk)
}
)
}
private def fetchFeaturedGame(tour: Tournament): Fu[Option[FeaturedGame]] =
tour.featuredId.ifTrue(tour.isStarted) ?? PairingRepo.byId flatMap {
_ ?? { pairing =>
GameRepo game pairing.gameId flatMap {
_ ?? { game =>
cached ranking tour flatMap { ranking =>
PlayerRepo.pairByTourAndUserIds(tour.id, pairing.user1, pairing.user2) map { pairOption =>
for {
(p1, p2) <- pairOption
rp1 <- RankedPlayer(ranking)(p1)
rp2 <- RankedPlayer(ranking)(p2)
} yield FeaturedGame(game, rp1, rp2)
}
}
}
}
}
}
private def sheetNbs(userId: String, sheet: ScoreSheet) = sheet match {
case s: arena.ScoringSystem.Sheet => Json.obj(
"game" -> s.scores.size,
"berserk" -> s.scores.count(_.isBerserk),
"win" -> s.scores.count(_.res == arena.ScoringSystem.ResWin)
)
}
private def computeStanding(tour: Tournament, page: Int): Fu[JsObject] = for {
rankedPlayers <- PlayerRepo.bestByTourWithRankByPage(tour.id, 10, page max 1)
sheets <- rankedPlayers.map { p =>
cached.sheet(tour, p.player.userId) map { sheet =>
p.player.userId -> sheet
}
}.sequenceFu.map(_.toMap)
players <- rankedPlayers.map(playerJson(sheets, tour)).sequenceFu
} yield Json.obj(
"page" -> page,
"players" -> players
)
private val firstPageCache = asyncCache.clearable[String, JsObject](
name = "tournament.firstPage",
id => TournamentRepo byId id flatten s"No such tournament: $id" flatMap { computeStanding(_, 1) },
expireAfter = _.ExpireAfterWrite(1 second)
)
private val cachableData = asyncCache.clearable[String, CachableData](
name = "tournament.json.cachable",
id =>
for {
pairings <- PairingRepo.recentByTour(id, 40)
jsonPairings <- pairings.map(pairingJson).sequenceFu
tour <- TournamentRepo byId id
featured <- tour ?? fetchFeaturedGame
podium <- tour.??(_.isFinished) ?? podiumJsonCache.get(id)
next <- tour.filter(_.isFinished) ?? cached.findNext map2 nextJson
} yield CachableData(
pairings = JsArray(jsonPairings),
featured = featured map featuredJson,
podium = podium,
next = next
),
expireAfter = _.ExpireAfterWrite(1 second)
)
private def nextJson(tour: Tournament) = Json.obj(
"id" -> tour.id,
"name" -> tour.fullName,
"perf" -> tour.perfType,
"nbPlayers" -> tour.nbPlayers,
"finishesAt" -> tour.isStarted.option(tour.finishesAt).map(formatDate),
"startsAt" -> tour.isCreated.option(tour.startsAt).map(formatDate)
)
private def featuredJson(featured: FeaturedGame) = {
val game = featured.game
def ofPlayer(rp: RankedPlayer, p: lila.game.Player) = {
val light = lightUserApi sync rp.player.userId
Json.obj(
"rank" -> rp.rank,
"name" -> light.fold(rp.player.userId)(_.name),
"rating" -> rp.player.rating,
"ratingDiff" -> rp.player.ratingDiff
).add("title" -> light.flatMap(_.title))
.add("berserk" -> p.berserk)
}
Json.obj(
"id" -> game.id,
"fen" -> (chess.format.Forsyth exportBoard game.toChess.board),
"color" -> (game.variant match {
case chess.variant.RacingKings => chess.White
case _ => game.firstColor
}).name,
"lastMove" -> ~game.castleLastMoveTime.lastMoveString,
"white" -> ofPlayer(featured.white, game player chess.White),
"black" -> ofPlayer(featured.black, game player chess.Black)
)
}
private def myInfoJson(u: Option[User])(i: PlayerInfo) = Json.obj(
"rank" -> i.rank,
"withdraw" -> i.withdraw,
"username" -> u.map(_.titleUsername)
)
private def gameUserJson(userId: Option[String], rating: Option[Int]): JsObject = {
val light = userId flatMap lightUserApi.sync
Json.obj("rating" -> rating)
.add("name" -> light.map(_.name))
.add("title" -> light.flatMap(_.title))
}
private def sheetJson(sheet: ScoreSheet) = sheet match {
case s: arena.ScoringSystem.Sheet => Json.obj(
"scores" -> s.scores.reverse.map(arenaSheetScoreJson),
"total" -> s.total
).add("fire" -> s.onFire)
}
private def arenaSheetScoreJson(score: arena.ScoringSystem.Score) =
if (score.flag == arena.ScoringSystem.Normal) JsNumber(score.value)
else Json.arr(score.value, score.flag.id)
private def sheetScoreJson(score: Score) = score match {
case s: arena.ScoringSystem.Score => arenaSheetScoreJson(s)
case s => JsNumber(score.value)
}
private def playerJson(sheets: Map[String, ScoreSheet], tour: Tournament)(rankedPlayer: RankedPlayer): Fu[JsObject] =
playerJson(sheets get rankedPlayer.player.userId, tour, rankedPlayer)
private def playerJson(sheet: Option[ScoreSheet], tour: Tournament, rankedPlayer: RankedPlayer): Fu[JsObject] = {
val p = rankedPlayer.player
lightUserApi async p.userId map { light =>
Json.obj(
"name" -> light.fold(p.userId)(_.name),
"rank" -> rankedPlayer.rank,
"rating" -> p.rating,
"score" -> p.score,
"ratingDiff" -> p.ratingDiff,
"sheet" -> sheet.map(sheetJson)
).add("title" -> light.flatMap(_.title))
.add("provisional" -> p.provisional)
.add("withdraw" -> p.withdraw)
}
}
private val podiumJsonCache = asyncCache.multi[Tournament.ID, Option[JsArray]](
name = "tournament.podiumJson",
id => TournamentRepo finishedById id flatMap {
_ ?? { tour =>
PlayerRepo.bestByTourWithRank(id, 3).flatMap {
_.map {
case rp @ RankedPlayer(_, player) => for {
sheet <- cached.sheet(tour, player.userId)
tpr <- performance(tour, player, sheet)
json <- playerJson(sheet.some, tour, rp)
} yield json ++ Json.obj(
"nb" -> sheetNbs(player.userId, sheet),
"performance" -> tpr
)
}.sequenceFu
} map { l => JsArray(l).some }
}
},
expireAfter = _.ExpireAfterWrite(10 seconds)
)
private def pairingUserJson(userId: String): Fu[String] =
lightUserApi.async(userId).map(_.fold(userId)(_.name))
private def pairingJson(p: Pairing): Fu[JsObject] = for {
u1 <- pairingUserJson(p.user1)
u2 <- pairingUserJson(p.user2)
} yield Json.obj(
"id" -> p.gameId,
"u" -> Json.arr(u1, u2),
"s" -> (if (p.finished) p.winner match {
case Some(w) if w == p.user1 => 2
case Some(w) => 3
case _ => 1
}
else 0)
)
}
object JsonView {
def top(t: TournamentTop, getLightUser: LightUser.GetterSync): JsArray = JsArray {
t.value.map { p =>
val light = getLightUser(p.userId)
Json.obj(
"n" -> light.fold(p.userId)(_.name),
"s" -> p.score
).add("t" -> light.flatMap(_.title))
.add("f" -> p.fire)
.add("w" -> p.withdraw)
}
}
private def formatDate(date: DateTime) = ISODateTimeFormat.dateTime print date
private[tournament] def scheduleJson(s: Schedule) = Json.obj(
"freq" -> s.freq.name,
"speed" -> s.speed.name
)
implicit val clockWrites: OWrites[chess.Clock.Config] = OWrites { clock =>
Json.obj(
"limit" -> clock.limitSeconds,
"increment" -> clock.incrementSeconds
)
}
private[tournament] def positionJson(s: chess.StartingPosition) = Json.obj(
"eco" -> s.eco,
"name" -> s.name,
"wikiPath" -> s.wikiPath,
"fen" -> s.fen
)
private[tournament] implicit val spotlightWrites: OWrites[Spotlight] = OWrites { s =>
Json.obj()
.add("iconImg" -> s.iconImg)
.add("iconFont" -> s.iconFont)
}
private[tournament] implicit val perfTypeWrites: OWrites[PerfType] = OWrites { pt =>
Json.obj(
"icon" -> pt.iconChar.toString,
"name" -> pt.name
)
}
private[tournament] implicit val statsWrites: Writes[TournamentStats] = Json.writes[TournamentStats]
}