reimplement AI actors

This commit is contained in:
Thibault Duplessis 2013-06-06 01:33:53 +02:00
parent 84875d9b4b
commit 602ca57da0
9 changed files with 135 additions and 128 deletions

View file

@ -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

View file

@ -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 = _
private val process = processBuilder(
out self ! Out(out),
err self ! Err(err),
config.debug)
override def preStart() {
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(Out(t), _) stay
case Event(GetQueueSize, data) sender ! QueueSize(data.size); stay
case Event(e @ RebootException, _) throw e
case Event(req: Req, _) {
logerr("[ai] stockfish FSM unhandled request " + req)
stay
}
case Event(Out(t), _) stay
}
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
}
}

View file

@ -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)

View file

@ -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()
}

View 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
}
}

View file

@ -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)))
}

View file

@ -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
}

View 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])

View file

@ -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 {