Defer Crafty AI processing to a remote server, and check its health

This commit is contained in:
Thibault Duplessis 2012-04-05 21:19:05 +02:00
parent 3075b1a734
commit 1746584c58
10 changed files with 209 additions and 87 deletions

View file

@ -33,8 +33,16 @@ final class Cron(env: SystemEnv)(implicit app: Application) {
spawn("game_auto_finish") { _.gameFinishCommand.apply() }
spawn("remote_ai_health") { env
for {
health env.remoteAi.health
_ io { env.remoteAiHealth = health }
_ health.fold(io(), putStrLn("remote AI is down"))
} yield ()
}
def spawn(name: String)(f: SystemEnv IO[Unit]) = {
val freq = env.getMilliseconds("cron.%s.frequency" format name) millis
val freq = env.getMilliseconds("cron.frequency.%s" format name) millis
val actor = Akka.system.actorOf(Props(new Actor {
def receive = {
case "tick" f(env).unsafePerformIO

View file

@ -82,7 +82,7 @@ object AppXhrC extends LilaController {
)
}
def ping() = Action { implicit request
def ping = Action { implicit request
Ok(env.pinger.ping(
username = get("username"),
playerKey = get("player_key"),
@ -92,6 +92,19 @@ object AppXhrC extends LilaController {
).unsafePerformIO) as JSON
}
def ai = Action { implicit request
Async {
Akka.future {
env.craftyServer(fen = getOr("fen", ""), level = getIntOr("level", 1))
} map { res
res.fold(
err BadRequest(err.list mkString "\n"),
op Ok(op.unsafePerformIO)
)
}
}
}
def nbPlayers = Action { Ok(env.aliveMemo.count) }
def nbGames = Action { Ok(env.gameRepo.countPlaying.unsafePerformIO) }

View file

@ -37,19 +37,28 @@ memo {
hook.timeout = 6 seconds
finisher_lock.timeout = 30 seconds
}
crafty {
exec_path = "/usr/bin/crafty"
book_path = "/usr/share/crafty"
ai {
use = remote
crafty {
exec_path = "/usr/bin/crafty"
book_path = "/usr/share/crafty"
}
remote {
url = "http://en.l.org/lila/ai"
}
}
moretime {
seconds = 15 seconds
}
cron {
online_username.frequency = 2.1 seconds
hook_cleanup_dead.frequency = 2 seconds
hook_cleanup_old.frequency = 21 seconds
game_cleanup_unplayed.frequency = 2.11 hour
game_auto_finish.frequency = 1.07 hour
frequency {
online_username = 2.1 seconds
hook_cleanup_dead = 2 seconds
hook_cleanup_old = 21 seconds
game_cleanup_unplayed = 2.11 hour
game_auto_finish = 1.07 hour
remote_ai_health = 10 seconds
}
}
application.secret="CiebwjgIM9cHQ;I?Xk:sfqDJ;BhIe:jsL?r=?IPF[saf>s^r0]?0grUq4>q?5mP^"

View file

@ -16,6 +16,7 @@ GET /draw-cancel/:fullId controllers.AppXhrC.drawCancel(fullId: S
GET /draw-decline/:fullId controllers.AppXhrC.drawDecline(fullId: String)
POST /talk/:fullId controllers.AppXhrC.talk(fullId: String)
POST /moretime/:fullId controllers.AppXhrC.moretime(fullId: String)
GET /ai controllers.AppXhrC.ai
# App Private API
GET /api/show/:fullId controllers.AppApiC.show(fullId: String)

View file

@ -24,6 +24,7 @@ trait Dependencies {
val jodaConvert = "org.joda" % "joda-convert" % "1.2"
val scalaTime = "org.scala-tools.time" %% "time" % "0.5"
val slf4jNop = "org.slf4j" % "slf4j-nop" % "1.6.4"
val dispatch = "net.databinder" %% "dispatch-http" % "0.8.7"
// benchmark
val instrumenter = "com.google.code.java-allocation-instrumenter" % "java-allocation-instrumenter" % "2.0"
@ -54,7 +55,7 @@ object ApplicationBuild extends Build with Resolvers with Dependencies {
) dependsOn (system)
lazy val system = Project("system", file("system"), settings = buildSettings).settings(
libraryDependencies ++= Seq(scalaz, config, json, casbah, salat, guava, apache, jodaTime, jodaConvert, scalaTime)
libraryDependencies ++= Seq(scalaz, config, json, casbah, salat, guava, apache, jodaTime, jodaConvert, scalaTime, dispatch)
) dependsOn (chess)
lazy val chess = Project("chess", file("chess"), settings = buildSettings).settings(

View file

@ -90,11 +90,27 @@ final class SystemEnv(config: Config) {
versionMemo = versionMemo,
entryMemo = entryMemo)
lazy val ai: Ai = craftyAi
// tells whether the remote AI is healthy or not
// frequently updated by a scheduled actor
var remoteAiHealth = false
def ai: Ai = config getString "ai.use" match {
case "remote" remoteAiHealth.fold(remoteAi, craftyAi)
case "crafty" craftyAi
case _ stupidAi
}
lazy val remoteAi = new RemoteAi(
remoteUrl = config getString "ai.remote.url")
lazy val craftyAi = new CraftyAi(
execPath = config getString "crafty.exec_path",
bookPath = Some(config getString "crafty.book_path") filter ("" !=))
server = craftyServer)
lazy val craftyServer = new CraftyServer(
execPath = config getString "ai.crafty.exec_path",
bookPath = Some(config getString "ai.crafty.book_path") filter ("" !=))
lazy val stupidAi = new StupidAi
lazy val gameRepo = new GameRepo(
mongodb(config getString "mongo.collection.game"))

View file

@ -1,8 +1,7 @@
package lila.system
package ai
import lila.chess.{ Game, Move, ReverseEngineering }
import lila.chess.format.Forsyth
import lila.chess.{ Game, Move }
import model._
import java.io.File
@ -10,81 +9,16 @@ import scala.io.Source
import scala.sys.process.Process
import scalaz.effects._
final class CraftyAi(
execPath: String,
bookPath: Option[String] = None) extends Ai {
final class CraftyAi(server: CraftyServer) extends Ai with FenBased {
def apply(dbGame: DbGame): IO[Valid[(Game, Move)]] = {
val oldGame = dbGame.toChess
val oldFen = toFen(oldGame, dbGame.variant)
val forsyth = Forsyth >> (dbGame.variant match {
case Chess960 oldGame updateBoard { board
board updateHistory (_.withoutAnyCastles)
}
case _ oldGame
})
runCrafty(forsyth, dbGame.aiLevel | 1) map { newFen
for {
newSituation Forsyth << newFen toValid "Cannot parse engine FEN: " + newFen
reverseEngineer = new ReverseEngineering(oldGame, newSituation.board)
poss = reverseEngineer.move.mapFail(msgs
(dbGame.id + " ReverseEngineering failure: " + (msgs.list mkString "\n") + "\n--------\n" + oldGame.board + "\n" + newSituation.board + "\n" + forsyth + "\n" + newFen).wrapNel
).err
(orig, dest) = poss
newGameAndMove oldGame(orig, dest)
} yield newGameAndMove
}
}
def runCrafty(oldFen: String, level: Int): IO[String] = for {
file writeFile("lichess_crafty_", input(oldFen, level))
output io { Process(command(level)) #< file !! }
} yield extractFen(output)
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) = List(
"skill %d" format craftySkill(level),
"book random 1",
"book width 10",
"setboard %s" format fen,
"move",
"savepos",
"quit")
private def craftyTime(level: Int) = (level / 10f).toString take 4
private def craftySkill(level: Int) = level match {
case 8 100
case l l * 12
}
private def writeFile(prefix: String, data: List[String]): IO[File] = io {
File.createTempFile(prefix, ".tmp") ~ { file
try {
file.deleteOnExit
}
catch {
case e println("Error deleting crafty file on exit: " + e.getMessage)
}
printToFile(file)(p data foreach p.println)
}
}
private def printToFile(f: java.io.File)(op: java.io.PrintWriter Unit) {
val p = new java.io.PrintWriter(f)
try { op(p) } finally { p.close() }
server(oldFen, dbGame.aiLevel | 1).fold(
err io(failure(err)),
iop iop map { newFen applyFen(oldGame, newFen) }
)
}
}

View file

@ -0,0 +1,70 @@
package lila.system
package ai
import lila.chess.{ Game, Move }
import model._
import java.io.File
import scala.io.Source
import scala.sys.process.Process
import scalaz.effects._
final class CraftyServer(
execPath: String,
bookPath: Option[String] = None) {
def apply(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] = for {
file writeFile("lichess_crafty_", input(oldFen, level))
output io { Process(command(level)) #< file !! }
} yield extractFen(output)
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) = List(
"skill %d" format craftySkill(level),
"book random 1",
"book width 10",
"setboard %s" format fen,
"move",
"savepos",
"quit")
private def craftyTime(level: Int) = (level / 10f).toString take 4
private def craftySkill(level: Int) = level match {
case 8 100
case l l * 12
}
private def writeFile(prefix: String, data: List[String]): IO[File] = io {
File.createTempFile(prefix, ".tmp") ~ { file
try {
file.deleteOnExit
}
catch {
case e println("Error deleting crafty file on exit: " + e.getMessage)
}
printToFile(file)(p data foreach p.println)
}
}
private def printToFile(f: java.io.File)(op: java.io.PrintWriter Unit) {
val p = new java.io.PrintWriter(f)
try { op(p) } finally { p.close() }
}
}

View file

@ -0,0 +1,26 @@
package lila.system
package ai
import lila.chess.{ Game, Move, ReverseEngineering }
import lila.chess.format.Forsyth
import model._
trait FenBased {
def applyFen(game: Game, fen: String): Valid[(Game, Move)] = for {
newSituation Forsyth << fen toValid "Cannot parse engine FEN: " + fen
reverseEngineer = new ReverseEngineering(game, newSituation.board)
poss = reverseEngineer.move.mapFail(msgs
("ReverseEngineering failure: " + (msgs.list mkString "\n") + "\n--------\n" + game.board + "\n" + newSituation.board + "\n" + fen).wrapNel
).err
(orig, dest) = poss
newGameAndMove game(orig, dest)
} yield newGameAndMove
def toFen(game: Game, variant: Variant): String = Forsyth >> (variant match {
case Chess960 game updateBoard { board
board updateHistory (_.withoutAnyCastles)
}
case _ game
})
}

View file

@ -0,0 +1,44 @@
package lila.system
package ai
import lila.chess.{ Game, Move }
import model.DbGame
import scalaz.effects._
import dispatch._
final class RemoteAi(remoteUrl: String) extends Ai with FenBased {
private class AiHttp extends Http with thread.Safety {
override def make_logger = new Logger {
def info(msg: String, items: Any*) {}
def warn(msg: String, items: Any*) { println("WARN: " + msg.format(items: _*)) }
}
}
private lazy val http = new AiHttp
private lazy val urlObj = url(remoteUrl)
def apply(dbGame: DbGame): IO[Valid[(Game, Move)]] = {
val oldGame = dbGame.toChess
val oldFen = toFen(oldGame, dbGame.variant)
fetchNewFen(oldFen, dbGame.aiLevel | 1) map { newFen
applyFen(oldGame, newFen)
}
}
private def fetchNewFen(oldFen: String, level: Int): IO[String] = io {
http(urlObj <<? Map(
"fen" -> oldFen,
"level" -> level.toString
) as_str)
}
def health: IO[Boolean] = fetchNewFen(
oldFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq",
level = 1
).catchLeft map (_.isRight)
}