implement and document new analyse API
parent
33598abdc3
commit
80e2e25ae1
77
README.md
77
README.md
|
@ -184,8 +184,7 @@ name | type | default | description
|
|||
"blunder": 1,
|
||||
"inaccuracy": 0,
|
||||
"mistake": 2
|
||||
},
|
||||
"moveTimes": [1, 15, 15, 10, 20, 15, 15, 20, 30, 10, 15, 20, 20, 30, 40, 30, 20, 20, 15, 30, 20, 10]
|
||||
}
|
||||
},
|
||||
"black": ... // other player
|
||||
}
|
||||
|
@ -214,7 +213,79 @@ One could use that API to provide chess capabilities to an online chat, for inst
|
|||
"white": "http://l.org/8pmigk36t1hp",
|
||||
"black": "http://l.org/8pmigk36ov6m"
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
### `GET /api/analysis` fetch many analysis
|
||||
|
||||
Analysis are returned by descendant chronological order.
|
||||
All parameters are optional.
|
||||
|
||||
name | type | default | description
|
||||
--- | --- | --- | ---
|
||||
**nb** | int | 10 | maximum number of analysis to return
|
||||
|
||||
```
|
||||
> curl http://en.lichess.org/api/analysis?nb=10
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
"id": "lkvinsaq",
|
||||
"players": {
|
||||
"black": {
|
||||
"analysis": {
|
||||
"blunder": 0,
|
||||
"inaccuracy": 0,
|
||||
"mistake": 0
|
||||
},
|
||||
"rating": 1505,
|
||||
"userId": "neio"
|
||||
},
|
||||
"white": {
|
||||
"analysis": {
|
||||
"blunder": 0,
|
||||
"inaccuracy": 1,
|
||||
"mistake": 0
|
||||
},
|
||||
"rating": 1692,
|
||||
"userId": "thibault"
|
||||
}
|
||||
},
|
||||
"rated": true,
|
||||
"status": "resign",
|
||||
"timestamp": 1386797718059,
|
||||
"turns": 3,
|
||||
"url": "http://l.org/lkvinsaq/black",
|
||||
"variant": "chess960",
|
||||
"winner": "black"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Read the move stream
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ object Api extends LilaController {
|
|||
|
||||
private val userApi = Env.api.userApi
|
||||
private val gameApi = Env.api.gameApi
|
||||
private val analysisApi = Env.api.analysisApi
|
||||
|
||||
def user(username: String) = ApiResult { req ⇒
|
||||
userApi.one(
|
||||
|
@ -34,6 +35,12 @@ object Api extends LilaController {
|
|||
) map (_.some)
|
||||
}
|
||||
|
||||
def analysis = ApiResult { req ⇒
|
||||
analysisApi.list(
|
||||
nb = getInt("nb", req)
|
||||
) map (_.some)
|
||||
}
|
||||
|
||||
private def ApiResult(js: RequestHeader ⇒ Fu[Option[JsValue]]) = Action async { req ⇒
|
||||
js(req) map {
|
||||
case None ⇒ NotFound
|
||||
|
|
|
@ -213,6 +213,7 @@ POST /api/game/new controllers.Setup.api
|
|||
GET /api/user controllers.Api.users
|
||||
GET /api/user/:id controllers.Api.user(id: String)
|
||||
GET /api/game controllers.Api.games
|
||||
GET /api/analysis controllers.Api.analysis
|
||||
|
||||
# Misc
|
||||
POST /cli controllers.Cli.command
|
||||
|
|
|
@ -53,5 +53,8 @@ object AnalysisRepo {
|
|||
_.fold($find byId id) { staled ⇒ $remove byId id inject none }
|
||||
}
|
||||
|
||||
def recent(nb: Int): Fu[List[Analysis]] =
|
||||
$find($query(Json.obj("done" -> true)) sort $sort.desc("date"), nb)
|
||||
|
||||
def count = $count($select.all)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package lila.api
|
||||
|
||||
import chess.format.pgn.Pgn
|
||||
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.{ GameRepo, PgnDump }
|
||||
import lila.hub.actorApi.{ router ⇒ R }
|
||||
|
||||
private[api] final class AnalysisApi(
|
||||
makeUrl: Any ⇒ Fu[String],
|
||||
pgnDump: PgnDump) {
|
||||
|
||||
private def makeNb(nb: Option[Int]) = math.min(100, nb | 10)
|
||||
|
||||
def list(nb: Option[Int]): Fu[JsObject] = AnalysisRepo recent 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) ⇒ pgnDump(g) zip
|
||||
makeUrl(R.Watcher(g.id, g.firstPlayer.color.name)) map {
|
||||
case (pgn, url) ⇒ (g, a, url, pgn)
|
||||
}
|
||||
}.sequenceFu map { tuples ⇒
|
||||
Json.obj(
|
||||
"list" -> JsArray(tuples map {
|
||||
case (game, analysis, url, pgn) ⇒ Json.obj(
|
||||
"game" -> GameApi.gameToJson(game, url, analysis.some),
|
||||
"analysis" -> AnalysisApi.analysisToJson(analysis, pgn)
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
|
@ -8,6 +8,7 @@ final class Env(
|
|||
renderer: akka.actor.ActorSelection,
|
||||
router: akka.actor.ActorSelection,
|
||||
bus: lila.common.Bus,
|
||||
pgnDump: lila.game.PgnDump,
|
||||
userEnv: lila.user.Env,
|
||||
userIdsSharingIp: String ⇒ Fu[List[String]],
|
||||
val isProd: Boolean) {
|
||||
|
@ -36,6 +37,10 @@ final class Env(
|
|||
apiToken = apiToken,
|
||||
isOnline = userEnv.isOnline)
|
||||
|
||||
val analysisApi = new AnalysisApi(
|
||||
makeUrl = apiUrl,
|
||||
pgnDump = pgnDump)
|
||||
|
||||
private def apiUrl(msg: Any): Fu[String] = {
|
||||
import akka.pattern.ask
|
||||
import makeTimeout.short
|
||||
|
@ -52,6 +57,7 @@ object Env {
|
|||
renderer = lila.hub.Env.current.actor.renderer,
|
||||
router = lila.hub.Env.current.actor.router,
|
||||
userEnv = lila.user.Env.current,
|
||||
pgnDump = lila.game.Env.current.pgnDump,
|
||||
userIdsSharingIp = lila.security.Env.current.api.userIdsSharingIp,
|
||||
bus = lila.common.PlayApp.system.lilaBus,
|
||||
isProd = lila.common.PlayApp.isProd)
|
||||
|
|
|
@ -2,10 +2,11 @@ package lila.api
|
|||
|
||||
import play.api.libs.json._
|
||||
|
||||
import lila.analyse.AnalysisRepo
|
||||
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.hub.actorApi.{ router ⇒ R }
|
||||
|
@ -29,32 +30,14 @@ private[api] final class GameApi(
|
|||
(games map { g ⇒
|
||||
makeUrl(R.Watcher(g.id, g.firstPlayer.color.name)) zip (AnalysisRepo doneById g.id)
|
||||
}).sequenceFu map { data ⇒
|
||||
val validToken = check(token)
|
||||
Json.obj(
|
||||
"list" -> JsArray(
|
||||
games zip data map {
|
||||
case (g, (url, analysisOption)) ⇒ Json.obj(
|
||||
"id" -> g.id,
|
||||
"rated" -> g.rated,
|
||||
"variant" -> g.variant.name,
|
||||
"timestamp" -> g.createdAt.getDate,
|
||||
"turns" -> g.turns,
|
||||
"status" -> g.status.name.toLowerCase,
|
||||
"players" -> JsObject(g.players.zipWithIndex map {
|
||||
case (p, i) ⇒ p.color.name -> Json.obj(
|
||||
"userId" -> p.userId,
|
||||
"rating" -> p.rating,
|
||||
"moveTimes" -> g.moveTimes.zipWithIndex.filter(_._2 % 2 == i).map(_._1),
|
||||
"blurs" -> check(token).option(p.blurs),
|
||||
"analysis" -> analysisOption.map(_.summary).flatMap(_.find(_._1 == p.color).map(_._2)).map(s ⇒
|
||||
JsObject(s map {
|
||||
case (nag, nb) ⇒ nag.toString.toLowerCase -> JsNumber(nb)
|
||||
})
|
||||
)
|
||||
).noNull
|
||||
}),
|
||||
"winner" -> g.winnerColor.map(_.name),
|
||||
"url" -> url
|
||||
).noNull
|
||||
case (g, (url, analysisOption)) ⇒
|
||||
GameApi.gameToJson(g, url, analysisOption,
|
||||
withBlurs = validToken,
|
||||
withMoveTimes = validToken)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -63,3 +46,37 @@ private[api] final class GameApi(
|
|||
|
||||
private def check(token: Option[String]) = token ?? (apiToken==)
|
||||
}
|
||||
|
||||
private[api] object GameApi {
|
||||
|
||||
def gameToJson(
|
||||
g: Game,
|
||||
url: String,
|
||||
analysisOption: Option[Analysis],
|
||||
withBlurs: Boolean = false,
|
||||
withMoveTimes: Boolean = false) = Json.obj(
|
||||
"id" -> g.id,
|
||||
"rated" -> g.rated,
|
||||
"variant" -> g.variant.name,
|
||||
"timestamp" -> g.createdAt.getDate,
|
||||
"turns" -> g.turns,
|
||||
"status" -> g.status.name.toLowerCase,
|
||||
"players" -> JsObject(g.players.zipWithIndex map {
|
||||
case (p, i) ⇒ p.color.name -> Json.obj(
|
||||
"userId" -> p.userId,
|
||||
"rating" -> p.rating,
|
||||
"moveTimes" -> (withMoveTimes.fold(
|
||||
g.moveTimes.zipWithIndex.filter(_._2 % 2 == i).map(_._1),
|
||||
JsNull)),
|
||||
"blurs" -> withBlurs.option(p.blurs),
|
||||
"analysis" -> analysisOption.map(_.summary).flatMap(_.find(_._1 == p.color).map(_._2)).map(s ⇒
|
||||
JsObject(s map {
|
||||
case (nag, nb) ⇒ nag.toString.toLowerCase -> JsNumber(nb)
|
||||
})
|
||||
)
|
||||
).noNull
|
||||
}),
|
||||
"winner" -> g.winnerColor.map(_.name),
|
||||
"url" -> url
|
||||
).noNull
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 2337480c6f859bb894a94749616987683991801c
|
||||
Subproject commit eb18548811840876a9264da3d799ede75fe66e13
|
|
@ -11,7 +11,7 @@ import org.joda.time.format.DateTimeFormat
|
|||
import lila.hub.actorApi.router.{ Abs, Watcher }
|
||||
import lila.user.User
|
||||
|
||||
private[game] final class PgnDump(
|
||||
final class PgnDump(
|
||||
router: ActorSelection,
|
||||
findUser: String ⇒ Fu[Option[User]]) {
|
||||
|
||||
|
|
Loading…
Reference in New Issue