Merge branch 'stockfish'

* stockfish:
  upgrade scalachess and increase AI aggressiveness
  tweak featured game elo heuristic
  upgrade to scalachess 2.0
  fix ai server http api
  lazy FSM queue that picks easy jobs first
  refactor AI clients and servers
  upgrade scalachess to 1.17 to fix stockfish 960 castle
  chess960 UCI fen
  support chess960 with stockfish AI
  fix AI name
  improve and configure stockfish AI
  first version of working engine FSM!
  progress on stockfish AI

Conflicts:
	app/core/Global.scala
	todo
This commit is contained in:
Thibault Duplessis 2012-07-06 09:31:05 +02:00
commit 385e98415b
41 changed files with 578 additions and 143 deletions

View file

@ -8,5 +8,5 @@ import scalaz.effects._
trait Ai {
def apply(dbGame: DbGame): IO[Valid[(Game, Move)]]
def apply(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]]
}

View file

@ -5,26 +5,46 @@ 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 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(
server = stockfishServer)
lazy val stockfishClient = new stockfish.Client(
remoteUrl = AiStockfishRemoteUrl)
lazy val stockfishServer = new stockfish.Server(
execPath = AiStockfishExecPath,
config = stockfishConfig)
lazy val stockfishConfig = new stockfish.Config(settings)
lazy val stupidAi = new StupidAi
def isServer = AiServerMode
val isServer = AiServerMode
}

13
app/ai/AiHelper.scala Normal file
View file

@ -0,0 +1,13 @@
package lila
package ai
import http.Context
import i18n.I18nHelper
trait AiHelper extends I18nHelper {
def aiName = "Stockfish AI"
def aiName(level: Int)(implicit ctx: Context) =
trans.aiNameLevelAiLevel("Crafty A.I.", level)
}

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): 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

