load analysis move destinations on demand

This commit is contained in:
Thibault Duplessis 2015-05-13 17:44:15 +02:00
parent ee686d74dc
commit 1893d47fc3
12 changed files with 142 additions and 35 deletions

View file

@ -64,8 +64,7 @@ object Analyse extends LilaController {
tv = none,
analysis.map(pgn -> _),
initialFenO = initialFen.some,
withMoveTimes = true,
withPossibleMoves = true) map { data =>
withMoveTimes = true) map { data =>
Ok(html.analyse.replay(
pov,
data,

View file

@ -33,7 +33,7 @@ private[api] final class RoundApi(
blindMode _ compose
withTournament(pov, tourOption)_ compose
withSimul(pov, simulOption)_ compose
withSteps(pov, none, initialFen, false)_ compose
withSteps(pov, none, initialFen)_ compose
withNote(note)_
)(json)
}
@ -42,8 +42,7 @@ private[api] final class RoundApi(
def watcher(pov: Pov, apiVersion: Int, tv: Option[Boolean],
analysis: Option[(Pgn, Analysis)] = None,
initialFenO: Option[Option[String]] = None,
withMoveTimes: Boolean = false,
withPossibleMoves: Boolean = false)(implicit ctx: Context): Fu[JsObject] =
withMoveTimes: Boolean = false)(implicit ctx: Context): Fu[JsObject] =
initialFenO.fold(GameRepo initialFen pov.game)(fuccess) flatMap { initialFen =>
jsonView.watcherJson(pov, ctx.pref, apiVersion, ctx.me, tv,
withBlurs = ctx.me ?? Granter(_.ViewBlurs),
@ -57,17 +56,17 @@ private[api] final class RoundApi(
withTournament(pov, tourOption)_ compose
withSimul(pov, simulOption)_ compose
withNote(note)_ compose
withSteps(pov, analysis, initialFen, withPossibleMoves)_ compose
withSteps(pov, analysis, initialFen)_ compose
withAnalysis(analysis)_
)(json)
}
}
def userAnalysisJson(pov: Pov, pref: Pref, initialFen: Option[String]) =
jsonView.userAnalysisJson(pov, pref) map withSteps(pov, none, initialFen, true)_
jsonView.userAnalysisJson(pov, pref) map withSteps(pov, none, initialFen)_
private def withSteps(pov: Pov, a: Option[(Pgn, Analysis)], initialFen: Option[String], possibleMoves: Boolean)(obj: JsObject) =
obj + ("steps" -> lila.round.StepBuilder(pov.game.id, pov.game.pgnMoves, pov.game.variant, a, initialFen, possibleMoves))
private def withSteps(pov: Pov, a: Option[(Pgn, Analysis)], initialFen: Option[String])(obj: JsObject) =
obj + ("steps" -> lila.round.StepBuilder(pov.game.id, pov.game.pgnMoves, pov.game.variant, a, initialFen))
private def withNote(note: String)(json: JsObject) =
if (note.isEmpty) json else json + ("note" -> JsString(note))

View file

@ -15,14 +15,12 @@ object StepBuilder {
pgnMoves: List[String],
variant: Variant,
a: Option[(Pgn, Analysis)],
initialFen: Option[String],
possibleMoves: Boolean): JsArray = {
initialFen: Option[String]): JsArray = {
chess.Replay.gameWhileValid(pgnMoves, initialFen, variant) match {
case (games, error) =>
error foreach logChessError(id)
val lastPly = games.lastOption.??(_.turns)
val steps = games.map { g =>
val withDests = possibleMoves && !(lastPly == g.turns && g.situation.end)
Step(
ply = g.turns,
move = for {
@ -31,13 +29,13 @@ object StepBuilder {
} yield Step.Move(pos._1, pos._2, san),
fen = Forsyth >> g,
check = g.situation.check,
dests = withDests ?? g.situation.destinations)
dests = None)
}
JsArray(a.fold[Seq[Step]](steps) {
case (pgn, analysis) => applyAnalysisAdvices(
id,
applyAnalysisEvals(steps.toList, analysis),
pgn, analysis, variant, possibleMoves)
applyAnalysisEvals(steps, analysis),
pgn, analysis, variant)
}.map(_.toJson))
}
}
@ -57,8 +55,7 @@ object StepBuilder {
steps: List[Step],
pgn: Pgn,
analysis: Analysis,
variant: Variant,
possibleMoves: Boolean): List[Step] =
variant: Variant): List[Step] =
analysis.advices.foldLeft(steps) {
case (steps, ad) => (for {
before <- steps lift (ad.ply - 1)
@ -67,17 +64,16 @@ object StepBuilder {
nag = ad.nag.symbol.some,
comments = ad.makeComment(false, true) :: after.comments,
variations = if (ad.info.variation.isEmpty) after.variations
else makeVariation(gameId, before, ad.info, variant, possibleMoves).toList :: after.variations))
else makeVariation(gameId, before, ad.info, variant).toList :: after.variations))
) | steps
}
private def makeVariation(gameId: String, fromStep: Step, info: Info, variant: Variant, possibleMoves: Boolean): List[Step] = {
private def makeVariation(gameId: String, fromStep: Step, info: Info, variant: Variant): List[Step] = {
chess.Replay.gameWhileValid(info.variation take 20, fromStep.fen.some, variant) match {
case (games, error) =>
error foreach logChessError(gameId)
val lastPly = games.lastOption.??(_.turns)
games.drop(1).map { g =>
val withDests = possibleMoves && !(lastPly == g.turns && g.situation.end)
Step(
ply = g.turns,
move = for {
@ -87,7 +83,7 @@ object StepBuilder {
} yield Step.Move(orig, dest, san),
fen = Forsyth >> g,
check = g.situation.check,
dests = withDests ?? g.situation.destinations)
dests = None)
}
}
}

View file

@ -16,7 +16,7 @@ class StepBuilderPerfTest extends Specification {
// val iterations = 1
def runOne(moves: List[String]) =
StepBuilder("abcd1234", moves, chess.variant.Standard, None, None, true)
StepBuilder("abcd1234", moves, chess.variant.Standard, None, None)
def run { gameMoves foreach runOne }
"playing a game" should {

View file

@ -0,0 +1,27 @@
package lila.socket
import lila.common.PimpedJson._
import play.api.libs.json.JsObject
case class AnaDests(
variant: chess.variant.Variant,
fen: String,
path: String) {
def dests: String = chess.Game(variant.some, fen.some).situation.destinations map {
case (orig, dests) => s"${orig.piotr}${dests.map(_.piotr).mkString}"
} mkString " "
}
object AnaDests {
def parse(o: JsObject) = for {
d o obj "d"
variant = chess.variant.Variant orDefault ~d.str("variant")
fen d str "fen"
path d str "path"
} yield AnaDests(
variant = variant,
fen = fen,
path = path)
}

View file

@ -20,7 +20,7 @@ case class AnaMove(
},
fen = chess.format.Forsyth >> game,
check = game.situation.check,
dests = !game.situation.end ?? game.situation.destinations)
dests = Some(!game.situation.end ?? game.situation.destinations))
}
}
@ -30,7 +30,7 @@ object AnaMove {
d o obj "d"
orig d str "orig" flatMap chess.Pos.posAt
dest d str "dest" flatMap chess.Pos.posAt
variant d str "variant" map chess.variant.Variant.orDefault
variant = chess.variant.Variant orDefault ~d.str("variant")
fen d str "fen"
path d str "path"
prom = d str "promotion" flatMap chess.Role.promotable

View file

@ -39,6 +39,16 @@ object Handler {
member push lila.socket.Socket.makeMessage("stepFailure", err.toString)
}
}
case ("anaDests", o) =>
AnaDests parse o match {
case Some(req) =>
member push lila.socket.Socket.makeMessage("dests", Json.obj(
"dests" -> req.dests,
"path" -> req.path
))
case None =>
member push lila.socket.Socket.makeMessage("destsFailure", "Bad dests request")
}
case _ => // logwarn("Unhandled msg: " + msg)
}

