puzzle v2 mobile API BC WIP

pull/7691/head
Thibault Duplessis 2020-12-08 11:33:59 +01:00
parent ca074bcb7b
commit 848cc7cbf9
7 changed files with 138 additions and 127 deletions

View File

@ -27,13 +27,7 @@ final class Api(
Json.obj(
"api" -> Json.obj(
"current" -> api.currentVersion.value,
"olds" -> api.oldVersions.map { old =>
Json.obj(
"version" -> old.version.value,
"deprecatedAt" -> old.deprecatedAt,
"unsupportedAt" -> old.unsupportedAt
)
}
"olds" -> Json.arr()
)
)
}

View File

@ -6,6 +6,7 @@ import views._
import lila.api.Context
import lila.app._
import lila.common.ApiVersion
import lila.common.config.MaxPerSecond
import lila.puzzle.PuzzleTheme
import lila.puzzle.{ Result, PuzzleRound, PuzzleDifficulty, Puzzle => Puz }
@ -15,10 +16,18 @@ final class Puzzle(
apiC: => Api
) extends LilaController(env) {
private def renderJson(puzzle: Puz, theme: PuzzleTheme, newUser: Option[lila.user.User] = None)(implicit
private def renderJson(
puzzle: Puz,
theme: PuzzleTheme,
newUser: Option[lila.user.User] = None,
apiVersion: Option[ApiVersion] = None
)(implicit
ctx: Context
): Fu[JsObject] =
env.puzzle.jsonView(puzzle = puzzle, theme = theme, user = newUser orElse ctx.me)
if (apiVersion.exists(!_.puzzleV2))
env.puzzle.jsonView.bc(puzzle = puzzle, theme = theme, user = newUser orElse ctx.me)
else
env.puzzle.jsonView(puzzle = puzzle, theme = theme, user = newUser orElse ctx.me)
private def renderShow(puzzle: Puz, theme: PuzzleTheme)(implicit ctx: Context) =
renderJson(puzzle, theme) zip
@ -36,7 +45,7 @@ final class Puzzle(
}) { puzzle =>
negotiate(
html = renderShow(puzzle, PuzzleTheme.any),
api = _ => renderJson(puzzle, PuzzleTheme.any) map { Ok(_) }
api = v => renderJson(puzzle, PuzzleTheme.any, apiVersion = v.some) dmap { Ok(_) }
) map NoCache
}
}

View File

