implement and document new analyse API

pull/83/head
Thibault Duplessis 2014-01-27 23:20:08 +01:00
parent 33598abdc3
commit 80e2e25ae1
9 changed files with 188 additions and 29 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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
})
}

View File

@ -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)

View File

@ -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

View File

@ -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]]) {