puzzle v2 mobile API BC WIP
parent
ca074bcb7b
commit
848cc7cbf9
|
@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue