cache UCI data to speed up AI
This commit is contained in:
parent
3087cd05d4
commit
4e6b45dc90
|
@ -13,7 +13,7 @@ object Ai extends LilaController {
|
||||||
def playStockfish = Action.async { req ⇒
|
def playStockfish = Action.async { req ⇒
|
||||||
IfServer {
|
IfServer {
|
||||||
stockfishServer.move(
|
stockfishServer.move(
|
||||||
pgn = ~get("pgn", req),
|
uciMoves = ~get("uciMoves", req),
|
||||||
initialFen = get("initialFen", req),
|
initialFen = get("initialFen", req),
|
||||||
level = getInt("level", req) | 1
|
level = getInt("level", req) | 1
|
||||||
) fold (
|
) fold (
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package lila.ai
|
package lila.ai
|
||||||
|
|
||||||
import chess.format.UciMove
|
import chess.format.{ UciMove, UciDump }
|
||||||
import chess.Move
|
import chess.Move
|
||||||
|
|
||||||
import lila.analyse.AnalysisMaker
|
import lila.analyse.AnalysisMaker
|
||||||
import lila.game.{ Game, Progress, GameRepo, PgnRepo }
|
import lila.game.{ Game, Progress, GameRepo, PgnRepo, UciMemo }
|
||||||
|
|
||||||
trait Ai {
|
trait Ai {
|
||||||
|
|
||||||
|
@ -12,18 +12,21 @@ trait Ai {
|
||||||
for {
|
for {
|
||||||
fen ← game.variant.exotic ?? { GameRepo initialFen game.id }
|
fen ← game.variant.exotic ?? { GameRepo initialFen game.id }
|
||||||
pgn ← PgnRepo get game.id
|
pgn ← PgnRepo get game.id
|
||||||
moveStr ← move(pgn, fen, level)
|
uciMoves ← uciMemo.get(game) map (_ mkString " ")
|
||||||
result ← (for {
|
moveStr ← move(uciMoves, fen, level)
|
||||||
uciMove ← UciMove(moveStr) toValid "Wrong bestmove: " + moveStr
|
uciMove ← (UciMove(moveStr) toValid "Wrong bestmove: " + moveStr).future
|
||||||
result ← (game.toChess withPgnMoves pgn)(uciMove.orig, uciMove.dest)
|
result ← (game.toChess withPgnMoves pgn)(uciMove.orig, uciMove.dest).future
|
||||||
} yield result).future
|
|
||||||
(c, m) = result
|
(c, m) = result
|
||||||
(progress, pgn2) = game.update(c, m)
|
(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
|
} 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]
|
def analyse(pgn: String, initialFen: Option[String]): Fu[AnalysisMaker]
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import lila.common.PimpedConfig._
|
||||||
|
|
||||||
final class Env(
|
final class Env(
|
||||||
config: Config,
|
config: Config,
|
||||||
|
uciMemo: lila.game.UciMemo,
|
||||||
system: ActorSystem) {
|
system: ActorSystem) {
|
||||||
|
|
||||||
private val settings = new {
|
private val settings = new {
|
||||||
|
@ -70,11 +71,13 @@ final class Env(
|
||||||
scheduler = system.scheduler
|
scheduler = system.scheduler
|
||||||
)), name = "stockfish-dispatcher"),
|
)), name = "stockfish-dispatcher"),
|
||||||
fallback = stockfishServer,
|
fallback = stockfishServer,
|
||||||
config = stockfishConfig)
|
config = stockfishConfig,
|
||||||
|
uciMemo = uciMemo)
|
||||||
|
|
||||||
lazy val stockfishServer = new stockfish.Server(
|
lazy val stockfishServer = new stockfish.Server(
|
||||||
queue = stockfishQueue,
|
queue = stockfishQueue,
|
||||||
config = stockfishConfig)
|
config = stockfishConfig,
|
||||||
|
uciMemo = uciMemo)
|
||||||
|
|
||||||
def nbStockfishRemotes = StockfishRemotes.size
|
def nbStockfishRemotes = StockfishRemotes.size
|
||||||
|
|
||||||
|
@ -92,5 +95,6 @@ object Env {
|
||||||
|
|
||||||
lazy val current = "[boot] ai" describes new Env(
|
lazy val current = "[boot] ai" describes new Env(
|
||||||
config = lila.common.PlayApp loadConfig "ai",
|
config = lila.common.PlayApp loadConfig "ai",
|
||||||
|
uciMemo = lila.game.Env.current.uciMemo,
|
||||||
system = lila.common.PlayApp.system)
|
system = lila.common.PlayApp.system)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,16 +17,16 @@ import lila.hub.actorApi.ai.GetLoad
|
||||||
final class Client(
|
final class Client(
|
||||||
dispatcher: ActorRef,
|
dispatcher: ActorRef,
|
||||||
fallback: lila.ai.Ai,
|
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)
|
implicit val timeout = makeTimeout(config.playTimeout)
|
||||||
dispatcher ? Play(pgn, ~initialFen, level) mapTo manifest[String]
|
dispatcher ? Play(uciMoves, ~initialFen, level) mapTo manifest[String]
|
||||||
} recoverWith {
|
} 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] = {
|
def analyse(pgn: String, initialFen: Option[String]): Fu[AnalysisMaker] = {
|
||||||
implicit val timeout = makeTimeout(config.analyseTimeout)
|
implicit val timeout = makeTimeout(config.analyseTimeout)
|
||||||
dispatcher ? Analyse(pgn, ~initialFen) mapTo manifest[String] flatMap { str ⇒
|
dispatcher ? Analyse(pgn, ~initialFen) mapTo manifest[String] flatMap { str ⇒
|
||||||
|
|
|
@ -5,30 +5,31 @@ import scala.concurrent.duration._
|
||||||
|
|
||||||
import akka.actor.{ Props, Actor, ActorRef, Kill }
|
import akka.actor.{ Props, Actor, ActorRef, Kill }
|
||||||
import akka.pattern.{ ask, AskTimeoutException }
|
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.libs.concurrent.Akka.system
|
||||||
import play.api.Play.current
|
import play.api.Play.current
|
||||||
|
|
||||||
import actorApi._
|
import actorApi._
|
||||||
import chess.format.Forsyth
|
|
||||||
import chess.format.UciDump
|
|
||||||
import chess.Variant.Chess960
|
|
||||||
import lila.analyse.AnalysisMaker
|
import lila.analyse.AnalysisMaker
|
||||||
import lila.hub.actorApi.ai.GetLoad
|
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)
|
implicit val timeout = makeTimeout(config.playTimeout)
|
||||||
UciDump(pgn, initialFen, initialFen.isDefined option Chess960).future flatMap { moves ⇒
|
queue ? PlayReq(uciMoves, initialFen map chess960Fen, level) mapTo
|
||||||
queue ? PlayReq(moves, initialFen map chess960Fen, level) mapTo
|
manifest[Valid[String]] flatMap (_.future)
|
||||||
manifest[Valid[String]] flatMap (_.future)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def analyse(pgn: String, initialFen: Option[String]): Fu[AnalysisMaker] = {
|
def analyse(pgn: String, initialFen: Option[String]): Fu[AnalysisMaker] = {
|
||||||
implicit val timeout = makeTimeout(config.analyseTimeout)
|
implicit val timeout = makeTimeout(config.analyseTimeout)
|
||||||
UciDump(pgn, initialFen, initialFen.isDefined option Chess960).future flatMap { moves ⇒
|
UciDump(pgn, initialFen, initialFen.isDefined.fold(Variant.Chess960, Variant.Standard)).future flatMap { moves ⇒
|
||||||
queue ? FullAnalReq(moves, initialFen map chess960Fen) mapTo
|
queue ? FullAnalReq(moves mkString " ", initialFen map chess960Fen) mapTo
|
||||||
manifest[Valid[AnalysisMaker]] flatMap (_.future)
|
manifest[Valid[AnalysisMaker]] flatMap (_.future)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,8 @@ private[ai] final class Connection(
|
||||||
case Failure(e) ⇒ load = none
|
case Failure(e) ⇒ load = none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case Play(pgn, fen, level) ⇒ WS.url(router.play).withQueryString(
|
case Play(uciMoves, fen, level) ⇒ WS.url(router.play).withQueryString(
|
||||||
"pgn" -> pgn,
|
"uciMoves" -> uciMoves,
|
||||||
"initialFen" -> fen,
|
"initialFen" -> fen,
|
||||||
"level" -> level.toString
|
"level" -> level.toString
|
||||||
).get() map (_.body) pipeTo sender
|
).get() map (_.body) pipeTo sender
|
||||||
|
|
|
@ -4,5 +4,5 @@ import lila.game.Game
|
||||||
|
|
||||||
case object CalculateLoad
|
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)
|
case class Analyse(pgn: String, fen: String)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 89cdb68fd8a6617de5ef8395fc3dc5937e2d00b2
|
Subproject commit 01ffb2ba6fea914327e69364e0730157047237cc
|
|
@ -26,6 +26,7 @@ final class Env(
|
||||||
val ActorName = config getString "actor.name"
|
val ActorName = config getString "actor.name"
|
||||||
val FeaturedContinue = config duration "featured.continue"
|
val FeaturedContinue = config duration "featured.continue"
|
||||||
val FeaturedDisrupt = config duration "featured.disrupt"
|
val FeaturedDisrupt = config duration "featured.disrupt"
|
||||||
|
val UciMemoTtl = config duration "uci_memo.ttl"
|
||||||
}
|
}
|
||||||
import settings._
|
import settings._
|
||||||
|
|
||||||
|
@ -53,6 +54,8 @@ final class Env(
|
||||||
|
|
||||||
lazy val gameJs = new GameJs(path = jsPath, useCache = isProd)
|
lazy val gameJs = new GameJs(path = jsPath, useCache = isProd)
|
||||||
|
|
||||||
|
lazy val uciMemo = new UciMemo(UciMemoTtl)
|
||||||
|
|
||||||
// load captcher actor
|
// load captcher actor
|
||||||
private val captcher = system.actorOf(Props(new Captcher), name = CaptcherName)
|
private val captcher = system.actorOf(Props(new Captcher), name = CaptcherName)
|
||||||
|
|
||||||
|
|
39
modules/game/src/main/UciMemo.scala
Normal file
39
modules/game/src/main/UciMemo.scala
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ final class Env(
|
||||||
ai: lila.ai.Ai,
|
ai: lila.ai.Ai,
|
||||||
getUsername: String ⇒ Fu[Option[String]],
|
getUsername: String ⇒ Fu[Option[String]],
|
||||||
getUsernameOrAnon: String ⇒ Fu[String],
|
getUsernameOrAnon: String ⇒ Fu[String],
|
||||||
|
uciMemo: lila.game.UciMemo,
|
||||||
i18nKeys: lila.i18n.I18nKeys,
|
i18nKeys: lila.i18n.I18nKeys,
|
||||||
scheduler: lila.common.Scheduler) {
|
scheduler: lila.common.Scheduler) {
|
||||||
|
|
||||||
|
@ -91,7 +92,8 @@ final class Env(
|
||||||
notifyMove = notifyMove,
|
notifyMove = notifyMove,
|
||||||
finisher = finisher,
|
finisher = finisher,
|
||||||
cheatDetector = cheatDetector,
|
cheatDetector = cheatDetector,
|
||||||
roundMap = hub.actor.roundMap)
|
roundMap = hub.actor.roundMap,
|
||||||
|
uciMemo = uciMemo)
|
||||||
|
|
||||||
private lazy val drawer = new Drawer(
|
private lazy val drawer = new Drawer(
|
||||||
messenger = messenger,
|
messenger = messenger,
|
||||||
|
@ -166,6 +168,7 @@ object Env {
|
||||||
ai = lila.ai.Env.current.ai,
|
ai = lila.ai.Env.current.ai,
|
||||||
getUsername = lila.user.Env.current.usernameOption,
|
getUsername = lila.user.Env.current.usernameOption,
|
||||||
getUsernameOrAnon = lila.user.Env.current.usernameOrAnonymous,
|
getUsernameOrAnon = lila.user.Env.current.usernameOrAnonymous,
|
||||||
|
uciMemo = lila.game.Env.current.uciMemo,
|
||||||
i18nKeys = lila.i18n.Env.current.keys,
|
i18nKeys = lila.i18n.Env.current.keys,
|
||||||
scheduler = lila.common.PlayApp.scheduler)
|
scheduler = lila.common.PlayApp.scheduler)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import chess.{ Status, Role, Color }
|
||||||
|
|
||||||
import actorApi.round.{ HumanPlay, AiPlay, DrawNo, TakebackNo, PlayResult, Cheat }
|
import actorApi.round.{ HumanPlay, AiPlay, DrawNo, TakebackNo, PlayResult, Cheat }
|
||||||
import lila.ai.Ai
|
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
|
import lila.hub.actorApi.map.Tell
|
||||||
|
|
||||||
private[round] final class Player(
|
private[round] final class Player(
|
||||||
|
@ -14,7 +14,8 @@ private[round] final class Player(
|
||||||
notifyMove: (String, String, Option[String]) ⇒ Unit,
|
notifyMove: (String, String, Option[String]) ⇒ Unit,
|
||||||
finisher: Finisher,
|
finisher: Finisher,
|
||||||
cheatDetector: CheatDetector,
|
cheatDetector: CheatDetector,
|
||||||
roundMap: akka.actor.ActorSelection) {
|
roundMap: akka.actor.ActorSelection,
|
||||||
|
uciMemo: UciMemo) {
|
||||||
|
|
||||||
def human(play: HumanPlay)(pov: Pov): Fu[Events] = play match {
|
def human(play: HumanPlay)(pov: Pov): Fu[Events] = play match {
|
||||||
case HumanPlay(playerId, origS, destS, promS, blur, lag, onFailure) ⇒ pov 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
|
chessGame = game.toChess withPgnMoves pgnString
|
||||||
newChessGameAndMove ← chessGame(orig, dest, promotion, lag)
|
newChessGameAndMove ← chessGame(orig, dest, promotion, lag)
|
||||||
(newChessGame, move) = newChessGameAndMove
|
(newChessGame, move) = newChessGameAndMove
|
||||||
} yield game.update(newChessGame, move, blur)).prefixFailuresWith(playerId + " - ").fold(fufail(_), {
|
} yield game.update(newChessGame, move, blur) -> move).prefixFailuresWith(playerId + " - ").future flatMap {
|
||||||
case (progress, pgn) ⇒
|
case ((progress, pgn), move) ⇒
|
||||||
(GameRepo save progress) >>
|
(GameRepo save progress) >>
|
||||||
PgnRepo.save(pov.gameId, pgn) >>-
|
PgnRepo.save(pov.gameId, pgn) >>- {
|
||||||
|
if (pov.game.hasAi) uciMemo.add(pov.game, move)
|
||||||
|
} >>-
|
||||||
notifyProgress(progress) >>
|
notifyProgress(progress) >>
|
||||||
progress.game.finished.fold(
|
progress.game.finished.fold(
|
||||||
moveFinish(progress.game, color) map { progress.events ::: _ }, {
|
moveFinish(progress.game, color) map { progress.events ::: _ }, {
|
||||||
|
@ -43,7 +46,7 @@ private[round] final class Player(
|
||||||
}
|
}
|
||||||
} inject progress.events
|
} inject progress.events
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
} addFailureEffect onFailure
|
} addFailureEffect onFailure
|
||||||
case _ ⇒ fufail("Not your turn")
|
case _ ⇒ fufail("Not your turn")
|
||||||
}
|
}
|
||||||
|
@ -52,7 +55,7 @@ private[round] final class Player(
|
||||||
def ai(play: AiPlay)(game: Game): Fu[Events] =
|
def ai(play: AiPlay)(game: Game): Fu[Events] =
|
||||||
(game.playable && game.player.isAi).fold(
|
(game.playable && game.player.isAi).fold(
|
||||||
engine.play(game, game.aiLevel | 1) flatMap { progress ⇒
|
engine.play(game, game.aiLevel | 1) flatMap { progress ⇒
|
||||||
notifyProgress(progress)
|
notifyProgress(progress)
|
||||||
moveFinish(progress.game, game.turnColor) map { progress.events ::: _ }
|
moveFinish(progress.game, game.turnColor) map { progress.events ::: _ }
|
||||||
} addFailureEffect play.onFailure,
|
} addFailureEffect play.onFailure,
|
||||||
fufail("not AI turn")
|
fufail("not AI turn")
|
||||||
|
|
|
@ -111,7 +111,7 @@ object ApplicationBuild extends Build {
|
||||||
)
|
)
|
||||||
|
|
||||||
lazy val ai = project("ai", Seq(common, hub, chess, game, analyse)).settings(
|
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(
|
lazy val security = project("security", Seq(common, hub, db, user)).settings(
|
||||||
|
|
Loading…
Reference in a new issue