Defer Crafty AI processing to a remote server, and check its health
This commit is contained in:
parent
3075b1a734
commit
1746584c58
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -37,19 +37,28 @@ memo {
|
|||
hook.timeout = 6 seconds
|
||||
finisher_lock.timeout = 30 seconds
|
||||
}
|
||||
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^"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
70
system/src/main/scala/ai/CraftyServer.scala
Normal file
70
system/src/main/scala/ai/CraftyServer.scala
Normal 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() }
|
||||
}
|
||||
}
|
26
system/src/main/scala/ai/FenBased.scala
Normal file
26
system/src/main/scala/ai/FenBased.scala
Normal 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
|
||||
})
|
||||
}
|
44
system/src/main/scala/ai/RemoteAi.scala
Normal file
44
system/src/main/scala/ai/RemoteAi.scala
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue