reimplement AI actors
This commit is contained in:
parent
84875d9b4b
commit
602ca57da0
|
@ -15,13 +15,22 @@ final class Env(
|
|||
val EngineName = config getString "engine"
|
||||
val IsServer = config getBoolean "server"
|
||||
val IsClient = config getBoolean "client"
|
||||
val StockfishExecPath = config getString "stockfish.exec_path"
|
||||
val StockfishPlayUrl = config getString "stockfish.play.url"
|
||||
val StockfishAnalyseUrl = config getString "stockfish.analyse.url"
|
||||
val ActorName = config getString "actor.name"
|
||||
}
|
||||
import settings._
|
||||
|
||||
private val stockfishConfig = new stockfish.Config(
|
||||
execPath = config getString "stockfish.exec_path",
|
||||
hashSize = config getInt "stockfish.hash_size",
|
||||
nbThreads = config getInt "stockfish.threads",
|
||||
playMaxMoveTime = config duration "stockfish.play.movetime",
|
||||
analyseMoveTime = config duration "stockfish.analyse.movetime",
|
||||
playTimeout = config duration "stockfish.play.timeout",
|
||||
analyseTimeout = config duration "stockfish.play.timeout",
|
||||
debug = config getBoolean "stockfish.debug")
|
||||
|
||||
val ai: () ⇒ Ai = () ⇒ (EngineName, IsClient) match {
|
||||
case ("stockfish", true) ⇒ stockfishClient or stockfishAi
|
||||
case ("stockfish", false) ⇒ stockfishAi
|
||||
|
@ -53,28 +62,16 @@ final class Env(
|
|||
scheduler.once(10 millis) { clientDiagnose }
|
||||
}
|
||||
|
||||
private lazy val stockfishAi = new stockfish.Ai(
|
||||
server = stockfishServer)
|
||||
private lazy val stockfishAi = new stockfish.Ai(stockfishServer)
|
||||
|
||||
private lazy val stockfishClient = new stockfish.Client(
|
||||
playUrl = StockfishPlayUrl,
|
||||
analyseUrl = StockfishAnalyseUrl)
|
||||
|
||||
lazy val stockfishServer = new stockfish.Server(
|
||||
execPath = StockfishExecPath,
|
||||
config = stockfishConfig)
|
||||
lazy val stockfishServer = new stockfish.Server(stockfishConfig)
|
||||
|
||||
private lazy val stupidAi = new StupidAi
|
||||
|
||||
private lazy val stockfishConfig = new stockfish.Config(
|
||||
hashSize = config getInt "stockfish.hash_size",
|
||||
nbThreads = config getInt "stockfish.threads",
|
||||
playMaxMoveTime = config duration "stockfish.play.movetime",
|
||||
analyseMoveTime = config duration "stockfish.analyse.movetime",
|
||||
playTimeout = config duration "stockfish.play.timeout",
|
||||
analyseTimeout = config duration "stockfish.play.timeout",
|
||||
debug = config getBoolean "stockfish.debug")
|
||||
|
||||
private lazy val client = (EngineName, IsClient) match {
|
||||
case ("stockfish", true) ⇒ stockfishClient.some
|
||||
case _ ⇒ none
|
||||
|
|
|
@ -1,101 +1,77 @@
|
|||
package lila.ai
|
||||
package stockfish
|
||||
|
||||
import akka.actor.{ Props, Actor, ActorRef, Status, FSM ⇒ AkkaFSM, LoggingFSM }
|
||||
import akka.actor.{ Props, Actor, ActorRef, Status, FSM ⇒ AkkaFSM }
|
||||
import model._
|
||||
import model.analyse._
|
||||
|
||||
import actorApi._
|
||||
|
||||
final class ActorFSM(
|
||||
processBuilder: Process.Builder,
|
||||
config: Config)
|
||||
extends Actor with LoggingFSM[State, Data] {
|
||||
extends Actor with AkkaFSM[State, Option[(Req, ActorRef)]] {
|
||||
|
||||
var process: Process = _
|
||||
|
||||
override def preStart() {
|
||||
process = processBuilder(
|
||||
private val process = processBuilder(
|
||||
out ⇒ self ! Out(out),
|
||||
err ⇒ self ! Err(err),
|
||||
config.debug)
|
||||
}
|
||||
|
||||
startWith(Starting, Todo())
|
||||
startWith(Starting, none)
|
||||
|
||||
when(Starting) {
|
||||
case Event(Out(t), _) if t startsWith "Stockfish" ⇒ {
|
||||
case Event(Out(t), data) if t startsWith "Stockfish" ⇒ {
|
||||
process write "uci"
|
||||
stay
|
||||
}
|
||||
case Event(Out("uciok"), data) ⇒ {
|
||||
config.init foreach process.write
|
||||
nextTask(data)
|
||||
data.fold(goto(Idle))(start)
|
||||
}
|
||||
case Event(task: analyse.Task.Builder, data) ⇒
|
||||
stay using (data enqueue task(sender))
|
||||
case Event(task: play.Task.Builder, data) ⇒
|
||||
stay using (data enqueue task(sender))
|
||||
case Event(req: Req, none) ⇒ stay using (req, sender).some
|
||||
}
|
||||
when(Idle) {
|
||||
case Event(Out(t), _) ⇒ { log.warning(t); stay }
|
||||
case Event(Out(t), _) ⇒ { logwarn(t); stay }
|
||||
case Event(req: Req, _) ⇒ start(req, sender)
|
||||
}
|
||||
when(IsReady) {
|
||||
case Event(Out("readyok"), doing: Doing) ⇒ {
|
||||
val lines = config go doing.current
|
||||
lines.lastOption foreach { line =>
|
||||
println("[%d] %s - %s".format(
|
||||
doing.size,
|
||||
doing.current.fold(_ ⇒ "P", _ ⇒ "A"),
|
||||
line))
|
||||
case Event(Out("readyok"), Some((req, _))) ⇒ {
|
||||
val lines = config go req
|
||||
lines.lastOption foreach { line ⇒
|
||||
println(req.analyse.fold("A", "P") + line.replace("go movetime", ""))
|
||||
}
|
||||
lines foreach process.write
|
||||
goto(Running)
|
||||
}
|
||||
}
|
||||
when(Running) {
|
||||
case Event(Out(t), doing: Doing) if t startsWith "info depth" ⇒
|
||||
stay using (doing map (_.right map (_ buffer t)))
|
||||
case Event(Out(t), doing: Doing) if t startsWith "bestmove" ⇒
|
||||
doing.current.fold(
|
||||
play ⇒ {
|
||||
play.ref ! model.play.BestMove(t.split(' ') lift 1)
|
||||
nextTask(doing.done)
|
||||
},
|
||||
anal ⇒ (anal buffer t).flush.fold(
|
||||
err ⇒ {
|
||||
log error err.shows
|
||||
anal.ref ! Status.Failure(lila.common.LilaException(err))
|
||||
nextTask(doing.done)
|
||||
},
|
||||
task ⇒ task.isDone.fold({
|
||||
task.ref ! task.analysis.done
|
||||
nextTask(doing.done)
|
||||
},
|
||||
nextTask(doing.done enqueue task)
|
||||
)
|
||||
)
|
||||
)
|
||||
// TODO accumulate output for analysis parsing
|
||||
// case Event(Out(t), Some(req)) if t startsWith "info depth" ⇒
|
||||
// stay using (doing map (_.right map (_ buffer t)))
|
||||
case Event(Out(t), Some((req, sender))) if t startsWith "bestmove" ⇒ {
|
||||
sender ! req.analyse.fold(
|
||||
Status.Failure(new Exception("Not implemented")),
|
||||
BestMove(t.split(' ') lift 1))
|
||||
goto(Idle) using none
|
||||
}
|
||||
}
|
||||
whenUnhandled {
|
||||
case Event(task: analyse.Task.Builder, data) ⇒ nextTask(data enqueue task(sender))
|
||||
case Event(task: play.Task.Builder, data) ⇒ nextTask(data enqueue task(sender))
|
||||
case Event(req: Req, _) ⇒ {
|
||||
logerr("[ai] stockfish FSM unhandled request " + req)
|
||||
stay
|
||||
}
|
||||
case Event(Out(t), _) ⇒ stay
|
||||
case Event(GetQueueSize, data) ⇒ sender ! QueueSize(data.size); stay
|
||||
case Event(e @ RebootException, _) ⇒ throw e
|
||||
}
|
||||
|
||||
def nextTask(data: Data) = data.fold(
|
||||
todo ⇒ todo.doing(
|
||||
doing ⇒ {
|
||||
config prepare doing.current foreach process.write
|
||||
goto(IsReady) using doing
|
||||
},
|
||||
todo ⇒ goto(Idle) using todo
|
||||
),
|
||||
doing ⇒ stay using doing
|
||||
)
|
||||
def start(data: (Req, ActorRef)) = data match {
|
||||
case (req, sender) ⇒ {
|
||||
config prepare req foreach process.write
|
||||
process write "isready"
|
||||
goto(IsReady) using (req, sender).some
|
||||
}
|
||||
}
|
||||
|
||||
override def postStop() {
|
||||
process.destroy()
|
||||
process = null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,16 +2,18 @@ package lila.ai
|
|||
package stockfish
|
||||
|
||||
import model._
|
||||
import actorApi._
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
private[ai] final class Config(
|
||||
private[ai] case class Config(
|
||||
execPath: String,
|
||||
hashSize: Int,
|
||||
nbThreads: Int,
|
||||
playMaxMoveTime: FiniteDuration,
|
||||
analyseMoveTime: FiniteDuration,
|
||||
val playTimeout: FiniteDuration,
|
||||
val analyseTimeout: FiniteDuration,
|
||||
val debug: Boolean) {
|
||||
playTimeout: FiniteDuration,
|
||||
analyseTimeout: FiniteDuration,
|
||||
debug: Boolean) {
|
||||
|
||||
import Config._
|
||||
|
||||
|
@ -37,30 +39,28 @@ private[ai] final class Config(
|
|||
setoption("Threads", nbThreads),
|
||||
setoption("Ponder", false))
|
||||
|
||||
def prepare(task: Task) = task.fold(
|
||||
play ⇒ List(
|
||||
setoption("Uci_AnalyseMode", false),
|
||||
setoption("Skill Level", skill(play.level)),
|
||||
setoption("UCI_Chess960", play.chess960),
|
||||
setoption("OwnBook", ownBook(play.level)),
|
||||
"isready"),
|
||||
anal ⇒ List(
|
||||
def prepare(req: Req) = req.analyse.fold(
|
||||
List(
|
||||
setoption("Uci_AnalyseMode", true),
|
||||
setoption("Skill Level", skillMax),
|
||||
setoption("UCI_Chess960", anal.chess960),
|
||||
setoption("OwnBook", true),
|
||||
"isready"))
|
||||
setoption("UCI_Chess960", req.chess960),
|
||||
setoption("OwnBook", true)),
|
||||
List(
|
||||
setoption("Uci_AnalyseMode", false),
|
||||
setoption("Skill Level", skill(req.level)),
|
||||
setoption("UCI_Chess960", req.chess960),
|
||||
setoption("OwnBook", ownBook(req.level))))
|
||||
|
||||
def go(task: Task): List[String] = task.fold(
|
||||
play ⇒ List(
|
||||
position(play.fen, play.moves),
|
||||
def go(req: Req): List[String] = req.analyse.fold(
|
||||
List(
|
||||
position(req.fen, req.moves),
|
||||
"go movetime %d".format(analyseMoveTime.toMillis)),
|
||||
List(
|
||||
position(req.fen, req.moves),
|
||||
"go movetime %d%s".format(
|
||||
moveTime(play.level),
|
||||
~depth(play.level).map(" depth " + _)
|
||||
)),
|
||||
anal ⇒ List(
|
||||
position(anal.fen, anal.pastMoves),
|
||||
"go movetime %d".format(analyseMoveTime.toMillis)))
|
||||
moveTime(req.level),
|
||||
~depth(req.level).map(" depth " + _)
|
||||
)))
|
||||
|
||||
private def position(fen: Option[String], moves: String) =
|
||||
"position %s moves %s".format(fen.fold("startpos")("fen " + _), moves)
|
||||
|
|
|
@ -15,7 +15,7 @@ private [stockfish] final class Process(
|
|||
doLog("Start process")
|
||||
|
||||
def write(msg: String) {
|
||||
log(msg)
|
||||
log("> " + msg)
|
||||
in write (msg + "\n").getBytes("UTF-8")
|
||||
in.flush()
|
||||
}
|
||||
|
|
27
modules/ai/src/main/stockfish/Queue.scala
Normal file
27
modules/ai/src/main/stockfish/Queue.scala
Normal file
|
@ -0,0 +1,27 @@
|
|||
package lila.ai
|
||||
package stockfish
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import akka.actor._
|
||||
import akka.pattern.{ ask, pipe }
|
||||
|
||||
import actorApi._
|
||||
|
||||
private[stockfish] final class Queue(config: Config) extends Actor {
|
||||
|
||||
private val playTimeout = makeTimeout(config.playMaxMoveTime + 100.millis)
|
||||
private val process = Process(config.execPath, "StockFish") _
|
||||
private val actor = context.actorOf(Props(new ActorFSM(process, config)))
|
||||
private var actorReady = false
|
||||
|
||||
def receive = {
|
||||
|
||||
case req: Req ⇒ {
|
||||
implicit def timeout = playTimeout
|
||||
actor ? req map {
|
||||
case bestMove: BestMove ⇒ sender ! ~bestMove.move
|
||||
}
|
||||
} await playTimeout
|
||||
}
|
||||
}
|
|
@ -5,39 +5,36 @@ import scala.concurrent.duration._
|
|||
|
||||
import akka.actor.{ Props, Actor, ActorRef, Kill }
|
||||
import akka.pattern.{ ask, AskTimeoutException }
|
||||
import model.play.BestMove
|
||||
import model.{ GetQueueSize, QueueSize }
|
||||
import play.api.libs.concurrent.Akka.system
|
||||
import play.api.Play.current
|
||||
|
||||
import actorApi._
|
||||
import chess.format.Forsyth
|
||||
import chess.format.UciDump
|
||||
import chess.Rook
|
||||
import lila.analyse.Analysis
|
||||
|
||||
final class Server(execPath: String, config: Config) {
|
||||
final class Server(config: Config) {
|
||||
|
||||
def play(pgn: String, initialFen: Option[String], level: Int): Fu[String] = {
|
||||
implicit val timeout = makeTimeout(config.playTimeout)
|
||||
UciDump(pgn, initialFen) map { moves ⇒
|
||||
model.play.Task.Builder(moves, initialFen map chess960Fen, level)
|
||||
} fold (
|
||||
UciDump(pgn, initialFen) fold (
|
||||
err ⇒ fufail(err),
|
||||
play ⇒ {
|
||||
(actor ? play).mapTo[BestMove] map (~_.move)
|
||||
} ~ { _ onFailure reboot }
|
||||
moves ⇒ actor ? Req(moves, initialFen map chess960Fen, level, false) mapTo manifest[String]
|
||||
)
|
||||
}
|
||||
|
||||
def analyse(pgn: String, initialFen: Option[String]): Fu[String ⇒ Analysis] =
|
||||
UciDump(pgn, initialFen).fold(
|
||||
err ⇒ fufail(err),
|
||||
moves ⇒ {
|
||||
val analyse = model.analyse.Task.Builder(moves, initialFen map chess960Fen)
|
||||
implicit val timeout = makeTimeout(config.analyseTimeout)
|
||||
(actor ? analyse).mapTo[String ⇒ Analysis] ~ { _ onFailure reboot }
|
||||
}
|
||||
)
|
||||
fufail("not implemented")
|
||||
// UciDump(pgn, initialFen).fold(
|
||||
// err ⇒ fufail(err),
|
||||
// moves ⇒ {
|
||||
// val analyse = model.analyse.Task.Builder(moves, initialFen map chess960Fen)
|
||||
// implicit val timeout = makeTimeout(config.analyseTimeout)
|
||||
// (actor ? analyse).mapTo[String ⇒ Analysis] ~ { _ onFailure reboot }
|
||||
// }
|
||||
// )
|
||||
|
||||
private def chess960Fen(fen: String) = (Forsyth << fen).fold(fen) { situation ⇒
|
||||
fen.replace("KQkq", situation.board.pieces.toList filter {
|
||||
|
@ -53,7 +50,5 @@ final class Server(execPath: String, config: Config) {
|
|||
case e: AskTimeoutException ⇒ actor ! model.RebootException
|
||||
}
|
||||
|
||||
private lazy val process = Process(execPath, "StockFish") _
|
||||
private lazy val actor = system.actorOf(Props(
|
||||
new ActorFSM(process, config)))
|
||||
private lazy val actor = system.actorOf(Props(new Queue(config)))
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
package lila.ai
|
||||
package stockfish
|
||||
|
||||
import model.play.BestMove
|
||||
import actorApi.BestMove
|
||||
|
||||
import chess.Game
|
||||
import chess.format.UciMove
|
||||
|
||||
object Stockfish {
|
||||
|
||||
def applyMove(game: Game, pgn: String, move: String): Fu[(Game, chess.Move)] = (for {
|
||||
bestMove ← model.play.BestMove(move.some filter ("" !=)).parse toValid "Wrong bestmove " + move
|
||||
bestMove ← UciMove(~BestMove(move.some filter ("" !=)).move) toValid "Wrong bestmove " + move
|
||||
result ← (game withPgnMoves pgn)(bestMove.orig, bestMove.dest)
|
||||
} yield result).future
|
||||
}
|
||||
|
|
15
modules/ai/src/main/stockfish/actorApi.scala
Normal file
15
modules/ai/src/main/stockfish/actorApi.scala
Normal file
|
@ -0,0 +1,15 @@
|
|||
package lila.ai.stockfish
|
||||
package actorApi
|
||||
|
||||
import akka.actor.ActorRef
|
||||
|
||||
case class Req(
|
||||
moves: String,
|
||||
fen: Option[String],
|
||||
level: Int,
|
||||
analyse: Boolean) {
|
||||
|
||||
def chess960 = fen.isDefined
|
||||
}
|
||||
|
||||
case class BestMove(move: Option[String])
|
|
@ -54,10 +54,6 @@ object model {
|
|||
def apply(sender: ActorRef) = new Task(moves, fen, level, sender)
|
||||
}
|
||||
}
|
||||
|
||||
case class BestMove(move: Option[String]) {
|
||||
def parse = UciMove(~move)
|
||||
}
|
||||
}
|
||||
|
||||
object analyse {
|
||||
|
|
Loading…
Reference in a new issue