more progress on AI rewrite

This commit is contained in:
Thibault Duplessis 2013-06-06 13:56:24 +02:00
parent 602ca57da0
commit 9db79ad555
15 changed files with 234 additions and 116 deletions

View file

@ -12,26 +12,30 @@ private[app] final class AiStresser(env: lila.ai.Env, system: ActorSystem) {
def apply {
(1 to 1024) foreach { i
system.scheduler.scheduleOnce((i*97) millis) {
play(i % 8 + 1)
(1 to 64) foreach { i
system.scheduler.scheduleOnce((i * 97) millis) {
play(i % 8 + 1, true)
}
}
(1 to 1) foreach { i
system.scheduler.scheduleOnce((i * 131) millis) {
analyse(true)
}
}
}
private def play(level: Int) = system.actorOf(Props(new Actor {
def newGame = lila.game.PgnRepo getOneRandom 30000 map { pgn
Game((~pgn).split(' ').toList, 1)
}
private def play(level: Int, loop: Boolean) = system.actorOf(Props(new Actor {
override def preStart {
newGame pipeTo self
}
def receive = {
case Game(moves, it) if it >= moves.size newGame pipeTo self
case Game(moves, it) if it >= moves.size {
loginfo("play complete")
if (loop) newGame pipeTo self
}
case Game(moves, it)
ai.play(moves take it mkString " ", none, level).effectFold(e {
logwarn("[ai] server play: " + e)
@ -44,6 +48,34 @@ private[app] final class AiStresser(env: lila.ai.Env, system: ActorSystem) {
}
}))
private def analyse(loop: Boolean) = system.actorOf(Props(new Actor {
override def preStart {
newGame pipeTo self
}
def receive = {
case Game(moves, _)
ai.analyse(moves mkString " ", none).effectFold(e {
logwarn("[ai] server analyse: " + e)
if (loop) newGame pipeTo self
}, { _
loginfo("analyse complete")
if (loop) newGame pipeTo self
})
}
}))
// private def newGame = fuccess {
// val pgn = "e3 Nc6 Nf3 Nf6 Nc3 d6 d3 Be6 d4 Rb8 b3 Rg8 g3 g5 Nxg5 Rxg5 f4 Bg4 fxg5 Bxd1 Kxd1 Ng4"
// // val pgn = "e3 Nc6 Nf3 Nf6"
// Game(pgn.split(' ').toList, 1)
// }
private def newGame = lila.game.PgnRepo getOneRandom 30000 map { pgn
Game((~pgn).split(' ').toList, 1)
}
private def randomize(d: FiniteDuration, ratio: Float = 0.1f): FiniteDuration =
approximatly(ratio)(d.toMillis) millis

View file

@ -1,11 +1,11 @@
package lila.ai
import chess.{ Game, Move }
import lila.analyse.Analysis
import lila.analyse.AnalysisMaker
trait Ai {
def play(game: Game, pgn: String, initialFen: Option[String], level: Int): Fu[(Game, Move)]
def analyse(pgn: String, initialFen: Option[String]): Fu[String Analysis]
def analyse(pgn: String, initialFen: Option[String]): Fu[AnalysisMaker]
}

View file

@ -17,6 +17,8 @@ final class Env(
val IsClient = config getBoolean "client"
val StockfishPlayUrl = config getString "stockfish.play.url"
val StockfishAnalyseUrl = config getString "stockfish.analyse.url"
val StockfishQueueName = config getString "stockfish.queue.name"
val StockfishQueueDispatcher = config getString "stockfish.queue.dispatcher"
val ActorName = config getString "actor.name"
}
import settings._
@ -28,7 +30,7 @@ final class Env(
playMaxMoveTime = config duration "stockfish.play.movetime",
analyseMoveTime = config duration "stockfish.analyse.movetime",
playTimeout = config duration "stockfish.play.timeout",
analyseTimeout = config duration "stockfish.play.timeout",
analyseTimeout = config duration "stockfish.analyse.timeout",
debug = config getBoolean "stockfish.debug")
val ai: () Ai = () (EngineName, IsClient) match {
@ -68,7 +70,16 @@ final class Env(
playUrl = StockfishPlayUrl,
analyseUrl = StockfishAnalyseUrl)
lazy val stockfishServer = new stockfish.Server(stockfishConfig)
lazy val stockfishServer = new stockfish.Server(
queue = stockfishQueue,
config = stockfishConfig)
// preload stockfish
if (!IsClient && EngineName == "stockfish") stockfishServer
private lazy val stockfishQueue = system.actorOf(Props(
new stockfish.Queue(stockfishConfig)
) withDispatcher StockfishQueueDispatcher, name = StockfishQueueName)
private lazy val stupidAi = new StupidAi
@ -76,11 +87,6 @@ final class Env(
case ("stockfish", true) stockfishClient.some
case _ none
}
private lazy val server = (EngineName, IsServer) match {
case ("stockfish", true) stockfishServer.some
case _ none
}
}
object Env {

View file

@ -6,11 +6,12 @@ import model._
import model.analyse._
import actorApi._
import lila.analyse.Analysis
final class ActorFSM(
private[stockfish] final class ActorFSM(
processBuilder: Process.Builder,
config: Config)
extends Actor with AkkaFSM[State, Option[(Req, ActorRef)]] {
extends Actor with AkkaFSM[State, Option[Job]] {
private val process = processBuilder(
out self ! Out(out),
@ -20,22 +21,23 @@ final class ActorFSM(
startWith(Starting, none)
when(Starting) {
case Event(Out(t), data) if t startsWith "Stockfish" {
case Event(Out(t), _) if t startsWith "Stockfish" {
process write "uci"
stay
}
case Event(Out("uciok"), data) {
case Event(Out("uciok"), job) {
config.init foreach process.write
data.fold(goto(Idle))(start)
loginfo("[ai] stockfish is ready")
job.fold(goto(Idle))(start)
}
case Event(req: Req, none) stay using (req, sender).some
case Event(req: Req, none) stay using Job(req, sender, Nil).some
}
when(Idle) {
case Event(Out(t), _) { logwarn(t); stay }
case Event(req: Req, _) start(req, sender)
case Event(req: Req, _) start(Job(req, sender, Nil))
}
when(IsReady) {
case Event(Out("readyok"), Some((req, _))) {
case Event(Out("readyok"), Some(Job(req, _, _))) {
val lines = config go req
lines.lastOption foreach { line
println(req.analyse.fold("A", "P") + line.replace("go movetime", ""))
@ -45,13 +47,10 @@ final class ActorFSM(
}
}
when(Running) {
// 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))
case Event(Out(t), Some(job)) if t startsWith "info depth"
stay using (job + t).some
case Event(Out(t), Some(job)) if t startsWith "bestmove" {
job.sender ! (job complete t)
goto(Idle) using none
}
}
@ -63,11 +62,11 @@ final class ActorFSM(
case Event(Out(t), _) stay
}
def start(data: (Req, ActorRef)) = data match {
case (req, sender) {
def start(job: Job) = job match {
case Job(req, sender, _) {
config prepare req foreach process.write
process write "isready"
goto(IsReady) using (req, sender).some
goto(IsReady) using Job(req, sender, Nil).some
}
}

View file

@ -2,7 +2,7 @@ package lila.ai
package stockfish
import chess.{ Game, Move }
import lila.analyse.Analysis
import lila.analyse.AnalysisMaker
final class Ai(server: Server) extends lila.ai.Ai {
@ -15,7 +15,7 @@ final class Ai(server: Server) extends lila.ai.Ai {
}
}
def analyse(pgn: String, initialFen: Option[String]): Fu[String Analysis] =
def analyse(pgn: String, initialFen: Option[String]): Fu[AnalysisMaker] =
server.analyse(pgn, initialFen)
private def withValidSituation[A](game: Game)(op: Fu[A]): Fu[A] =

View file

@ -9,7 +9,7 @@ import play.api.Play.current
import chess.format.UciMove
import chess.{ Game, Move }
import lila.analyse.{ Analysis, AnalysisMaker }
import lila.analyse.AnalysisMaker
final class Client(
val playUrl: String,
@ -18,7 +18,7 @@ final class Client(
def play(game: Game, pgn: String, initialFen: Option[String], level: Int): Fu[(Game, Move)] =
fetchMove(pgn, ~initialFen, level) flatMap { Stockfish.applyMove(game, pgn, _) }
def analyse(pgn: String, initialFen: Option[String]): Fu[String Analysis] =
def analyse(pgn: String, initialFen: Option[String]): Fu[AnalysisMaker] =
fetchAnalyse(pgn, ~initialFen) flatMap { str
(AnalysisMaker(str, true) toValid "Can't read analysis results").future
}

View file

@ -1,10 +1,12 @@
package lila.ai
package stockfish
import model._
import actorApi._
import scala.concurrent.duration.FiniteDuration
import model._
import actorApi._
private[ai] case class Config(
execPath: String,
hashSize: Int,
@ -30,8 +32,8 @@ private[ai] case class Config(
4 -> 4,
5 -> 6,
6 -> 8,
7 -> 10
// 8 -> inf
7 -> 10,
8 -> 12
) get levelBox(level)
def init = List(
@ -39,28 +41,27 @@ private[ai] case class Config(
setoption("Threads", nbThreads),
setoption("Ponder", false))
def prepare(req: Req) = req.analyse.fold(
List(
def prepare(req: Req) = req match {
case r: PlayReq List(
setoption("Uci_AnalyseMode", false),
setoption("Skill Level", skill(r.level)),
setoption("UCI_Chess960", r.chess960),
setoption("OwnBook", ownBook(r.level)))
case r: AnalReq List(
setoption("Uci_AnalyseMode", true),
setoption("Skill Level", skillMax),
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))))
setoption("UCI_Chess960", r.chess960),
setoption("OwnBook", true))
}
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(req.level),
~depth(req.level).map(" depth " + _)
)))
def go(req: Req): List[String] = req match {
case r: PlayReq List(
position(r.fen, r.moves),
"go movetime %d%s".format(moveTime(r.level), ~depth(r.level).map(" depth " + _)))
case r: AnalReq List(
position(r.fen, r.moves),
"go movetime %d".format(analyseMoveTime.toMillis))
}
private def position(fen: Option[String], moves: String) =
"position %s moves %s".format(fen.fold("startpos")("fen " + _), moves)

View file

@ -0,0 +1,18 @@
package lila.ai
package stockfish
import akka.actor.ActorSystem
import akka.dispatch.PriorityGenerator
import akka.dispatch.UnboundedPriorityMailbox
import com.typesafe.config.{ Config TypesafeConfig }
import actorApi._
// We inherit, in this case, from UnboundedPriorityMailbox
// and seed it with the priority generator
final class MailBox(settings: ActorSystem.Settings, config: TypesafeConfig)
extends UnboundedPriorityMailbox(PriorityGenerator {
case _: PlayReq 0
case _: AnalReq 1
case _ 3
})

View file

@ -5,7 +5,7 @@ import java.io.OutputStream
import scala.io.Source.fromInputStream
import scala.sys.process.{ Process SProcess, ProcessBuilder, ProcessIO }
private [stockfish] final class Process(
private[stockfish] final class Process(
builder: ProcessBuilder,
name: String,
out: String Unit,

View file

@ -7,21 +7,45 @@ import akka.actor._
import akka.pattern.{ ask, pipe }
import actorApi._
import lila.analyse.{ AnalysisMaker, Info }
private[stockfish] final class Queue(config: Config) extends Actor {
private[ai] final class Queue(config: Config) extends Actor {
private val playTimeout = makeTimeout(config.playMaxMoveTime + 100.millis)
private val process = Process(config.execPath, "StockFish") _
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
case req: PlayReq {
implicit def timeout = makeTimeout((config moveTime req.level).millis + 100.millis)
actor ? req mapTo manifest[Valid[String]] map {
case scalaz.Success(move) sender ! move
case failure logwarn("[ai] stockfish play " + failure.toString)
} await timeout
}
case req: AnalReq {
implicit def timeout = makeTimeout(config.analyseMoveTime + 100.millis)
(actor ? req) mapTo manifest[Valid[Int Info]] map {
case scalaz.Success(info) sender ! info
case failure logwarn("[ai] stockfish analyse " + failure.toString)
} await timeout
}
case FullAnalReq(moveString, fen) {
implicit def timeout = makeTimeout(config.analyseTimeout)
val moves = moveString.split(' ').toList
((1 to moves.size - 1).toList map moves.take map { serie
self ? AnalReq(serie mkString " ", fen)
}).sequence mapTo manifest[List[Int Info]] map { infos
AnalysisMaker(infos.zipWithIndex map (x x._1 -> (x._2 + 1)) map {
case (info, turn) (turn % 2 == 0).fold(
info(turn),
info(turn) |> { i i.copy(score = i.score map (_.negate)) }
).pp
}, true, none)
} pipeTo sender
}
}
}

View file

@ -12,43 +12,33 @@ import play.api.Play.current
import actorApi._
import chess.format.Forsyth
import chess.format.UciDump
import chess.Rook
import lila.analyse.Analysis
import lila.analyse.AnalysisMaker
final class Server(config: Config) {
private[ai] final class Server(queue: ActorRef, config: Config) {
def play(pgn: String, initialFen: Option[String], level: Int): Fu[String] = {
implicit val timeout = makeTimeout(config.playTimeout)
UciDump(pgn, initialFen) fold (
err fufail(err),
moves actor ? Req(moves, initialFen map chess960Fen, level, false) mapTo manifest[String]
moves queue ? PlayReq(moves, initialFen map chess960Fen, level) mapTo manifest[String]
)
}
def analyse(pgn: String, initialFen: Option[String]): Fu[String Analysis] =
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 }
// }
// )
def analyse(pgn: String, initialFen: Option[String]): Fu[AnalysisMaker] = {
implicit val timeout = makeTimeout(config.analyseTimeout)
UciDump(pgn, initialFen).fold(
err fufail(err),
moves queue ? FullAnalReq(moves, initialFen map chess960Fen) mapTo manifest[AnalysisMaker]
)
}
private def chess960Fen(fen: String) = (Forsyth << fen).fold(fen) { situation
fen.replace("KQkq", situation.board.pieces.toList filter {
case (_, piece) piece is Rook
case (_, piece) piece is chess.Rook
} sortBy {
case (pos, _) (pos.y, pos.x)
} map {
case (pos, piece) piece.color.fold(pos.file.toUpperCase, pos.file)
} mkString "")
}
private val reboot: PartialFunction[Throwable, Unit] = {
case e: AskTimeoutException actor ! model.RebootException
}
private lazy val actor = system.actorOf(Props(new Queue(config)))
}

View file

@ -1,15 +1,13 @@
package lila.ai
package stockfish
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 UciMove(~BestMove(move.some filter ("" !=)).move) toValid "Wrong bestmove " + move
bestMove UciMove(~(move.some filter (_.nonEmpty))) toValid "Wrong bestmove " + move
result (game withPgnMoves pgn)(bestMove.orig, bestMove.dest)
} yield result).future
}

View file

@ -1,15 +1,52 @@
package lila.ai.stockfish
package lila.ai
package stockfish
package actorApi
import akka.actor.ActorRef
case class Req(
moves: String,
fen: Option[String],
level: Int,
analyse: Boolean) {
sealed trait Req {
def moves: String
def fen: Option[String]
def analyse: Boolean
def chess960 = fen.isDefined
def moveList = moves.split(' ').toList
}
case class PlayReq(
moves: String,
fen: Option[String],
level: Int) extends Req {
def analyse = false
}
case class AnalReq(
moves: String,
fen: Option[String]) extends Req {
def analyse = true
// def nextMove: Option[String] = moves lift analysis.size
// def flush = for {
// move nextMove toValid "No move to flush"
// info AnalyseParser(infoBuffer)(move)
// } yield copy(analysis = analysis + info, infoBuffer = Nil)
}
case class FullAnalReq(moves: String, fen: Option[String])
case class Job(req: Req, sender: akka.actor.ActorRef, buffer: List[String]) {
def +(str: String) = req.analyse.fold(copy(buffer = str :: buffer), this)
// bestmove xyxy ponder xyxy
def complete(str: String): Valid[Any] =
req.analyse.fold(
(this + str) |> {
case Job(req, _, buffer)
req.moveList.lastOption toValid "empty move list" flatMap AnalyseParser(buffer)
},
str.split(' ') lift 1 toValid "no bestmove found in " + str)
}
case class BestMove(move: Option[String])

View file

@ -37,11 +37,6 @@ object Analysis {
val separator = " "
def make(str: String, done: Boolean): Option[String Analysis] =
Analysis.decodeInfos(str) map { infos
(id: String) new Analysis(id, infos, done, none)
}
def decodeInfos(enc: String): Option[List[Info]] =
(enc.split(separator).toList.zipWithIndex map {
case (info, index) Info.decode(index + 1, info)
@ -67,12 +62,14 @@ object Analysis {
)
}
// NICETOHAVE
// this belongs to the Analysis object
// but was moved here because of scala 2.10.1 compiler bug
case class AnalysisMaker(infos: List[Info], done: Boolean, fail: Option[String]) {
def apply(id: String) = Analysis(id, infos, done, fail)
}
object AnalysisMaker {
def apply(str: String, done: Boolean): Option[String Analysis] =
Analysis.make(str, done)
def apply(str: String, done: Boolean): Option[AnalysisMaker] =
Analysis.decodeInfos(str) map { AnalysisMaker(_, done, none) }
}
final class AnalysisBuilder(infos: List[Info]) {
@ -81,7 +78,7 @@ final class AnalysisBuilder(infos: List[Info]) {
def +(info: Int Info) = new AnalysisBuilder(info(infos.size + 1) :: infos)
def done: String Analysis = id new Analysis(id, infos.reverse.zipWithIndex map {
def done: AnalysisMaker = AnalysisMaker(infos.reverse.zipWithIndex map {
case (info, turn) (turn % 2 == 0).fold(
info,
info.copy(score = info.score map (_.negate))
@ -99,7 +96,7 @@ private[analyse] case class RawAnalysis(
case (true, "") new Analysis(id, Nil, false, fail orElse "No move infos".some).some
case (true, en) Analysis.decodeInfos(en) map { infos
new Analysis(id, infos, done, none)
}
}
case (false, _) new Analysis(id, Nil, false, fail).some
}
}

16
todo
View file

@ -61,6 +61,22 @@ account closed accounts in team counts
IE10 no sound toggle http://en.lichess.org/forum/lichess-feedback/notification-of-game-creation#3
filter current games for watching http://en.lichess.org/forum/lichess-feedback/viewing-current-games-suggestion#2
stockfish hangs http://en.lichess.org/inbox/wtd62jrd#bottom
IP troll
[Event "Casual game"]
[Site "http://lichess.org/paf5gpiq"]
[Date "2013-06-06"]
[White "turf"]
[Black "Anonymous"]
[Result "1-0"]
[WhiteElo "1226"]
[BlackElo "?"]
[PlyCount "45"]
[Variant "Chess960"]
[FEN "nrbbnqkr/pppppppp/8/8/8/8/PPPPPPPP/NRBBNQKR w KQkq - 0 1"]
[SetUp "1"]
1. e4 e5 2. Nb3 c5 3. Nf3 d6 4. d3 Be6 5. Qe1 Bxb3 6. axb3 b5 7. Ra1 b4 8. Rxa7 Bb6 9. Ra4 Nac7 10. c3 Ra8 11. Rxa8 Nxa8 12. cxb4 cxb4 13. Qxb4 Nec7 14. Be3 Bxe3 15. fxe3 h6 16. O-O d5 17. Qxf8+ Kxf8 18. Nxe5 dxe4 19. dxe4 f6 20. Ng6+ Kg8 21. Nxh8 Kxh8 22. Bf3 Nb6 23. Rc1 1-0
---