View file

@ -9,7 +9,8 @@ case class Step(
move: Option[Step.Move],
fen: String,
check: Boolean,
dests: Map[Pos, List[Pos]],
// None when not computed yet
dests: Option[Map[Pos, List[Pos]]],
eval: Option[Int] = None,
mate: Option[Int] = None,
nag: Option[String] = None,
@ -36,15 +37,17 @@ object Step {
add("mate", mate) _ compose
add("nag", nag) _ compose
add("comments", comments, comments.nonEmpty) _ compose
add("variations", variations, variations.nonEmpty) _
add("variations", variations, variations.nonEmpty) _ compose
add("dests", dests.map {
_.map {
case (orig, dests) => s"${orig.piotr}${dests.map(_.piotr).mkString}"
}.mkString(" ")
})
)(Json.obj(
"ply" -> ply,
"uci" -> move.map(_.uci),
"san" -> move.map(_.san),
"fen" -> fen,
"dests" -> dests.map {
case (orig, dests) => s"${orig.piotr}${dests.map(_.piotr).mkString}"
}.mkString(" ")))
"fen" -> fen))
}
private def add[A](k: String, v: A, cond: Boolean)(o: JsObject)(implicit writes: Writes[A]): JsObject =

View file

@ -53,4 +53,21 @@ module.exports = function(steps, analysis) {
tree.push(step);
return nextPath;
}.bind(this);
this.addDests = function(dests, path) {
var tree = this.tree;
for (var j in path) {
var p = path[j];
for (var i = 0, nb = tree.length; i < nb; i++) {
if (p.ply === tree[i].ply) {
if (p.variation) {
tree = tree[i].variations[p.variation - 1];
break;
}
tree[i].dests = dests;
return;
}
}
}
}.bind(this);
}

