Merge branch 'iofuture'

* iofuture:
  safer chess engine choice
  remove crafty support
  more work on IO -> Future
  start replacing AI IO monads with akka futures
This commit is contained in:
Thibault Duplessis 2012-07-09 21:34:57 +02:00
commit d01aa0c8ff
18 changed files with 108 additions and 225 deletions

View file

@ -10,7 +10,7 @@ import akka.dispatch.Future
trait Ai {
def play(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]]
def play(dbGame: DbGame, initialFen: Option[String]): Future[Valid[(Game, Move)]]
def analyse(dbGame: DbGame, initialFen: Option[String]): Future[Valid[Analysis]]
}

View file

@ -13,21 +13,9 @@ final class AiEnv(settings: Settings) {
lazy val ai: () Ai = (AiChoice, isClient) match {
case (AiStockfish, true) () stockfishClient or stockfishAi
case (AiStockfish, false) () stockfishAi
case (AiCrafty, true) () craftyClient or craftyAi
case (AiCrafty, false) () craftyAi
case _ () stupidAi
}
lazy val craftyAi = new crafty.Ai(
server = craftyServer)
lazy val craftyClient = new crafty.Client(
playUrl = AiCraftyPlayUrl)
lazy val craftyServer = new crafty.Server(
execPath = AiCraftyExecPath,
bookPath = AiCraftyBookPath)
lazy val stockfishAi = new stockfish.Ai(
server = stockfishServer)
@ -56,12 +44,13 @@ final class AiEnv(settings: Settings) {
stockfishServer.report
}
private lazy val client = isClient option {
AiChoice match {
case AiStockfish stockfishClient
case AiCrafty craftyClient
}
private lazy val client = (AiChoice, isClient) match {
case (AiStockfish, true) stockfishClient.some
case _ none
}
private lazy val server = (isServer && AiChoice == AiStockfish) option stockfishServer
private lazy val server = (AiChoice, isServer) match {
case (AiStockfish, true) stockfishServer.some
case _ none
}
}

View file

@ -9,7 +9,7 @@ trait Client extends Ai {
val playUrl: String
protected def tryPing: IO[Option[Int]]
protected def tryPing: Option[Int]
// tells whether the remote AI is healthy or not
// frequently updated by a scheduled actor
@ -25,7 +25,7 @@ trait Client extends Ai {
def currentHealth = ping.fold(_ < pingAlert, false)
val diagnose: IO[Unit] = for {
p tryPing
p io(tryPing)
_ p.fold(_ < pingAlert, false).fold(
currentHealth.fold(io(), putStrLn("remote AI is up, ping = " + p)),
putStrLn("remote AI is down, ping = " + p))

10
app/ai/Server.scala Normal file
View file

@ -0,0 +1,10 @@
package lila
package ai
trait Server {
val levelRange = 1 to 8
def validateLevel(level: Int) =
level.validIf(levelRange contains level, "Invalid AI level: " + level)
}

View file

@ -5,10 +5,11 @@ import chess.{ Game, Move }
import game.DbGame
import scalaz.effects._
import akka.dispatch.Future
final class StupidAi extends Ai {
final class StupidAi extends Ai with core.Futuristic {
def play(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] = io {
def play(dbGame: DbGame, initialFen: Option[String]): Future[Valid[(Game, Move)]] = Future {
val game = dbGame.toChess

View file

@ -1,29 +0,0 @@
package lila
package ai
package crafty
import chess.{ Game, Move }
import game.DbGame
import java.io.File
import scala.io.Source
import scala.sys.process.Process
import scalaz.effects._
import akka.dispatch.Future
final class Ai(server: Server) extends ai.Ai with FenBased {
def play(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] = {
val oldGame = dbGame.toChess
val oldFen = toFen(oldGame, dbGame.variant)
server.play(oldFen, dbGame.aiLevel | 1).fold(
err io(failure(err)),
iop iop map { newFen applyFen(oldGame, newFen) }
)
}
def analyse(dbGame: DbGame, initialFen: Option[String]) =
throw new RuntimeException("Crafty analysis is not implemented")
}

View file

@ -1,40 +0,0 @@
package lila
package ai
package crafty
import chess.{ Game, Move }
import game.DbGame
import scalaz.effects._
final class Client(val playUrl: String) extends ai.Client with FenBased {
def play(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] = {
val oldGame = dbGame.toChess
val oldFen = toFen(oldGame, dbGame.variant)
fetchNewFen(oldFen, dbGame.aiLevel | 1) map { newFen
applyFen(oldGame, newFen)
}
}
def analyse(dbGame: DbGame, initialFen: Option[String]) =
throw new RuntimeException("Crafty analysis is not implemented")
private def fetchNewFen(oldFen: String, level: Int): IO[String] = io {
http(playUrlObj <<? Map(
"fen" -> oldFen,
"level" -> level.toString
) as_str)
}
protected def tryPing: IO[Option[Int]] = for {
start io(nowMillis)
received fetchNewFen(
oldFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq",
level = 1
).catchLeft map (_.isRight)
delay io(nowMillis - start)
} yield received option delay.toInt
}

View file

@ -1,50 +0,0 @@
package lila
package ai
package crafty
import chess.{ Game, Move }
import java.io.ByteArrayInputStream
import scala.io.Source
import scala.sys.process.Process
import scalaz.effects._
final class Server(
execPath: String,
bookPath: Option[String] = None) {
def play(fen: String, level: Int): Valid[IO[String]] =
if (level < 1 || level > 8) "Invalid ai level".failNel
else if (fen.isEmpty) "Empty fen".failNel
else success(runCrafty(fen, level))
def runCrafty(oldFen: String, level: Int): IO[String] =
io { Process(command(level)) #< input(oldFen, level) !! } map extractFen
private def extractFen(output: String) =
output.lines.find(_ contains "setboard") map { line
"""^.+setboard\s([\w\d/]+\s\w).*$""".r.replaceAllIn(line, m m group 1)
} getOrElse "Crafty output does not contain setboard"
private def command(level: Int) =
"""%s learn=off log=off bookpath=%s ponder=off smpmt=1 st=%s""".format(
execPath,
bookPath | "",
craftyTime(level))
private def input(fen: String, level: Int) = new ByteArrayInputStream(List(
"skill %d" format craftySkill(level),
"book random 1",
"book width 10",
"setboard %s" format fen,
"move",
"savepos",
"quit") mkString "\n" getBytes "UTF-8")
private def craftyTime(level: Int) = (level / 10f).toString take 4
private def craftySkill(level: Int) = level match {
case 8 100
case l l * 12
}
}

View file

@ -8,19 +8,20 @@ import analyse.Analysis
import scalaz.effects._
import akka.dispatch.Future
import play.api.Play.current
import play.api.libs.concurrent._
final class Ai(server: Server) extends lila.ai.Ai with Stockfish {
import model._
def play(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] =
server.play(dbGame.pgn, initialFen, dbGame.aiLevel | 1).fold(
err io(failure(err)),
iop iop map {
applyMove(dbGame, _)
}
)
def play(dbGame: DbGame, initialFen: Option[String]): Future[Valid[(Game, Move)]] =
server.play(dbGame.pgn, initialFen, dbGame.aiLevel | 1) map { validMove
validMove flatMap { applyMove(dbGame, _) }
}
def analyse(dbGame: DbGame, initialFen: Option[String]): Future[Valid[Analysis]] =
server.analyse(dbGame.pgn, initialFen)
private implicit val executor = Akka.system.dispatcher
}

View file

@ -9,7 +9,8 @@ import analyse.Analysis
import scalaz.effects._
import dispatch.{ url }
import akka.dispatch.Future
import akka.dispatch.{ Future, Await }
import akka.util.duration._
import play.api.Play.current
import play.api.libs.concurrent._
@ -17,7 +18,7 @@ final class Client(
val playUrl: String,
analyseUrl: String) extends ai.Client with Stockfish {
def play(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] = {
def play(dbGame: DbGame, initialFen: Option[String]): Future[Valid[(Game, Move)]] = {
fetchMove(dbGame.pgn, initialFen | "", dbGame.aiLevel | 1) map {
applyMove(dbGame, _)
}
@ -30,20 +31,16 @@ final class Client(
private lazy val analyseUrlObj = url(analyseUrl)
protected lazy val tryPing: IO[Option[Int]] = for {
start io(nowMillis)
received fetchMove(
pgn = "",
initialFen = "",
level = 1
).catchLeft map (_ match {
case Right(move) UciMove(move).isDefined
case _ false
})
delay io(nowMillis - start)
} yield received option delay.toInt
protected lazy val tryPing: Option[Int] = nowMillis |> { start
(unsafe {
Await.result(fetchMove(pgn = "", initialFen = "", level = 1), 5 seconds)
}).toOption flatMap {
case move if UciMove(move).isDefined Some(nowMillis - start) map (_.toInt)
case _ none[Int]
}
}
private def fetchMove(pgn: String, initialFen: String, level: Int): IO[String] = io {
private def fetchMove(pgn: String, initialFen: String, level: Int): Future[String] = Future {
http(playUrlObj <<? Map(
"pgn" -> pgn,
"initialFen" -> initialFen,

View file

@ -21,17 +21,20 @@ import scalaz.effects._
final class Server(
execPath: String,
playConfig: PlayConfig,
analyseConfig: AnalyseConfig) {
analyseConfig: AnalyseConfig) extends lila.ai.Server {
def play(pgn: String, initialFen: Option[String], level: Int): Valid[IO[String]] = {
def play(pgn: String, initialFen: Option[String], level: Int): Future[Valid[String]] = {
implicit val timeout = new Timeout(playAtMost)
if (level < 1 || level > 8) "Invalid ai level".failNel
else for {
(for {
moves UciDump(pgn, initialFen)
play = model.play.Play(moves, initialFen map chess960Fen, level)
} yield io {
Await.result(playActor ? play mapTo manifest[model.play.BestMove], playAtMost)
} map (_.move | "")
validLevel validateLevel(level)
play = model.play.Play(moves, initialFen map chess960Fen, validLevel)
} yield play).fold(
err Future(failure(err)),
play playActor ? play mapTo manifest[model.play.BestMove] map { m
success(m.move | "")
}
)
}
def analyse(pgn: String, initialFen: Option[String]): Future[Valid[Analysis]] =
@ -46,7 +49,7 @@ final class Server(
def report = {
implicit val timeout = new Timeout(playAtMost)
(playActor ? GetQueueSize) zip (analyseActor ? GetQueueSize) map {
case (QueueSize(play), QueueSize(analyse)) => play -> analyse
case (QueueSize(play), QueueSize(analyse)) play -> analyse
}
}

View file

@ -12,42 +12,22 @@ import akka.dispatch.Future
object Ai extends LilaController {
val craftyServer = env.ai.craftyServer
val stockfishServer = env.ai.stockfishServer
val isServer = env.ai.isServer
implicit val executor = Akka.system.dispatcher
def playCrafty = Action { implicit req
IfServer {
implicit val ctx = Context(req, None)
Async {
Akka.future {
craftyServer.play(fen = getOr("fen", ""), level = getIntOr("level", 1))
} map { res
res.fold(
err BadRequest(err.shows),
op Ok(op.unsafePerformIO)
)
}
}
}
}
def playStockfish = Action { implicit req
implicit val ctx = Context(req, None)
IfServer {
Async {
Akka.future {
stockfishServer.play(
pgn = getOr("pgn", ""),
initialFen = get("initialFen"),
level = getIntOr("level", 1))
} map { res
res.fold(
stockfishServer.play(
pgn = getOr("pgn", ""),
initialFen = get("initialFen"),
level = getIntOr("level", 1)
).asPromise map (_.fold(
err BadRequest(err.shows),
op Ok(op.unsafePerformIO)
)
}
Ok(_)
))
}
}
}
@ -59,12 +39,10 @@ object Ai extends LilaController {
stockfishServer.analyse(
pgn = getOr("pgn", ""),
initialFen = get("initialFen")
).asPromise map { res
res.fold(
err InternalServerError(err.shows),
analyse Ok(analyse.encode)
)
}
).asPromise map (_.fold(
err InternalServerError(err.shows),
analyse Ok(analyse.encode)
))
}
}
}

30
app/core/IOFuture.scala Normal file
View file

@ -0,0 +1,30 @@
package lila
package core
import play.api.Play.current
import play.api.libs.concurrent._
import akka.dispatch.{ Future, Await }
import akka.util.Timeout
import akka.util.Duration
import akka.util.duration._
import scalaz.effects._
trait Futuristic {
protected implicit val ttl = 1 minute
protected implicit val executor = Akka.system.dispatcher
implicit def ioToFuture[A](ioa: IO[A]) = new {
def toFuture: Future[A] = Future(ioa.unsafePerformIO)
}
implicit def futureToIo[A](fa: Future[A]) = new {
def toIo: IO[A] = toIo(1 minute)
def toIo(duration: Duration): IO[A] = io {
Await.result(fa, duration)
}
}
}

View file

@ -66,17 +66,18 @@ final class Settings(config: Config) {
val FinisherLockTimeout = millis("memo.finisher_lock.timeout")
val AiChoice = getString("ai.use")
val AiCrafty = "crafty"
val AiStockfish = "stockfish"
sealed trait AiEngine
case object AiStockfish extends AiEngine
case object AiStupid extends AiEngine
val AiChoice: AiEngine = getString("ai.use") match {
case "stockfish" AiStockfish
case _ AiStupid
}
val AiServerMode = getBoolean("ai.server")
val AiClientMode = getBoolean("ai.client")
val AiCraftyExecPath = getString("ai.crafty.exec_path")
val AiCraftyBookPath = Some(getString("ai.crafty.book_path")) filter ("" !=)
val AiCraftyPlayUrl = getString("ai.crafty.play.url")
val AiStockfishExecPath = getString("ai.stockfish.exec_path")
val AiStockfishPlayUrl = getString("ai.stockfish.play.url")

View file

@ -22,7 +22,7 @@ final class Hand(
ai: () Ai,
finisher: Finisher,
hubMaster: ActorRef,
moretimeSeconds: Int) extends Handler(gameRepo) {
moretimeSeconds: Int) extends Handler(gameRepo) with core.Futuristic {
type IOValidEvents = IO[Valid[List[Event]]]
type PlayResult = IO[Valid[(List[Event], String)]]
@ -55,7 +55,7 @@ final class Hand(
initialFen progress.game.variant.standard.fold(
io(none[String]),
gameRepo initialFen progress.game.id)
aiResult ai().play(progress.game, initialFen)
aiResult ai().play(progress.game, initialFen).toIo
eventsAndFen aiResult.fold(
err io(failure(err)), {
case (newChessGame, move) for {

View file

@ -18,7 +18,7 @@ final class Processor(
gameRepo: GameRepo,
fisherman: Fisherman,
timelinePush: DbGame IO[Unit],
ai: () Ai) {
ai: () Ai) extends core.Futuristic {
def ai(config: AiConfig)(implicit ctx: Context): IO[Pov] = for {
_ ctx.me.fold(
@ -38,7 +38,7 @@ final class Processor(
initialFen game.variant.standard.fold(
io(none[String]),
gameRepo initialFen game.id)
aiResult ai().play(game, initialFen) map (_.err)
aiResult { ai().play(game, initialFen) map (_.err) }.toIo
(newChessGame, move) = aiResult
progress = game.update(newChessGame, move)
_ gameRepo save progress

View file

@ -109,13 +109,6 @@ ai {
use = stockfish
server = false
client = true
crafty {
exec_path = "/usr/bin/crafty"
book_path = "/usr/share/crafty"
play {
url = "http://188.165.194.171:9072/ai/crafty/play"
}
}
stockfish {
exec_path = "/usr/bin/stockfish"
play {

View file

@ -91,7 +91,6 @@ GET /wiki controllers.Wiki.home
GET /wiki/:slug controllers.Wiki.show(slug: String)
# AI
GET /ai/crafty/play controllers.Ai.playCrafty
GET /ai/stockfish/play controllers.Ai.playStockfish
GET /ai/stockfish/analyse controllers.Ai.analyseStockfish
GET /ai/stockfish/report controllers.Ai.reportStockfish