unify game API and analysis API
parent
5f846df65b
commit
a62754f33f
48
README.md
48
README.md
|
@ -262,54 +262,6 @@ name | type | default | description
|
|||
|
||||
(1) All game statuses: https://github.com/ornicar/scalachess/blob/master/src/main/scala/Status.scala#L16-L25
|
||||
|
||||
### `GET /api/analysis` fetch many analysis
|
||||
|
||||
This API requires a secret token to work.
|
||||
Analysis are returned by descendant chronological order.
|
||||
All parameters are optional.
|
||||
|
||||
name | type | default | description
|
||||
--- | --- | --- | ---
|
||||
**token** | string | - | security token
|
||||
**nb** | int | 10 | maximum number of analysis to return
|
||||
|
||||
```
|
||||
> curl http://en.lichess.org/api/analysis?nb=10
|
||||
```
|
||||
|
||||
```javascript
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"analysis": [
|
||||
{
|
||||
"eval": -26, // board evaluation in centipawns
|
||||
"move": "e4",
|
||||
"ply": 1
|
||||
},
|
||||
{
|
||||
"eval": -8,
|
||||
"move": "b5",
|
||||
"ply": 2
|
||||
},
|
||||
{
|
||||
"comment": "(-0.08 → -0.66) Inaccuracy. The best move was c4.",
|
||||
"eval": -66,
|
||||
"move": "Nfe3",
|
||||
"ply": 3,
|
||||
"variation": "c4 bxc4 Nfe3 c5 Qf1 f6 Rxc4 Bb7 b4 Ba6"
|
||||
},
|
||||
// ... more moves
|
||||
],
|
||||
"game": {
|
||||
// similar to the game API format, see above
|
||||
},
|
||||
"uci": "e2e4 e7e5 d2d4 e5d4 g1f3 g8f6" // UCI compatible game moves
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/puzzle/<id>` fetch one puzzle
|
||||
|
||||
```
|
||||
|
|
|
@ -9,7 +9,6 @@ object Api extends LilaController {
|
|||
|
||||
private val userApi = Env.api.userApi
|
||||
private val gameApi = Env.api.gameApi
|
||||
private val analysisApi = Env.api.analysisApi
|
||||
private val puzzleApi = Env.api.puzzleApi
|
||||
|
||||
def user(username: String) = ApiResult { req =>
|
||||
|
@ -32,7 +31,7 @@ object Api extends LilaController {
|
|||
username = get("username", req),
|
||||
rated = getBoolOpt("rated", req),
|
||||
analysed = getBoolOpt("analysed", req),
|
||||
withAnalysis = getBoolOpt("with_analysis", req),
|
||||
withAnalysis = getBool("with_analysis", req),
|
||||
token = get("token", req),
|
||||
nb = getInt("nb", req)
|
||||
) map (_.some)
|
||||
|
@ -41,17 +40,10 @@ object Api extends LilaController {
|
|||
def game(id: String) = ApiResult { req =>
|
||||
gameApi.one(
|
||||
id = id take lila.game.Game.gameIdSize,
|
||||
withAnalysis = getBoolOpt("with_analysis", req),
|
||||
withAnalysis = getBool("with_analysis", req),
|
||||
token = get("token", req))
|
||||
}
|
||||
|
||||
def analysis = ApiResult { req =>
|
||||
analysisApi.list(
|
||||
nb = getInt("nb", req),
|
||||
token = get("token", req)
|
||||
) map (_.some)
|
||||
}
|
||||
|
||||
def puzzle(id: String) = ApiResult { req =>
|
||||
(id, parseIntOption(id)) match {
|
||||
case ("daily", _) => puzzleApi.daily
|
||||
|
|
|
@ -246,7 +246,6 @@ GET /api/user controllers.Api.users
|
|||
GET /api/user/:id controllers.Api.user(id: String)
|
||||
GET /api/game controllers.Api.games
|
||||
GET /api/game/:id controllers.Api.game(id: String)
|
||||
GET /api/analysis controllers.Api.analysis
|
||||
GET /api/puzzle/:id controllers.Api.puzzle(id: String)
|
||||
POST /api/puzzle controllers.Puzzle.importBatch
|
||||
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
package lila.api
|
||||
|
||||
import chess.format.pgn.Pgn
|
||||
import chess.format.UciDump
|
||||
import chess.Replay
|
||||
import play.api.libs.json._
|
||||
|
||||
import lila.analyse.{ Analysis, AnalysisRepo }
|
||||
import lila.common.PimpedJson._
|
||||
import lila.db.api._
|
||||
import lila.db.Implicits._
|
||||
import lila.game.{ Game, GameRepo, PgnDump }
|
||||
import lila.hub.actorApi.{ router => R }
|
||||
|
||||
private[api] final class AnalysisApi(
|
||||
apiToken: String,
|
||||
makeUrl: Any => Fu[String],
|
||||
nbAnalysis: () => Fu[Int],
|
||||
pgnDump: PgnDump) {
|
||||
|
||||
private def makeNb(nb: Option[Int]) = math.min(100, nb | 10)
|
||||
|
||||
private def makeSkip = nbAnalysis() map { nb => scala.util.Random.nextInt(nb) }
|
||||
|
||||
def list(nb: Option[Int], token: Option[String]): Fu[JsObject] =
|
||||
if (~token != apiToken) fuccess(Json.obj("oh" -> "bummer"))
|
||||
else makeSkip flatMap { skip =>
|
||||
AnalysisRepo.skipping(skip, makeNb(nb)) flatMap { as =>
|
||||
GameRepo games as.map(_.id) flatMap { games =>
|
||||
games.map { g =>
|
||||
as find (_.id == g.id) map { _ -> g }
|
||||
}.flatten.map {
|
||||
case (a, g) => GameRepo initialFen g.id flatMap { initialFen =>
|
||||
pgnDump(g) zip makeUrl(R.Watcher(g.id, g.firstPlayer.color.name)) map {
|
||||
case (pgn, url) => (g, a, url, pgn, initialFen)
|
||||
}
|
||||
}
|
||||
}.sequenceFu map { tuples =>
|
||||
Json.obj(
|
||||
"list" -> JsArray(tuples map {
|
||||
case (game, analysis, url, pgn, fen) => Json.obj(
|
||||
"game" -> (GameApi.gameToJson(game, url, analysis.some) ++ {
|
||||
fen ?? { f => Json.obj("initialFen" -> f) }
|
||||
}),
|
||||
"analysis" -> AnalysisApi.analysisToJson(analysis, pgn),
|
||||
"uci" -> uciMovesOf(game, fen).map(_.mkString(" "))
|
||||
).noNull
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def uciMovesOf(game: Game, initialFen: Option[String]): Option[List[String]] =
|
||||
Replay(game.pgnMoves mkString " ", initialFen, game.variant).toOption map UciDump.apply
|
||||
}
|
||||
|
||||
private[api] object AnalysisApi {
|
||||
|
||||
def analysisToJson(analysis: Analysis, pgn: Pgn) = JsArray(analysis.infoAdvices zip pgn.moves map {
|
||||
case ((info, adviceOption), move) => Json.obj(
|
||||
"ply" -> info.ply,
|
||||
"move" -> move.san,
|
||||
"eval" -> info.score.map(_.centipawns),
|
||||
"mate" -> info.mate,
|
||||
"variation" -> info.variation.isEmpty.fold(JsNull, info.variation mkString " "),
|
||||
"comment" -> adviceOption.map(_.makeComment(true, true))
|
||||
).noNull
|
||||
})
|
||||
}
|
|
@ -44,12 +44,6 @@ final class Env(
|
|||
val gameApi = new GameApi(
|
||||
makeUrl = apiUrl,
|
||||
apiToken = apiToken,
|
||||
isOnline = userEnv.isOnline)
|
||||
|
||||
val analysisApi = new AnalysisApi(
|
||||
apiToken = apiToken,
|
||||
makeUrl = apiUrl,
|
||||
nbAnalysis = () => analyseEnv.cached.nbAnalysis,
|
||||
pgnDump = pgnDump)
|
||||
|
||||
val puzzleApi = new PuzzleApi(
|
||||
|
|
|
@ -2,20 +2,21 @@ package lila.api
|
|||
|
||||
import play.api.libs.json._
|
||||
|
||||
import chess.format.pgn.Pgn
|
||||
import lila.analyse.{ AnalysisRepo, Analysis }
|
||||
import lila.common.PimpedJson._
|
||||
import lila.db.api._
|
||||
import lila.db.Implicits._
|
||||
import lila.game.Game
|
||||
import lila.game.Game.{ BSONFields => G }
|
||||
import lila.game.tube.gameTube
|
||||
import lila.game.{ Game, PgnDump }
|
||||
import lila.hub.actorApi.{ router => R }
|
||||
import makeTimeout.short
|
||||
|
||||
private[api] final class GameApi(
|
||||
makeUrl: Any => Fu[String],
|
||||
apiToken: String,
|
||||
isOnline: String => Boolean) {
|
||||
pgnDump: PgnDump) {
|
||||
|
||||
private def makeNb(nb: Option[Int]) = math.min(100, nb | 10)
|
||||
|
||||
|
@ -23,7 +24,7 @@ private[api] final class GameApi(
|
|||
username: Option[String],
|
||||
rated: Option[Boolean],
|
||||
analysed: Option[Boolean],
|
||||
withAnalysis: Option[Boolean],
|
||||
withAnalysis: Boolean,
|
||||
token: Option[String],
|
||||
nb: Option[Int]): Fu[JsObject] = $find($query(Json.obj(
|
||||
G.status -> $gte(chess.Status.Mate.id),
|
||||
|
@ -37,32 +38,34 @@ private[api] final class GameApi(
|
|||
|
||||
def one(
|
||||
id: String,
|
||||
withAnalysis: Option[Boolean],
|
||||
withAnalysis: Boolean,
|
||||
token: Option[String]): Fu[Option[JsObject]] =
|
||||
$find byId id map (_.toList) flatMap gamesJson(withAnalysis, token) map (_.headOption)
|
||||
|
||||
private def gamesJson(withAnalysis: Option[Boolean], token: Option[String])(games: List[Game]): Fu[List[JsObject]] =
|
||||
private def gamesJson(withAnalysis: Boolean, token: Option[String])(games: List[Game]): Fu[List[JsObject]] =
|
||||
AnalysisRepo doneByIds games.map(_.id) flatMap { analysisOptions =>
|
||||
(games map { g => makeUrl(R.Watcher(g.id, g.firstPlayer.color.name)) }).sequenceFu map { urls =>
|
||||
val validToken = check(token)
|
||||
games zip urls zip analysisOptions map {
|
||||
case ((g, url), analysisOption) =>
|
||||
GameApi.gameToJson(g, url, analysisOption,
|
||||
withBlurs = validToken,
|
||||
withMoveTimes = validToken)
|
||||
(games map { g => withAnalysis ?? (pgnDump(g) map (_.some)) }).sequenceFu flatMap { pgns =>
|
||||
(games map { g => makeUrl(R.Watcher(g.id, g.firstPlayer.color.name)) }).sequenceFu map { urls =>
|
||||
val validToken = check(token)
|
||||
games zip urls zip analysisOptions zip pgns map {
|
||||
case (((g, url), analysisOption), pgnOption) =>
|
||||
gameToJson(g, url, analysisOption, pgnOption,
|
||||
withAnalysis = withAnalysis,
|
||||
withBlurs = validToken,
|
||||
withMoveTimes = validToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def check(token: Option[String]) = token ?? (apiToken==)
|
||||
}
|
||||
|
||||
private[api] object GameApi {
|
||||
|
||||
def gameToJson(
|
||||
private def gameToJson(
|
||||
g: Game,
|
||||
url: String,
|
||||
analysisOption: Option[Analysis],
|
||||
pgnOption: Option[Pgn],
|
||||
withAnalysis: Boolean,
|
||||
withBlurs: Boolean = false,
|
||||
withMoveTimes: Boolean = false) = Json.obj(
|
||||
"id" -> g.id,
|
||||
|
@ -93,6 +96,18 @@ private[api] object GameApi {
|
|||
)
|
||||
).noNull
|
||||
}),
|
||||
"analysis" -> analysisOption.ifTrue(withAnalysis).|@|(pgnOption).apply {
|
||||
case (analysis, pgn) => JsArray(analysis.infoAdvices zip pgn.moves map {
|
||||
case ((info, adviceOption), move) => Json.obj(
|
||||
"ply" -> info.ply,
|
||||
"move" -> move.san,
|
||||
"eval" -> info.score.map(_.centipawns),
|
||||
"mate" -> info.mate,
|
||||
"variation" -> info.variation.isEmpty.fold(JsNull, info.variation mkString " "),
|
||||
"comment" -> adviceOption.map(_.makeComment(true, true))
|
||||
).noNull
|
||||
})
|
||||
},
|
||||
"winner" -> g.winnerColor.map(_.name),
|
||||
"url" -> url
|
||||
).noNull
|
||||
|
|
Loading…
Reference in New Issue