View file

@ -10,6 +10,7 @@ var autoplay = require('./autoplay');
var control = require('./control');
var promotion = require('./promotion');
var readDests = require('./util').readDests;
var debounce = require('./util').debounce;
var socket = require('./socket');
var m = require('mithril');
@ -63,8 +64,8 @@ module.exports = function(opts) {
fen: s.fen,
turnColor: color,
movable: {
color: Object.keys(dests).length === 0 ? null : color,
dests: dests
color: dests && Object.keys(dests).length > 0 ? color : null,
dests: dests || {}
},
check: s.check,
lastMove: s.uci ? [s.uci.substr(0, 2), s.uci.substr(2, 2)] : null,
@ -75,8 +76,18 @@ module.exports = function(opts) {
this.chessground = ground.make(this.data, config, userMove);
this.chessground.set(config);
if (opts.onChange) opts.onChange(config.fen, this.vm.path);
if (!dests) getDests();
}.bind(this);
var getDests = debounce(function() {
if (this.vm.step.dests) return;
this.socket.sendAnaDests({
variant: this.data.game.variant.key,
fen: this.vm.step.fen,
path: this.vm.pathStr
});
}.bind(this), 200, false);
this.jump = function(path) {
this.vm.path = path;
this.vm.pathStr = treePath.write(path);
@ -132,6 +143,11 @@ module.exports = function(opts) {
this.chessground.playPremove();
}.bind(this);
this.addDests = function(dests, path) {
this.analyse.addDests(dests, treePath.read(path));
if (path === this.vm.pathStr) showGround();
}.bind(this);
this.reset = function() {
this.chessground.set(this.vm.situation);
m.redraw();

View file

@ -3,6 +3,7 @@ module.exports = function(send, ctrl) {
this.send = send;
var anaMoveTimeout;
var anaDestsTimeout;
var handlers = {
step: function(data) {
@ -13,6 +14,14 @@ module.exports = function(send, ctrl) {
console.log(data);
clearTimeout(anaMoveTimeout);
ctrl.reset();
},
dests: function(data) {
ctrl.addDests(data.dests, data.path);
clearTimeout(anaDestsTimeout);
},
destsFailure: function(data) {
console.log(data);
clearTimeout(anaDestsTimeout);
}
};
@ -24,8 +33,19 @@ module.exports = function(send, ctrl) {
return false;
}.bind(this);
this.sendAnaMove = function(move) {
this.send('anaMove', move);
anaMoveTimeout = setTimeout(this.sendAnaMove.bind(this, move), 3000);
this.sendAnaMove = function(req) {
withoutStandardVariant(req);
this.send('anaMove', req);
anaMoveTimeout = setTimeout(this.sendAnaMove.bind(this, req), 3000);
}.bind(this);
this.sendAnaDests = function(req) {
withoutStandardVariant(req);
this.send('anaDests', req);
anaDestsTimeout = setTimeout(this.sendAnaDests.bind(this, req), 3000);
}.bind(this);
var withoutStandardVariant = function(obj) {
if (obj.variant === 'standard') delete obj.variant;
};
}

View file

@ -2,6 +2,7 @@ var piotr2key = require('game').piotr.piotr2key;
module.exports = {
readDests: function(lines) {
if (typeof lines === 'undefined') return null;
var dests = {};
if (lines) lines.split(' ').forEach(function(line) {
dests[piotr2key[line[0]]] = line.split('').slice(1).map(function(c) {
@ -15,5 +16,24 @@ module.exports = {
},
empty: function(a) {
return !a || a.length === 0;
},
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
debounce: function(func, wait, immediate) {
var timeout;
return function() {
var context = this,
args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
};