Merge branch 'master' into serverchess

* master:
  fix A.I. variant support, add support for threecheck play & analysis
  fix AI BC for chess960
  remove AI debug
  prepare multiple variant support for A.I. nodes
  ignore new stockfish extra output
  remove stockfish OwnBook option
  more analysis tweaks
  properly render equal eval in analysis UI
  Fix pluralization
This commit is contained in:
Thibault Duplessis 2015-05-04 00:21:51 +02:00
commit 76b7a6db12
23 changed files with 106 additions and 71 deletions

View file

@ -2,6 +2,7 @@ package controllers
import play.api.mvc._
import lila.ai.actorApi.{ Chess960, KingOfTheHill, Variant }
import lila.api.Context
import lila.app._
@ -10,12 +11,17 @@ import play.api.Play.current
object Ai extends LilaController {
private def requestVariant(req: RequestHeader): Variant =
if (get("initialFen", req).isDefined) Chess960
else if (getBool("kingOfTheHill", req)) KingOfTheHill
else Variant(~get("variant", req))
def move = Action.async { req =>
Env.ai.server.move(
uciMoves = get("uciMoves", req) ?? (_.split(' ').toList),
initialFen = get("initialFen", req),
level = getInt("level", req) | 1,
kingOfTheHill = getBool("kingOfTheHill", req)
variant = requestVariant(req)
) fold (
err => {
logwarn("[ai] stockfish server play: " + err)
@ -27,12 +33,11 @@ object Ai extends LilaController {
def analyse = Action.async { req =>
get("replyUrl", req) foreach { replyToUrl =>
println(s"analyse gameId ${get("gameId", req)}")
Env.ai.server.analyse(
uciMoves = get("uciMoves", req) ?? (_.split(' ').toList),
initialFen = get("initialFen", req),
requestedByHuman = getBool("human", req),
kingOfTheHill = getBool("kingOfTheHill", req)
variant = requestVariant(req)
).effectFold(
err => WS.url(replyToUrl).post(err.toString),
infos => WS.url(replyToUrl).post(lila.analyse.Info encodeList infos)

View file

@ -44,8 +44,8 @@ object Analyse extends LilaController {
}
def postAnalysis(id: String) = Action.async(parse.text) { req =>
env.analyser.complete(id, req.body, req.remoteAddress) recoverWith {
case e: lila.common.LilaException => fufail(s"${req.remoteAddress} ${e.message}")
env.analyser.complete(id, req.body, req.remoteAddress) recover {
case e: lila.common.LilaException => logwarn(s"AI ${req.remoteAddress} ${e.message}")
} andThenAnyway {
Env.hub.socket.round ! Tell(id, AnalysisAvailable)
} inject Ok

View file

@ -54,9 +54,10 @@ trait SetupHelper { self: I18nHelper =>
translatedVariantChoices(ctx) :+
variantTuple(chess.variant.FromPosition)
def translatedVariantChoicesWithFenAndKingOfTheHill(implicit ctx: Context) =
def translatedAiVariantChoices(implicit ctx: Context) =
translatedVariantChoices(ctx) :+
variantTuple(chess.variant.KingOfTheHill) :+
variantTuple(chess.variant.ThreeCheck) :+
variantTuple(chess.variant.FromPosition)
def translatedVariantChoicesWithVariantsAndFen(implicit ctx: Context) =

View file

@ -1,7 +1,7 @@
@(form: Form[_], ratings: Map[Int, Int])(implicit ctx: Context)
@fields = {
@setup.variant(form, translatedVariantChoicesWithFenAndKingOfTheHill)
@setup.variant(form, translatedAiVariantChoices)
@fenInput(form("fen"), true)
@setup.timeMode(form, lila.setup.AiConfig)
@trans.level()

View file

@ -17,9 +17,9 @@
<h2>Is it rated?</h2>
@rated.map { r =>
@if(r) {
This tournaments is rated and will affect your rating.
This tournament is rated and will affect your rating.
} else {
This tournaments is *not* rated and will *not* affect your rating.
This tournament is *not* rated and will *not* affect your rating.
}
}.getOrElse {
Some tournaments are rated and will affect your rating.

View file

@ -28,11 +28,13 @@ private[ai] final class ActorFSM(
config.init foreach process.write
loginfo(s"[$name] stockfish is ready")
job.fold(goto(Idle))(start)
case Event(req: Req, none) => stay using Job(req, sender, Nil).some
case Event(req: Req, none) => stay using Job(req, sender, None).some
}
when(Idle) {
case Event(Out(t), _) => sys error s"[$name] Unexpected engine output $t"
case Event(req: Req, _) => start(Job(req, sender, Nil))
case Event(Out(t), _) if t.nonEmpty =>
logwarn(s"""[$name] Unexpected engine output: "$t"""")
stay
case Event(req: Req, _) => start(Job(req, sender, None))
}
when(IsReady) {
case Event(Out("readyok"), Some(Job(req, _, _))) =>
@ -45,7 +47,7 @@ private[ai] final class ActorFSM(
goto(Running)
}
when(Running) {
case Event(Out(t), Some(job)) if t startsWith "info depth" =>
case Event(Out(t), Some(job)) if (t startsWith "info depth") && relevantLine(t) =>
stay using (job + t).some
case Event(Out(t), Some(job)) if t startsWith "bestmove" =>
job.sender ! (job complete t)
@ -60,9 +62,14 @@ private[ai] final class ActorFSM(
case Job(req, sender, _) =>
config prepare req foreach process.write
process write "isready"
goto(IsReady) using Job(req, sender, Nil).some
goto(IsReady) using Job(req, sender, None).some
}
private def relevantLine(l: String) =
!(l contains "currmovenumber") &&
!(l contains "lowerbound") &&
!(l contains "upperbound")
override def postStop() {
println(s"======== $name\n${lastWrite mkString "\n"}\n========")
process.destroy()

View file

@ -26,7 +26,7 @@ final class Client(
for {
fen GameRepo initialFen game
uciMoves uciMemo get game
moveResult move(uciMoves.toList, fen, level, game.variant.kingOfTheHill)
moveResult move(uciMoves.toList, fen, level, Variant(game.variant))
uciMove (UciMove(moveResult.move) toValid s"${game.id} wrong bestmove: $moveResult").future
result game.toChess(uciMove.orig, uciMove.dest, uciMove.promotion).future
(c, move) = result
@ -37,25 +37,25 @@ final class Client(
private val networkLatency = 1 second
def move(uciMoves: List[String], initialFen: Option[String], level: Int, kingOfTheHill: Boolean): Fu[MoveResult] = {
def move(uciMoves: List[String], initialFen: Option[String], level: Int, variant: Variant): Fu[MoveResult] = {
implicit val timeout = makeTimeout(config.playTimeout + networkLatency)
sendRequest(true) {
WS.url(s"$endpoint/move").withQueryString(
"uciMoves" -> uciMoves.mkString(" "),
"initialFen" -> ~initialFen,
"level" -> level.toString,
"kingOfTheHill" -> (kingOfTheHill ?? "1"))
"variant" -> variant.toString)
} map MoveResult.apply
}
def analyse(gameId: String, uciMoves: List[String], initialFen: Option[String], requestedByHuman: Boolean, kingOfTheHill: Boolean) {
def analyse(gameId: String, uciMoves: List[String], initialFen: Option[String], requestedByHuman: Boolean, variant: Variant) {
WS.url(s"$endpoint/analyse").withQueryString(
"replyUrl" -> callbackUrl.replace("%", gameId),
"uciMoves" -> uciMoves.mkString(" "),
"initialFen" -> ~initialFen,
"human" -> requestedByHuman.fold("1", "0"),
"gameId" -> gameId,
"kingOfTheHill" -> (kingOfTheHill ?? "1")).post("go")
"variant" -> variant.toString).post("go")
}
private def sendRequest(retriable: Boolean)(req: WSRequestHolder): Fu[String] =

View file

@ -39,18 +39,13 @@ private[ai] case class Config(
setoption("Threads", nbThreads),
setoption("Ponder", false))
def prepare(req: Req) = req match {
case r: PlayReq => List(
setoption("Skill Level", skill(r.level)),
setoption("UCI_Chess960", r.chess960),
setoption("UCI_KingOfTheHill", r.kingOfTheHill),
setoption("OwnBook", ownBook(r.level)))
case r: AnalReq => List(
setoption("Skill Level", skillMax),
setoption("UCI_Chess960", r.chess960),
setoption("UCI_KingOfTheHill", r.kingOfTheHill),
setoption("OwnBook", true))
}
def prepare(req: Req) = (req match {
case r: PlayReq => setoption("Skill Level", skill(r.level))
case r: AnalReq => setoption("Skill Level", skillMax)
}) :: List(
setoption("UCI_Chess960", req.variant == Chess960),
setoption("UCI_KingOfTheHill", req.variant == KingOfTheHill),
setoption("UCI_3Check", req.variant == ThreeCheck))
def go(req: Req): List[String] = req match {
case r: PlayReq => List(

View file

@ -43,8 +43,8 @@ final class Env(
// api actor
system.actorOf(Props(new Actor {
def receive = {
case lila.hub.actorApi.ai.Analyse(gameId, uciMoves, fen, requestedByHuman, kingOfTheHill) =>
client.analyse(gameId, uciMoves, fen, requestedByHuman, kingOfTheHill)
case lila.hub.actorApi.ai.Analyse(gameId, uciMoves, fen, requestedByHuman, variant) =>
client.analyse(gameId, uciMoves, fen, requestedByHuman, actorApi.Variant(variant))
}
}), name = ActorName)

View file

@ -58,14 +58,14 @@ private[ai] final class Queue(config: Config) extends Actor {
tasks += Task(req, sender, timeout)
}
case FullAnalReq(moves, fen, requestedByHuman, kingOfTheHill) if (requestedByHuman || tasks.size < maxTasks) =>
case FullAnalReq(moves, fen, requestedByHuman, variant) if (requestedByHuman || tasks.size < maxTasks) =>
val mrSender = sender
val size = moves.size
implicit val timeout = makeTimeout {
if (requestedByHuman) 1.hour else 24.hours
}
val futures = (0 to size) map moves.take map { serie =>
self ? AnalReq(serie, fen, size, requestedByHuman, kingOfTheHill) mapTo manifest[Option[Evaluation]]
self ? AnalReq(serie, fen, size, requestedByHuman, variant) mapTo manifest[Option[Evaluation]]
}
Future.fold(futures)(Vector[Option[Evaluation]]())(_ :+ _) addFailureEffect {
case e => mrSender ! Status.Failure(e)

View file

@ -15,17 +15,17 @@ private[ai] final class Server(
config: Config,
uciMemo: lila.game.UciMemo) {
def move(uciMoves: List[String], initialFen: Option[String], level: Int, kingOfTheHill: Boolean): Fu[MoveResult] = {
def move(uciMoves: List[String], initialFen: Option[String], level: Int, variant: Variant): Fu[MoveResult] = {
implicit val timeout = makeTimeout(config.playTimeout)
queue ? PlayReq(uciMoves, initialFen map chess960Fen, level, kingOfTheHill) mapTo
queue ? PlayReq(uciMoves, initialFen map chess960Fen, level, variant) mapTo
manifest[Option[String]] flatten "[stockfish] play failed" map MoveResult.apply
}
def analyse(uciMoves: List[String], initialFen: Option[String], requestedByHuman: Boolean, kingOfTheHill: Boolean): Fu[List[Info]] = {
def analyse(uciMoves: List[String], initialFen: Option[String], requestedByHuman: Boolean, variant: Variant): Fu[List[Info]] = {
implicit val timeout = makeTimeout {
if (requestedByHuman) 1.hour else 24.hours
}
(queue ? FullAnalReq(uciMoves take config.analyseMaxPlies, initialFen map chess960Fen, requestedByHuman, kingOfTheHill)) mapTo manifest[List[Info]]
(queue ? FullAnalReq(uciMoves take config.analyseMaxPlies, initialFen map chess960Fen, requestedByHuman, variant)) mapTo manifest[List[Info]]
}
private def chess960Fen(fen: String) = (Forsyth << fen).fold(fen) { situation =>

View file

@ -18,22 +18,36 @@ sealed trait Stream { def text: String }
case class Out(text: String) extends Stream
case class Err(text: String) extends Stream
sealed trait Variant
case object Standard extends Variant
case object Chess960 extends Variant
case object KingOfTheHill extends Variant
case object ThreeCheck extends Variant
object Variant {
def apply(str: String): Variant = str.toLowerCase match {
case "chess960" => Chess960
case "kingofthehill" => KingOfTheHill
case "threecheck" => ThreeCheck
case _ => Standard
}
def apply(v: chess.variant.Variant): Variant = apply(v.key)
}
sealed trait Req {
def moves: List[String]
def fen: Option[String]
def analyse: Boolean
def requestedByHuman: Boolean
def priority: Int
def kingOfTheHill: Boolean
def chess960 = fen.isDefined
def variant: Variant
}
case class PlayReq(
moves: List[String],
fen: Option[String],
level: Int,
kingOfTheHill: Boolean) extends Req {
variant: Variant) extends Req {
val analyse = false
val requestedByHuman = true
@ -46,7 +60,7 @@ case class AnalReq(
fen: Option[String],
totalSize: Int,
requestedByHuman: Boolean,
kingOfTheHill: Boolean) extends Req {
variant: Variant) extends Req {
val priority =
if (requestedByHuman) -totalSize
@ -61,15 +75,15 @@ case class FullAnalReq(
moves: List[String],
fen: Option[String],
requestedByHuman: Boolean,
kingOfTheHill: Boolean)
variant: Variant)
case class Job(req: Req, sender: akka.actor.ActorRef, buffer: List[String]) {
case class Job(req: Req, sender: akka.actor.ActorRef, lastLine: Option[String]) {
def +(str: String) = if (req.analyse) copy(buffer = str :: buffer) else this
def +(str: String) = if (req.analyse) copy(lastLine = str.some) else this
// bestmove xyxy ponder xyxy
def complete(str: String): Option[Any] = req match {
case _: PlayReq => str split ' ' lift 1
case _: AnalReq => buffer.headOption map EvaluationParser.apply
case _: AnalReq => lastLine map EvaluationParser.apply
}
}

View file

@ -50,7 +50,7 @@ final class Analyser(
chess.Replay(game.pgnMoves, initialFen, game.variant).fold(
fufail(_),
replay => {
ai ! lila.hub.actorApi.ai.Analyse(game.id, UciDump(replay), initialFen, requestedByHuman = !auto, game.variant.kingOfTheHill)
ai ! lila.hub.actorApi.ai.Analyse(game.id, UciDump(replay), initialFen, requestedByHuman = !auto, game.variant)
AnalysisRepo byId id flatten "Missing analysis"
}
)

View file

@ -9,6 +9,8 @@ case class Evaluation(
def checkMate = mate == Some(0)
def invalid = score.isEmpty && mate.isEmpty
override def toString = s"Evaluation ${score.fold("?")(_.showPawns)} ${mate | 0} ${line.mkString(" ")}"
}

View file

@ -44,13 +44,13 @@ object Info {
lazy val start = Info(0, Evaluation.start.score, none, Nil)
def decode(ply: Int, str: String): Option[Info] = str.split(separator).toList match {
case Nil => Info(ply).some
case List(cp) => Info(ply, Score(cp)).some
case List(cp, ma) => Info(ply, Score(cp), parseIntOption(ma)).some
case List(cp, ma, va) => Info(ply, Score(cp), parseIntOption(ma), va.split(' ').toList).some
case List(cp, ma, va, be) => Info(ply, Score(cp), parseIntOption(ma), va.split(' ').toList, UciMove piotr be).some
case _ => none
def decode(ply: Int, str: String): Option[Info] = str.split(separator) match {
case Array() => Info(ply).some
case Array(cp) => Info(ply, Score(cp)).some
case Array(cp, ma) => Info(ply, Score(cp), parseIntOption(ma)).some
case Array(cp, ma, va) => Info(ply, Score(cp), parseIntOption(ma), va.split(' ').toList).some
case Array(cp, ma, va, be) => Info(ply, Score(cp), parseIntOption(ma), va.split(' ').toList, UciMove piotr be).some
case _ => none
}
def decodeList(str: String): Option[List[Info]] = {

View file

@ -426,7 +426,8 @@ object Game {
val analysableVariants: Set[Variant] = Set(
chess.variant.Standard,
chess.variant.Chess960,
chess.variant.KingOfTheHill)
chess.variant.KingOfTheHill,
chess.variant.ThreeCheck)
val unanalysableVariants: Set[Variant] = Variant.all.toSet -- analysableVariants

View file

@ -49,11 +49,11 @@ case class Shutup(userId: String, text: String)
}
package shutup {
case class RecordPublicForumMessage(userId: String, text: String)
case class RecordTeamForumMessage(userId: String, text: String)
case class RecordPrivateMessage(userId: String, toUserId: String, text: String)
case class RecordPrivateChat(chatId: String, userId: String, text: String)
case class RecordPublicChat(chatId: String, userId: String, text: String)
case class RecordPublicForumMessage(userId: String, text: String)
case class RecordTeamForumMessage(userId: String, text: String)
case class RecordPrivateMessage(userId: String, toUserId: String, text: String)
case class RecordPrivateChat(chatId: String, userId: String, text: String)
case class RecordPublicChat(chatId: String, userId: String, text: String)
}
package mod {
@ -163,7 +163,12 @@ case class MakeTeam(id: String, name: String)
}
package ai {
case class Analyse(gameId: String, uciMoves: List[String], initialFen: Option[String], requestedByHuman: Boolean, kingOfTheHill: Boolean)
case class Analyse(
gameId: String,
uciMoves: List[String],
initialFen: Option[String],
requestedByHuman: Boolean,
variant: chess.variant.Variant)
case class AutoAnalyse(gameId: String)
}

View file

@ -103,8 +103,8 @@ trait BaseConfig {
val variantDefault = chess.variant.Standard
val variantsWithFen = variants :+ chess.variant.FromPosition.id
val variantsWithFenAndKingOfTheHill =
variants :+ chess.variant.KingOfTheHill.id :+ chess.variant.FromPosition.id
val aiVariants =
variants :+ chess.variant.KingOfTheHill.id :+ chess.variant.ThreeCheck.id :+ chess.variant.FromPosition.id
val variantsWithVariants =
variants :+ chess.variant.KingOfTheHill.id :+ chess.variant.ThreeCheck.id :+ chess.variant.Antichess.id :+ chess.variant.Atomic.id :+ chess.variant.Horde.id
val variantsWithFenAndVariants =

View file

@ -35,7 +35,7 @@ private[setup] final class FormFactory(casualOnly: Boolean) {
def ai(ctx: UserContext) = Form(
mapping(
"variant" -> variantWithFenAndKingOfTheHill,
"variant" -> aiVariants,
"timeMode" -> timeMode,
"time" -> time,
"increment" -> increment,

View file

@ -11,7 +11,7 @@ object Mappings {
val variant = number.verifying(Config.variants contains _)
val variantWithFen = number.verifying(Config.variantsWithFen contains _)
val variantWithFenAndKingOfTheHill = number.verifying(Config.variantsWithFenAndKingOfTheHill contains _)
val aiVariants = number.verifying(Config.aiVariants contains _)
val variantWithVariants = number.verifying(Config.variantsWithVariants contains _)
val variantWithFenAndVariants = number.verifying(Config.variantsWithFenAndVariants contains _)
val time = number.verifying(HookConfig validateTime _)

View file

@ -1,4 +1,5 @@
var treePath = require('./path');
var defined = require('./util').defined;
module.exports = function(game, analysis) {
@ -20,8 +21,8 @@ module.exports = function(game, analysis) {
var applyAnalysis = function(tree, analysed) {
analysed.forEach(function(ana, i) {
if (!tree[i]) return;
if (ana.mate) tree[i].mate = ana.mate;
else if (ana.eval) tree[i].eval = ana.eval;
if (defined(ana.mate)) tree[i].mate = ana.mate;
else if (defined(ana.eval)) tree[i].eval = ana.eval;
if (ana.comment) tree[i].comments.push(ana.comment);
if (ana.variation) tree[i].variations.push(makeTree(ana.variation.split(' '), i + 1));
});

View file

@ -9,5 +9,8 @@ module.exports = {
});
});
return dests;
},
defined: function(v) {
return typeof v !== 'undefined';
}
};

View file

@ -1,6 +1,7 @@
var m = require('mithril');
var chessground = require('chessground');
var classSet = require('chessground').util.classSet;
var defined = require('./util').defined;
var game = require('game').game;
var partial = require('chessground').util.partial;
var renderStatus = require('game').view.status;
@ -43,8 +44,8 @@ function renderMove(ctrl, move, path) {
'href': '#' + path[0].ply
},
children: [
move.eval ? renderEvalTag(renderEval(move.eval)) : (
move.mate ? renderEvalTag('#' + move.mate) : null
defined(move.eval) ? renderEvalTag(renderEval(move.eval)) : (
defined(move.mate) ? renderEvalTag('#' + move.mate) : null
),
move.san
]