@ -22,35 +22,9 @@ object Mobile {
object Api {
case class Old(
version: ApiVersion,
// date when a newer version was released
deprecatedAt: DateTime,
// date when the server stops accepting requests
unsupportedAt: DateTime
)
val currentVersion = ApiVersion(6)
val currentVersion = ApiVersion(5)
val acceptedVersions: Set[ApiVersion] = Set(1, 2, 3, 4, 5) map ApiVersion.apply
val oldVersions: List[Old] = List(
Old( // chat messages are html escaped
version = ApiVersion(1),
deprecatedAt = new DateTime("2016-08-13"),
unsupportedAt = new DateTime("2016-11-13")
),
Old( // old puzzle API
version = ApiVersion(2),
deprecatedAt = new DateTime("2017-10-23"),
unsupportedAt = new DateTime("2018-03-23")
)
// Old( // old ping API
// version = ApiVersion(3),
// deprecatedAt = new DateTime("2018-12-14"),
// unsupportedAt = new DateTime("2019-12-14")
// )
)
val acceptedVersions: Set[ApiVersion] = Set(1, 2, 3, 4, 5, 6) map ApiVersion.apply
def requestVersion(req: RequestHeader): Option[ApiVersion] =
HTTPRequest apiVersion req filter acceptedVersions.contains

View File

@ -6,6 +6,7 @@ case class ApiVersion(value: Int) extends AnyVal with IntValue with Ordered[ApiV
def compare(other: ApiVersion) = Integer.compare(value, other.value)
def gt(other: Int) = value > other
def gte(other: Int) = value >= other
def puzzleV2 = value >= 6
}
case class AssetVersion(value: String) extends AnyVal with StringValue

View File

@ -6,6 +6,7 @@ import scala.concurrent.duration._
import lila.game.{ Game, GameRepo, PerfPicker }
import lila.i18n.defaultLang
import lila.tree.Node.{ minimalNodeJsonWriter, partitionTreeJsonWriter }
import chess.format.Forsyth
final private class GameJson(
gameRepo: GameRepo,
@ -13,13 +14,13 @@ final private class GameJson(
lightUserApi: lila.user.LightUserApi
)(implicit ec: scala.concurrent.ExecutionContext) {
def apply(gameId: Game.ID, plies: Int): Fu[JsObject] =
cache get CacheKey(gameId, plies)
def apply(gameId: Game.ID, plies: Int, bc: Boolean): Fu[JsObject] =
cache get CacheKey(gameId, plies, bc)
def noCache(game: Game, plies: Int): Fu[JsObject] =
generate(game, plies)
// def noCache(game: Game, plies: Int): Fu[JsObject] =
// generate(game, plies)
private case class CacheKey(gameId: Game.ID, plies: Int)
private case class CacheKey(gameId: Game.ID, plies: Int, bc: Boolean)
private val cache = cacheApi[CacheKey, JsObject](1024, "puzzle.gameJson") {
_.expireAfterAccess(5 minutes)
@ -29,70 +30,67 @@ final private class GameJson(
private def generate(ck: CacheKey): Fu[JsObject] =
ck match {
case CacheKey(gameId, plies) =>
gameRepo game gameId orFail s"Missing puzzle game $gameId!" flatMap {
generate(_, plies)
case CacheKey(gameId, plies, bc) =>
gameRepo game gameId orFail s"Missing puzzle game $gameId!" flatMap { game =>
lightUserApi preloadMany game.userIds map { _ =>
if (bc) generateBc(game, plies)
else generate(game, plies)
}
}
}
private def generate(game: Game, plies: Int): Fu[JsObject] =
lightUserApi preloadMany game.userIds map { _ =>
val perfType = lila.rating.PerfType orDefault PerfPicker.key(game)
val tree = treeBuilder(game, plies)
Json
.obj(
"id" -> game.id,
"perf" -> Json.obj(
"icon" -> perfType.iconChar.toString,
"name" -> perfType.trans(defaultLang)
),
"rated" -> game.rated,
"players" -> JsArray(game.players.map { p =>
Json.obj(
"userId" -> p.userId,
"name" -> lila.game.Namer.playerTextBlocking(p, withRating = true)(lightUserApi.sync),
"color" -> p.color.name
)
}),
"pgn" -> game.chess.pgnMoves.take(plies + 1)
)
.add("clock", game.clock.map(_.config.show))
}
private def generate(game: Game, plies: Int): JsObject =
Json
.obj(
"id" -> game.id,
"perf" -> perfJson(game),
"rated" -> game.rated,
"players" -> playersJson(game),
"pgn" -> game.chess.pgnMoves.take(plies + 1)
)
.add("clock", game.clock.map(_.config.show))
private def treeBuilder(game: Game, plies: Int): lila.tree.Root = {
import chess.format.{ Forsyth, Uci, UciCharPair }
chess.Replay.gameMoveWhileValid(game.pgnMoves take plies, Forsyth.initial, game.variant) match {
case (init, games, error) =>
error foreach logChessError(game.id)
val fen = Forsyth >> init
val root = lila.tree.Root(
ply = init.turns,
fen = fen,
check = init.situation.check,
crazyData = None
)
def makeBranch(g: chess.Game, m: Uci.WithSan) = {
val fen = Forsyth >> g
lila.tree.Branch(
id = UciCharPair(m.uci),
ply = g.turns,
move = m,
fen = fen,
check = g.situation.check,
crazyData = None
)
}
games.reverse match {
case Nil => root
case (g, m) :: rest =>
root prependChild rest.foldLeft(makeBranch(g, m)) { case (node, (g, m)) =>
makeBranch(g, m) prependChild node
}
}
}
private def perfJson(game: Game) = {
val perfType = lila.rating.PerfType orDefault PerfPicker.key(game)
Json.obj(
"icon" -> perfType.iconChar.toString,
"name" -> perfType.trans(defaultLang)
)
}
private val logChessError = (id: Game.ID) =>
(err: String) =>
logger.warn(s"TreeBuilder https://lichess.org/$id ${err.linesIterator.toList.headOption}")
private def playersJson(game: Game) = JsArray(game.players.map { p =>
Json.obj(
"userId" -> p.userId,
"name" -> lila.game.Namer.playerTextBlocking(p, withRating = true)(lightUserApi.sync),
"color" -> p.color.name
)
})
private def generateBc(game: Game, plies: Int): JsObject =
Json
.obj(
"id" -> game.id,
"perf" -> perfJson(game),
"players" -> playersJson(game),
"rated" -> game.rated,
"treeParts" -> {
val pgnMoves = game.pgnMoves take plies
for {
pgnMove <- pgnMoves.lastOption
situation <- chess.Replay
.situations(pgnMoves, None, game.variant)
.valueOr { err =>
sys.error(s"GameJson.generateBc ${game.id} $err")
}
.lastOption
uciMove <- situation.board.history.lastMove
} yield Json.obj(
"fen" -> Forsyth.>>(situation).value,
"ply" -> plies,
"san" -> pgnMove,
"uci" -> uciMove.uci
)
}
)
.add("clock", game.clock.map(_.config.show))
}

View File

@ -23,7 +23,8 @@ final class JsonView(
): Fu[JsObject] = {
gameJson(
gameId = puzzle.gameId,
plies = puzzle.initialPly
plies = puzzle.initialPly,
bc = false
) map { gameJson =>
Json
.obj(
@ -86,26 +87,61 @@ final class JsonView(
"themes" -> puzzle.themes
)
private def makeSolution(puzzle: Puzzle): Option[tree.Branch] = {
import chess.format._
val init = chess.Game(none, puzzle.fenAfterInitialMove.some).withTurns(puzzle.initialPly)
val (_, branchList) = puzzle.line.tail.foldLeft[(chess.Game, List[tree.Branch])]((init, Nil)) {
case ((prev, branches), uci) =>
val (game, move) =
prev(uci.orig, uci.dest, uci.promotion).fold(err => sys error s"puzzle ${puzzle.id} $err", identity)
val branch = tree.Branch(
id = UciCharPair(move.toUci),
ply = game.turns,
move = Uci.WithSan(move.toUci, game.pgnMoves.last),
fen = chess.format.Forsyth >> game,
check = game.situation.check,
crazyData = none
)
(game, branch :: branches)
object bc {
def apply(puzzle: Puzzle, theme: PuzzleTheme, user: Option[User])(implicit
lang: Lang
): Fu[JsObject] = {
gameJson(
gameId = puzzle.gameId,
plies = puzzle.initialPly,
bc = true
) map { gameJson =>
Json
.obj(
"game" -> gameJson,
"puzzle" -> Json.obj(
"id" -> puzzle.id,
"rating" -> puzzle.glicko.intRating,
"attempts" -> puzzle.plays,
"fen" -> puzzle.fenAfterInitialMove,
"color" -> puzzle.color.name,
"initialMove" -> puzzle.line.head.uci,
"initialPly" -> puzzle.initialPly,
"gameId" -> puzzle.gameId,
"lines" -> puzzle.line.tail.reverse.foldLeft[JsValue](JsString("win")) { case (acc, move) =>
Json.obj(move.uci -> acc)
},
"vote" -> 0,
"branch" -> makeBranch(puzzle).map(defaultNodeJsonWriter.writes)
)
)
.add("user" -> user.map(userJson))
}
}
branchList.foldLeft[Option[tree.Branch]](None) {
case (None, branch) => branch.some
case (Some(child), branch) => Some(branch addChild child)
private def makeBranch(puzzle: Puzzle): Option[tree.Branch] = {
import chess.format._
val init = chess.Game(none, puzzle.fenAfterInitialMove.some).withTurns(puzzle.initialPly)
val (_, branchList) = puzzle.line.tail.foldLeft[(chess.Game, List[tree.Branch])]((init, Nil)) {
case ((prev, branches), uci) =>
val (game, move) =
prev(uci.orig, uci.dest, uci.promotion)
.fold(err => sys error s"puzzle ${puzzle.id} $err", identity)
val branch = tree.Branch(
id = UciCharPair(move.toUci),
ply = game.turns,
move = Uci.WithSan(move.toUci, game.pgnMoves.last),
fen = chess.format.Forsyth >> game,
check = game.situation.check,
crazyData = none
)
(game, branch :: branches)
}
branchList.foldLeft[Option[tree.Branch]](None) {
case (None, branch) => branch.some
case (Some(child), branch) => Some(branch addChild child)
}
}
}
}

View File

@ -14,21 +14,20 @@ case class Puzzle(
plays: Int,
themes: Set[PuzzleTheme.Key]
) {
def color = fen.color | chess.White
// ply after "initial move" when we start solving
def initialPly: Int =
fen.fullMove ?? { fm =>
fm * 2 - color.fold(2, 1)
}
def fenAfterInitialMove: FEN = {
lazy val fenAfterInitialMove: FEN = {
for {
sit1 <- Forsyth << fen
sit2 <- sit1.move(line.head).toOption.map(_.situationAfter)
} yield Forsyth >> sit2
} err s"Can't apply puzzle $id first move"
def color = fenAfterInitialMove.color | chess.White
}
object Puzzle {