refactor AI clients and servers

This commit is contained in:
Thibault Duplessis 2012-06-24 17:09:11 +02:00
parent 5eec6816a4
commit 5189903e82
19 changed files with 248 additions and 133 deletions

View file

@ -5,27 +5,41 @@ import com.mongodb.casbah.MongoCollection
import core.Settings
final class AiEnv(
settings: Settings) {
final class AiEnv(settings: Settings) {
import settings._
val ai: () Ai = AiChoice match {
case AiStockfish () stockfishAi
case AiRemote () remoteAi or craftyAi
case AiCrafty () craftyAi
case _ () stupidAi
val ai: () Ai = (AiChoice, AiClientMode) match {
case (AiStockfish, false) () stockfishAi
case (AiStockfish, true) () stockfishClient or stockfishAi
case (AiCrafty, false) () craftyAi
case (AiCrafty, true) () craftyClient or craftyAi
case _ () stupidAi
}
lazy val remoteAi = new RemoteAi(remoteUrl = AiRemoteUrl)
lazy val client: Client = AiChoice match {
case AiStockfish stockfishClient
case AiCrafty craftyClient
}
lazy val craftyAi = new CraftyAi(server = craftyServer)
lazy val craftyAi = new crafty.Ai(
server = craftyServer)
lazy val craftyServer = new CraftyServer(
lazy val craftyClient = new crafty.Client(
remoteUrl = AiCraftyRemoteUrl)
lazy val craftyServer = new crafty.Server(
execPath = AiCraftyExecPath,
bookPath = AiCraftyBookPath)
lazy val stockfishAi = new stockfish.Ai(execPath = AiStockfishExecPath)
lazy val stockfishAi = new stockfish.Ai(
server = stockfishServer)
lazy val stockfishClient = new stockfish.Client(
remoteUrl = AiStockfishRemoteUrl)
lazy val stockfishServer = new stockfish.Server(
execPath = AiStockfishExecPath)
lazy val stupidAi = new StupidAi

34
app/ai/Client.scala Normal file
View file

@ -0,0 +1,34 @@
package lila
package ai
import dispatch.{ Http, NoLogging, url }
import dispatch.thread.{ Safety ThreadSafety }
import scalaz.effects._
trait Client extends Ai {
val remoteUrl: String
protected def tryPing: IO[Option[Int]]
// tells whether the remote AI is healthy or not
// frequently updated by a scheduled actor
protected var ping = none[Int]
protected val pingAlert = 3000
protected lazy val http = new Http with ThreadSafety with NoLogging
protected lazy val urlObj = url(remoteUrl)
def or(fallback: Ai) = if (currentHealth) this else fallback
def currentPing = ping
def currentHealth = ping.fold(_ < pingAlert, false)
def diagnose: IO[Unit] = for {
p tryPing
_ p.fold(_ < pingAlert, false).fold(
currentHealth.fold(io(), putStrLn("remote AI is up, ping = " + p)),
putStrLn("remote AI is down, ping = " + p))
_ io { ping = p }
} yield ()
}

View file

@ -1,60 +0,0 @@
package lila
package ai
import chess.{ Game, Move }
import game.DbGame
import scalaz.effects._
import dispatch.{ Http, NoLogging, url }
import dispatch.thread.{ Safety ThreadSafety }
final class RemoteAi(
remoteUrl: String) extends Ai with FenBased {
// tells whether the remote AI is healthy or not
// frequently updated by a scheduled actor
private var ping = none[Int]
val pingAlert = 3000
private lazy val http = new Http with ThreadSafety with NoLogging
private lazy val urlObj = url(remoteUrl)
def apply(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 or(fallback: Ai) = if (currentHealth) this else fallback
def currentPing = ping
def currentHealth = ping.fold(_ < pingAlert, false)
def diagnose: IO[Unit] = for {
p tryPing
_ p.fold(_ < pingAlert, false).fold(
currentHealth.fold(io(), putStrLn("remote AI is up, ping = " + p)),
putStrLn("remote AI is down, ping = " + p))
_ io { ping = p }
} yield ()
private def fetchNewFen(oldFen: String, level: Int): IO[String] = io {
http(urlObj <<? Map(
"fen" -> oldFen,
"level" -> level.toString
) as_str)
}
private 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,5 +1,6 @@
package lila
package ai
package crafty
import chess.{ Game, Move }
import game.DbGame
@ -9,7 +10,7 @@ import scala.io.Source
import scala.sys.process.Process
import scalaz.effects._
final class CraftyAi(server: CraftyServer) extends Ai with FenBased {
final class Ai(server: Server) extends ai.Ai with FenBased {
def apply(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] = {

View file

@ -0,0 +1,37 @@
package lila
package ai
package crafty
import chess.{ Game, Move }
import game.DbGame
import scalaz.effects._
final class Client(val remoteUrl: String) extends ai.Client with FenBased {
def apply(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)
}
}
private def fetchNewFen(oldFen: String, level: Int): IO[String] = io {
http(urlObj <<? 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,5 +1,6 @@
package lila
package ai
package crafty
import chess.{ Game, Move }
@ -8,7 +9,7 @@ import scala.io.Source
import scala.sys.process.Process
import scalaz.effects._
final class CraftyServer(
final class Server(
execPath: String,
bookPath: Option[String] = None) {

View file

@ -2,52 +2,20 @@ package lila
package ai
package stockfish
import chess.{ Game, Move, Rook }
import chess.format.UciDump
import chess.format.Forsyth
import chess.{ Game, Move }
import game.DbGame
import akka.util.Timeout
import akka.util.Duration
import akka.util.duration._
import akka.dispatch.{ Future, Await }
import akka.actor.{ Props, Actor, ActorRef }
import akka.pattern.ask
import play.api.Play.current
import play.api.libs.concurrent._
import scalaz.effects._
final class Ai(execPath: String) extends lila.ai.Ai {
final class Ai(server: Server) extends lila.ai.Ai with Stockfish {
import model._
def apply(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] = io {
for {
moves UciDump(dbGame.pgn, initialFen)
play = Play(moves, initialFen map chess960Fen, dbGame.aiLevel | 1)
bestMove unsafe {
Await.result(actor ? play mapTo manifest[BestMove], atMost)
def apply(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] =
server(dbGame.pgn, initialFen, dbGame.aiLevel | 1).fold(
err io(failure(err)),
iop iop map {
applyMove(dbGame, _)
}
result bestMove.parse toValid "Invalid engine move"
(orig, dest) = result
result dbGame.toChess(orig, dest)
} yield result
}
private def chess960Fen(fen: String) = (Forsyth << fen).fold(
situation fen.replace("KQkq", situation.board.pieces.toList filter {
case (_, piece) piece is Rook
} sortBy {
case (pos, _) (pos.y, pos.x)
} map {
case (pos, piece) piece.color.fold(pos.file.toUpperCase, pos.file)
} mkString ""),
fen)
private val atMost = 5 seconds
private implicit val timeout = new Timeout(atMost)
private val process = Process(execPath) _
private val config = new Config
private val actor = Akka.system.actorOf(Props(new FSM(process, config)))
)
}

View file

@ -0,0 +1,35 @@
package lila
package ai
package stockfish
import chess.{ Game, Move }
import game.DbGame
import scalaz.effects._
final class Client(val remoteUrl: String) extends ai.Client with Stockfish {
def apply(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] = {
fetchMove(dbGame.pgn, initialFen | "", dbGame.aiLevel | 1) map {
applyMove(dbGame, _)
}
}
protected def tryPing: IO[Option[Int]] = for {
start io(nowMillis)
received fetchMove(
pgn = "",
initialFen = "",
level = 1
).catchLeft map (_.isRight)
delay io(nowMillis - start)
} yield received option delay.toInt
private def fetchMove(pgn: String, initialFen: String, level: Int): IO[String] = io {
http(urlObj <<? Map(
"pgn" -> pgn,
"initialFen" -> initialFen,
"level" -> level.toString
) as_str)
}
}

View file

@ -18,8 +18,10 @@ final class Config {
setoption("UCI_Chess960", play.chess960)
)
def maxMoveTime = 500
def moveTime(level: Int): Int =
(level >= 1 && level <= 8).fold(1000 / (9 - level), 100)
maxMoveTime / (level >= 1 && level <= 8).fold(9 - level, 8)
private def skill(level: Int) =
(level >= 1 && level <= 8).fold(math.round((level -1) * (20 / 7f)), 0)

View file

@ -50,6 +50,6 @@ object Process {
builder = SProcess(execPath),
out = out,
err = err,
debug = true)
debug = false)
}

View file

@ -0,0 +1,48 @@
package lila
package ai
package stockfish
import chess.Rook
import chess.format.UciDump
import chess.format.Forsyth
import akka.util.Timeout
import akka.util.Duration
import akka.util.duration._
import akka.dispatch.{ Future, Await }
import akka.actor.{ Props, Actor, ActorRef }
import akka.pattern.ask
import play.api.Play.current
import play.api.libs.concurrent._
import scalaz.effects._
final class Server(execPath: String) {
import model._
def apply(pgn: String, initialFen: Option[String], level: Int): Valid[IO[String]] =
if (level < 1 || level > 8) "Invalid ai level".failNel
else for {
moves UciDump(pgn, initialFen)
play = Play(moves, initialFen map chess960Fen, level)
} yield io {
Await.result(actor ? play mapTo manifest[BestMove], atMost)
} map (_.move | "")
private def chess960Fen(fen: String) = (Forsyth << fen).fold(
situation fen.replace("KQkq", situation.board.pieces.toList filter {
case (_, piece) piece is Rook
} sortBy {
case (pos, _) (pos.y, pos.x)
} map {
case (pos, piece) piece.color.fold(pos.file.toUpperCase, pos.file)
} mkString ""),
fen)
private val atMost = 5 seconds
private implicit val timeout = new Timeout(atMost)
private val process = Process(execPath) _
private val config = new Config
private val actor = Akka.system.actorOf(Props(new FSM(process, config)))
}

View file

@ -0,0 +1,14 @@
package lila
package ai.stockfish
import model.BestMove
import game.DbGame
trait Stockfish {
protected def applyMove(dbGame: DbGame, move: String) = for {
bestMove BestMove(move.some filter ("" !=)).parse toValid "Wrong bestmove " + move
(orig, dest) = bestMove
result dbGame.toChess(orig, dest)
} yield result
}

View file

@ -12,8 +12,9 @@ import play.api.Play.current
object Ai extends LilaController {
private val craftyServer = env.ai.craftyServer
private val stockfishServer = env.ai.stockfishServer
def run = Action { implicit req
def playCrafty = Action { implicit req
implicit val ctx = Context(req, None)
Async {
Akka.future {
@ -26,4 +27,21 @@ object Ai extends LilaController {
}
}
}
def playStockfish = Action { implicit req
implicit val ctx = Context(req, None)
Async {
Akka.future {
stockfishServer(
pgn = getOr("pgn", ""),
initialFen = get("initialFen"),
level = getIntOr("level", 1))
} map { res
res.fold(
err BadRequest(err.shows),
op Ok(op.unsafePerformIO)
)
}
}
}
}

View file

@ -68,11 +68,11 @@ object Cron {
env.titivate.finishByClock
}
effect(10 seconds, "ai diagnose") {
env.ai.remoteAi.diagnose
}
env.ai.remoteAi.diagnose.unsafePerformIO
env.ai.client.diagnose.unsafePerformIO
}
effect(10 seconds, "ai diagnose") {
env.ai.client.diagnose
}
def message(freq: Duration)(to: (ActorRef, Any)) {
Akka.system.scheduler.schedule(freq, freq.randomize(), to._1, to._2)

View file

@ -50,11 +50,12 @@ final class Settings(config: Config) {
val AiChoice = getString("ai.use")
val AiServerMode = getBoolean("ai.server")
val AiRemoteUrl = getString("ai.remote.url")
val AiClientMode = getBoolean("ai.client")
val AiCraftyExecPath = getString("ai.crafty.exec_path")
val AiCraftyBookPath = Some(getString("ai.crafty.book_path")) filter ("" !=)
val AiCraftyRemoteUrl = getString("ai.crafty.remote_url")
val AiStockfishExecPath = getString("ai.stockfish.exec_path")
val AiRemote = "remote"
val AiStockfishRemoteUrl = getString("ai.stockfish.remote_url")
val AiCrafty = "crafty"
val AiStockfish = "stockfish"

View file

@ -39,7 +39,7 @@ final class Reporting(
var mps = 0
var cpu = 0
var mongoStatus = MongoStatus.default
var remoteAi = none[Int]
var clientAi = none[Int]
var displays = 0
@ -87,7 +87,7 @@ final class Reporting(
rps = rpsProvider.rps
mps = mpsProvider.rps
cpu = ((cpuStats.getCpuUsage() * 1000).round / 10.0).toInt
remoteAi = env.ai.remoteAi.currentPing
clientAi = env.ai.client.currentPing
}
} onComplete {
case Left(a) println("Reporting: " + a.getMessage)
@ -111,7 +111,7 @@ final class Reporting(
"load" -> loadAvg.toString.replace("0.", "."),
"mem" -> memory,
"cpu" -> cpu,
"AI" -> remoteAi.isDefined.fold("1", "0")
"AI" -> clientAi.isDefined.fold("1", "0")
))
if (displays % 8 == 0) println(data.header)
@ -125,7 +125,7 @@ final class Reporting(
nbGames,
game.nbHubs,
loadAvg.toString,
(remoteAi | 9999)
(clientAi | 9999)
) mkString " "
private def monitorData = List(
@ -143,7 +143,7 @@ final class Reporting(
"dbConn" -> mongoStatus.connection,
"dbQps" -> mongoStatus.qps,
"dbLock" -> math.round(mongoStatus.lock * 10) / 10d,
"ai" -> (remoteAi | 9999)
"ai" -> (clientAi | 9999)
) map {
case (name, value) value + ":" + name
}

View file

@ -78,17 +78,18 @@ memo {
username.timeout = 7 seconds
}
ai {
use = stockfish
server = false
use = stockfish # remote
client = true
crafty {
exec_path = "/usr/bin/crafty"
book_path = "/usr/share/crafty"
remote_url = "http://localhost:9000/ai/play/crafty"
}
stockfish {
exec_path = "/usr/bin/stockfish"
}
remote {
url = "http://188.165.194.171:9071/ai"
#url = "http://188.165.194.171:9071/ai/play/stockfish"
remote_url = "http://localhost:9000/ai/play/stockfish"
}
}
moretime.seconds = 15 seconds

View file

@ -89,7 +89,8 @@ GET /wiki controllers.Wiki.home
GET /wiki/:slug controllers.Wiki.show(slug: String)
# AI
GET /ai controllers.Ai.run
GET /ai/play/crafty controllers.Ai.playCrafty
GET /ai/play/stockfish controllers.Ai.playStockfish
# Lobby
GET / controllers.Lobby.home

View file

@ -13,7 +13,7 @@ trait Resolvers {
}
trait Dependencies {
val scalachess = "com.github.ornicar" %% "scalachess" % "1.17"
val scalachess = "com.github.ornicar" %% "scalachess" % "1.18"
val scalaz = "org.scalaz" %% "scalaz-core" % "6.0.4"
val specs2 = "org.specs2" %% "specs2" % "1.11"
val casbah = "com.mongodb.casbah" %% "casbah" % "2.1.5-1"