refactor AI clients and servers
This commit is contained in:
parent
5eec6816a4
commit
5189903e82
|
@ -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
34
app/ai/Client.scala
Normal 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 ()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)]] = {
|
||||
|
37
app/ai/crafty/Client.scala
Normal file
37
app/ai/crafty/Client.scala
Normal 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
|
||||
}
|
|
@ -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) {
|
||||
|
|
@ -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)))
|
||||
)
|
||||
}
|
||||
|
|
35
app/ai/stockfish/Client.scala
Normal file
35
app/ai/stockfish/Client.scala
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -50,6 +50,6 @@ object Process {
|
|||
builder = SProcess(execPath),
|
||||
out = out,
|
||||
err = err,
|
||||
debug = true)
|
||||
debug = false)
|
||||
}
|
||||
|
||||
|
|
48
app/ai/stockfish/Server.scala
Normal file
48
app/ai/stockfish/Server.scala
Normal 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)))
|
||||
}
|
14
app/ai/stockfish/Stockfish.scala
Normal file
14
app/ai/stockfish/Stockfish.scala
Normal 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
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue