From 4e6b45dc90e5b4da90c1b5e9bffb6e33bc248cf1 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 2 Oct 2013 10:17:31 +0200 Subject: [PATCH] cache UCI data to speed up AI --- app/controllers/Ai.scala | 2 +- modules/ai/src/main/Ai.scala | 21 +++++----- modules/ai/src/main/Env.scala | 8 +++- modules/ai/src/main/stockfish/Client.scala | 10 ++--- modules/ai/src/main/stockfish/Server.scala | 23 +++++------ .../main/stockfish/remote/Connection.scala | 4 +- .../src/main/stockfish/remote/actorApi.scala | 2 +- modules/chess | 2 +- modules/game/src/main/Env.scala | 3 ++ modules/game/src/main/UciMemo.scala | 39 +++++++++++++++++++ modules/round/src/main/Env.scala | 5 ++- modules/round/src/main/Player.scala | 17 ++++---- project/Build.scala | 2 +- 13 files changed, 97 insertions(+), 41 deletions(-) create mode 100644 modules/game/src/main/UciMemo.scala diff --git a/app/controllers/Ai.scala b/app/controllers/Ai.scala index 63b94e9b2a..3384e940bb 100644 --- a/app/controllers/Ai.scala +++ b/app/controllers/Ai.scala @@ -13,7 +13,7 @@ object Ai extends LilaController { def playStockfish = Action.async { req ⇒ IfServer { stockfishServer.move( - pgn = ~get("pgn", req), + uciMoves = ~get("uciMoves", req), initialFen = get("initialFen", req), level = getInt("level", req) | 1 ) fold ( diff --git a/modules/ai/src/main/Ai.scala b/modules/ai/src/main/Ai.scala index 58c8a910ac..5c3c85d16a 100644 --- a/modules/ai/src/main/Ai.scala +++ b/modules/ai/src/main/Ai.scala @@ -1,10 +1,10 @@ package lila.ai -import chess.format.UciMove +import chess.format.{ UciMove, UciDump } import chess.Move import lila.analyse.AnalysisMaker -import lila.game.{ Game, Progress, GameRepo, PgnRepo } +import lila.game.{ Game, Progress, GameRepo, PgnRepo, UciMemo } trait Ai { @@ -12,18 +12,21 @@ trait Ai { for { fen ← game.variant.exotic ?? { GameRepo initialFen game.id } pgn ← PgnRepo get game.id - moveStr ← move(pgn, fen, level) - result ← (for { - uciMove ← UciMove(moveStr) toValid "Wrong bestmove: " + moveStr - result ← (game.toChess withPgnMoves pgn)(uciMove.orig, uciMove.dest) - } yield result).future + uciMoves ← uciMemo.get(game) map (_ mkString " ") + moveStr ← move(uciMoves, fen, level) + uciMove ← (UciMove(moveStr) toValid "Wrong bestmove: " + moveStr).future + result ← (game.toChess withPgnMoves pgn)(uciMove.orig, uciMove.dest).future (c, m) = result (progress, pgn2) = game.update(c, m) - _ ← (GameRepo save progress) >> PgnRepo.save(game.id, pgn2) + _ ← (GameRepo save progress) >> + PgnRepo.save(game.id, pgn2) >>- + uciMemo.add(game, uciMove.uci) } yield progress } - def move(pgn: String, initialFen: Option[String], level: Int): Fu[String] + def uciMemo: UciMemo + + def move(uciMoves: String, initialFen: Option[String], level: Int): Fu[String] def analyse(pgn: String, initialFen: Option[String]): Fu[AnalysisMaker] diff --git a/modules/ai/src/main/Env.scala b/modules/ai/src/main/Env.scala index a76a1fc99e..b3189ca5d0 100644 --- a/modules/ai/src/main/Env.scala +++ b/modules/ai/src/main/Env.scala @@ -10,6 +10,7 @@ import lila.common.PimpedConfig._ final class Env( config: Config, + uciMemo: lila.game.UciMemo, system: ActorSystem) { private val settings = new { @@ -70,11 +71,13 @@ final class Env( scheduler = system.scheduler )), name = "stockfish-dispatcher"), fallback = stockfishServer, - config = stockfishConfig) + config = stockfishConfig, + uciMemo = uciMemo) lazy val stockfishServer = new stockfish.Server( queue = stockfishQueue, - config = stockfishConfig) + config = stockfishConfig, + uciMemo = uciMemo) def nbStockfishRemotes = StockfishRemotes.size @@ -92,5 +95,6 @@ object Env { lazy val current = "[boot] ai" describes new Env( config = lila.common.PlayApp loadConfig "ai", + uciMemo = lila.game.Env.current.uciMemo, system = lila.common.PlayApp.system) } diff --git a/modules/ai/src/main/stockfish/Client.scala b/modules/ai/src/main/stockfish/Client.scala index 2ea0f6495a..5f7a66074a 100644 --- a/modules/ai/src/main/stockfish/Client.scala +++ b/modules/ai/src/main/stockfish/Client.scala @@ -17,16 +17,16 @@ import lila.hub.actorApi.ai.GetLoad final class Client( dispatcher: ActorRef, fallback: lila.ai.Ai, - config: Config) extends lila.ai.Ai { + config: Config, + val uciMemo: lila.game.UciMemo) extends lila.ai.Ai { - def move(pgn: String, initialFen: Option[String], level: Int): Fu[String] = { + def move(uciMoves: String, initialFen: Option[String], level: Int): Fu[String] = { implicit val timeout = makeTimeout(config.playTimeout) - dispatcher ? Play(pgn, ~initialFen, level) mapTo manifest[String] + dispatcher ? Play(uciMoves, ~initialFen, level) mapTo manifest[String] } recoverWith { - case e: Exception ⇒ fallback.move( pgn, initialFen, level) + case e: Exception ⇒ fallback.move(uciMoves, initialFen, level) } - def analyse(pgn: String, initialFen: Option[String]): Fu[AnalysisMaker] = { implicit val timeout = makeTimeout(config.analyseTimeout) dispatcher ? Analyse(pgn, ~initialFen) mapTo manifest[String] flatMap { str ⇒ diff --git a/modules/ai/src/main/stockfish/Server.scala b/modules/ai/src/main/stockfish/Server.scala index 7881fd3150..c349ab9abf 100644 --- a/modules/ai/src/main/stockfish/Server.scala +++ b/modules/ai/src/main/stockfish/Server.scala @@ -5,30 +5,31 @@ import scala.concurrent.duration._ import akka.actor.{ Props, Actor, ActorRef, Kill } import akka.pattern.{ ask, AskTimeoutException } +import chess.format.Forsyth +import chess.format.UciDump +import chess.Variant import play.api.libs.concurrent.Akka.system import play.api.Play.current import actorApi._ -import chess.format.Forsyth -import chess.format.UciDump -import chess.Variant.Chess960 import lila.analyse.AnalysisMaker import lila.hub.actorApi.ai.GetLoad -private[ai] final class Server(queue: ActorRef, config: Config) extends lila.ai.Ai { +private[ai] final class Server( + queue: ActorRef, + config: Config, + val uciMemo: lila.game.UciMemo) extends lila.ai.Ai { - def move(pgn: String, initialFen: Option[String], level: Int): Fu[String] = { + def move(uciMoves: String, initialFen: Option[String], level: Int): Fu[String] = { implicit val timeout = makeTimeout(config.playTimeout) - UciDump(pgn, initialFen, initialFen.isDefined option Chess960).future flatMap { moves ⇒ - queue ? PlayReq(moves, initialFen map chess960Fen, level) mapTo - manifest[Valid[String]] flatMap (_.future) - } + queue ? PlayReq(uciMoves, initialFen map chess960Fen, level) mapTo + manifest[Valid[String]] flatMap (_.future) } def analyse(pgn: String, initialFen: Option[String]): Fu[AnalysisMaker] = { implicit val timeout = makeTimeout(config.analyseTimeout) - UciDump(pgn, initialFen, initialFen.isDefined option Chess960).future flatMap { moves ⇒ - queue ? FullAnalReq(moves, initialFen map chess960Fen) mapTo + UciDump(pgn, initialFen, initialFen.isDefined.fold(Variant.Chess960, Variant.Standard)).future flatMap { moves ⇒ + queue ? FullAnalReq(moves mkString " ", initialFen map chess960Fen) mapTo manifest[Valid[AnalysisMaker]] flatMap (_.future) } } diff --git a/modules/ai/src/main/stockfish/remote/Connection.scala b/modules/ai/src/main/stockfish/remote/Connection.scala index 356f91b826..14dc7560ac 100644 --- a/modules/ai/src/main/stockfish/remote/Connection.scala +++ b/modules/ai/src/main/stockfish/remote/Connection.scala @@ -30,8 +30,8 @@ private[ai] final class Connection( case Failure(e) ⇒ load = none } } - case Play(pgn, fen, level) ⇒ WS.url(router.play).withQueryString( - "pgn" -> pgn, + case Play(uciMoves, fen, level) ⇒ WS.url(router.play).withQueryString( + "uciMoves" -> uciMoves, "initialFen" -> fen, "level" -> level.toString ).get() map (_.body) pipeTo sender diff --git a/modules/ai/src/main/stockfish/remote/actorApi.scala b/modules/ai/src/main/stockfish/remote/actorApi.scala index a65d4630be..8d01c6c7c8 100644 --- a/modules/ai/src/main/stockfish/remote/actorApi.scala +++ b/modules/ai/src/main/stockfish/remote/actorApi.scala @@ -4,5 +4,5 @@ import lila.game.Game case object CalculateLoad -case class Play(pgn: String, fen: String, level: Int) +case class Play(uciMoves: String, fen: String, level: Int) case class Analyse(pgn: String, fen: String) diff --git a/modules/chess b/modules/chess index 89cdb68fd8..01ffb2ba6f 160000 --- a/modules/chess +++ b/modules/chess @@ -1 +1 @@ -Subproject commit 89cdb68fd8a6617de5ef8395fc3dc5937e2d00b2 +Subproject commit 01ffb2ba6fea914327e69364e0730157047237cc diff --git a/modules/game/src/main/Env.scala b/modules/game/src/main/Env.scala index 44e4f649e6..7c54539cc1 100644 --- a/modules/game/src/main/Env.scala +++ b/modules/game/src/main/Env.scala @@ -26,6 +26,7 @@ final class Env( val ActorName = config getString "actor.name" val FeaturedContinue = config duration "featured.continue" val FeaturedDisrupt = config duration "featured.disrupt" + val UciMemoTtl = config duration "uci_memo.ttl" } import settings._ @@ -53,6 +54,8 @@ final class Env( lazy val gameJs = new GameJs(path = jsPath, useCache = isProd) + lazy val uciMemo = new UciMemo(UciMemoTtl) + // load captcher actor private val captcher = system.actorOf(Props(new Captcher), name = CaptcherName) diff --git a/modules/game/src/main/UciMemo.scala b/modules/game/src/main/UciMemo.scala new file mode 100644 index 0000000000..6216e09cbd --- /dev/null +++ b/modules/game/src/main/UciMemo.scala @@ -0,0 +1,39 @@ +package lila.game + +import scala.concurrent.duration._ + +import chess.format.UciDump + +final class UciMemo(ttl: Duration) { + + private val memo = lila.memo.Builder.expiry[String, Vector[String]](ttl) + + def add(game: Game, uciMove: String) { + val current = Option(memo getIfPresent game.id) | Vector.empty + memo.put(game.id, current :+ uciMove) + } + def add(game: Game, move: chess.Move) { + add(game, UciDump.move(game.variant)(move)) + } + + def set(game: Game, uciMoves: Seq[String]) { + memo.put(game.id, uciMoves.toVector) + } + + def get(game: Game): Fu[Vector[String]] = + Option(memo getIfPresent game.id).filter(_.size == game.turns) match { + case Some(moves) ⇒ fuccess(moves) + case _ ⇒ compute(game) addEffect { set(game, _) } + } + + def delete(game: Game) { + // TODO figure out a best way + memo.put(game.id, Vector.empty) + } + + private def compute(game: Game): Fu[Vector[String]] = for { + fen ← game.variant.exotic ?? { GameRepo initialFen game.id } + pgn ← PgnRepo get game.id + uciMoves ← UciDump(pgn, fen, game.variant).future + } yield uciMoves.toVector +} diff --git a/modules/round/src/main/Env.scala b/modules/round/src/main/Env.scala index f67f4e73ce..bf9ab3768d 100644 --- a/modules/round/src/main/Env.scala +++ b/modules/round/src/main/Env.scala @@ -19,6 +19,7 @@ final class Env( ai: lila.ai.Ai, getUsername: String ⇒ Fu[Option[String]], getUsernameOrAnon: String ⇒ Fu[String], + uciMemo: lila.game.UciMemo, i18nKeys: lila.i18n.I18nKeys, scheduler: lila.common.Scheduler) { @@ -91,7 +92,8 @@ final class Env( notifyMove = notifyMove, finisher = finisher, cheatDetector = cheatDetector, - roundMap = hub.actor.roundMap) + roundMap = hub.actor.roundMap, + uciMemo = uciMemo) private lazy val drawer = new Drawer( messenger = messenger, @@ -166,6 +168,7 @@ object Env { ai = lila.ai.Env.current.ai, getUsername = lila.user.Env.current.usernameOption, getUsernameOrAnon = lila.user.Env.current.usernameOrAnonymous, + uciMemo = lila.game.Env.current.uciMemo, i18nKeys = lila.i18n.Env.current.keys, scheduler = lila.common.PlayApp.scheduler) } diff --git a/modules/round/src/main/Player.scala b/modules/round/src/main/Player.scala index 9ce8934568..11e90928f7 100644 --- a/modules/round/src/main/Player.scala +++ b/modules/round/src/main/Player.scala @@ -6,7 +6,7 @@ import chess.{ Status, Role, Color } import actorApi.round.{ HumanPlay, AiPlay, DrawNo, TakebackNo, PlayResult, Cheat } import lila.ai.Ai -import lila.game.{ Game, GameRepo, PgnRepo, Pov, Progress } +import lila.game.{ Game, GameRepo, PgnRepo, Pov, Progress, UciMemo } import lila.hub.actorApi.map.Tell private[round] final class Player( @@ -14,7 +14,8 @@ private[round] final class Player( notifyMove: (String, String, Option[String]) ⇒ Unit, finisher: Finisher, cheatDetector: CheatDetector, - roundMap: akka.actor.ActorSelection) { + roundMap: akka.actor.ActorSelection, + uciMemo: UciMemo) { def human(play: HumanPlay)(pov: Pov): Fu[Events] = play match { case HumanPlay(playerId, origS, destS, promS, blur, lag, onFailure) ⇒ pov match { @@ -27,10 +28,12 @@ private[round] final class Player( chessGame = game.toChess withPgnMoves pgnString newChessGameAndMove ← chessGame(orig, dest, promotion, lag) (newChessGame, move) = newChessGameAndMove - } yield game.update(newChessGame, move, blur)).prefixFailuresWith(playerId + " - ").fold(fufail(_), { - case (progress, pgn) ⇒ + } yield game.update(newChessGame, move, blur) -> move).prefixFailuresWith(playerId + " - ").future flatMap { + case ((progress, pgn), move) ⇒ (GameRepo save progress) >> - PgnRepo.save(pov.gameId, pgn) >>- + PgnRepo.save(pov.gameId, pgn) >>- { + if (pov.game.hasAi) uciMemo.add(pov.game, move) + } >>- notifyProgress(progress) >> progress.game.finished.fold( moveFinish(progress.game, color) map { progress.events ::: _ }, { @@ -43,7 +46,7 @@ private[round] final class Player( } } inject progress.events }) - }) + } } addFailureEffect onFailure case _ ⇒ fufail("Not your turn") } @@ -52,7 +55,7 @@ private[round] final class Player( def ai(play: AiPlay)(game: Game): Fu[Events] = (game.playable && game.player.isAi).fold( engine.play(game, game.aiLevel | 1) flatMap { progress ⇒ - notifyProgress(progress) + notifyProgress(progress) moveFinish(progress.game, game.turnColor) map { progress.events ::: _ } } addFailureEffect play.onFailure, fufail("not AI turn") diff --git a/project/Build.scala b/project/Build.scala index 65a660ebcf..46b735ba18 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -111,7 +111,7 @@ object ApplicationBuild extends Build { ) lazy val ai = project("ai", Seq(common, hub, chess, game, analyse)).settings( - libraryDependencies ++= provided(play.api) + libraryDependencies ++= provided(play.api, reactivemongo, playReactivemongo) ) lazy val security = project("security", Seq(common, hub, db, user)).settings(