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:
commit
d01aa0c8ff
|
@ -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]]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
10
app/ai/Server.scala
Normal 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)
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
30
app/core/IOFuture.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue