diff --git a/app/ai/Ai.scala b/app/ai/Ai.scala index 08fa024043..9e4a1b6add 100644 --- a/app/ai/Ai.scala +++ b/app/ai/Ai.scala @@ -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]] } diff --git a/app/ai/AiEnv.scala b/app/ai/AiEnv.scala index 57f2a33850..b60824498f 100644 --- a/app/ai/AiEnv.scala +++ b/app/ai/AiEnv.scala @@ -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 + } } diff --git a/app/ai/Client.scala b/app/ai/Client.scala index d2b2039aa2..6d337eeffb 100644 --- a/app/ai/Client.scala +++ b/app/ai/Client.scala @@ -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)) diff --git a/app/ai/Server.scala b/app/ai/Server.scala new file mode 100644 index 0000000000..2ab86c57b7 --- /dev/null +++ b/app/ai/Server.scala @@ -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) +} diff --git a/app/ai/StupidAi.scala b/app/ai/StupidAi.scala index da8d2e7116..be3e0d3dc8 100644 --- a/app/ai/StupidAi.scala +++ b/app/ai/StupidAi.scala @@ -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 diff --git a/app/ai/crafty/Ai.scala b/app/ai/crafty/Ai.scala deleted file mode 100644 index 3aafd093d4..0000000000 --- a/app/ai/crafty/Ai.scala +++ /dev/null @@ -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") -} diff --git a/app/ai/crafty/Client.scala b/app/ai/crafty/Client.scala deleted file mode 100644 index 1600db363c..0000000000 --- a/app/ai/crafty/Client.scala +++ /dev/null @@ -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 < 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 -} diff --git a/app/ai/crafty/Server.scala b/app/ai/crafty/Server.scala deleted file mode 100644 index d911808e61..0000000000 --- a/app/ai/crafty/Server.scala +++ /dev/null @@ -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 - } -} diff --git a/app/ai/stockfish/Ai.scala b/app/ai/stockfish/Ai.scala index 832ed6451a..300408d53e 100644 --- a/app/ai/stockfish/Ai.scala +++ b/app/ai/stockfish/Ai.scala @@ -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 } diff --git a/app/ai/stockfish/Client.scala b/app/ai/stockfish/Client.scala index 5ecf950969..100c9ced29 100644 --- a/app/ai/stockfish/Client.scala +++ b/app/ai/stockfish/Client.scala @@ -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 < pgn, "initialFen" -> initialFen, diff --git a/app/ai/stockfish/Server.scala b/app/ai/stockfish/Server.scala index 67dfc5f10a..5dd3bcd70b 100644 --- a/app/ai/stockfish/Server.scala +++ b/app/ai/stockfish/Server.scala @@ -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 } } diff --git a/app/controllers/Ai.scala b/app/controllers/Ai.scala index e87cc90956..0842f76ca4 100644 --- a/app/controllers/Ai.scala +++ b/app/controllers/Ai.scala @@ -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) + )) } } } diff --git a/app/core/IOFuture.scala b/app/core/IOFuture.scala new file mode 100644 index 0000000000..d9ffa6f5d6 --- /dev/null +++ b/app/core/IOFuture.scala @@ -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) + } + } +} diff --git a/app/core/Settings.scala b/app/core/Settings.scala index f8bf2df3bc..677d42b183 100644 --- a/app/core/Settings.scala +++ b/app/core/Settings.scala @@ -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") diff --git a/app/round/Hand.scala b/app/round/Hand.scala index d2aba0981c..ebe1eed5db 100644 --- a/app/round/Hand.scala +++ b/app/round/Hand.scala @@ -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 { diff --git a/app/setup/Processor.scala b/app/setup/Processor.scala index 1b707800ad..8122d943ae 100644 --- a/app/setup/Processor.scala +++ b/app/setup/Processor.scala @@ -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 diff --git a/conf/application.conf b/conf/application.conf index 4be145a4a5..874773a85b 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -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 { diff --git a/conf/routes b/conf/routes index 4307b9ffdf..cf3bb1f91a 100644 --- a/conf/routes +++ b/conf/routes @@ -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