@ -8,7 +8,7 @@ import scalaz.effects._
final class StupidAi extends Ai {
def apply(dbGame: DbGame): IO[Valid[(Game, Move)]] = io {
def apply(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] = io {
val game = dbGame.toChess

View file

@ -1,5 +1,6 @@
package lila
package ai
package crafty
import chess.{ Game, Move }
import game.DbGame
@ -9,9 +10,9 @@ 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): IO[Valid[(Game, Move)]] = {
def apply(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] = {
val oldGame = dbGame.toChess
val oldFen = toFen(oldGame, dbGame.variant)

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) {
@ -20,11 +21,10 @@ final class CraftyServer(
def runCrafty(oldFen: String, level: Int): IO[String] =
io { Process(command(level)) #< input(oldFen, level) !! } map extractFen
private def extractFen(output: String) = {
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(

21
app/ai/stockfish/Ai.scala Normal file
View file

@ -0,0 +1,21 @@
package lila
package ai
package stockfish
import chess.{ Game, Move }
import game.DbGame
import scalaz.effects._
final class Ai(server: Server) extends lila.ai.Ai with Stockfish {
import model._
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, _)
}
)
}

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

@ -0,0 +1,33 @@
package lila
package ai.stockfish
import model._
import core.Settings
final class Config(settings: Settings) {
type Instructions = List[String]
def init: Instructions = List(
setoption("Hash", settings.AiStockfishHashSize),
setoption("Threads", 8),
setoption("Ponder", false),
setoption("Aggressiveness", settings.AiStockfishAggressiveness) // 0 - 200
)
def game(play: Play): Instructions = List(
setoption("Skill Level", skill(play.level)),
setoption("UCI_Chess960", play.chess960)
)
def maxMoveTime = 500
def moveTime(level: Int): Int =
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)
private def setoption(name: String, value: Any) =
"setoption name %s value %s".format(name, value)
}

View file

@ -0,0 +1,72 @@
package lila
package ai.stockfish
import model._
import akka.actor.{ Props, Actor, ActorRef, FSM AkkaFSM, LoggingFSM }
import scalaz.effects._
final class FSM(
processBuilder: (String Unit, String Unit) Process,
config: Config)
extends Actor with LoggingFSM[model.State, model.Data] {
val process = processBuilder(out self ! Out(out), err self ! Err(err))
startWith(Starting, Todo(Vector.empty))
when(Starting) {
case Event(Out(t), _) if t startsWith "Stockfish" {
process write "uci"
stay
}
case Event(Out(t), data) if t contains "uciok" {
config.init foreach process.write
next(data)
}
case Event(play: Play, data)
stay using (data enqueue Task(play, sender))
}
when(Ready) {
case Event(Out(t), _) { log.warning(t); stay }
}
when(UciNewGame) {
case Event(Out(t), data @ Doing(Task(play, _), _)) if t contains "readyok" {
process write play.position
process write (play go config.moveTime)
goto(Go)
}
}
when(Go) {
case Event(Out(t), data @ Doing(Task(_, ref), _)) if t contains "bestmove" {
ref ! BestMove(t.split(' ') lift 1)
goto(Ready) using data.done
}
}
whenUnhandled {
case Event(play: Play, data)
next(data enqueue Task(play, sender))
case Event(Out(""), _) stay
case Event(Out(t), _) if t startsWith "id " stay
case Event(Out(t), _) if t startsWith "info " stay
case Event(Out(t), _) if t startsWith "option name " stay
case Event(Err(t), _) { log.error(t); stay }
}
def next(data: Data) = data match {
case todo: Todo todo.doing(
doing {
config game doing.current.play foreach process.write
process write "ucinewgame"
process write "isready"
goto(UciNewGame) using doing
},
t goto(Ready) using t
)
case doing: Doing stay using data
}
def onTermination() {
process.destroy()
}
}

View file

@ -0,0 +1,55 @@
package lila
package ai.stockfish
import java.io.OutputStream
import scala.sys.process.{ Process => SProcess, ProcessBuilder, ProcessIO }
import scala.io.Source.fromInputStream
final class Process(
builder: ProcessBuilder,
out: String Unit,
err: String Unit,
debug: Boolean = false) {
def write(msg: String) {
log(msg)
in write (msg + "\n").getBytes("UTF-8")
in.flush()
}
def destroy() {
write("quit")
Thread sleep 300
process.destroy()
}
private var in: OutputStream = _
private val processIO = new ProcessIO(
i {
in = i
},
o fromInputStream(o).getLines foreach { line
log(line)
out(line)
},
e fromInputStream(e).getLines foreach { line
log(line)
err(line)
})
private val process = builder run processIO
private def log(msg: String) {
if (debug) println(msg)
}
}
object Process {
def apply(execPath: String)(out: String Unit, err: String Unit) =
new Process(
builder = SProcess(execPath),
out = out,
err = err,
debug = true)
}

View file

@ -0,0 +1,47 @@
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, config: Config) {
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 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

@ -0,0 +1,54 @@
package lila
package ai.stockfish
import chess.Pos.posAt
import akka.actor.ActorRef
object model {
case class Play(moves: String, fen: Option[String], level: Int) {
def position = "position %s moves %s".format(
fen.fold("fen " + _, "startpos"),
moves)
def go(moveTime: Int Int) = "go movetime %d" format moveTime(level)
def chess960 = fen.isDefined
}
case class BestMove(move: Option[String]) {
def parse = for {
m move
orig posAt(m take 2)
dest posAt(m drop 2)
} yield orig -> dest
}
sealed trait State
case object Starting extends State
case object Ready extends State
case object UciNewGame extends State
case object Go extends State
case class Task(play: Play, ref: ActorRef)
sealed trait Data {
def queue: Vector[Task]
def enqueue(task: Task): Data
}
case class Todo(queue: Vector[Task]) extends Data {
def enqueue(task: Task) = copy(queue = queue :+ task)
def doing[A](withTask: Doing A, without: Todo A) =
easierTaskInQueue.fold(
task withTask(Doing(task, queue.tail)),
without(Todo(Vector.empty))
)
private def easierTaskInQueue = queue sortBy (_.play.level) headOption
}
case class Doing(current: Task, queue: Vector[Task]) extends Data {
def enqueue(task: Task) = copy(queue = queue :+ task)
def done = Todo(queue)
}
sealed trait Stream { def text: String }
case class Out(text: String) extends Stream
case class Err(text: String) extends Stream
}

View file

@ -48,7 +48,7 @@ final class PgnDump(gameRepo: GameRepo, userRepo: UserRepo) {
io(none))
def player(p: DbPlayer, u: Option[User]) = p.aiLevel.fold(
"Crafty level " + _,
"AI level " + _,
u.fold(_.username, "Anonymous"))
def moves(game: DbGame) = (game.pgnList grouped 2).zipWithIndex map {

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

@ -20,12 +20,14 @@ object Global extends GlobalSettings {
else core.Cron start env
}
override def onRouteRequest(req: RequestHeader): Option[Handler] = {
env.monitor.rpsProvider.countRequest()
env.security.firewall.requestHandler(req) orElse
env.i18n.requestHandler(req) orElse
override def onRouteRequest(req: RequestHeader): Option[Handler] =
if (env.ai.isServer) super.onRouteRequest(req)
else {
env.monitor.rpsProvider.countRequest()
env.security.firewall.requestHandler(req) orElse
env.i18n.requestHandler(req) orElse
super.onRouteRequest(req)
}
}
override def onHandlerNotFound(req: RequestHeader): Result = {
controllers.Lobby handleNotFound req

View file

@ -2,6 +2,7 @@ package lila
package core
import com.typesafe.config.Config
import scalaz.{ Success, Failure }
final class Settings(config: Config) {
@ -49,12 +50,20 @@ final class Settings(config: Config) {
val FinisherLockTimeout = millis("memo.finisher_lock.timeout")
val AiChoice = getString("ai.use")
val AiCrafty = "crafty"
val AiStockfish = "stockfish"
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 AiRemote = "remote"
val AiCrafty = "crafty"
val AiCraftyRemoteUrl = getString("ai.crafty.remote_url")
val AiStockfishExecPath = getString("ai.stockfish.exec_path")
val AiStockfishRemoteUrl = getString("ai.stockfish.remote_url")
val AiStockfishHashSize = getInt("ai.stockfish.hash_size")
val AiStockfishAggressiveness = getInt("ai.stockfish.aggressiveness")
val MongoHost = getString("mongo.host")
val MongoPort = getInt("mongo.port")
@ -96,4 +105,13 @@ final class Settings(config: Config) {
private def millis(name: String): Int = getMilliseconds(name).toInt
private def seconds(name: String): Int = millis(name) / 1000
implicit def validAny[A](a: A) = new {
def valid(f: A Valid[A]): A = f(a) match {
case Success(a) a
case Failure(err) throw new Invalid(err.shows)
}
}
private class Invalid(msg: String) extends Exception(msg)
}

View file

@ -95,7 +95,7 @@ case class DbGame(
}
Game(
board = Board(pieces, toChessHistory),
board = Board(pieces, toChessHistory, variant),
player = Color(0 == turns % 2),
pgnMoves = pgn,
clock = clock,

View file

@ -48,8 +48,6 @@ case class DbPlayer(
def hasMoveTimes = moveTimes.size > 8
def eloEstimation = elo | 1200
def moveTimeList: List[Int] =
moveTimes.split(" ").toList map parseIntOption flatten

View file

@ -70,7 +70,7 @@ object Featured {
private type Heuristic = DbGame Float
private val heuristicBox = box(0 to 1) _
private val eloBox = box(1200 to 2000) _
private val eloBox = box(1000 to 2000) _
private val timeBox = box(60 to 300) _
private val turnBox = box(1 to 21) _
@ -81,7 +81,7 @@ object Featured {
progressHeuristic -> 0.5f)
def eloHeuristic(color: Color): Heuristic = game
eloBox(game.player(color).eloEstimation)
eloBox(game.player(color).elo | 1000)
def speedHeuristic: Heuristic = game
1 - timeBox(game.estimateTotalTime)

View file

@ -6,6 +6,7 @@ import chess.{ Status, Variant, Color, Clock, Mode }
import user.{ User, UserHelper }
import http.Context
import i18n.I18nHelper
import ai.AiHelper
import templating.StringHelper
import controllers.routes
@ -13,9 +14,7 @@ import controllers.routes
import play.api.templates.Html
import play.api.mvc.Call
trait GameHelper { self: I18nHelper with UserHelper with StringHelper
val aiName = "Crafty A.I."
trait GameHelper { self: I18nHelper with UserHelper with StringHelper with AiHelper
def variantName(variant: Variant)(implicit ctx: Context) = variant match {
case Variant.Standard trans.standard.str()
@ -63,9 +62,7 @@ trait GameHelper { self: I18nHelper with UserHelper with StringHelper ⇒
},
"""<span class="user_link %s">%s</span>""".format(
cssClass | "",
player.aiLevel.fold(
trans.aiNameLevelAiLevel(aiName, _),
User.anonymous)
player.aiLevel.fold(aiName, User.anonymous)
)
)
}

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

@ -25,6 +25,7 @@ final class Hand(
moretimeSeconds: Int) extends Handler(gameRepo) {
type IOValidEvents = IO[Valid[List[Event]]]
type PlayResult = IO[Valid[(List[Event], String)]]
def play(
povRef: PovRef,
@ -32,7 +33,7 @@ final class Hand(
destString: String,
promString: Option[String] = None,
blur: Boolean = false,
lag: Int = 0): IO[Valid[(List[Event], String)]] = fromPov(povRef) {
lag: Int = 0): PlayResult = fromPov(povRef) {
case Pov(g1, color) (for {
g2 g1.validIf(g1 playableBy color, "Game not playable")
orig posAt(origString) toValid "Wrong orig " + origString
@ -44,27 +45,36 @@ final class Hand(
).prefixFailuresWith(povRef + " - ").fold(
e io(failure(e)),
progress for {
eventsAndBoard if (progress.game.finished) for {
eventsAndFen if (progress.game.finished) for {
_ gameRepo save progress
finishEvents finisher.moveFinish(progress.game, color)
events = progress.events ::: finishEvents
fen = fenBoard(progress)
} yield events -> fen
} yield success(events -> fen)
else if (progress.game.player.isAi && progress.game.playable) for {
aiResult ai()(progress.game) map (_.err)
(newChessGame, move) = aiResult
progress2 = progress flatMap { _.update(newChessGame, move) }
_ gameRepo save progress2
finishEvents finisher.moveFinish(progress2.game, !color)
events = progress2.events ::: finishEvents
fen = fenBoard(progress2)
} yield events -> fen
initialFen progress.game.variant.standard.fold(
io(none[String]),
gameRepo initialFen progress.game.id)
aiResult ai()(progress.game, initialFen)
eventsAndFen aiResult.fold(
err io(failure(err)), {
case (newChessGame, move) for {
progress2 io {
progress flatMap { _.update(newChessGame, move) }
}
_ gameRepo save progress2
finishEvents finisher.moveFinish(progress2.game, !color)
events = progress2.events ::: finishEvents
fen = fenBoard(progress2)
} yield success(events -> fen)
}): PlayResult
} yield eventsAndFen
else for {
_ gameRepo save progress
events = progress.events
events = progress.events
fen = fenBoard(progress)
} yield events -> fen
} yield success(eventsAndBoard)
} yield success(events -> fen)
} yield eventsAndFen
)
}
@ -75,13 +85,13 @@ final class Hand(
def resignForce(fullId: String): IO[Valid[List[Event]]] =
gameRepo pov fullId flatMap { povOption
(povOption toValid "No such game" flatMap { pov
implicit val timeout = Timeout(100 millis)
implicit val timeout = Timeout(1 second)
Await.result(
hubMaster ? round.IsGone(pov.game.id, !pov.color) map {
case true finisher resignForce pov
case _ !!("Opponent is not gone")
},
100 millis
1 second
)
}).fold(err io(failure(err)), _ map success)
}
@ -242,6 +252,6 @@ final class Hand(
} toValid "cannot add moretime"
)
private def fenBoard(progress: Progress) =
private def fenBoard(progress: Progress) =
Forsyth exportBoard progress.game.toChess.board
}

View file

@ -12,7 +12,7 @@ case class AiConfig(
def >> = (variant.id, level, color.name).some
def game = DbGame(
game = Game(board = Board(pieces = variant.pieces)),
game = Game(board = Board init variant),
ai = Some(!creatorColor -> level),
whitePlayer = DbPlayer(
color = ChessColor.White,

View file

@ -13,7 +13,7 @@ case object ApiConfig extends Config with GameGenerator {
def game = DbGame(
game = Game(
board = Board(pieces = variant.pieces),
board = Board init variant,
clock = none),
ai = None,
whitePlayer = DbPlayer.white,

View file

@ -17,7 +17,7 @@ case class FriendConfig(
def game = DbGame(
game = Game(
board = Board(pieces = variant.pieces),
board = Board init variant,
clock = makeClock),
ai = None,
whitePlayer = DbPlayer.white,

View file

@ -54,7 +54,7 @@ final class HookJoiner(
def makeGame(hook: Hook) = DbGame(
game = Game(
board = Board(pieces = hook.realVariant.pieces),
board = Board init hook.realVariant,
clock = hook.hasClock.fold(
hook.time |@| hook.increment apply { (limit, inc)
Clock(limit = limit, increment = inc)

View file

@ -35,7 +35,10 @@ final class Processor(
pov2 game.player.isHuman.fold(
io(pov),
for {
aiResult ai()(game) map (_.err)
initialFen game.variant.standard.fold(
io(none[String]),
gameRepo initialFen game.id)
aiResult ai()(game, initialFen) map (_.err)
(newChessGame, move) = aiResult
progress = game.update(newChessGame, move)
_ gameRepo save progress

View file

@ -65,7 +65,7 @@ final class Rematcher(
} yield pov.fullId -> p2.events
private def returnGame(pov: Pov): IO[DbGame] = for {
board pov.game.variant.standard.fold(
pieces pov.game.variant.standard.fold(
io(pov.game.variant.pieces),
gameRepo initialFen pov.game.id map { fenOption
(fenOption flatMap Forsyth.<< map { situation
@ -76,7 +76,7 @@ final class Rematcher(
blackPlayer returnPlayer(pov.game, Black)
} yield DbGame(
game = Game(
board = Board(board),
board = Board(pieces, variant = pov.game.variant),
clock = pov.game.clock map (_.reset)),
whitePlayer = whitePlayer,
blackPlayer = blackPlayer,

View file

@ -21,6 +21,7 @@ object Environment
with setup.SetupHelper
with message.MessageHelper
with round.RoundHelper
with ai.AiHelper
with game.GameHelper
with user.UserHelper
with forum.ForumHelper

View file

@ -2,7 +2,7 @@
@ai(level: Int) = {
<div class="username connected">
@trans.aiNameLevelAiLevel("Crafty A.I.", level)
@aiName(level)
</div>
}

View file

@ -78,13 +78,22 @@ memo {
username.timeout = 7 seconds
}
ai {
server = false
use = remote
use = stockfish
server = true
client = true
crafty {
exec_path = "/usr/bin/crafty"
book_path = "/usr/share/crafty"
remote_url = "http://188.165.194.171:9071/ai/play/crafty"
}
stockfish {
exec_path = "/usr/bin/stockfish"
#remote_url = "http://188.165.194.171:9071/ai/play/stockfish"
remote_url = "http://localhost:9000/ai/play/stockfish"
#hash_size = 4096
hash_size = 1024
aggressiveness = 150
}
remote.url = "http://188.165.194.171:9071/ai"
}
moretime.seconds = 15 seconds
@ -117,7 +126,7 @@ logger {
akka {
loglevel = INFO
stdout-loglevel = INFO
stdout-loglevel = DEBUG
log-config-on-start = off
event-handlers = ["lila.core.AkkaLogger"]
}

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

@ -14,7 +14,7 @@ trait Resolvers {
}
trait Dependencies {
val scalachess = "com.github.ornicar" %% "scalachess" % "1.14"
val scalachess = "com.github.ornicar" %% "scalachess" % "2.1"
val scalaz = "org.scalaz" %% "scalaz-core" % "6.0.4"
val specs2 = "org.specs2" %% "specs2" % "1.11"
val salat = "com.novus" %% "salat-core" % "1.9-SNAPSHOT"

View file

@ -35,7 +35,7 @@ class FeaturedTest extends LilaSpec {
}
val game1 = DbGame(
game = chess.Game(),
game = chess.Game(chess.Variant.default),
whitePlayer = DbPlayer.white.copy(elo = 1600.some),
blackPlayer = DbPlayer.black,
ai = None,
@ -55,7 +55,7 @@ class FeaturedTest extends LilaSpec {
"elo" in {
"game1 white" in {
eloHeuristic(chess.Color.White)(game1) must_== 0.5f
eloHeuristic(chess.Color.White)(game1) must_== 0.6f
}
"game1 black" in {
eloHeuristic(chess.Color.Black)(game1) must_== 0f
@ -85,13 +85,13 @@ class FeaturedTest extends LilaSpec {
}
"score" in {
"game1" in {
score(game1) must_== 0.5f + 0f + 1f * 0.5f
score(game1) must_== 0.6f + 0f + 1f * 0.5f
}
"game2" in {
score(game2) must_== 0.5f + 0.5f + 0.5f * 0.5f
score(game2) must_== 0.6f + 0.5f + 0.5f * 0.5f
}
"game3" in {
score(game3) must_== 0.5f + 1f + 0f * 0.5f
score(game3) must_== 0.6f + 1f + 0f * 0.5f
}
}
"best" in {

2
todo
View file

@ -24,3 +24,5 @@ style sound toggle
show lobby chat to anon (and rated games?)
or show empty chat
play with a friend possible bug http://en.lichess.org/forum/lichess-feedback/play-with-a-friend-bug
game chat box css issue http://en.lichess.org/forum/lichess-feedback/problem-with-the-chat-box#1
show last move on miniboards