297 lines
12 KiB
Scala
297 lines
12 KiB
Scala
package lila.round
|
|
|
|
import actorApi.SocketStatus
|
|
import chess.format.{ FEN, Forsyth }
|
|
import chess.{ Clock, Color }
|
|
import play.api.libs.json._
|
|
import scala.math
|
|
|
|
import lila.common.ApiVersion
|
|
import lila.common.Json._
|
|
import lila.game.JsonView._
|
|
import lila.game.{ Pov, Game, Player => GamePlayer }
|
|
import lila.pref.Pref
|
|
import lila.user.{ User, UserRepo }
|
|
|
|
final class JsonView(
|
|
userRepo: UserRepo,
|
|
userJsonView: lila.user.JsonView,
|
|
gameJsonView: lila.game.JsonView,
|
|
getSocketStatus: Game => Fu[SocketStatus],
|
|
takebacker: Takebacker,
|
|
moretimer: Moretimer,
|
|
divider: lila.game.Divider,
|
|
evalCache: lila.evalCache.EvalCacheApi,
|
|
isOfferingRematch: Pov => Boolean
|
|
)(implicit ec: scala.concurrent.ExecutionContext) {
|
|
|
|
import JsonView._
|
|
|
|
private def checkCount(game: Game, color: Color) =
|
|
(game.variant == chess.variant.ThreeCheck) option game.history.checkCount(color)
|
|
|
|
private def commonPlayerJson(g: Game, p: GamePlayer, user: Option[User], withFlags: WithFlags): JsObject =
|
|
Json
|
|
.obj("color" -> p.color.name)
|
|
.add("user" -> user.map { userJsonView.roundPlayer(_, g.perfType, withRating = withFlags.rating) })
|
|
.add("rating" -> p.rating.ifTrue(withFlags.rating))
|
|
.add("ratingDiff" -> p.ratingDiff.ifTrue(withFlags.rating))
|
|
.add("provisional" -> (p.provisional && withFlags.rating))
|
|
.add("offeringRematch" -> isOfferingRematch(Pov(g, p)))
|
|
.add("offeringDraw" -> p.isOfferingDraw)
|
|
.add("proposingTakeback" -> p.isProposingTakeback)
|
|
.add("checks" -> checkCount(g, p.color))
|
|
.add("berserk" -> p.berserk)
|
|
.add("blurs" -> (withFlags.blurs ?? blurs(g, p)))
|
|
|
|
def playerJson(
|
|
pov: Pov,
|
|
pref: Pref,
|
|
apiVersion: ApiVersion,
|
|
playerUser: Option[User],
|
|
initialFen: Option[FEN],
|
|
withFlags: WithFlags,
|
|
nvui: Boolean
|
|
): Fu[JsObject] =
|
|
getSocketStatus(pov.game) zip
|
|
(pov.opponent.userId ?? userRepo.byId) zip
|
|
takebacker.isAllowedIn(pov.game) zip
|
|
moretimer.isAllowedIn(pov.game) map { case (((socket, opponentUser), takebackable), moretimeable) =>
|
|
import pov._
|
|
Json
|
|
.obj(
|
|
"game" -> gameJsonView(game, initialFen),
|
|
"player" -> {
|
|
commonPlayerJson(game, player, playerUser, withFlags) ++ Json.obj(
|
|
"id" -> playerId,
|
|
"version" -> socket.version.value
|
|
)
|
|
}.add("onGame" -> (player.isAi || socket.onGame(player.color))),
|
|
"opponent" -> {
|
|
commonPlayerJson(game, opponent, opponentUser, withFlags) ++ Json.obj(
|
|
"color" -> opponent.color.name,
|
|
"ai" -> opponent.aiLevel
|
|
)
|
|
}.add("isGone" -> (!opponent.isAi && socket.isGone(opponent.color)))
|
|
.add("onGame" -> (opponent.isAi || socket.onGame(opponent.color))),
|
|
"url" -> Json.obj(
|
|
"socket" -> s"/play/$fullId/v$apiVersion",
|
|
"round" -> s"/$fullId"
|
|
),
|
|
"pref" -> Json
|
|
.obj(
|
|
"animationDuration" -> animationMillis(pov, pref),
|
|
"coords" -> pref.coords,
|
|
"resizeHandle" -> pref.resizeHandle,
|
|
"replay" -> pref.replay,
|
|
"autoQueen" -> (if (pov.game.variant == chess.variant.Antichess) Pref.AutoQueen.NEVER
|
|
else pref.autoQueen),
|
|
"clockTenths" -> pref.clockTenths,
|
|
"moveEvent" -> pref.moveEvent
|
|
// "ratings" -> pref.showRatings
|
|
)
|
|
.add("is3d" -> pref.is3d)
|
|
.add("clockBar" -> pref.clockBar)
|
|
.add("clockSound" -> pref.clockSound)
|
|
.add("confirmResign" -> (!nvui && pref.confirmResign == Pref.ConfirmResign.YES))
|
|
.add("keyboardMove" -> (!nvui && pref.keyboardMove == Pref.KeyboardMove.YES))
|
|
.add("rookCastle" -> (pref.rookCastle == Pref.RookCastle.YES))
|
|
.add("blindfold" -> pref.isBlindfold)
|
|
.add("highlight" -> pref.highlight)
|
|
.add("destination" -> (pref.destination && !pref.isBlindfold))
|
|
.add("enablePremove" -> pref.premove)
|
|
.add("showCaptured" -> pref.captured)
|
|
.add("submitMove" -> {
|
|
import Pref.SubmitMove._
|
|
pref.submitMove match {
|
|
case _ if game.hasAi || nvui => false
|
|
case ALWAYS => true
|
|
case CORRESPONDENCE_UNLIMITED if game.isCorrespondence => true
|
|
case CORRESPONDENCE_ONLY if game.hasCorrespondenceClock => true
|
|
case _ => false
|
|
}
|
|
})
|
|
)
|
|
.add("clock" -> game.clock.map(clockJson))
|
|
.add("correspondence" -> game.correspondenceClock)
|
|
.add("takebackable" -> takebackable)
|
|
.add("moretimeable" -> moretimeable)
|
|
.add("crazyhouse" -> pov.game.board.crazyData)
|
|
.add("possibleMoves" -> possibleMoves(pov, apiVersion))
|
|
.add("possibleDrops" -> possibleDrops(pov))
|
|
.add("expiration" -> game.expirable.option {
|
|
Json.obj(
|
|
"idleMillis" -> (nowMillis - game.movedAt.getMillis),
|
|
"millisToMove" -> game.timeForFirstMove.millis
|
|
)
|
|
})
|
|
}
|
|
|
|
private def commonWatcherJson(g: Game, p: GamePlayer, user: Option[User], withFlags: WithFlags): JsObject =
|
|
Json
|
|
.obj(
|
|
"color" -> p.color.name,
|
|
"name" -> p.name
|
|
)
|
|
.add("user" -> user.map { userJsonView.roundPlayer(_, g.perfType, withRating = withFlags.rating) })
|
|
.add("ai" -> p.aiLevel)
|
|
.add("rating" -> p.rating.ifTrue(withFlags.rating))
|
|
.add("ratingDiff" -> p.ratingDiff.ifTrue(withFlags.rating))
|
|
.add("provisional" -> (p.provisional && withFlags.rating))
|
|
.add("checks" -> checkCount(g, p.color))
|
|
.add("berserk" -> p.berserk)
|
|
.add("blurs" -> (withFlags.blurs ?? blurs(g, p)))
|
|
|
|
def watcherJson(
|
|
pov: Pov,
|
|
pref: Pref,
|
|
apiVersion: ApiVersion,
|
|
me: Option[User],
|
|
tv: Option[OnTv],
|
|
initialFen: Option[FEN] = None,
|
|
withFlags: WithFlags
|
|
) =
|
|
getSocketStatus(pov.game) zip
|
|
userRepo.pair(pov.player.userId, pov.opponent.userId) map { case (socket, (playerUser, opponentUser)) =>
|
|
import pov._
|
|
Json
|
|
.obj(
|
|
"game" -> gameJsonView(game, initialFen)
|
|
.add("moveCentis" -> (withFlags.movetimes ?? game.moveTimes.map(_.map(_.centis))))
|
|
.add("division" -> withFlags.division.option(divider(game, initialFen)))
|
|
.add("opening" -> game.opening)
|
|
.add("importedBy" -> game.pgnImport.flatMap(_.user)),
|
|
"clock" -> game.clock.map(clockJson),
|
|
"correspondence" -> game.correspondenceClock,
|
|
"player" -> {
|
|
commonWatcherJson(game, player, playerUser, withFlags) ++ Json.obj(
|
|
"version" -> socket.version.value,
|
|
"spectator" -> true,
|
|
"id" -> me.flatMap(game.player).map(_.id)
|
|
)
|
|
}.add("onGame" -> (player.isAi || socket.onGame(player.color))),
|
|
"opponent" -> commonWatcherJson(game, opponent, opponentUser, withFlags).add(
|
|
"onGame" -> (opponent.isAi || socket.onGame(opponent.color))
|
|
),
|
|
"orientation" -> pov.color.name,
|
|
"url" -> Json.obj(
|
|
"socket" -> s"/watch/$gameId/${color.name}/v$apiVersion",
|
|
"round" -> s"/$gameId/${color.name}"
|
|
),
|
|
"pref" -> Json
|
|
.obj(
|
|
"animationDuration" -> animationMillis(pov, pref),
|
|
"coords" -> pref.coords,
|
|
"resizeHandle" -> pref.resizeHandle,
|
|
"replay" -> pref.replay,
|
|
"clockTenths" -> pref.clockTenths
|
|
)
|
|
.add("is3d" -> pref.is3d)
|
|
.add("clockBar" -> pref.clockBar)
|
|
.add("highlight" -> pref.highlight)
|
|
.add("destination" -> (pref.destination && !pref.isBlindfold))
|
|
.add("rookCastle" -> (pref.rookCastle == Pref.RookCastle.YES))
|
|
.add("showCaptured" -> pref.captured),
|
|
"evalPut" -> JsBoolean(me.??(evalCache.shouldPut))
|
|
)
|
|
.add("evalPut" -> me.??(evalCache.shouldPut))
|
|
.add("tv" -> tv.collect { case OnLichessTv(channel, flip) =>
|
|
Json.obj("channel" -> channel, "flip" -> flip)
|
|
})
|
|
.add("userTv" -> tv.collect { case OnUserTv(userId) =>
|
|
Json.obj("id" -> userId)
|
|
})
|
|
|
|
}
|
|
|
|
def userAnalysisJson(
|
|
pov: Pov,
|
|
pref: Pref,
|
|
initialFen: Option[FEN],
|
|
orientation: chess.Color,
|
|
owner: Boolean,
|
|
me: Option[User],
|
|
division: Option[chess.Division] = none
|
|
) = {
|
|
import pov._
|
|
val fen = Forsyth >> game.chess
|
|
Json
|
|
.obj(
|
|
"game" -> Json
|
|
.obj(
|
|
"id" -> gameId,
|
|
"variant" -> game.variant,
|
|
"opening" -> game.opening,
|
|
"fen" -> fen,
|
|
"turns" -> game.turns,
|
|
"player" -> game.turnColor.name,
|
|
"status" -> game.status
|
|
)
|
|
.add("initialFen", initialFen)
|
|
.add("division", division)
|
|
.add("winner", game.winner.map(_.color.name)),
|
|
"player" -> Json.obj(
|
|
"id" -> owner.option(pov.playerId),
|
|
"color" -> color.name
|
|
),
|
|
"opponent" -> Json.obj(
|
|
"color" -> opponent.color.name,
|
|
"ai" -> opponent.aiLevel
|
|
),
|
|
"orientation" -> orientation.name,
|
|
"pref" -> Json
|
|
.obj(
|
|
"animationDuration" -> animationMillis(pov, pref),
|
|
"coords" -> pref.coords,
|
|
"moveEvent" -> pref.moveEvent,
|
|
"showCaptured" -> pref.captured
|
|
)
|
|
.add("rookCastle" -> (pref.rookCastle == Pref.RookCastle.YES))
|
|
.add("is3d" -> pref.is3d)
|
|
.add("highlight" -> pref.highlight)
|
|
.add("destination" -> (pref.destination && !pref.isBlindfold)),
|
|
"path" -> pov.game.turns,
|
|
"userAnalysis" -> true
|
|
)
|
|
.add("evalPut" -> me.??(evalCache.shouldPut))
|
|
}
|
|
|
|
private def blurs(game: Game, player: lila.game.Player) =
|
|
player.blurs.nonEmpty option {
|
|
blursWriter.writes(player.blurs) +
|
|
("percent" -> JsNumber(game.playerBlurPercent(player.color)))
|
|
}
|
|
|
|
private def clockJson(clock: Clock): JsObject =
|
|
clockWriter.writes(clock) + ("moretime" -> JsNumber(actorApi.round.Moretime.defaultDuration.toSeconds))
|
|
|
|
private def possibleMoves(pov: Pov, apiVersion: ApiVersion): Option[JsValue] =
|
|
(pov.game playableBy pov.player) option
|
|
lila.game.Event.PossibleMoves.json(pov.game.situation.destinations, apiVersion)
|
|
|
|
private def possibleDrops(pov: Pov): Option[JsValue] =
|
|
(pov.game playableBy pov.player) ?? {
|
|
pov.game.situation.drops map { drops =>
|
|
JsString(drops.map(_.key).mkString)
|
|
}
|
|
}
|
|
|
|
private def animationMillis(pov: Pov, pref: Pref) =
|
|
pref.animationMillis * {
|
|
if (pov.game.finished) 1
|
|
else math.max(0, math.min(1.2, ((pov.game.estimateTotalTime - 60) / 60) * 0.2))
|
|
}
|
|
}
|
|
|
|
object JsonView {
|
|
|
|
case class WithFlags(
|
|
opening: Boolean = false,
|
|
movetimes: Boolean = false,
|
|
division: Boolean = false,
|
|
clocks: Boolean = false,
|
|
blurs: Boolean = false,
|
|
rating: Boolean = true
|
|
)
|
|
}
|