Merge branch 'analysis'
* analysis: (36 commits) improve AI client ping fix AI server urls change AI server port tweak AI server routing AI server only answers AI requests simple 404 pages for AI server add NotificationHelper and display analysis request forum under condition show advantage chart with only one line more analysis UI tweaks improve analysis request form send notifications through websockets improve advantage chart by always showing both lines add email address to error page ignore analysis moves with cp=0 remove debug code fix analysis UI connect the advantage chart to the replay implement notifications improve analysis UI refactor configuration ... Conflicts: todo
This commit is contained in:
commit
681213d981
10
README.md
10
README.md
|
@ -85,9 +85,15 @@ server {
|
|||
Run it
|
||||
------
|
||||
|
||||
Open lila play console and give it a ride
|
||||
Open lila play console and give it a ride.
|
||||
|
||||
>To open a play console using development configuration, use:
|
||||
>
|
||||
> bin/dev
|
||||
>
|
||||
>This will use the `conf/application_dev.conf` configuration file.
|
||||
|
||||
```
|
||||
play
|
||||
bin/dev
|
||||
[lila] $ run
|
||||
```
|
||||
|
|
|
@ -3,10 +3,14 @@ package ai
|
|||
|
||||
import chess.{ Game, Move }
|
||||
import game.DbGame
|
||||
import analyse.Analysis
|
||||
|
||||
import scalaz.effects._
|
||||
import akka.dispatch.Future
|
||||
|
||||
trait Ai {
|
||||
|
||||
def apply(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]]
|
||||
def play(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]]
|
||||
|
||||
def analyse(dbGame: DbGame, initialFen: Option[String]): Future[Valid[Analysis]]
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package lila
|
|||
package ai
|
||||
|
||||
import com.mongodb.casbah.MongoCollection
|
||||
import scalaz.effects._
|
||||
|
||||
import core.Settings
|
||||
|
||||
|
@ -9,24 +10,19 @@ final class AiEnv(settings: Settings) {
|
|||
|
||||
import settings._
|
||||
|
||||
val ai: () ⇒ Ai = (AiChoice, AiClientMode) match {
|
||||
case (AiStockfish, false) ⇒ () ⇒ stockfishAi
|
||||
lazy val ai: () ⇒ Ai = (AiChoice, isClient) match {
|
||||
case (AiStockfish, true) ⇒ () ⇒ stockfishClient or stockfishAi
|
||||
case (AiCrafty, false) ⇒ () ⇒ craftyAi
|
||||
case (AiStockfish, false) ⇒ () ⇒ stockfishAi
|
||||
case (AiCrafty, true) ⇒ () ⇒ craftyClient or craftyAi
|
||||
case (AiCrafty, false) ⇒ () ⇒ craftyAi
|
||||
case _ ⇒ () ⇒ stupidAi
|
||||
}
|
||||
|
||||
lazy val client: Client = AiChoice match {
|
||||
case AiStockfish ⇒ stockfishClient
|
||||
case AiCrafty ⇒ craftyClient
|
||||
}
|
||||
|
||||
lazy val craftyAi = new crafty.Ai(
|
||||
server = craftyServer)
|
||||
|
||||
lazy val craftyClient = new crafty.Client(
|
||||
remoteUrl = AiCraftyRemoteUrl)
|
||||
playUrl = AiCraftyPlayUrl)
|
||||
|
||||
lazy val craftyServer = new crafty.Server(
|
||||
execPath = AiCraftyExecPath,
|
||||
|
@ -36,15 +32,30 @@ final class AiEnv(settings: Settings) {
|
|||
server = stockfishServer)
|
||||
|
||||
lazy val stockfishClient = new stockfish.Client(
|
||||
remoteUrl = AiStockfishRemoteUrl)
|
||||
playUrl = AiStockfishPlayUrl,
|
||||
analyseUrl = AiStockfishAnalyseUrl)
|
||||
|
||||
lazy val stockfishServer = new stockfish.Server(
|
||||
execPath = AiStockfishExecPath,
|
||||
config = stockfishConfig)
|
||||
playConfig = stockfishPlayConfig,
|
||||
analyseConfig = stockfishAnalyseConfig)
|
||||
|
||||
lazy val stockfishConfig = new stockfish.Config(settings)
|
||||
lazy val stockfishPlayConfig = new stockfish.PlayConfig(settings)
|
||||
lazy val stockfishAnalyseConfig = new stockfish.AnalyseConfig(settings)
|
||||
|
||||
lazy val stupidAi = new StupidAi
|
||||
|
||||
val isServer = AiServerMode
|
||||
lazy val isClient = AiClientMode
|
||||
lazy val isServer = AiServerMode
|
||||
|
||||
lazy val clientDiagnose = client.fold(_.diagnose, io())
|
||||
|
||||
def clientPing = client flatMap (_.currentPing)
|
||||
|
||||
private lazy val client = isClient option {
|
||||
AiChoice match {
|
||||
case AiStockfish ⇒ stockfishClient
|
||||
case AiCrafty ⇒ craftyClient
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import scalaz.effects._
|
|||
|
||||
trait Client extends Ai {
|
||||
|
||||
val remoteUrl: String
|
||||
val playUrl: String
|
||||
|
||||
protected def tryPing: IO[Option[Int]]
|
||||
|
||||
|
@ -17,14 +17,14 @@ trait Client extends Ai {
|
|||
protected val pingAlert = 3000
|
||||
|
||||
protected lazy val http = new Http with ThreadSafety with NoLogging
|
||||
protected lazy val urlObj = url(remoteUrl)
|
||||
protected lazy val playUrlObj = url(playUrl)
|
||||
|
||||
def or(fallback: Ai) = if (currentHealth) this else fallback
|
||||
|
||||
def currentPing = ping
|
||||
def currentHealth = ping.fold(_ < pingAlert, false)
|
||||
|
||||
def diagnose: IO[Unit] = for {
|
||||
val diagnose: IO[Unit] = for {
|
||||
p ← tryPing
|
||||
_ ← p.fold(_ < pingAlert, false).fold(
|
||||
currentHealth.fold(io(), putStrLn("remote AI is up, ping = " + p)),
|
||||
|
|
|
@ -8,7 +8,7 @@ import scalaz.effects._
|
|||
|
||||
final class StupidAi extends Ai {
|
||||
|
||||
def apply(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] = io {
|
||||
def play(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] = io {
|
||||
|
||||
val game = dbGame.toChess
|
||||
|
||||
|
@ -19,4 +19,7 @@ final class StupidAi extends Ai {
|
|||
newChessGameAndMove ← game(orig, dest)
|
||||
} yield newChessGameAndMove
|
||||
}
|
||||
|
||||
def analyse(dbGame: DbGame, initialFen: Option[String]) =
|
||||
throw new RuntimeException("Stupid analysis is not implemented")
|
||||
}
|
||||
|
|
|
@ -9,17 +9,21 @@ import java.io.File
|
|||
import scala.io.Source
|
||||
import scala.sys.process.Process
|
||||
import scalaz.effects._
|
||||
import akka.dispatch.Future
|
||||
|
||||
final class Ai(server: Server) extends ai.Ai with FenBased {
|
||||
|
||||
def apply(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] = {
|
||||
def play(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] = {
|
||||
|
||||
val oldGame = dbGame.toChess
|
||||
val oldFen = toFen(oldGame, dbGame.variant)
|
||||
|
||||
server(oldFen, dbGame.aiLevel | 1).fold(
|
||||
server.play(oldFen, dbGame.aiLevel | 1).fold(
|
||||
err ⇒ io(failure(err)),
|
||||
iop ⇒ iop map { newFen ⇒ applyFen(oldGame, newFen) }
|
||||
)
|
||||
}
|
||||
|
||||
def analyse(dbGame: DbGame, initialFen: Option[String]) =
|
||||
throw new RuntimeException("Crafty analysis is not implemented")
|
||||
}
|
||||
|
|
|
@ -7,9 +7,9 @@ import game.DbGame
|
|||
|
||||
import scalaz.effects._
|
||||
|
||||
final class Client(val remoteUrl: String) extends ai.Client with FenBased {
|
||||
final class Client(val playUrl: String) extends ai.Client with FenBased {
|
||||
|
||||
def apply(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] = {
|
||||
def play(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] = {
|
||||
|
||||
val oldGame = dbGame.toChess
|
||||
val oldFen = toFen(oldGame, dbGame.variant)
|
||||
|
@ -19,8 +19,11 @@ final class Client(val remoteUrl: String) extends ai.Client with FenBased {
|
|||
}
|
||||
}
|
||||
|
||||
def analyse(dbGame: DbGame, initialFen: Option[String]) =
|
||||
throw new RuntimeException("Crafty analysis is not implemented")
|
||||
|
||||
private def fetchNewFen(oldFen: String, level: Int): IO[String] = io {
|
||||
http(urlObj <<? Map(
|
||||
http(playUrlObj <<? Map(
|
||||
"fen" -> oldFen,
|
||||
"level" -> level.toString
|
||||
) as_str)
|
||||
|
|
|
@ -13,7 +13,7 @@ final class Server(
|
|||
execPath: String,
|
||||
bookPath: Option[String] = None) {
|
||||
|
||||
def apply(fen: String, level: Int): Valid[IO[String]] =
|
||||
def play(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))
|
||||
|
|
|
@ -4,18 +4,23 @@ package stockfish
|
|||
|
||||
import chess.{ Game, Move }
|
||||
import game.DbGame
|
||||
import analyse.Analysis
|
||||
|
||||
import scalaz.effects._
|
||||
import akka.dispatch.Future
|
||||
|
||||
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(
|
||||
def play(dbGame: DbGame, initialFen: Option[String]): IO[Valid[(Game, Move)]] =
|
||||
server.play(dbGame.pgn, initialFen, dbGame.aiLevel | 1).fold(
|
||||
err ⇒ io(failure(err)),
|
||||
iop ⇒ iop map {
|
||||
applyMove(dbGame, _)
|
||||
}
|
||||
)
|
||||
|
||||
def analyse(dbGame: DbGame, initialFen: Option[String]): Future[Valid[Analysis]] =
|
||||
server.analyse(dbGame.pgn, initialFen)
|
||||
}
|
||||
|
|
24
app/ai/stockfish/AnalyseConfig.scala
Normal file
24
app/ai/stockfish/AnalyseConfig.scala
Normal file
|
@ -0,0 +1,24 @@
|
|||
package lila
|
||||
package ai.stockfish
|
||||
|
||||
import model._
|
||||
import model.analyse._
|
||||
import core.Settings
|
||||
|
||||
final class AnalyseConfig(settings: Settings) extends Config {
|
||||
|
||||
type Instructions = List[String]
|
||||
|
||||
def init: Instructions = List(
|
||||
setoption("Uci_AnalyseMode", true),
|
||||
setoption("Hash", settings.AiStockfishAnalyseHashSize),
|
||||
setoption("Threads", 8),
|
||||
setoption("Ponder", false)
|
||||
)
|
||||
|
||||
def game(analyse: Analyse): Instructions = List(
|
||||
setoption("UCI_Chess960", analyse.chess960)
|
||||
)
|
||||
|
||||
def moveTime = settings.AiStockfishAnalyseMoveTime
|
||||
}
|
90
app/ai/stockfish/AnalyseFSM.scala
Normal file
90
app/ai/stockfish/AnalyseFSM.scala
Normal file
|
@ -0,0 +1,90 @@
|
|||
package lila
|
||||
package ai.stockfish
|
||||
|
||||
import model._
|
||||
import model.analyse._
|
||||
|
||||
import akka.actor.{ Props, Actor, ActorRef, FSM ⇒ AkkaFSM, LoggingFSM }
|
||||
import scalaz.effects._
|
||||
|
||||
final class AnalyseFSM(
|
||||
processBuilder: (String ⇒ Unit, String ⇒ Unit) ⇒ Process,
|
||||
config: AnalyseConfig)
|
||||
extends Actor with LoggingFSM[State, 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
|
||||
nextAnalyse(data)
|
||||
}
|
||||
case Event(analyse: Analyse, data) ⇒
|
||||
stay using (data enqueue Task(analyse, sender))
|
||||
}
|
||||
when(Ready) {
|
||||
case Event(Out(t), _) ⇒ { log.warning(t); stay }
|
||||
}
|
||||
when(UciNewGame) {
|
||||
case Event(Out(t), data: Doing) if t contains "readyok" ⇒
|
||||
nextInfo(data)
|
||||
}
|
||||
when(Running) {
|
||||
case Event(Out(t), data: Doing) if t startsWith "info depth" ⇒ {
|
||||
goto(Running) using (data buffer t)
|
||||
}
|
||||
case Event(Out(t), data: Doing) if t contains "bestmove" ⇒
|
||||
(data buffer t).flush.fold(
|
||||
err ⇒ {
|
||||
log.error(err.shows)
|
||||
data.current.ref ! failure(err)
|
||||
nextAnalyse(data.done)
|
||||
},
|
||||
nextData ⇒ nextInfo(nextData)
|
||||
)
|
||||
}
|
||||
whenUnhandled {
|
||||
case Event(analyse: Analyse, data) ⇒
|
||||
nextAnalyse(data enqueue Task(analyse, sender))
|
||||
case Event(Out(t), _) if isNoise(t) ⇒ stay
|
||||
case Event(Out(t), _) ⇒ { log.warning(t); stay }
|
||||
case Event(Err(t), _) ⇒ { log.error(t); stay }
|
||||
}
|
||||
|
||||
def nextAnalyse(data: Data) = data match {
|
||||
case todo: Todo ⇒ todo.doing(
|
||||
doing ⇒ {
|
||||
config game doing.current.analyse 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 nextInfo(doing: Doing) = doing.current |> { task ⇒
|
||||
(task.analyse go config.moveTime).fold(
|
||||
instructions ⇒ {
|
||||
instructions foreach process.write
|
||||
goto(Running) using doing
|
||||
}, {
|
||||
task.ref ! success(task.analyse.analysis.done)
|
||||
nextAnalyse(doing.done)
|
||||
})
|
||||
}
|
||||
|
||||
def isNoise(t: String) =
|
||||
t.isEmpty || (t startsWith "id ") || (t startsWith "info ") || (t startsWith "option name ")
|
||||
|
||||
def onTermination() {
|
||||
process.destroy()
|
||||
}
|
||||
}
|
32
app/ai/stockfish/AnalyseParser.scala
Normal file
32
app/ai/stockfish/AnalyseParser.scala
Normal file
|
@ -0,0 +1,32 @@
|
|||
package lila
|
||||
package ai.stockfish
|
||||
|
||||
import analyse.Info
|
||||
|
||||
object AnalyseParser {
|
||||
|
||||
// info depth 4 seldepth 5 score cp -3309 nodes 1665 nps 43815 time 38 multipv 1 pv f2e3 d4c5 c1d1 c5g5 d1d2 g5g2 d2c1 e8e3
|
||||
def apply(lines: List[String]): String ⇒ Valid[Int ⇒ Info] =
|
||||
move ⇒
|
||||
findBestMove(lines) toValid "Analysis bestmove not found" flatMap { best ⇒
|
||||
Info(move, best, findCp(lines), findMate(lines))
|
||||
}
|
||||
|
||||
private val bestMoveRegex = """^bestmove\s(\w+).*$""".r
|
||||
private def findBestMove(lines: List[String]) =
|
||||
lines.headOption map { line ⇒
|
||||
bestMoveRegex.replaceAllIn(line, m ⇒ m group 1)
|
||||
}
|
||||
|
||||
private val cpRegex = """^info.*\scp\s(\-?\d+).*$""".r
|
||||
private def findCp(lines: List[String]) =
|
||||
lines.tail.headOption map { line ⇒
|
||||
cpRegex.replaceAllIn(line, m ⇒ m group 1)
|
||||
} flatMap parseIntOption filter (0 !=)
|
||||
|
||||
private val mateRegex = """^info.*\smate\s(\-?\d+).*$""".r
|
||||
private def findMate(lines: List[String]) =
|
||||
lines.tail.headOption map { line ⇒
|
||||
mateRegex.replaceAllIn(line, m ⇒ m group 1)
|
||||
} flatMap parseIntOption
|
||||
}
|
|
@ -3,33 +3,62 @@ package ai
|
|||
package stockfish
|
||||
|
||||
import chess.{ Game, Move }
|
||||
import chess.format.UciMove
|
||||
import game.DbGame
|
||||
import analyse.Analysis
|
||||
|
||||
import scalaz.effects._
|
||||
import dispatch.{ url }
|
||||
import akka.dispatch.Future
|
||||
import play.api.Play.current
|
||||
import play.api.libs.concurrent._
|
||||
|
||||
final class Client(val remoteUrl: String) extends ai.Client with Stockfish {
|
||||
final class Client(
|
||||
val playUrl: String,
|
||||
analyseUrl: 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 {
|
||||
def play(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 {
|
||||
def analyse(dbGame: DbGame, initialFen: Option[String]): Future[Valid[Analysis]] =
|
||||
fetchAnalyse(dbGame.pgn, initialFen | "") map {
|
||||
_ flatMap { Analysis(_, true) }
|
||||
}
|
||||
|
||||
private lazy val analyseUrlObj = url(analyseUrl)
|
||||
|
||||
protected lazy val tryPing: IO[Option[Int]] = for {
|
||||
start ← io(nowMillis)
|
||||
received ← fetchMove(
|
||||
pgn = "",
|
||||
initialFen = "",
|
||||
level = 1
|
||||
).catchLeft map (_.isRight)
|
||||
).catchLeft map (_ match {
|
||||
case Right(move) ⇒ UciMove(move).isDefined
|
||||
case _ ⇒ false
|
||||
})
|
||||
delay ← io(nowMillis - start)
|
||||
} yield received option delay.toInt
|
||||
|
||||
private def fetchMove(pgn: String, initialFen: String, level: Int): IO[String] = io {
|
||||
http(urlObj <<? Map(
|
||||
http(playUrlObj <<? Map(
|
||||
"pgn" -> pgn,
|
||||
"initialFen" -> initialFen,
|
||||
"level" -> level.toString
|
||||
) as_str)
|
||||
}
|
||||
|
||||
private def fetchAnalyse(pgn: String, initialFen: String): Future[Valid[String]] = Future {
|
||||
unsafe {
|
||||
http(analyseUrlObj <<? Map(
|
||||
"pgn" -> pgn,
|
||||
"initialFen" -> initialFen
|
||||
) as_str)
|
||||
}
|
||||
}
|
||||
|
||||
private implicit val executor = Akka.system.dispatcher
|
||||
}
|
||||
|
|
|
@ -1,33 +1,8 @@
|
|||
package lila
|
||||
package ai.stockfish
|
||||
|
||||
import model._
|
||||
import core.Settings
|
||||
trait Config {
|
||||
|
||||
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) =
|
||||
protected def setoption(name: String, value: Any) =
|
||||
"setoption name %s value %s".format(name, value)
|
||||
}
|
||||
|
|
31
app/ai/stockfish/PlayConfig.scala
Normal file
31
app/ai/stockfish/PlayConfig.scala
Normal file
|
@ -0,0 +1,31 @@
|
|||
package lila
|
||||
package ai.stockfish
|
||||
|
||||
import model._
|
||||
import model.play._
|
||||
import core.Settings
|
||||
|
||||
final class PlayConfig(settings: Settings) extends Config {
|
||||
|
||||
type Instructions = List[String]
|
||||
|
||||
def init: Instructions = List(
|
||||
setoption("Hash", settings.AiStockfishPlayHashSize),
|
||||
setoption("Threads", 8),
|
||||
setoption("Ponder", false),
|
||||
setoption("Aggressiveness", settings.AiStockfishPlayAggressiveness) // 0 - 200
|
||||
)
|
||||
|
||||
def game(play: Play): Instructions = List(
|
||||
setoption("Skill Level", skill(play.level)),
|
||||
setoption("UCI_Chess960", play.chess960)
|
||||
)
|
||||
|
||||
def maxMoveTime = settings.AiStockfishPlayMaxMoveTime
|
||||
|
||||
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)
|
||||
}
|
|
@ -2,14 +2,15 @@ package lila
|
|||
package ai.stockfish
|
||||
|
||||
import model._
|
||||
import model.play._
|
||||
|
||||
import akka.actor.{ Props, Actor, ActorRef, FSM ⇒ AkkaFSM, LoggingFSM }
|
||||
import akka.actor.{ Props, Actor, ActorRef, FSM ⇒ AkkaFSM }
|
||||
import scalaz.effects._
|
||||
|
||||
final class FSM(
|
||||
final class PlayFSM(
|
||||
processBuilder: (String ⇒ Unit, String ⇒ Unit) ⇒ Process,
|
||||
config: Config)
|
||||
extends Actor with LoggingFSM[model.State, model.Data] {
|
||||
config: PlayConfig)
|
||||
extends Actor with AkkaFSM[State, Data] {
|
||||
|
||||
val process = processBuilder(out ⇒ self ! Out(out), err ⇒ self ! Err(err))
|
||||
|
||||
|
@ -32,15 +33,14 @@ final class FSM(
|
|||
}
|
||||
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)
|
||||
play go config.moveTime foreach process.write
|
||||
goto(Running)
|
||||
}
|
||||
}
|
||||
when(Go) {
|
||||
when(Running) {
|
||||
case Event(Out(t), data @ Doing(Task(_, ref), _)) if t contains "bestmove" ⇒ {
|
||||
ref ! BestMove(t.split(' ') lift 1)
|
||||
goto(Ready) using data.done
|
||||
next(data.done)
|
||||
}
|
||||
}
|
||||
whenUnhandled {
|
|
@ -18,6 +18,7 @@ final class Process(
|
|||
}
|
||||
|
||||
def destroy() {
|
||||
write("stop")
|
||||
write("quit")
|
||||
Thread sleep 300
|
||||
process.destroy()
|
||||
|
|
|
@ -5,6 +5,7 @@ package stockfish
|
|||
import chess.Rook
|
||||
import chess.format.UciDump
|
||||
import chess.format.Forsyth
|
||||
import analyse.Analysis
|
||||
|
||||
import akka.util.Timeout
|
||||
import akka.util.Duration
|
||||
|
@ -16,18 +17,30 @@ import play.api.Play.current
|
|||
import play.api.libs.concurrent._
|
||||
import scalaz.effects._
|
||||
|
||||
final class Server(execPath: String, config: Config) {
|
||||
final class Server(
|
||||
execPath: String,
|
||||
playConfig: PlayConfig,
|
||||
analyseConfig: AnalyseConfig) {
|
||||
|
||||
import model._
|
||||
|
||||
def apply(pgn: String, initialFen: Option[String], level: Int): Valid[IO[String]] =
|
||||
def play(pgn: String, initialFen: Option[String], level: Int): Valid[IO[String]] = {
|
||||
implicit val timeout = new Timeout(playAtMost)
|
||||
if (level < 1 || level > 8) "Invalid ai level".failNel
|
||||
else for {
|
||||
moves ← UciDump(pgn, initialFen)
|
||||
play = Play(moves, initialFen map chess960Fen, level)
|
||||
play = model.play.Play(moves, initialFen map chess960Fen, level)
|
||||
} yield io {
|
||||
Await.result(actor ? play mapTo manifest[BestMove], atMost)
|
||||
Await.result(playActor ? play mapTo manifest[model.play.BestMove], playAtMost)
|
||||
} map (_.move | "")
|
||||
}
|
||||
|
||||
def analyse(pgn: String, initialFen: Option[String]): Future[Valid[Analysis]] =
|
||||
UciDump(pgn, initialFen).fold(
|
||||
err ⇒ Future(failure(err)),
|
||||
moves ⇒ {
|
||||
val analyse = model.analyse.Analyse(moves, initialFen map chess960Fen)
|
||||
implicit val timeout = Timeout(1 hour)
|
||||
analyseActor ? analyse mapTo manifest[Valid[Analysis]]
|
||||
})
|
||||
|
||||
private def chess960Fen(fen: String) = (Forsyth << fen).fold(
|
||||
situation ⇒ fen.replace("KQkq", situation.board.pieces.toList filter {
|
||||
|
@ -39,9 +52,14 @@ final class Server(execPath: String, config: Config) {
|
|||
} mkString ""),
|
||||
fen)
|
||||
|
||||
private val atMost = 5 seconds
|
||||
private implicit val timeout = new Timeout(atMost)
|
||||
private implicit val executor = Akka.system.dispatcher
|
||||
|
||||
private val process = Process(execPath) _
|
||||
private val actor = Akka.system.actorOf(Props(new FSM(process, config)))
|
||||
private val playAtMost = 10 seconds
|
||||
private lazy val playProcess = Process(execPath) _
|
||||
private lazy val playActor = Akka.system.actorOf(Props(
|
||||
new PlayFSM(playProcess, playConfig)))
|
||||
|
||||
private lazy val analyseProcess = Process(execPath) _
|
||||
private lazy val analyseActor = Akka.system.actorOf(Props(
|
||||
new AnalyseFSM(analyseProcess, analyseConfig)))
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
package lila
|
||||
package ai.stockfish
|
||||
|
||||
import model.BestMove
|
||||
import model.play.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)
|
||||
result ← dbGame.toChess(bestMove.orig, bestMove.dest)
|
||||
} yield result
|
||||
}
|
||||
|
|
|
@ -2,51 +2,113 @@ package lila
|
|||
package ai.stockfish
|
||||
|
||||
import chess.Pos.posAt
|
||||
import chess.format.UciMove
|
||||
import analyse.{ Analysis, AnalysisBuilder }
|
||||
|
||||
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
|
||||
object play {
|
||||
|
||||
case class Play(moves: String, fen: Option[String], level: Int) {
|
||||
def go(moveTime: Int ⇒ Int) = List(
|
||||
"position %s moves %s".format(fen.fold("fen " + _, "startpos"), moves),
|
||||
"go movetime %d" format moveTime(level)
|
||||
)
|
||||
def chess960 = fen.isDefined
|
||||
}
|
||||
case class BestMove(move: Option[String]) {
|
||||
def parse = UciMove(move | "")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
case class BestMove(move: Option[String]) {
|
||||
def parse = for {
|
||||
m ← move
|
||||
orig ← posAt(m take 2)
|
||||
dest ← posAt(m drop 2)
|
||||
} yield orig -> dest
|
||||
|
||||
object analyse {
|
||||
|
||||
case class Analyse(
|
||||
moves: IndexedSeq[String],
|
||||
fen: Option[String],
|
||||
analysis: AnalysisBuilder,
|
||||
infoBuffer: List[String]) {
|
||||
|
||||
def go(moveTime: Int) = nextMove.isDefined option List(
|
||||
"position %s moves %s".format(
|
||||
fen.fold("fen " + _, "startpos"),
|
||||
moves take analysis.size mkString " "),
|
||||
"go movetime %d".format(moveTime)
|
||||
)
|
||||
|
||||
def nextMove = moves lift analysis.size
|
||||
|
||||
def buffer(str: String) = copy(infoBuffer = str :: infoBuffer)
|
||||
|
||||
def flush = for {
|
||||
move ← nextMove toValid "No move to flush"
|
||||
info ← AnalyseParser(infoBuffer)(move)
|
||||
} yield copy(
|
||||
analysis = analysis + info,
|
||||
infoBuffer = Nil)
|
||||
|
||||
def chess960 = fen.isDefined
|
||||
}
|
||||
object Analyse {
|
||||
def apply(moves: String, fen: Option[String]) = new Analyse(
|
||||
moves = moves.split(' ').toIndexedSeq,
|
||||
fen = fen,
|
||||
analysis = Analysis.builder,
|
||||
infoBuffer = Nil)
|
||||
}
|
||||
|
||||
case class Task(analyse: Analyse, ref: ActorRef) {
|
||||
def buffer(str: String) = copy(analyse = analyse buffer str)
|
||||
def flush = analyse.flush map { a ⇒ copy(analyse = a) }
|
||||
}
|
||||
|
||||
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) =
|
||||
queue.headOption.fold(
|
||||
task ⇒ withTask(Doing(task, queue.tail)),
|
||||
without(Todo(Vector.empty))
|
||||
)
|
||||
}
|
||||
case class Doing(current: Task, queue: Vector[Task]) extends Data {
|
||||
def enqueue(task: Task) = copy(queue = queue :+ task)
|
||||
def done = Todo(queue)
|
||||
def buffer(str: String) = copy(current = current buffer str)
|
||||
def flush = current.flush map { c ⇒ copy(current = c) }
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
case object Running extends State
|
||||
|
||||
sealed trait Stream { def text: String }
|
||||
case class Out(text: String) extends Stream
|
||||
|
|
41
app/analyse/AdvantageChart.scala
Normal file
41
app/analyse/AdvantageChart.scala
Normal file
|
@ -0,0 +1,41 @@
|
|||
package lila
|
||||
package analyse
|
||||
|
||||
import com.codahale.jerkson.Json
|
||||
|
||||
final class AdvantageChart(advices: Analysis.InfoAdvices) {
|
||||
|
||||
val max = 10
|
||||
|
||||
def columns = AdvantageChart.columns
|
||||
|
||||
def rows = Json generate chartValues
|
||||
|
||||
private lazy val values: List[(String, Float)] =
|
||||
(advices sliding 2 collect {
|
||||
case (info, advice) :: (next, _) :: Nil ⇒
|
||||
(next.score, next.mate) match {
|
||||
case (Some(score), _) ⇒ move(info, advice) -> box(score.pawns)
|
||||
case (_, Some(mate)) ⇒ move(info, advice) -> box(info.color.fold(-mate, mate) * max)
|
||||
case _ ⇒ move(info, none) -> box(0)
|
||||
}
|
||||
}).toList
|
||||
|
||||
private def chartValues: List[List[Any]] = values map {
|
||||
case (move, score) ⇒ List(move, (score == 0).fold(null, score))
|
||||
}
|
||||
|
||||
private def box(v: Float) = math.min(max, math.max(-max, v))
|
||||
|
||||
private def move(info: Info, advice: Option[Advice]) = info.color.fold(
|
||||
"%d. %s", "%d... %s"
|
||||
).format(info.turn, info.move.uci) + advice.fold(" " + _.nag.symbol, "")
|
||||
|
||||
}
|
||||
|
||||
object AdvantageChart {
|
||||
|
||||
val columns = Json generate List(
|
||||
"string" :: "Move" :: Nil,
|
||||
"number" :: "Advantage" :: Nil)
|
||||
}
|
|
@ -1,18 +1,43 @@
|
|||
package lila
|
||||
package analyse
|
||||
|
||||
import game.GameRepo
|
||||
import game.{ DbGame, GameRepo }
|
||||
import user.UserRepo
|
||||
import core.Settings
|
||||
|
||||
import com.mongodb.casbah.MongoCollection
|
||||
import scalaz.effects._
|
||||
import akka.dispatch.Future
|
||||
|
||||
final class AnalyseEnv(
|
||||
settings: Settings,
|
||||
gameRepo: GameRepo,
|
||||
userRepo: UserRepo) {
|
||||
userRepo: UserRepo,
|
||||
mongodb: String ⇒ MongoCollection,
|
||||
generator: () ⇒ (DbGame, Option[String]) ⇒ Future[Valid[Analysis]]) {
|
||||
|
||||
import settings._
|
||||
|
||||
lazy val pgnDump = new PgnDump(
|
||||
userRepo = userRepo,
|
||||
gameRepo = gameRepo)
|
||||
gameRepo = gameRepo,
|
||||
analyser = analyser,
|
||||
userRepo = userRepo)
|
||||
|
||||
lazy val analysisRepo = new AnalysisRepo(
|
||||
mongodb(AnalyseCollectionAnalysis))
|
||||
|
||||
lazy val analyser = new Analyser(
|
||||
analysisRepo = analysisRepo,
|
||||
gameRepo = gameRepo,
|
||||
generator = generator)
|
||||
|
||||
lazy val paginator = new PaginatorBuilder(
|
||||
analysisRepo = analysisRepo,
|
||||
cached = cached,
|
||||
gameRepo = gameRepo,
|
||||
maxPerPage = GamePaginatorMaxPerPage)
|
||||
|
||||
lazy val cached = new Cached(
|
||||
analysisRepo = analysisRepo,
|
||||
nbTtl = AnalyseCachedNbTtl)
|
||||
}
|
||||
|
|
17
app/analyse/AnalyseHelper.scala
Normal file
17
app/analyse/AnalyseHelper.scala
Normal file
|
@ -0,0 +1,17 @@
|
|||
package lila
|
||||
package analyse
|
||||
|
||||
import core.CoreEnv
|
||||
import http.Context
|
||||
import user.{ User, UserHelper }
|
||||
|
||||
import play.api.templates.Html
|
||||
import play.api.mvc.Call
|
||||
|
||||
trait AnalyseHelper {
|
||||
|
||||
protected def env: CoreEnv
|
||||
|
||||
def canRequestAnalysis(implicit ctx: Context) =
|
||||
ctx.isAuth
|
||||
}
|
51
app/analyse/Analyser.scala
Normal file
51
app/analyse/Analyser.scala
Normal file
|
@ -0,0 +1,51 @@
|
|||
package lila
|
||||
package analyse
|
||||
|
||||
import game.{ DbGame, GameRepo }
|
||||
|
||||
import scalaz.effects._
|
||||
import play.api.libs.concurrent.Akka
|
||||
import play.api.Play.current
|
||||
import akka.dispatch.Future
|
||||
import akka.util.duration._
|
||||
import akka.util.Timeout
|
||||
|
||||
final class Analyser(
|
||||
analysisRepo: AnalysisRepo,
|
||||
gameRepo: GameRepo,
|
||||
generator: () ⇒ (DbGame, Option[String]) ⇒ Future[Valid[Analysis]]) {
|
||||
|
||||
private implicit val executor = Akka.system.dispatcher
|
||||
private implicit val timeout = Timeout(5 minutes)
|
||||
|
||||
def get(id: String): IO[Option[Analysis]] = analysisRepo byId id
|
||||
|
||||
def getOrGenerate(id: String, userId: String): Future[Valid[Analysis]] =
|
||||
getOrGenerateIO(id, userId)
|
||||
|
||||
private def getOrGenerateIO(id: String, userId: String): Future[Valid[Analysis]] = for {
|
||||
a ← ioToFuture(analysisRepo byId id)
|
||||
b ← a.fold(
|
||||
x ⇒ Future(success(x)),
|
||||
for {
|
||||
gameOption ← ioToFuture(gameRepo game id)
|
||||
result ← gameOption.fold(
|
||||
game ⇒ for {
|
||||
_ ← ioToFuture(analysisRepo.progress(id, userId))
|
||||
initialFen ← ioToFuture(gameRepo initialFen id)
|
||||
analysis ← generator()(game, initialFen)
|
||||
_ ← ioToFuture(analysis.fold(
|
||||
analysisRepo.fail(id, _),
|
||||
analysisRepo.done(id, _)
|
||||
))
|
||||
} yield analysis,
|
||||
Future(!!("No such game " + id): Valid[Analysis])
|
||||
)
|
||||
} yield result
|
||||
)
|
||||
} yield b
|
||||
|
||||
private def ioToFuture[A](ioa: IO[A]) = Future {
|
||||
ioa.unsafePerformIO
|
||||
}
|
||||
}
|
202
app/analyse/Analysis.scala
Normal file
202
app/analyse/Analysis.scala
Normal file
|
@ -0,0 +1,202 @@
|
|||
package lila
|
||||
package analyse
|
||||
|
||||
import chess.{ Pos, Color, White, Black }
|
||||
import chess.format.{ UciMove, Nag }
|
||||
|
||||
case class Analysis(
|
||||
infos: List[Info],
|
||||
done: Boolean,
|
||||
fail: Option[String] = None) {
|
||||
|
||||
lazy val infoAdvices: Analysis.InfoAdvices = (infos sliding 2 collect {
|
||||
case info :: next :: Nil ⇒ info -> Advice(info, next)
|
||||
}).toList
|
||||
|
||||
def advices: List[Advice] = infoAdvices.map(_._2).flatten
|
||||
|
||||
lazy val advantageChart = new AdvantageChart(infoAdvices)
|
||||
|
||||
def encode: String = infos map (_.encode) mkString Analysis.separator
|
||||
}
|
||||
|
||||
object Analysis {
|
||||
|
||||
type InfoAdvices = List[(Info, Option[Advice])]
|
||||
private val separator = " "
|
||||
|
||||
def apply(str: String, done: Boolean): Valid[Analysis] = decode(str) map { infos ⇒
|
||||
new Analysis(infos, done)
|
||||
}
|
||||
|
||||
def decode(str: String): Valid[List[Info]] =
|
||||
(str.split(separator).toList.zipWithIndex map {
|
||||
case (info, index) ⇒ Info.decode(index + 1, info)
|
||||
}).sequence
|
||||
|
||||
def builder = new AnalysisBuilder(Nil)
|
||||
}
|
||||
|
||||
sealed trait Advice {
|
||||
def severity: Severity
|
||||
def info: Info
|
||||
def next: Info
|
||||
def text: String
|
||||
|
||||
def ply = info.ply
|
||||
def turn = info.turn
|
||||
def move = info.move
|
||||
def color = info.color
|
||||
def nag = severity.nag
|
||||
}
|
||||
|
||||
sealed abstract class Severity(val nag: Nag)
|
||||
|
||||
sealed abstract class CpSeverity(val delta: Int, nag: Nag) extends Severity(nag)
|
||||
case object CpBlunder extends CpSeverity(-300, Nag.Blunder)
|
||||
case object CpMistake extends CpSeverity(-100, Nag.Mistake)
|
||||
case object CpInaccuracy extends CpSeverity(-50, Nag.Inaccuracy)
|
||||
object CpSeverity {
|
||||
val all = List(CpInaccuracy, CpMistake, CpBlunder)
|
||||
def apply(delta: Int): Option[CpSeverity] = all.foldLeft(none[CpSeverity]) {
|
||||
case (_, severity) if severity.delta > delta ⇒ severity.some
|
||||
case (acc, _) ⇒ acc
|
||||
}
|
||||
}
|
||||
|
||||
case class CpAdvice(
|
||||
severity: CpSeverity,
|
||||
info: Info,
|
||||
next: Info) extends Advice {
|
||||
|
||||
def text = severity.nag.toString
|
||||
}
|
||||
|
||||
sealed abstract class MateSeverity(nag: Nag, val desc: String) extends Severity(nag: Nag)
|
||||
case class MateDelayed(before: Int, after: Int) extends MateSeverity(Nag.Inaccuracy,
|
||||
desc = "Detected checkmate in %s moves, but player moved for mate in %s".format(before, after + 1))
|
||||
case object MateLost extends MateSeverity(Nag.Mistake,
|
||||
desc = "Lost forced checkmate sequence")
|
||||
case object MateCreated extends MateSeverity(Nag.Blunder,
|
||||
desc = "Checkmate is now unavoidable")
|
||||
object MateSeverity {
|
||||
def apply(current: Option[Int], next: Option[Int]): Option[MateSeverity] =
|
||||
(current, next).some collect {
|
||||
case (None, Some(n)) if n < 0 ⇒ MateCreated
|
||||
case (Some(c), None) if c > 0 ⇒ MateLost
|
||||
case (Some(c), Some(n)) if (c > 0) && (n < 0) ⇒ MateLost
|
||||
case (Some(c), Some(n)) if c > 0 && n >= c ⇒ MateDelayed(c, n)
|
||||
}
|
||||
}
|
||||
case class MateAdvice(
|
||||
severity: MateSeverity,
|
||||
info: Info,
|
||||
next: Info) extends Advice {
|
||||
|
||||
def text = severity.toString
|
||||
}
|
||||
|
||||
object Advice {
|
||||
|
||||
def apply(info: Info, next: Info): Option[Advice] = {
|
||||
for {
|
||||
cp ← info.score map (_.centipawns)
|
||||
if info.move != info.best
|
||||
nextCp ← next.score map (_.centipawns)
|
||||
delta = nextCp - cp
|
||||
severity ← CpSeverity(info.color.fold(delta, -delta))
|
||||
} yield CpAdvice(severity, info, next)
|
||||
} orElse {
|
||||
MateSeverity(
|
||||
mateChance(info, info.color),
|
||||
mateChance(next, info.color)) map { severity ⇒
|
||||
MateAdvice(severity, info, next)
|
||||
}
|
||||
}
|
||||
|
||||
private def mateChance(info: Info, color: Color) =
|
||||
info.color.fold(info.mate, info.mate map (-_)) map { chance ⇒
|
||||
color.fold(chance, -chance)
|
||||
}
|
||||
}
|
||||
|
||||
final class AnalysisBuilder(infos: List[Info]) {
|
||||
|
||||
def size = infos.size
|
||||
|
||||
def +(info: Int ⇒ Info) = new AnalysisBuilder(info(infos.size + 1) :: infos)
|
||||
|
||||
def done = new Analysis(infos.reverse.zipWithIndex map {
|
||||
case (info, turn) ⇒
|
||||
(turn % 2 == 0).fold(info, info.copy(score = info.score map (_.negate)))
|
||||
}, true)
|
||||
}
|
||||
|
||||
case class Info(
|
||||
ply: Int,
|
||||
move: UciMove,
|
||||
best: UciMove,
|
||||
score: Option[Score],
|
||||
mate: Option[Int]) {
|
||||
|
||||
def turn = 1 + (ply - 1) / 2
|
||||
|
||||
def color = Color(ply % 2 == 1)
|
||||
|
||||
def encode: String = List(
|
||||
move.piotr,
|
||||
best.piotr,
|
||||
encode(score map (_.centipawns)),
|
||||
encode(mate)
|
||||
) mkString Info.separator
|
||||
|
||||
private def encode(oa: Option[Any]): String = oa.fold(_.toString, "_")
|
||||
}
|
||||
|
||||
object Info {
|
||||
|
||||
private val separator = ","
|
||||
|
||||
def decode(ply: Int, str: String): Valid[Info] = str.split(separator).toList match {
|
||||
case moveString :: bestString :: cpString :: mateString :: Nil ⇒ for {
|
||||
move ← UciMove piotr moveString toValid "Invalid info move piotr " + moveString
|
||||
best ← UciMove piotr bestString toValid "Invalid info best piotr " + bestString
|
||||
} yield Info(
|
||||
ply = ply,
|
||||
move = move,
|
||||
best = best,
|
||||
score = parseIntOption(cpString) map Score.apply,
|
||||
mate = parseIntOption(mateString))
|
||||
case _ ⇒ !!("Invalid encoded info " + str)
|
||||
}
|
||||
|
||||
def apply(
|
||||
moveString: String,
|
||||
bestString: String,
|
||||
score: Option[Int],
|
||||
mate: Option[Int]): Valid[Int ⇒ Info] = for {
|
||||
move ← UciMove(moveString) toValid "Invalid info move " + moveString
|
||||
best ← UciMove(bestString) toValid "Invalid info best " + bestString
|
||||
} yield ply ⇒ Info(
|
||||
ply = ply,
|
||||
move = move,
|
||||
best = best,
|
||||
score = score map Score.apply,
|
||||
mate = mate)
|
||||
}
|
||||
|
||||
case class Score(centipawns: Int) {
|
||||
|
||||
def pawns: Float = centipawns / 100f
|
||||
def showPawns: String = "%.2f" format pawns
|
||||
|
||||
private val percentMax = 5
|
||||
def percent: Int = math.round(box(0, 100,
|
||||
50 + (pawns / percentMax) * 50
|
||||
))
|
||||
|
||||
def negate = Score(-centipawns)
|
||||
|
||||
private def box(min: Float, max: Float, v: Float) =
|
||||
math.min(max, math.max(min, v))
|
||||
}
|
45
app/analyse/AnalysisRepo.scala
Normal file
45
app/analyse/AnalysisRepo.scala
Normal file
|
@ -0,0 +1,45 @@
|
|||
package lila
|
||||
package analyse
|
||||
|
||||
import com.mongodb.casbah.MongoCollection
|
||||
import com.mongodb.casbah.query.Imports._
|
||||
|
||||
import scalaz.effects._
|
||||
import org.joda.time.DateTime
|
||||
|
||||
final class AnalysisRepo(val collection: MongoCollection) {
|
||||
|
||||
def done(id: String, a: Analysis) = io {
|
||||
collection.update(
|
||||
DBObject("_id" -> id),
|
||||
$set("done" -> true, "encoded" -> a.encode)
|
||||
)
|
||||
}
|
||||
|
||||
def fail(id: String, err: Failures) = io {
|
||||
collection.update(
|
||||
DBObject("_id" -> id),
|
||||
$set("fail" -> err.shows)
|
||||
)
|
||||
}
|
||||
|
||||
def progress(id: String, userId: String) = io {
|
||||
collection.insert(DBObject(
|
||||
"_id" -> id,
|
||||
"uid" -> userId,
|
||||
"done" -> false,
|
||||
"date" -> DateTime.now))
|
||||
}
|
||||
|
||||
def byId(id: String): IO[Option[Analysis]] = io {
|
||||
for {
|
||||
obj ← collection.findOne(DBObject("_id" -> id))
|
||||
done = obj.getAs[Boolean]("done") | false
|
||||
fail = obj.getAs[String]("fail")
|
||||
infos = for {
|
||||
encoded ← obj.getAs[String]("encoded")
|
||||
decoded ← (Analysis decode encoded).toOption
|
||||
} yield decoded
|
||||
} yield Analysis(infos | Nil, done, fail)
|
||||
}
|
||||
}
|
29
app/analyse/Annotator.scala
Normal file
29
app/analyse/Annotator.scala
Normal file
|
@ -0,0 +1,29 @@
|
|||
package lila
|
||||
package analyse
|
||||
|
||||
import chess.format.pgn
|
||||
|
||||
object Annotator {
|
||||
|
||||
def apply(p: pgn.Pgn, analysis: Analysis): pgn.Pgn =
|
||||
annotateTurns(p, analysis.advices).copy(
|
||||
tags = p.tags :+ pgn.Tag("Annotator", "lichess.org")
|
||||
)
|
||||
|
||||
private def annotateTurns(p: pgn.Pgn, advices: List[Advice]): pgn.Pgn =
|
||||
advices.foldLeft(p) {
|
||||
case (acc, advice) ⇒ acc.updateTurn(advice.turn, turn ⇒
|
||||
turn.update(advice.color, move ⇒
|
||||
move.copy(
|
||||
nag = advice.nag.code.some,
|
||||
comment = makeComment(advice).some
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private def makeComment(advice: Advice): String = advice match {
|
||||
case CpAdvice(sev, info, _) ⇒ "%s. Best was %s.".format(sev.nag, info.best.uci)
|
||||
case MateAdvice(sev, info, _) ⇒ "%s. Best was %s.".format(sev.desc, info.best.uci)
|
||||
}
|
||||
}
|
28
app/analyse/Cached.scala
Normal file
28
app/analyse/Cached.scala
Normal file
|
@ -0,0 +1,28 @@
|
|||
package lila
|
||||
package analyse
|
||||
|
||||
import akka.util.duration._
|
||||
|
||||
import memo.ActorMemo
|
||||
|
||||
final class Cached(
|
||||
analysisRepo: AnalysisRepo,
|
||||
nbTtl: Int) {
|
||||
|
||||
import Cached._
|
||||
|
||||
def nbAnalysis: Int = memo(NbAnalysis)
|
||||
|
||||
private val memo = ActorMemo(loadFromDb, nbTtl, 5.seconds)
|
||||
|
||||
private def loadFromDb(key: Key) = key match {
|
||||
case NbAnalysis ⇒ analysisRepo.collection.count.toInt
|
||||
}
|
||||
}
|
||||
|
||||
object Cached {
|
||||
|
||||
sealed trait Key
|
||||
|
||||
case object NbAnalysis extends Key
|
||||
}
|
43
app/analyse/PaginatorBuilder.scala
Normal file
43
app/analyse/PaginatorBuilder.scala
Normal file
|
@ -0,0 +1,43 @@
|
|||
package lila
|
||||
package analyse
|
||||
|
||||
import game.{ DbGame, GameRepo }
|
||||
|
||||
import com.github.ornicar.paginator._
|
||||
import com.mongodb.casbah.Imports._
|
||||
import org.joda.time.DateTime
|
||||
|
||||
final class PaginatorBuilder(
|
||||
analysisRepo: AnalysisRepo,
|
||||
cached: Cached,
|
||||
gameRepo: GameRepo,
|
||||
maxPerPage: Int) {
|
||||
|
||||
def games(page: Int): Paginator[DbGame] = {
|
||||
paginator(GameAdapter, page)
|
||||
}
|
||||
|
||||
private def paginator(adapter: Adapter[DbGame], page: Int): Paginator[DbGame] =
|
||||
Paginator(
|
||||
adapter,
|
||||
currentPage = page,
|
||||
maxPerPage = maxPerPage
|
||||
).fold(_ ⇒ paginator(adapter, 0), identity)
|
||||
|
||||
private object GameAdapter extends Adapter[DbGame] {
|
||||
|
||||
def nbResults: Int = cached.nbAnalysis
|
||||
|
||||
def slice(offset: Int, length: Int): Seq[DbGame] = {
|
||||
val ids = ((analysisRepo.collection.find(query, select) sort sort skip offset limit length).toList map {
|
||||
_.getAs[String]("_id")
|
||||
}).flatten
|
||||
val games = (gameRepo games ids).unsafePerformIO
|
||||
ids map { id ⇒ games find (_.id == id) }
|
||||
} flatten
|
||||
|
||||
private def query = DBObject("done" -> true)
|
||||
private def select = DBObject("_id" -> true)
|
||||
private def sort = DBObject("date" -> -1)
|
||||
}
|
||||
}
|
|
@ -2,58 +2,26 @@ package lila
|
|||
package analyse
|
||||
|
||||
import chess.format.Forsyth
|
||||
import chess.format.pgn
|
||||
import chess.format.pgn.{ Pgn, Tag }
|
||||
import game.{ DbGame, DbPlayer, GameRepo }
|
||||
import user.{ User, UserRepo }
|
||||
|
||||
import org.joda.time.format.DateTimeFormat
|
||||
import scalaz.effects._
|
||||
|
||||
final class PgnDump(gameRepo: GameRepo, userRepo: UserRepo) {
|
||||
final class PgnDump(
|
||||
gameRepo: GameRepo,
|
||||
analyser: Analyser,
|
||||
userRepo: UserRepo) {
|
||||
|
||||
import PgnDump._
|
||||
|
||||
val dateFormat = DateTimeFormat forPattern "yyyy-MM-dd";
|
||||
|
||||
def >>(game: DbGame): IO[String] =
|
||||
header(game) map { headers ⇒
|
||||
"%s\n\n%s %s".format(headers, moves(game), result(game))
|
||||
}
|
||||
|
||||
def header(game: DbGame): IO[String] = for {
|
||||
whiteUser ← user(game.whitePlayer)
|
||||
blackUser ← user(game.blackPlayer)
|
||||
initialFen ← game.variant.standard.fold(io(none), gameRepo initialFen game.id)
|
||||
} yield List(
|
||||
"Event" -> game.rated.fold("Rated game", "Casual game"),
|
||||
"Site" -> ("http://lichess.org/" + game.id),
|
||||
"Date" -> game.createdAt.fold(dateFormat.print, "?"),
|
||||
"White" -> player(game.whitePlayer, whiteUser),
|
||||
"Black" -> player(game.blackPlayer, blackUser),
|
||||
"WhiteElo" -> elo(game.whitePlayer),
|
||||
"BlackElo" -> elo(game.blackPlayer),
|
||||
"Result" -> result(game),
|
||||
"PlyCount" -> game.turns,
|
||||
"Variant" -> game.variant.name
|
||||
) ++ game.variant.standard.fold(Map.empty, Map(
|
||||
"FEN" -> (initialFen | "?"),
|
||||
"SetUp" -> "1"
|
||||
)) map {
|
||||
case (name, value) ⇒ """[%s "%s"]""".format(name, value)
|
||||
} mkString "\n"
|
||||
|
||||
def elo(p: DbPlayer) = p.elo.fold(_.toString, "?")
|
||||
|
||||
def user(p: DbPlayer): IO[Option[User]] = p.userId.fold(
|
||||
userRepo.byId,
|
||||
io(none))
|
||||
|
||||
def player(p: DbPlayer, u: Option[User]) = p.aiLevel.fold(
|
||||
"AI level " + _,
|
||||
u.fold(_.username, "Anonymous"))
|
||||
|
||||
def moves(game: DbGame) = (game.pgnList grouped 2).zipWithIndex map {
|
||||
case (moves, turn) ⇒ "%d. %s".format((turn + 1), moves.mkString(" "))
|
||||
} mkString " "
|
||||
def >>(game: DbGame): IO[Pgn] = for {
|
||||
ts ← tags(game)
|
||||
pgnObj = Pgn(ts, turns(game))
|
||||
analysis ← analyser get game.id
|
||||
} yield analysis.fold(Annotator(pgnObj, _), pgnObj)
|
||||
|
||||
def filename(game: DbGame): IO[String] = for {
|
||||
whiteUser ← user(game.whitePlayer)
|
||||
|
@ -63,6 +31,47 @@ final class PgnDump(gameRepo: GameRepo, userRepo: UserRepo) {
|
|||
player(game.whitePlayer, whiteUser),
|
||||
player(game.blackPlayer, blackUser),
|
||||
game.id)
|
||||
|
||||
private val baseUrl = "http://lichess.org/"
|
||||
private val dateFormat = DateTimeFormat forPattern "yyyy-MM-dd";
|
||||
|
||||
private def elo(p: DbPlayer) = p.elo.fold(_.toString, "?")
|
||||
|
||||
private def user(p: DbPlayer): IO[Option[User]] = p.userId.fold(
|
||||
userRepo.byId,
|
||||
io(none))
|
||||
|
||||
private def player(p: DbPlayer, u: Option[User]) = p.aiLevel.fold(
|
||||
"AI level " + _,
|
||||
u.fold(_.username, "Anonymous"))
|
||||
|
||||
private def tags(game: DbGame): IO[List[Tag]] = for {
|
||||
whiteUser ← user(game.whitePlayer)
|
||||
blackUser ← user(game.blackPlayer)
|
||||
initialFen ← game.variant.standard.fold(io(none), gameRepo initialFen game.id)
|
||||
} yield List(
|
||||
Tag(_.Event, game.rated.fold("Rated game", "Casual game")),
|
||||
Tag(_.Site, baseUrl + game.id),
|
||||
Tag(_.Date, game.createdAt.fold(dateFormat.print, "?")),
|
||||
Tag(_.White, player(game.whitePlayer, whiteUser)),
|
||||
Tag(_.Black, player(game.blackPlayer, blackUser)),
|
||||
Tag(_.Result, result(game)),
|
||||
Tag("WhiteElo", elo(game.whitePlayer)),
|
||||
Tag("BlackElo", elo(game.blackPlayer)),
|
||||
Tag("PlyCount", game.turns),
|
||||
Tag(_.Variant, game.variant.name)
|
||||
) ::: game.variant.standard.fold(Nil, List(
|
||||
Tag(_.FEN, initialFen | "?"),
|
||||
Tag("SetUp", "1")
|
||||
))
|
||||
|
||||
private def turns(game: DbGame): List[pgn.Turn] =
|
||||
(game.pgnList grouped 2).zipWithIndex.toList map {
|
||||
case (moves, index) ⇒ pgn.Turn(
|
||||
number = index + 1,
|
||||
white = moves.headOption map { pgn.Move(_) },
|
||||
black = moves.tail.headOption map { pgn.Move(_) })
|
||||
}
|
||||
}
|
||||
|
||||
object PgnDump {
|
||||
|
|
|
@ -15,7 +15,7 @@ final class BookmarkEnv(
|
|||
|
||||
import settings._
|
||||
|
||||
lazy val bookmarkRepo = new BookmarkRepo(mongodb(MongoCollectionBookmark))
|
||||
lazy val bookmarkRepo = new BookmarkRepo(mongodb(BookmarkCollectionBookmark))
|
||||
|
||||
lazy val paginator = new PaginatorBuilder(
|
||||
bookmarkRepo = bookmarkRepo,
|
||||
|
|
|
@ -6,42 +6,67 @@ import http.Context
|
|||
import play.api._
|
||||
import mvc._
|
||||
|
||||
import play.api.libs.concurrent.Akka
|
||||
import play.api.libs.concurrent._
|
||||
import play.api.Play.current
|
||||
|
||||
object Ai extends LilaController {
|
||||
|
||||
private val craftyServer = env.ai.craftyServer
|
||||
private val stockfishServer = env.ai.stockfishServer
|
||||
private val isServer = env.ai.isServer
|
||||
|
||||
def playCrafty = Action { implicit req ⇒
|
||||
implicit val ctx = Context(req, None)
|
||||
Async {
|
||||
Akka.future {
|
||||
craftyServer(fen = getOr("fen", ""), level = getIntOr("level", 1))
|
||||
} map { res ⇒
|
||||
res.fold(
|
||||
err ⇒ BadRequest(err.shows),
|
||||
op ⇒ Ok(op.unsafePerformIO)
|
||||
)
|
||||
IfServer {
|
||||
implicit val ctx = Context(req, None)
|
||||
Async {
|
||||
Akka.future {
|
||||
craftyServer.play(fen = getOr("fen", ""), level = getIntOr("level", 1))
|
||||
} map { res ⇒
|
||||
res.fold(
|
||||
err ⇒ BadRequest(err.shows),
|
||||
op ⇒ Ok(op.unsafePerformIO)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
IfServer {
|
||||
Async {
|
||||
Akka.future {
|
||||
stockfishServer.play(
|
||||
pgn = getOr("pgn", ""),
|
||||
initialFen = get("initialFen"),
|
||||
level = getIntOr("level", 1))
|
||||
} map { res ⇒
|
||||
res.fold(
|
||||
err ⇒ BadRequest(err.shows),
|
||||
op ⇒ Ok(op.unsafePerformIO)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def analyseStockfish = Action { implicit req ⇒
|
||||
implicit val ctx = Context(req, None)
|
||||
IfServer {
|
||||
Async {
|
||||
stockfishServer.analyse(
|
||||
pgn = getOr("pgn", ""),
|
||||
initialFen = get("initialFen")
|
||||
).asPromise map { res ⇒
|
||||
res.fold(
|
||||
err ⇒ InternalServerError(err.shows),
|
||||
analyse ⇒ Ok(analyse.encode)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def IfServer(result: ⇒ Result) =
|
||||
isServer.fold(result, BadRequest("Not an AI server"))
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import analyse._
|
|||
import play.api.mvc._
|
||||
import play.api.http.ContentTypes
|
||||
import play.api.templates.Html
|
||||
import play.api.libs.concurrent._
|
||||
import scalaz.effects._
|
||||
|
||||
object Analyse extends LilaController {
|
||||
|
@ -17,6 +18,22 @@ object Analyse extends LilaController {
|
|||
def bookmarkApi = env.bookmark.api
|
||||
def roundMessenger = env.round.messenger
|
||||
def roundSocket = env.round.socket
|
||||
def analyser = env.analyse.analyser
|
||||
def notification = env.notificationApi
|
||||
|
||||
def computer(id: String, color: String) = Auth { implicit ctx ⇒
|
||||
me ⇒
|
||||
analyser.getOrGenerate(id, me.id) onComplete {
|
||||
case Left(e) ⇒ println(e.getMessage)
|
||||
case Right(a) ⇒ a.fold(
|
||||
err ⇒ println("Computer analysis failure: " + err.shows),
|
||||
analysis ⇒ {
|
||||
notification.add(me, views.html.analyse.notification(analysis, id).toString)
|
||||
}
|
||||
)
|
||||
}
|
||||
Redirect(routes.Analyse.replay(id, color))
|
||||
}
|
||||
|
||||
def replay(id: String, color: String) = Open { implicit ctx ⇒
|
||||
IOptionIOk(gameRepo.pov(id, color)) { pov ⇒
|
||||
|
@ -24,12 +41,14 @@ object Analyse extends LilaController {
|
|||
roomHtml ← roundMessenger renderWatcher pov.game
|
||||
bookmarkers ← bookmarkApi usersByGame pov.game
|
||||
pgn ← pgnDump >> pov.game
|
||||
analysis ← analyser get pov.game.id
|
||||
} yield html.analyse.replay(
|
||||
pov,
|
||||
pgn,
|
||||
pgn.toString,
|
||||
Html(roomHtml),
|
||||
bookmarkers,
|
||||
openingExplorer openingOf pov.game.pgn,
|
||||
analysis,
|
||||
roundSocket blockingVersion pov.gameId)
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +66,8 @@ object Analyse extends LilaController {
|
|||
gameOption ← gameRepo game id
|
||||
res ← gameOption.fold(
|
||||
game ⇒ for {
|
||||
content ← pgnDump >> game
|
||||
pgnObj ← pgnDump >> game
|
||||
content = pgnObj.toString
|
||||
filename ← pgnDump filename game
|
||||
} yield Ok(content).withHeaders(
|
||||
CONTENT_LENGTH -> content.size.toString,
|
||||
|
|
|
@ -10,7 +10,9 @@ object Game extends LilaController {
|
|||
|
||||
val gameRepo = env.game.gameRepo
|
||||
val paginator = env.game.paginator
|
||||
val analysePaginator = env.analyse.paginator
|
||||
val cached = env.game.cached
|
||||
val analyseCached = env.analyse.cached
|
||||
val bookmarkApi = env.bookmark.api
|
||||
val listMenu = env.game.listMenu
|
||||
|
||||
|
@ -47,6 +49,12 @@ object Game extends LilaController {
|
|||
}
|
||||
}
|
||||
|
||||
def analysed(page: Int) = Open { implicit ctx ⇒
|
||||
reasonable(page) {
|
||||
Ok(html.game.analysed(analysePaginator games page, makeListMenu))
|
||||
}
|
||||
}
|
||||
|
||||
def featuredJs(id: String) = Open { implicit ctx ⇒
|
||||
IOk(gameRepo game id map { gameOption =>
|
||||
html.game.featuredJs(gameOption)
|
||||
|
@ -57,5 +65,5 @@ object Game extends LilaController {
|
|||
(page < maxPage).fold(result, BadRequest("too old"))
|
||||
|
||||
private def makeListMenu(implicit ctx: Context) =
|
||||
listMenu(bookmarkApi.countByUser, ctx.me)
|
||||
listMenu(bookmarkApi.countByUser, analyseCached.nbAnalysis, ctx.me)
|
||||
}
|
||||
|
|
14
app/controllers/Notification.scala
Normal file
14
app/controllers/Notification.scala
Normal file
|
@ -0,0 +1,14 @@
|
|||
package controllers
|
||||
|
||||
import lila._
|
||||
import views._
|
||||
|
||||
object Notification extends LilaController {
|
||||
|
||||
def api = env.notificationApi
|
||||
|
||||
def remove(id: String) = Auth { implicit ctx ⇒
|
||||
me ⇒
|
||||
Ok(api.remove(me, id))
|
||||
}
|
||||
}
|
|
@ -16,6 +16,8 @@ final class CoreEnv private (application: Application, val settings: Settings) {
|
|||
implicit val app = application
|
||||
import settings._
|
||||
|
||||
def configName = ConfigName
|
||||
|
||||
lazy val mongodb = new lila.mongodb.MongoDbEnv(
|
||||
settings = settings)
|
||||
|
||||
|
@ -95,7 +97,9 @@ final class CoreEnv private (application: Application, val settings: Settings) {
|
|||
lazy val analyse = new lila.analyse.AnalyseEnv(
|
||||
settings = settings,
|
||||
gameRepo = game.gameRepo,
|
||||
userRepo = user.userRepo)
|
||||
userRepo = user.userRepo,
|
||||
mongodb = mongodb.apply _,
|
||||
() ⇒ ai.ai().analyse _)
|
||||
|
||||
lazy val bookmark = new lila.bookmark.BookmarkEnv(
|
||||
settings = settings,
|
||||
|
@ -117,6 +121,9 @@ final class CoreEnv private (application: Application, val settings: Settings) {
|
|||
lazy val metaHub = new lila.socket.MetaHub(
|
||||
List(site.hub, lobby.hub, round.hubMaster))
|
||||
|
||||
lazy val notificationApi = new lila.notification.Api(
|
||||
metaHub = metaHub)
|
||||
|
||||
lazy val monitor = new lila.monitor.MonitorEnv(
|
||||
app = app,
|
||||
mongodb = mongodb.connection,
|
||||
|
|
|
@ -68,11 +68,11 @@ object Cron {
|
|||
env.titivate.finishByClock
|
||||
}
|
||||
|
||||
env.ai.client.diagnose.unsafePerformIO
|
||||
env.ai.clientDiagnose.unsafePerformIO
|
||||
}
|
||||
effect(10 seconds, "ai diagnose") {
|
||||
env.ai.clientDiagnose
|
||||
}
|
||||
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)
|
||||
|
|
|
@ -15,13 +15,18 @@ object Global extends GlobalSettings {
|
|||
|
||||
coreEnv = CoreEnv(app)
|
||||
|
||||
println("Configured as " + env.configName)
|
||||
|
||||
if (env.ai.isServer) println("Running as AI server")
|
||||
else if (app.mode == Mode.Test) println("Running without cron")
|
||||
else core.Cron start env
|
||||
}
|
||||
|
||||
override def onRouteRequest(req: RequestHeader): Option[Handler] =
|
||||
if (env.ai.isServer) super.onRouteRequest(req)
|
||||
if (env.ai.isServer) {
|
||||
if (req.path startsWith "/ai/") super.onRouteRequest(req)
|
||||
else Action(NotFound).some
|
||||
}
|
||||
else {
|
||||
env.monitor.rpsProvider.countRequest()
|
||||
env.security.firewall.requestHandler(req) orElse
|
||||
|
@ -29,9 +34,9 @@ object Global extends GlobalSettings {
|
|||
super.onRouteRequest(req)
|
||||
}
|
||||
|
||||
override def onHandlerNotFound(req: RequestHeader): Result = {
|
||||
controllers.Lobby handleNotFound req
|
||||
}
|
||||
override def onHandlerNotFound(req: RequestHeader): Result =
|
||||
env.ai.isServer.fold(NotFound, controllers.Lobby handleNotFound req)
|
||||
|
||||
|
||||
override def onBadRequest(req: RequestHeader, error: String) = {
|
||||
BadRequest("Bad Request: " + error)
|
||||
|
|
|
@ -8,6 +8,8 @@ final class Settings(config: Config) {
|
|||
|
||||
import config._
|
||||
|
||||
val ConfigName = getString("config_name")
|
||||
|
||||
val SiteUidTimeout = millis("site.uid.timeout")
|
||||
|
||||
val MonitorTimeout = millis("monitor.timeout")
|
||||
|
@ -17,22 +19,36 @@ final class Settings(config: Config) {
|
|||
val I18nUpstreamDomain = getString("i18n.upstream.domain")
|
||||
val I18nHideCallsCookieName = getString("i18n.hide_calls.cookie.name")
|
||||
val I18nHideCallsCookieMaxAge = getInt("i18n.hide_calls.cookie.max_age")
|
||||
val I18nCollectionTranslation = getString("i18n.collection.translation")
|
||||
|
||||
val GameMessageLifetime = millis("game.message.lifetime")
|
||||
val GameUidTimeout = millis("game.uid.timeout")
|
||||
val GameHubTimeout = millis("game.hub.timeout")
|
||||
val GamePlayerTimeout = millis("game.player.timeout")
|
||||
val GameAnimationDelay = millis("game.animation.delay")
|
||||
val GameCachedNbTtl = millis("game.cached.nb.ttl")
|
||||
val GamePaginatorMaxPerPage = getInt("game.paginator.max_per_page")
|
||||
val GameCollectionGame = getString("game.collection.game")
|
||||
|
||||
val RoundMessageLifetime = millis("round.message.lifetime")
|
||||
val RoundUidTimeout = millis("round.uid.timeout")
|
||||
val RoundHubTimeout = millis("round.hub.timeout")
|
||||
val RoundPlayerTimeout = millis("round.player.timeout")
|
||||
val RoundAnimationDelay = millis("round.animation.delay")
|
||||
val RoundMoretime = seconds("round.moretime")
|
||||
val RoundCollectionRoom = getString("round.collection.room")
|
||||
val RoundCollectionWatcherRoom = getString("round.collection.watcher_room")
|
||||
|
||||
val AnalyseCachedNbTtl = millis("analyse.cached.nb.ttl")
|
||||
|
||||
val UserPaginatorMaxPerPage = getInt("user.paginator.max_per_page")
|
||||
val UserEloUpdaterFloor = getInt("user.elo_updater.floor")
|
||||
val UserCachedNbTtl = millis("user.cached.nb.ttl")
|
||||
val UserCollectionUser = getString("user.collection.user")
|
||||
val UserCollectionHistory = getString("user.collection.history")
|
||||
val UserCollectionConfig = getString("user.collection.config")
|
||||
|
||||
val ForumTopicMaxPerPage = getInt("forum.topic.max_per_page")
|
||||
val ForumPostMaxPerPage = getInt("forum.post.max_per_page")
|
||||
val ForumRecentTimeout = millis("forum.recent.timeout")
|
||||
val ForumCollectionCateg = getString("forum.collection.categ")
|
||||
val ForumCollectionTopic = getString("forum.collection.topic")
|
||||
val ForumCollectionPost = getString("forum.collection.post")
|
||||
|
||||
val MessageThreadMaxPerPage = getInt("message.thread.max_per_page")
|
||||
|
||||
|
@ -41,12 +57,13 @@ final class Settings(config: Config) {
|
|||
val LobbyEntryMax = getInt("lobby.entry.max")
|
||||
val LobbyMessageMax = getInt("lobby.message.max")
|
||||
val LobbyMessageLifetime = millis("lobby.message.lifetime")
|
||||
val LobbyCollectionHook = getString("lobby.collection.hook")
|
||||
val LobbyCollectionEntry = getString("lobby.collection.entry")
|
||||
val LobbyCollectionMessage = getString("lobby.collection.message")
|
||||
|
||||
val MemoHookTimeout = millis("memo.hook.timeout")
|
||||
val MemoUsernameTimeout = millis("memo.username.timeout")
|
||||
|
||||
val MoretimeSeconds = seconds("moretime.seconds")
|
||||
|
||||
val FinisherLockTimeout = millis("memo.finisher_lock.timeout")
|
||||
|
||||
val AiChoice = getString("ai.use")
|
||||
|
@ -58,12 +75,18 @@ final class Settings(config: Config) {
|
|||
|
||||
val AiCraftyExecPath = getString("ai.crafty.exec_path")
|
||||
val AiCraftyBookPath = Some(getString("ai.crafty.book_path")) filter ("" !=)
|
||||
val AiCraftyRemoteUrl = getString("ai.crafty.remote_url")
|
||||
val AiCraftyPlayUrl = getString("ai.crafty.play.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 AiStockfishPlayUrl = getString("ai.stockfish.play.url")
|
||||
val AiStockfishPlayHashSize = getInt("ai.stockfish.play.hash_size")
|
||||
val AiStockfishPlayMaxMoveTime = getInt("ai.stockfish.play.movetime")
|
||||
val AiStockfishPlayAggressiveness = getInt("ai.stockfish.play.aggressiveness")
|
||||
|
||||
val AiStockfishAnalyseUrl = getString("ai.stockfish.analyse.url")
|
||||
val AiStockfishAnalyseHashSize = getInt("ai.stockfish.analyse.hash_size")
|
||||
val AiStockfishAnalyseMoveTime = getInt("ai.stockfish.analyse.movetime")
|
||||
|
||||
val MongoHost = getString("mongo.host")
|
||||
val MongoPort = getInt("mongo.port")
|
||||
|
@ -73,32 +96,25 @@ final class Settings(config: Config) {
|
|||
val MongoConnectTimeout = millis("mongo.connectTimeout")
|
||||
val MongoBlockingThreads = getInt("mongo.threadsAllowedToBlockForConnectionMultiplier")
|
||||
|
||||
val MongoCollectionGame = getString("mongo.collection.game")
|
||||
val MongoCollectionHook = getString("mongo.collection.hook")
|
||||
val MongoCollectionEntry = getString("mongo.collection.entry")
|
||||
val MongoCollectionUser = getString("mongo.collection.user")
|
||||
val MongoCollectionMessage = getString("mongo.collection.message")
|
||||
val MongoCollectionHistory = getString("mongo.collection.history")
|
||||
val MongoCollectionRoom = getString("mongo.collection.room")
|
||||
val MongoCollectionWatcherRoom = getString("mongo.collection.watcher_room")
|
||||
val MongoCollectionConfig = getString("mongo.collection.config")
|
||||
val MongoCollectionCache = getString("mongo.collection.cache")
|
||||
val MongoCollectionSecurity = getString("mongo.collection.security")
|
||||
val MongoCollectionForumCateg = getString("mongo.collection.forum_categ")
|
||||
val MongoCollectionForumTopic = getString("mongo.collection.forum_topic")
|
||||
val MongoCollectionForumPost = getString("mongo.collection.forum_post")
|
||||
val MongoCollectionMessageThread = getString("mongo.collection.message_thread")
|
||||
val MongoCollectionWikiPage = getString("mongo.collection.wiki_page")
|
||||
val MongoCollectionFirewall = getString("mongo.collection.firewall")
|
||||
val MongoCollectionBookmark = getString("mongo.collection.bookmark")
|
||||
val MongoCollectionTranslation = getString("mongo.collection.translation")
|
||||
val AnalyseCollectionAnalysis = getString("analyse.collection.analysis")
|
||||
|
||||
val FirewallEnabled = getBoolean("firewall.enabled")
|
||||
val FirewallBlockCookie = getString("firewall.block_cookie")
|
||||
val FirewallCollectionFirewall = getString("firewall.collection.firewall")
|
||||
|
||||
val MessageCollectionThread = getString("message.collection.thread")
|
||||
|
||||
val WikiCollectionPage = getString("wiki.collection.page")
|
||||
|
||||
val BookmarkCollectionBookmark = getString("bookmark.collection.bookmark")
|
||||
|
||||
val CoreCollectionCache = getString("core.collection.cache")
|
||||
|
||||
val SecurityCollectionSecurity = getString("security.collection.security")
|
||||
|
||||
val ActorReporting = "reporting"
|
||||
val ActorSiteHub = "site_hub"
|
||||
val ActorGameHubMaster = "game_hub_master"
|
||||
val ActorRoundHubMaster = "game_hub_master"
|
||||
val ActorLobbyHub = "lobby_hub"
|
||||
val ActorMonitorHub = "monitor_hub"
|
||||
|
||||
|
|
|
@ -15,11 +15,11 @@ final class ForumEnv(
|
|||
|
||||
import settings._
|
||||
|
||||
lazy val categRepo = new CategRepo(mongodb(MongoCollectionForumCateg))
|
||||
lazy val categRepo = new CategRepo(mongodb(ForumCollectionCateg))
|
||||
|
||||
lazy val topicRepo = new TopicRepo(mongodb(MongoCollectionForumTopic))
|
||||
lazy val topicRepo = new TopicRepo(mongodb(ForumCollectionTopic))
|
||||
|
||||
lazy val postRepo = new PostRepo(mongodb(MongoCollectionForumPost))
|
||||
lazy val postRepo = new PostRepo(mongodb(ForumCollectionPost))
|
||||
|
||||
lazy val categApi = new CategApi(this)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import round.{ Event, Progress }
|
|||
import user.User
|
||||
import chess.{ History ⇒ ChessHistory, Role, Board, Move, Pos, Game, Clock, Status, Color, Piece, Variant, Mode }
|
||||
import Color._
|
||||
import chess.format.{ PgnReader, Fen }
|
||||
import chess.format.{ pgn => chessPgn }
|
||||
import chess.Pos.piotr
|
||||
import chess.Role.forsyth
|
||||
|
||||
|
@ -168,10 +168,10 @@ case class DbGame(
|
|||
}
|
||||
|
||||
def rewind(initialFen: Option[String]): Valid[Progress] = {
|
||||
PgnReader.withSans(
|
||||
chessPgn.Reader.withSans(
|
||||
pgn = pgn,
|
||||
op = _.init,
|
||||
tags = initialFen.fold(fen ⇒ List(Fen(fen)), Nil)
|
||||
tags = initialFen.fold(fen ⇒ List(chessPgn.Tag(_.FEN, fen)), Nil)
|
||||
) map { replay ⇒
|
||||
val rewindedGame = replay.game
|
||||
val rewindedHistory = rewindedGame.board.history
|
||||
|
|
|
@ -11,7 +11,7 @@ final class GameEnv(
|
|||
|
||||
import settings._
|
||||
|
||||
lazy val gameRepo = new GameRepo(mongodb(MongoCollectionGame))
|
||||
lazy val gameRepo = new GameRepo(mongodb(GameCollectionGame))
|
||||
|
||||
lazy val cached = new Cached(
|
||||
gameRepo = gameRepo,
|
||||
|
|
|
@ -9,16 +9,18 @@ case class ListMenu(
|
|||
nbGames: Int,
|
||||
nbMates: Int,
|
||||
nbPopular: Int,
|
||||
nbBookmarks: Option[Int])
|
||||
nbBookmarks: Option[Int],
|
||||
nbAnalysed: Int)
|
||||
|
||||
object ListMenu {
|
||||
|
||||
type CountBookmarks = User ⇒ Int
|
||||
|
||||
def apply(cached: Cached)(countBookmarks: CountBookmarks, me: Option[User]): ListMenu =
|
||||
def apply(cached: Cached)(countBookmarks: CountBookmarks, countAnalysed: ⇒ Int, me: Option[User]): ListMenu =
|
||||
new ListMenu(
|
||||
nbGames = cached.nbGames,
|
||||
nbMates = cached.nbMates,
|
||||
nbPopular = cached.nbPopular,
|
||||
nbBookmarks = me map countBookmarks)
|
||||
nbBookmarks = me map countBookmarks,
|
||||
nbAnalysed = countAnalysed)
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ final class I18nEnv(
|
|||
api = messagesApi,
|
||||
keys = keys)
|
||||
|
||||
lazy val translationRepo = new TranslationRepo(mongodb(MongoCollectionTranslation))
|
||||
lazy val translationRepo = new TranslationRepo(mongodb(I18nCollectionTranslation))
|
||||
|
||||
lazy val forms = new DataForm(
|
||||
repo = translationRepo,
|
||||
|
|
|
@ -82,6 +82,7 @@ final class I18nKeys(translator: Translator) {
|
|||
val viewNbCheckmates = new Key("viewNbCheckmates")
|
||||
val nbBookmarks = new Key("nbBookmarks")
|
||||
val nbPopularGames = new Key("nbPopularGames")
|
||||
val nbAnalysedGames = new Key("nbAnalysedGames")
|
||||
val bookmarkedByNbPlayers = new Key("bookmarkedByNbPlayers")
|
||||
val viewInFullSize = new Key("viewInFullSize")
|
||||
val logOut = new Key("logOut")
|
||||
|
@ -162,5 +163,5 @@ final class I18nKeys(translator: Translator) {
|
|||
val toggleBackground = new Key("toggleBackground")
|
||||
val freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents = new Key("freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents")
|
||||
|
||||
def keys = List(playWithAFriend, inviteAFriendToPlayWithYou, playWithTheMachine, challengeTheArtificialIntelligence, toInviteSomeoneToPlayGiveThisUrl, gameOver, waitingForOpponent, waiting, yourTurn, aiNameLevelAiLevel, level, toggleTheChat, toggleSound, chat, resign, checkmate, stalemate, white, black, createAGame, noGameAvailableRightNowCreateOne, whiteIsVictorious, blackIsVictorious, playWithTheSameOpponentAgain, newOpponent, playWithAnotherOpponent, yourOpponentWantsToPlayANewGameWithYou, joinTheGame, whitePlays, blackPlays, theOtherPlayerHasLeftTheGameYouCanForceResignationOrWaitForHim, makeYourOpponentResign, forceResignation, talkInChat, theFirstPersonToComeOnThisUrlWillPlayWithYou, whiteCreatesTheGame, blackCreatesTheGame, whiteJoinsTheGame, blackJoinsTheGame, whiteResigned, blackResigned, whiteLeftTheGame, blackLeftTheGame, shareThisUrlToLetSpectatorsSeeTheGame, youAreViewingThisGameAsASpectator, replayAndAnalyse, viewGameStats, flipBoard, threefoldRepetition, claimADraw, offerDraw, draw, nbConnectedPlayers, talkAboutChessAndDiscussLichessFeaturesInTheForum, seeTheGamesBeingPlayedInRealTime, gamesBeingPlayedRightNow, viewAllNbGames, viewNbCheckmates, nbBookmarks, nbPopularGames, bookmarkedByNbPlayers, viewInFullSize, logOut, signIn, signUp, people, games, forum, chessPlayers, minutesPerSide, variant, timeControl, start, username, password, haveAnAccount, allYouNeedIsAUsernameAndAPassword, learnMoreAboutLichess, rank, gamesPlayed, declineInvitation, cancel, timeOut, drawOfferSent, drawOfferDeclined, drawOfferAccepted, drawOfferCanceled, yourOpponentOffersADraw, accept, decline, playingRightNow, abortGame, gameAborted, standard, unlimited, mode, casual, rated, thisGameIsRated, rematch, rematchOfferSent, rematchOfferAccepted, rematchOfferCanceled, rematchOfferDeclined, cancelRematchOffer, viewRematch, play, inbox, chatRoom, spectatorRoom, composeMessage, sentMessages, incrementInSeconds, freeOnlineChess, spectators, nbWins, nbLosses, nbDraws, exportGames, color, eloRange, giveNbSeconds, searchAPlayer, whoIsOnline, allPlayers, namedPlayers, premoveEnabledClickAnywhereToCancel, thisPlayerUsesChessComputerAssistance, opening, takeback, proposeATakeback, takebackPropositionSent, takebackPropositionDeclined, takebackPropositionAccepted, takebackPropositionCanceled, yourOpponentProposesATakeback, bookmarkThisGame, toggleBackground, freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents)
|
||||
def keys = List(playWithAFriend, inviteAFriendToPlayWithYou, playWithTheMachine, challengeTheArtificialIntelligence, toInviteSomeoneToPlayGiveThisUrl, gameOver, waitingForOpponent, waiting, yourTurn, aiNameLevelAiLevel, level, toggleTheChat, toggleSound, chat, resign, checkmate, stalemate, white, black, createAGame, noGameAvailableRightNowCreateOne, whiteIsVictorious, blackIsVictorious, playWithTheSameOpponentAgain, newOpponent, playWithAnotherOpponent, yourOpponentWantsToPlayANewGameWithYou, joinTheGame, whitePlays, blackPlays, theOtherPlayerHasLeftTheGameYouCanForceResignationOrWaitForHim, makeYourOpponentResign, forceResignation, talkInChat, theFirstPersonToComeOnThisUrlWillPlayWithYou, whiteCreatesTheGame, blackCreatesTheGame, whiteJoinsTheGame, blackJoinsTheGame, whiteResigned, blackResigned, whiteLeftTheGame, blackLeftTheGame, shareThisUrlToLetSpectatorsSeeTheGame, youAreViewingThisGameAsASpectator, replayAndAnalyse, viewGameStats, flipBoard, threefoldRepetition, claimADraw, offerDraw, draw, nbConnectedPlayers, talkAboutChessAndDiscussLichessFeaturesInTheForum, seeTheGamesBeingPlayedInRealTime, gamesBeingPlayedRightNow, viewAllNbGames, viewNbCheckmates, nbBookmarks, nbPopularGames, nbAnalysedGames, bookmarkedByNbPlayers, viewInFullSize, logOut, signIn, signUp, people, games, forum, chessPlayers, minutesPerSide, variant, timeControl, start, username, password, haveAnAccount, allYouNeedIsAUsernameAndAPassword, learnMoreAboutLichess, rank, gamesPlayed, declineInvitation, cancel, timeOut, drawOfferSent, drawOfferDeclined, drawOfferAccepted, drawOfferCanceled, yourOpponentOffersADraw, accept, decline, playingRightNow, abortGame, gameAborted, standard, unlimited, mode, casual, rated, thisGameIsRated, rematch, rematchOfferSent, rematchOfferAccepted, rematchOfferCanceled, rematchOfferDeclined, cancelRematchOffer, viewRematch, play, inbox, chatRoom, spectatorRoom, composeMessage, sentMessages, incrementInSeconds, freeOnlineChess, spectators, nbWins, nbLosses, nbDraws, exportGames, color, eloRange, giveNbSeconds, searchAPlayer, whoIsOnline, allPlayers, namedPlayers, premoveEnabledClickAnywhereToCancel, thisPlayerUsesChessComputerAssistance, opening, takeback, proposeATakeback, takebackPropositionSent, takebackPropositionDeclined, takebackPropositionAccepted, takebackPropositionCanceled, yourOpponentProposesATakeback, bookmarkThisGame, toggleBackground, freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents)
|
||||
}
|
||||
|
|
|
@ -52,10 +52,10 @@ final class LobbyEnv(
|
|||
socket = socket)
|
||||
|
||||
lazy val messageRepo = new MessageRepo(
|
||||
collection = mongodb(MongoCollectionMessage),
|
||||
collection = mongodb(LobbyCollectionMessage),
|
||||
max = LobbyMessageMax)
|
||||
|
||||
lazy val hookRepo = new HookRepo(mongodb(MongoCollectionHook))
|
||||
lazy val hookRepo = new HookRepo(mongodb(LobbyCollectionHook))
|
||||
|
||||
lazy val hookMemo = new HookMemo(timeout = MemoHookTimeout)
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ final class MessageEnv(
|
|||
|
||||
import settings._
|
||||
|
||||
lazy val threadRepo = new ThreadRepo(mongodb(MongoCollectionMessageThread))
|
||||
lazy val threadRepo = new ThreadRepo(mongodb(MessageCollectionThread))
|
||||
|
||||
lazy val unreadCache = new UnreadCache(threadRepo)
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ final class MongoDbEnv(
|
|||
|
||||
def apply(coll: String) = connection(coll)
|
||||
|
||||
lazy val cache = new Cache(connection(MongoCollectionCache))
|
||||
lazy val cache = new Cache(connection(CoreCollectionCache))
|
||||
|
||||
lazy val connection = MongoConnection(server, options)(MongoDbName)
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ final class Reporting(
|
|||
rps = rpsProvider.rps
|
||||
mps = mpsProvider.rps
|
||||
cpu = ((cpuStats.getCpuUsage() * 1000).round / 10.0).toInt
|
||||
clientAi = env.ai.client.currentPing
|
||||
clientAi = env.ai.clientPing
|
||||
}
|
||||
} onComplete {
|
||||
case Left(a) ⇒ println("Reporting: " + a.getMessage)
|
||||
|
|
25
app/notification/Api.scala
Normal file
25
app/notification/Api.scala
Normal file
|
@ -0,0 +1,25 @@
|
|||
package lila
|
||||
package notification
|
||||
|
||||
import user.User
|
||||
import socket.MetaHub
|
||||
|
||||
import collection.mutable
|
||||
|
||||
final class Api(metaHub: MetaHub) {
|
||||
|
||||
private val repo = mutable.Map[String, List[Notification]]()
|
||||
|
||||
def add(user: User, html: String, from: Option[User] = None) {
|
||||
val notification = Notification(user, html, from)
|
||||
repo.update(user.id, notification :: get(user))
|
||||
metaHub.addNotification(user.id, views.html.notification.view(notification).toString)
|
||||
}
|
||||
|
||||
def get(user: User): List[Notification] = ~(repo get user.id)
|
||||
|
||||
def remove(user: User, id: String) {
|
||||
repo.update(user.id, get(user) filter (_.id != id))
|
||||
metaHub.removeNotification(user.id, id)
|
||||
}
|
||||
}
|
24
app/notification/Notification.scala
Normal file
24
app/notification/Notification.scala
Normal file
|
@ -0,0 +1,24 @@
|
|||
package lila
|
||||
package notification
|
||||
|
||||
import user.User
|
||||
|
||||
import ornicar.scalalib.OrnicarRandom.nextString
|
||||
|
||||
case class Notification(
|
||||
id: String,
|
||||
user: User,
|
||||
html: String,
|
||||
from: Option[User])
|
||||
|
||||
object Notification {
|
||||
|
||||
def apply(
|
||||
user: User,
|
||||
html: String,
|
||||
from: Option[User]): Notification = new Notification(
|
||||
id = nextString(8),
|
||||
user = user,
|
||||
html = html,
|
||||
from = from)
|
||||
}
|
19
app/notification/NotificationHelper.scala
Normal file
19
app/notification/NotificationHelper.scala
Normal file
|
@ -0,0 +1,19 @@
|
|||
package lila
|
||||
package notification
|
||||
|
||||
import core.CoreEnv
|
||||
import user.{ User, UserHelper }
|
||||
|
||||
import play.api.templates.Html
|
||||
import play.api.mvc.Call
|
||||
|
||||
trait NotificationHelper {
|
||||
|
||||
protected def env: CoreEnv
|
||||
private def api = env.notificationApi
|
||||
|
||||
def notifications(user: User): Html = {
|
||||
val notifs = api get user take 2 map { views.html.notification.view(_) }
|
||||
notifs.foldLeft(Html(""))(_ + _)
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ import play.api.libs.json.JsValue
|
|||
import play.api.libs.concurrent.Promise
|
||||
import play.api.libs.iteratee.{ Iteratee, Enumerator }
|
||||
import play.api.libs.iteratee.Concurrent.Channel
|
||||
import play.api.Play
|
||||
import play.api.Play.current
|
||||
|
||||
import com.novus.salat.{ Context, TypeHintFrequency, StringTypeHintStrategy }
|
||||
|
@ -31,8 +30,6 @@ package object lila
|
|||
val name = "Lila Context"
|
||||
override val typeHintStrategy = StringTypeHintStrategy(
|
||||
when = TypeHintFrequency.Never)
|
||||
} ~ { context ⇒
|
||||
context registerClassLoader Play.classloader
|
||||
}
|
||||
RegisterJodaTimeConversionHelpers()
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ final class Hand(
|
|||
initialFen ← progress.game.variant.standard.fold(
|
||||
io(none[String]),
|
||||
gameRepo initialFen progress.game.id)
|
||||
aiResult ← ai()(progress.game, initialFen)
|
||||
aiResult ← ai().play(progress.game, initialFen)
|
||||
eventsAndFen ← aiResult.fold(
|
||||
err ⇒ io(failure(err)), {
|
||||
case (newChessGame, move) ⇒ for {
|
||||
|
|
|
@ -30,14 +30,14 @@ final class RoundEnv(
|
|||
implicit val ctx = app
|
||||
import settings._
|
||||
|
||||
lazy val history = () ⇒ new History(timeout = GameMessageLifetime)
|
||||
lazy val history = () ⇒ new History(timeout = RoundMessageLifetime)
|
||||
|
||||
lazy val hubMaster = Akka.system.actorOf(Props(new HubMaster(
|
||||
makeHistory = history,
|
||||
uidTimeout = GameUidTimeout,
|
||||
hubTimeout = GameHubTimeout,
|
||||
playerTimeout = GamePlayerTimeout
|
||||
)), name = ActorGameHubMaster)
|
||||
uidTimeout = RoundUidTimeout,
|
||||
hubTimeout = RoundHubTimeout,
|
||||
playerTimeout = RoundPlayerTimeout
|
||||
)), name = ActorRoundHubMaster)
|
||||
|
||||
lazy val moveNotifier = new MoveNotifier(
|
||||
siteHubName = ActorSiteHub,
|
||||
|
@ -60,7 +60,7 @@ final class RoundEnv(
|
|||
finisher = finisher,
|
||||
takeback = takeback,
|
||||
hubMaster = hubMaster,
|
||||
moretimeSeconds = MoretimeSeconds)
|
||||
moretimeSeconds = RoundMoretime)
|
||||
|
||||
lazy val finisher = new Finisher(
|
||||
userRepo = userRepo,
|
||||
|
@ -84,8 +84,8 @@ final class RoundEnv(
|
|||
i18nKeys = i18nKeys)
|
||||
|
||||
lazy val roomRepo = new RoomRepo(
|
||||
mongodb(MongoCollectionRoom))
|
||||
mongodb(RoundCollectionRoom))
|
||||
|
||||
lazy val watcherRoomRepo = new WatcherRoomRepo(
|
||||
mongodb(MongoCollectionWatcherRoom))
|
||||
mongodb(RoundCollectionWatcherRoom))
|
||||
}
|
||||
|
|
|
@ -16,10 +16,10 @@ final class SecurityEnv(
|
|||
import settings._
|
||||
|
||||
lazy val store = new security.Store(
|
||||
collection = mongodb(MongoCollectionSecurity))
|
||||
collection = mongodb(SecurityCollectionSecurity))
|
||||
|
||||
lazy val firewall = new security.Firewall(
|
||||
collection = mongodb(MongoCollectionFirewall),
|
||||
collection = mongodb(FirewallCollectionFirewall),
|
||||
blockCookieName = FirewallBlockCookie,
|
||||
enabled = FirewallEnabled)
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ final class Processor(
|
|||
initialFen ← game.variant.standard.fold(
|
||||
io(none[String]),
|
||||
gameRepo initialFen game.id)
|
||||
aiResult ← ai()(game, initialFen) map (_.err)
|
||||
aiResult ← ai().play(game, initialFen) map (_.err)
|
||||
(newChessGame, move) = aiResult
|
||||
progress = game.update(newChessGame, move)
|
||||
_ ← gameRepo save progress
|
||||
|
|
|
@ -24,7 +24,7 @@ final class SetupEnv(
|
|||
|
||||
import settings._
|
||||
|
||||
lazy val configRepo = new UserConfigRepo(mongodb(MongoCollectionConfig))
|
||||
lazy val configRepo = new UserConfigRepo(mongodb(UserCollectionConfig))
|
||||
|
||||
lazy val formFactory = new FormFactory(
|
||||
configRepo = configRepo)
|
||||
|
|
|
@ -3,7 +3,8 @@ package site
|
|||
|
||||
import game._
|
||||
import chess.{ Game, Color }
|
||||
import chess.format.{ Forsyth, PgnReader }
|
||||
import chess.format.Forsyth
|
||||
import chess.format.pgn
|
||||
|
||||
import scalaz.{ NonEmptyList, NonEmptyLists }
|
||||
import scala.collection.mutable
|
||||
|
@ -81,7 +82,7 @@ final class Captcha(gameRepo: GameRepo) {
|
|||
} map (_.notation)
|
||||
|
||||
private def rewind(game: DbGame): Valid[Game] =
|
||||
PgnReader.withSans(game.pgn, _.init) map (_.game) mapFail failInfo(game)
|
||||
pgn.Reader.withSans(game.pgn, _.init) map (_.game) mapFail failInfo(game)
|
||||
|
||||
private def fen(game: Game): String = Forsyth >> game takeWhile (_ != ' ')
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import play.api.libs.json._
|
|||
final class MetaHub(hubs: List[ActorRef]) {
|
||||
|
||||
implicit val executor = Akka.system.dispatcher
|
||||
implicit val timeout = Timeout(200 millis)
|
||||
implicit val timeout = Timeout(1 second)
|
||||
|
||||
def !(message: Any) {
|
||||
hubs foreach (_ ! message)
|
||||
|
@ -24,8 +24,16 @@ final class MetaHub(hubs: List[ActorRef]) {
|
|||
hub ? message mapTo m
|
||||
}
|
||||
|
||||
def notifyUnread(userId: String, nb: Int) = this ! SendTo(
|
||||
userId,
|
||||
JsObject(Seq("t" -> JsString("nbm"), "d" -> JsNumber(nb)))
|
||||
)
|
||||
def notifyUnread(userId: String, nb: Int) =
|
||||
notify(userId, "nbm", JsNumber(nb))
|
||||
|
||||
def addNotification(userId: String, html: String) =
|
||||
notify(userId, "notificationAdd", JsString(html))
|
||||
|
||||
def removeNotification(userId: String, id: String) =
|
||||
notify(userId, "notificationRemove", JsString(id))
|
||||
|
||||
private def notify(userId: String, typ: String, data: JsValue) =
|
||||
this ! SendTo(userId, JsObject(Seq("t" -> JsString(typ), "d" -> data)))
|
||||
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import play.api.templates.Html
|
|||
|
||||
trait AssetHelper {
|
||||
|
||||
val assetVersion = 51
|
||||
val assetVersion = 52
|
||||
|
||||
def cssTag(name: String) = css("stylesheets/" + name)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ trait ConfigHelper {
|
|||
|
||||
protected def env: CoreEnv
|
||||
|
||||
def moretimeSeconds = env.settings.MoretimeSeconds
|
||||
def moretimeSeconds = env.settings.RoundMoretime
|
||||
|
||||
def gameAnimationDelay = env.settings.GameAnimationDelay
|
||||
def gameAnimationDelay = env.settings.RoundAnimationDelay
|
||||
}
|
||||
|
|
|
@ -27,7 +27,9 @@ object Environment
|
|||
with forum.ForumHelper
|
||||
with security.SecurityHelper
|
||||
with i18n.I18nHelper
|
||||
with bookmark.BookmarkHelper {
|
||||
with bookmark.BookmarkHelper
|
||||
with notification.NotificationHelper
|
||||
with analyse.AnalyseHelper {
|
||||
|
||||
protected def env = coreEnv
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package lila
|
|||
package timeline
|
||||
|
||||
import com.novus.salat.annotations._
|
||||
import com.mongodb.BasicDBList
|
||||
|
||||
case class Entry(
|
||||
gameId: String,
|
||||
|
|
|
@ -15,7 +15,7 @@ final class TimelineEnv(
|
|||
import settings._
|
||||
|
||||
lazy val entryRepo = new EntryRepo(
|
||||
collection = mongodb(settings.MongoCollectionEntry),
|
||||
collection = mongodb(settings.LobbyCollectionEntry),
|
||||
max = LobbyEntryMax)
|
||||
|
||||
lazy val push = new Push(
|
||||
|
|
|
@ -15,10 +15,10 @@ final class UserEnv(
|
|||
|
||||
import settings._
|
||||
|
||||
lazy val historyRepo = new HistoryRepo(mongodb(MongoCollectionHistory))
|
||||
lazy val historyRepo = new HistoryRepo(mongodb(UserCollectionHistory))
|
||||
|
||||
lazy val userRepo = new UserRepo(
|
||||
collection = mongodb(MongoCollectionUser))
|
||||
collection = mongodb(UserCollectionUser))
|
||||
|
||||
lazy val paginator = new PaginatorBuilder(
|
||||
userRepo = userRepo,
|
||||
|
|
5
app/views/analyse/notification.scala.html
Normal file
5
app/views/analyse/notification.scala.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
@(analysis: lila.analyse.Analysis, id: String)
|
||||
|
||||
<a href="@routes.Analyse.replay(id, "white")">
|
||||
The computer analysis you requested is now available
|
||||
</a>
|
|
@ -1,4 +1,4 @@
|
|||
@(pov: Pov, pgn: String, roomHtml: Html, bookmarkers: List[User], opening: Option[chess.OpeningExplorer.Opening], version: Int)(implicit ctx: Context)
|
||||
@(pov: Pov, pgn: String, roomHtml: Html, bookmarkers: List[User], opening: Option[chess.OpeningExplorer.Opening], analysis: Option[lila.analyse.Analysis], version: Int)(implicit ctx: Context)
|
||||
|
||||
@import pov._
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
|||
@moreJs = {
|
||||
@jsVendorTag("pgn4web/pgn4web.js")
|
||||
@jsTag("analyse.js")
|
||||
@jsTag("chart.js")
|
||||
}
|
||||
|
||||
@underchat = {
|
||||
|
@ -35,9 +36,28 @@ moreJs = moreJs) {
|
|||
</div>
|
||||
<div class="moves_wrap">
|
||||
<div id="GameText"></div>
|
||||
<div id="GameLastComment"></div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="pgnText" readonly="readonly">@Html(pgn)</textarea>
|
||||
@analysis.map { a =>
|
||||
@if(a.done) {
|
||||
<div
|
||||
class="adv_chart"
|
||||
data-title="Advantage"
|
||||
data-max="@a.advantageChart.max"
|
||||
data-columns="@a.advantageChart.columns"
|
||||
data-rows="@a.advantageChart.rows"></div>
|
||||
} else {
|
||||
<div class="undergame_box game_analysis">
|
||||
@a.fail.map { f =>
|
||||
<div class='inner'>Computer analysis has failed.<br />@f</div>
|
||||
}.getOrElse {
|
||||
<div class='inner'>Computer analysis in progress. You will be notified when it completes.</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@views.html.game.more(pov, bookmarkers) {
|
||||
@trans.opening() @opening.map { o =>
|
||||
<a href="http://www.chessgames.com/perl/chessopening?eco=@o.code">@o.code @o.name</a>
|
||||
|
@ -47,7 +67,13 @@ moreJs = moreJs) {
|
|||
<br />
|
||||
<a class="rotate_board" href="@routes.Analyse.replay(gameId, (!color).name)">@trans.flipBoard()</a>
|
||||
<br />
|
||||
<a class="view_pgn_toggle" href="@routes.Analyse.pgn(game.id)">View PGN</a>
|
||||
<a class="view_pgn_toggle" href="@routes.Analyse.pgn(game.id)">View PGN</a>
|
||||
<br />
|
||||
@if(canRequestAnalysis && game.finished && analysis.isEmpty) {
|
||||
<form class="request_analysis" action="@routes.Analyse.computer(gameId, color.name)" method="post">
|
||||
<a>Request a computer analysis</a>
|
||||
</form>
|
||||
}
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ title = "Internal server error") {
|
|||
<br />
|
||||
<br />
|
||||
<p>If the problem persists, please report it in the <a href="@routes.ForumCateg.show("lichess-feedback", 1)">forum</a>.</p>
|
||||
<p>Or send me an email at thibault.duplessis@gmail.com</p>
|
||||
<br />
|
||||
<br />
|
||||
<code>@ex.getMessage</code>
|
||||
|
|
|
@ -62,6 +62,9 @@
|
|||
<span></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="notifications">
|
||||
@ctx.me.map(notifications(_))
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="header">
|
||||
<h1>
|
||||
|
|
7
app/views/game/analysed.scala.html
Normal file
7
app/views/game/analysed.scala.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
@(paginator: Paginator[DbGame], listMenu: lila.game.ListMenu)(implicit ctx: Context)
|
||||
|
||||
@game.list(
|
||||
name = trans.nbAnalysedGames.str(listMenu.nbAnalysed.localize),
|
||||
paginator = paginator,
|
||||
next = paginator.nextPage map { n => routes.Game.analysed(n) },
|
||||
menu = sideMenu(listMenu, "analysed"))
|
|
@ -3,7 +3,7 @@
|
|||
@import pov._
|
||||
|
||||
@defining("http://lichess.org" + routes.Round.watcher(gameId, color.name)) { url =>
|
||||
<div class="game_more">
|
||||
<div class="undergame_box game_more">
|
||||
<div class="more_top">
|
||||
@bookmark.toggle(game)
|
||||
<a
|
||||
|
@ -11,7 +11,7 @@
|
|||
class="game_permalink blank_if_play"
|
||||
href="@url">@url</a>
|
||||
</div>
|
||||
<div class="game_extra">
|
||||
<div class="inner game_extra">
|
||||
@if(bookmarkers.nonEmpty) {
|
||||
<div class="bookmarkers">
|
||||
<p>@trans.bookmarkedByNbPlayers(bookmarkers.size)</p>
|
||||
|
|
|
@ -17,3 +17,6 @@
|
|||
<a class="@active.active("popular")" href="@routes.Game.popular()">
|
||||
@trans.nbPopularGames(listMenu.nbPopular.localize)
|
||||
</a>
|
||||
<a class="@active.active("analysed")" href="@routes.Game.analysed()">
|
||||
@trans.nbAnalysedGames(listMenu.nbAnalysed.localize)
|
||||
</a>
|
||||
|
|
11
app/views/notification/view.scala.html
Normal file
11
app/views/notification/view.scala.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
@(notif: lila.notification.Notification)
|
||||
|
||||
<div id="@notif.id" class="notification">
|
||||
<a class="close" href="@routes.Notification.remove(notif.id)">X</a>
|
||||
@notif.from.map { user =>
|
||||
@userLink(user, none)
|
||||
}
|
||||
<div class="inner">
|
||||
@Html(notif.html)
|
||||
</div>
|
||||
</div>
|
|
@ -11,7 +11,7 @@ final class WikiEnv(
|
|||
|
||||
import settings._
|
||||
|
||||
lazy val pageRepo = new PageRepo(mongodb(MongoCollectionWikiPage))
|
||||
lazy val pageRepo = new PageRepo(mongodb(WikiCollectionPage))
|
||||
|
||||
lazy val api = new Api(
|
||||
pageRepo = pageRepo)
|
||||
|
|
3
bin/aiserver
Executable file
3
bin/aiserver
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
play -Dconfig.file=conf/application_ai_server.conf
|
|
@ -6,7 +6,6 @@ require_once __DIR__.'/base_script.php';
|
|||
$remote="phobos";
|
||||
$remoteDir="/home/lila";
|
||||
$rsyncoptions="--archive --force --delete --progress --compress --checksum --exclude-from=bin/rsync_exclude";
|
||||
$testurl="http://en.lichess.org";
|
||||
|
||||
if (!file_exists('bin/rsync_exclude')) {
|
||||
exit("This script must be run from the project root");
|
||||
|
@ -14,4 +13,3 @@ if (!file_exists('bin/rsync_exclude')) {
|
|||
|
||||
$remoteTarget = "$remote:$remoteDir";
|
||||
show_run("Deploy to $remoteTarget", "rsync $rsyncoptions ./ $remoteTarget");
|
||||
//show_run("Run restart", "ssh $remote \"cd $remoteDir && bin/restart\"");
|
||||
|
|
3
bin/dev
Executable file
3
bin/dev
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
play -Dconfig.file=conf/application_dev.conf
|
|
@ -1,18 +0,0 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/base_script.php';
|
||||
|
||||
$testurl="http://en.lichess.org";
|
||||
$log = "logs/play.log";
|
||||
$search = "AskTimeoutException";
|
||||
$delay = 3600;
|
||||
echo "Restart every $delay seconds";
|
||||
|
||||
while (true) {
|
||||
sleep($delay);
|
||||
$time = time();
|
||||
$logsav = "logs/play_$time.log";
|
||||
show_run("Save $logsav", "cp $log $logsav");
|
||||
show_run("Restarting", "bin/restart");
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/base_script.php';
|
||||
|
||||
$threshold = 890;
|
||||
$delay = 10;
|
||||
echo "Restart when java connections exceed $threshold\n";
|
||||
|
||||
while (true) {
|
||||
sleep($delay);
|
||||
$level = exec('netstat -pan | grep "java" | wc -l');
|
||||
echo "$level ";
|
||||
if ($level > $threshold) {
|
||||
show_run("Restarting", "bin/restart");
|
||||
}
|
||||
}
|
|
@ -1,39 +1,40 @@
|
|||
config_name = "production"
|
||||
mongo {
|
||||
host = "127.0.0.1"
|
||||
port = 27017
|
||||
dbName = lichess
|
||||
collection {
|
||||
game = game2
|
||||
user = user2
|
||||
hook = hook
|
||||
entry = lobby_entry
|
||||
message = lobby_room
|
||||
history = user_history
|
||||
room = room
|
||||
watcher_room = watcher_room
|
||||
config = config
|
||||
cache = cache
|
||||
security = security
|
||||
forum_categ = f_categ
|
||||
forum_topic = f_topic
|
||||
forum_post = f_post
|
||||
message_thread = m_thread
|
||||
wiki_page = wiki
|
||||
firewall = firewall
|
||||
bookmark = bookmark
|
||||
translation = translation
|
||||
}
|
||||
connectionsPerHost = 100
|
||||
autoConnectRetry = true
|
||||
connectTimeout = 15 seconds
|
||||
threadsAllowedToBlockForConnectionMultiplier = 500
|
||||
}
|
||||
core {
|
||||
collection.cache = cache
|
||||
}
|
||||
bookmark {
|
||||
collection.bookmark = bookmark
|
||||
}
|
||||
analyse {
|
||||
collection.analysis = analysis
|
||||
}
|
||||
security {
|
||||
collection.security = security
|
||||
}
|
||||
firewall {
|
||||
enabled=true
|
||||
block_cookie=fEKHA4zI23ZrZrom
|
||||
collection.firewall = firewall
|
||||
}
|
||||
i18n {
|
||||
web_path.relative = "public/trans"
|
||||
file_path.relative = "conf"
|
||||
upstream.domain = "en.lichess.org"
|
||||
hide_calls.cookie.name="hide_i18n_calls"
|
||||
hide_calls.cookie.max_age=604800 # one week
|
||||
collection.translation = translation
|
||||
}
|
||||
notification {
|
||||
collection.notification = notification
|
||||
}
|
||||
monitor {
|
||||
timeout = 1 second
|
||||
|
@ -43,23 +44,45 @@ lobby {
|
|||
entry.max = 10
|
||||
message.lifetime = 30 seconds
|
||||
uid.timeout = 10 seconds
|
||||
collection {
|
||||
hook = hook
|
||||
entry = lobby_entry
|
||||
message = lobby_room
|
||||
}
|
||||
}
|
||||
game {
|
||||
cached.nb.ttl = 10 minutes
|
||||
paginator.max_per_page = 10
|
||||
collection.game = game2
|
||||
}
|
||||
round {
|
||||
message.lifetime = 30 seconds
|
||||
uid.timeout = 10 seconds
|
||||
hub.timeout = 2 minutes
|
||||
player.timeout = 1 minute
|
||||
player.timeout = 1 minutes
|
||||
animation.delay = 200 ms
|
||||
cached.nb.ttl = 10 minute
|
||||
paginator.max_per_page = 10
|
||||
moretime = 15 seconds
|
||||
collection {
|
||||
room = room
|
||||
watcher_room = watcher_room
|
||||
}
|
||||
}
|
||||
analyse {
|
||||
cached.nb.ttl = 10 minutes
|
||||
}
|
||||
forum {
|
||||
topic.max_per_page = 10
|
||||
post.max_per_page = 10
|
||||
recent.timeout = 1 hour
|
||||
collection {
|
||||
categ = f_categ
|
||||
topic = f_topic
|
||||
post = f_post
|
||||
}
|
||||
}
|
||||
message {
|
||||
thread.max_per_page = 30
|
||||
collection.thread = m_thread
|
||||
}
|
||||
setup {
|
||||
friend_config.memo.ttl = 1 day
|
||||
|
@ -71,6 +94,11 @@ user {
|
|||
paginator.max_per_page = 40
|
||||
elo_updater.floor = 800
|
||||
cached.nb.ttl = 10 minute
|
||||
collection {
|
||||
user = user2
|
||||
history = user_history
|
||||
config = config
|
||||
}
|
||||
}
|
||||
memo {
|
||||
hook.timeout = 5 seconds
|
||||
|
@ -79,24 +107,30 @@ memo {
|
|||
}
|
||||
ai {
|
||||
use = stockfish
|
||||
server = true
|
||||
server = false
|
||||
client = true
|
||||
crafty {
|
||||
exec_path = "/usr/bin/crafty"
|
||||
book_path = "/usr/share/crafty"
|
||||
remote_url = "http://188.165.194.171:9071/ai/play/crafty"
|
||||
play {
|
||||
url = "http://188.165.194.171:9072/ai/crafty/play"
|
||||
}
|
||||
}
|
||||
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
|
||||
play {
|
||||
hash_size = 2048
|
||||
movetime = 500
|
||||
aggressiveness = 150 # 0 - 200
|
||||
url = "http://188.165.194.171:9072/ai/stockfish/play"
|
||||
}
|
||||
analyse {
|
||||
hash_size = 2048
|
||||
movetime = 500
|
||||
url = "http://188.165.194.171:9072/ai/stockfish/analyse"
|
||||
}
|
||||
}
|
||||
}
|
||||
moretime.seconds = 15 seconds
|
||||
|
||||
application {
|
||||
langs="en,fr,ru,de,tr,sr,lv,bs,da,es,ro,it,fi,uk,pt,pl,nl,vi,sv,cs,sk,hu,ca,sl,az,nn,eo,tp,el,fp,lt,no,et,hy,af,hi,ar,zh,gl,tk,hr,mk,id,ja,bg,th,fa,he,mr,mn,cy,gd,ga,sq,be,ka,sw,ps,is"
|
||||
secret="CiebwjgIM9cHQ;I?Xk:sfqDJ;BhIe:jsL?r=?IPF[saf>s^r0]?0grUq4>q?5mP^"
|
||||
|
@ -106,9 +140,8 @@ session {
|
|||
cookieName="lila"
|
||||
maxAge=31536000
|
||||
}
|
||||
firewall {
|
||||
enabled=true
|
||||
block_cookie=fEKHA4zI23ZrZrom
|
||||
wiki {
|
||||
collection.page = wiki
|
||||
}
|
||||
|
||||
# trust proxy X-Forwarded-For header
|
||||
|
@ -125,55 +158,41 @@ logger {
|
|||
}
|
||||
|
||||
akka {
|
||||
loglevel = INFO
|
||||
stdout-loglevel = DEBUG
|
||||
log-config-on-start = off
|
||||
event-handlers = ["lila.core.AkkaLogger"]
|
||||
loglevel = INFO
|
||||
stdout-loglevel = INFO
|
||||
log-config-on-start = off
|
||||
event-handlers = ["lila.core.AkkaLogger"]
|
||||
}
|
||||
|
||||
play {
|
||||
|
||||
akka {
|
||||
|
||||
actor {
|
||||
|
||||
deployment {
|
||||
|
||||
/actions {
|
||||
router = round-robin
|
||||
nr-of-instances = 32
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
actions-dispatcher = {
|
||||
fork-join-executor {
|
||||
parallelism-factor = 64.0
|
||||
parallelism-max = 1024
|
||||
}
|
||||
}
|
||||
|
||||
promises-dispatcher = {
|
||||
fork-join-executor {
|
||||
parallelism-factor = 64.0
|
||||
parallelism-max = 1024
|
||||
}
|
||||
}
|
||||
|
||||
websockets-dispatcher = {
|
||||
fork-join-executor {
|
||||
parallelism-factor = 64.0
|
||||
parallelism-max = 1024
|
||||
}
|
||||
}
|
||||
|
||||
default-dispatcher = {
|
||||
fork-join-executor {
|
||||
parallelism-factor = 64.0
|
||||
parallelism-max = 1024
|
||||
}
|
||||
}
|
||||
play.akka.actor {
|
||||
deployment {
|
||||
/actions {
|
||||
router = round-robin
|
||||
nr-of-instances = 32
|
||||
}
|
||||
}
|
||||
actions-dispatcher = {
|
||||
fork-join-executor {
|
||||
parallelism-factor = 64.0
|
||||
parallelism-max = 1024
|
||||
}
|
||||
}
|
||||
promises-dispatcher = {
|
||||
fork-join-executor {
|
||||
parallelism-factor = 64.0
|
||||
parallelism-max = 1024
|
||||
}
|
||||
}
|
||||
websockets-dispatcher = {
|
||||
fork-join-executor {
|
||||
parallelism-factor = 64.0
|
||||
parallelism-max = 1024
|
||||
}
|
||||
}
|
||||
default-dispatcher = {
|
||||
fork-join-executor {
|
||||
parallelism-factor = 64.0
|
||||
parallelism-max = 1024
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
7
conf/application_ai_server.conf
Normal file
7
conf/application_ai_server.conf
Normal file
|
@ -0,0 +1,7 @@
|
|||
include "application"
|
||||
|
||||
config_name = "production AI server"
|
||||
ai {
|
||||
server = true
|
||||
client = false
|
||||
}
|
16
conf/application_dev.conf
Normal file
16
conf/application_dev.conf
Normal file
|
@ -0,0 +1,16 @@
|
|||
include "application"
|
||||
|
||||
config_name = "development"
|
||||
ai {
|
||||
server = false
|
||||
client = true
|
||||
stockfish {
|
||||
play {
|
||||
hash_size = 32
|
||||
}
|
||||
analyse {
|
||||
hash_size = 64
|
||||
movetime = 200
|
||||
}
|
||||
}
|
||||
}
|
|
@ -58,6 +58,7 @@ viewAllNbGames=%s Games
|
|||
viewNbCheckmates=%s Checkmates
|
||||
nbBookmarks=%s Bookmarks
|
||||
nbPopularGames=%s Popular Games
|
||||
nbAnalysedGames=%s Analysed Games
|
||||
bookmarkedByNbPlayers=Bookmarked by %s players
|
||||
viewInFullSize=View in full size
|
||||
logOut=Log out
|
||||
|
|
10
conf/routes
10
conf/routes
|
@ -7,6 +7,7 @@ GET /games/all controllers.Game.all(page: Int ?= 1)
|
|||
GET /games/checkmate controllers.Game.checkmate(page: Int ?= 1)
|
||||
GET /games/bookmark controllers.Game.bookmark(page: Int ?= 1)
|
||||
GET /games/popular controllers.Game.popular(page: Int ?= 1)
|
||||
GET /games/analysed controllers.Game.analysed(page: Int ?= 1)
|
||||
GET /games/featured/js controllers.Game.featuredJs(id: String ?= "")
|
||||
|
||||
# Round
|
||||
|
@ -37,6 +38,7 @@ GET /$gameId<[\w\-]{8}>/players controllers.Round.players(g
|
|||
# Analyse
|
||||
GET /analyse/$gameId<[\w\-]{8}> controllers.Analyse.replay(gameId: String, color: String = "white")
|
||||
GET /analyse/$gameId<[\w\-]{8}>/$color<white|black> controllers.Analyse.replay(gameId: String, color: String)
|
||||
POST /analyse/$gameId<[\w\-]{8}>/$color<white|black>/computer controllers.Analyse.computer(gameId: String, color: String)
|
||||
GET /$gameId<[\w\-]{8}>/stats controllers.Analyse.stats(gameId: String)
|
||||
GET /$gameId<[\w\-]{8}>/pgn controllers.Analyse.pgn(gameId: String)
|
||||
|
||||
|
@ -89,8 +91,9 @@ GET /wiki controllers.Wiki.home
|
|||
GET /wiki/:slug controllers.Wiki.show(slug: String)
|
||||
|
||||
# AI
|
||||
GET /ai/play/crafty controllers.Ai.playCrafty
|
||||
GET /ai/play/stockfish controllers.Ai.playStockfish
|
||||
GET /ai/crafty/play controllers.Ai.playCrafty
|
||||
GET /ai/stockfish/play controllers.Ai.playStockfish
|
||||
GET /ai/stockfish/analyse controllers.Ai.analyseStockfish
|
||||
|
||||
# Lobby
|
||||
GET / controllers.Lobby.home
|
||||
|
@ -121,6 +124,9 @@ GET /inbox/$id<[\w]{8}> controllers.Message.thread(id: String)
|
|||
POST /inbox/$id<[\w]{8}> controllers.Message.answer(id: String)
|
||||
POST /inbox/$id<[\w]{8}>/delete controllers.Message.delete(id: String)
|
||||
|
||||
# Notification
|
||||
DELETE /notification/$id<[\w]{8}> controllers.Notification.remove(id)
|
||||
|
||||
# Monitor
|
||||
GET /monitor controllers.Monitor.index
|
||||
GET /monitor/socket controllers.Monitor.websocket
|
||||
|
|
|
@ -14,7 +14,7 @@ trait Resolvers {
|
|||
}
|
||||
|
||||
trait Dependencies {
|
||||
val scalachess = "com.github.ornicar" %% "scalachess" % "2.1"
|
||||
val scalachess = "com.github.ornicar" %% "scalachess" % "2.10"
|
||||
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"
|
||||
|
@ -70,15 +70,6 @@ object ApplicationBuild extends Build with Resolvers with Dependencies {
|
|||
"lila.ui",
|
||||
"lila.http.Context",
|
||||
"com.github.ornicar.paginator.Paginator")
|
||||
//incrementalAssetsCompilation := true,
|
||||
//javascriptEntryPoints <<= (sourceDirectory in Compile)(base ⇒
|
||||
//((base / "assets" / "javascripts" ** "*.js")
|
||||
//--- (base / "assets" / "javascripts" ** "_*")
|
||||
//--- (base / "assets" / "javascripts" / "vendor" ** "*.js")
|
||||
//--- (base / "assets" / "javascripts" ** "*.min.js")
|
||||
//).get
|
||||
//),
|
||||
//lessEntryPoints <<= baseDirectory(_ / "app" / "assets" / "stylesheets" ** "*.less")
|
||||
)
|
||||
|
||||
lazy val cli = Project("cli", file("cli"), settings = buildSettings).settings(
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 8.2 KiB |
|
@ -43,8 +43,24 @@ function customFunctionOnPgnGameLoad() {
|
|||
return false;
|
||||
});
|
||||
redrawBoardMarks();
|
||||
$("#GameButtons table").css('width', '514px').buttonset();
|
||||
$("#autoplayButton").click(refreshButtonset);
|
||||
}
|
||||
|
||||
function customFunctionOnMove() {
|
||||
$('#GameLastComment').toggle($('#GameLastComment > .comment').text().length > 0);
|
||||
refreshButtonset();
|
||||
var chart = $("div.adv_chart").data("chart");
|
||||
if (chart) {
|
||||
var index = CurrentPly - 1;
|
||||
chart.setSelection([{ row: index, column: 1}]);
|
||||
}
|
||||
}
|
||||
|
||||
function redrawBoardMarks() {
|
||||
$.displayBoardMarks($('#GameBoard'), ! $('#GameBoard').hasClass('flip'));
|
||||
}
|
||||
|
||||
function refreshButtonset() {
|
||||
$("#autoplayButton").addClass("ui-button ui-widget ui-state-default");
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
function drawCharts() {
|
||||
|
||||
var light = $('body').hasClass('light');
|
||||
var bg = light ? '#ffffff' : '#2a2a2a';
|
||||
var bg = "transparent";
|
||||
var textcolor = {color: light ? '#848484' : '#a0a0a0'};
|
||||
var linecolor = {color: light ? '#9f9f9f' : '#505050'};
|
||||
var weak = light ? '#ccc' : '#3e3e3e';
|
||||
var strong = light ? '#a0a0a0' : '#606060';
|
||||
|
||||
function elemToData(elem) {
|
||||
var data = new google.visualization.DataTable();
|
||||
|
@ -29,7 +30,7 @@ function drawCharts() {
|
|||
chartArea:{left:"10%",top:"2%",width:"90%",height:"96%"},
|
||||
titlePosition: 'none',
|
||||
hAxis: {textPosition: "none"},
|
||||
vAxis: {textStyle: textcolor, gridlines: linecolor},
|
||||
vAxis: {textStyle: textcolor, gridlines: lineColor},
|
||||
backgroundColor: bg
|
||||
});
|
||||
});
|
||||
|
@ -86,10 +87,39 @@ function drawCharts() {
|
|||
titleTextStyle: textcolor,
|
||||
chartArea:{left:"5%",top:"5%",width:"78%",height:"90%"},
|
||||
backgroundColor: bg,
|
||||
vAxis: {textStyle: textcolor, gridlines: linecolor},
|
||||
vAxis: {textStyle: textcolor, gridlines: lineColor},
|
||||
legend: {textStyle: textcolor}
|
||||
});
|
||||
});
|
||||
|
||||
$('div.adv_chart').each(function() {
|
||||
var data = elemToData(this);
|
||||
var chart = new google.visualization.AreaChart(this);
|
||||
chart.draw(data, {
|
||||
width: 512,
|
||||
height: 150,
|
||||
title: $(this).data('title'),
|
||||
titleTextStyle: textcolor,
|
||||
titlePosition: "in",
|
||||
chartArea:{left:"0%",top:"0%",width:"100%",height:"100%"},
|
||||
backgroundColor: bg,
|
||||
vAxis: {
|
||||
maxValue: $(this).data('max'),
|
||||
minValue: -$(this).data('max'),
|
||||
baselineColor: strong,
|
||||
gridlines: {color: bg},
|
||||
minorGridlines: {color: bg},
|
||||
viewWindowMode: "maximized"
|
||||
},
|
||||
legend: {position: "none"},
|
||||
axisTitlesPosition: "none"
|
||||
});
|
||||
google.visualization.events.addListener(chart, 'select', function() {
|
||||
var sel = chart.getSelection()[0];
|
||||
GoToMove(sel.row + 1);
|
||||
});
|
||||
$(this).data("chart", chart);
|
||||
});
|
||||
}
|
||||
|
||||
$(function() {
|
||||
|
|
|
@ -16,8 +16,13 @@ var lichess = {
|
|||
}
|
||||
},
|
||||
nbm: function(e) {
|
||||
var $tag = $('#nb_messages');
|
||||
$tag.text(e || "0").toggleClass("unread", e > 0);
|
||||
$('#nb_messages').text(e || "0").toggleClass("unread", e > 0);
|
||||
},
|
||||
notificationAdd: function(html) {
|
||||
$('div.notifications ').prepend(html);
|
||||
},
|
||||
notificationRemove: function(id) {
|
||||
$('#' + id).remove();
|
||||
}
|
||||
},
|
||||
options: {
|
||||
|
@ -206,6 +211,24 @@ $(function() {
|
|||
return false;
|
||||
});
|
||||
|
||||
$("div.notifications").on("click", "div.notification a", function(e) {
|
||||
var $a = $(this);
|
||||
var $notif = $a.closest("div.notification");
|
||||
var follow = !$a.hasClass("close");
|
||||
$.ajax($notif.find("a.close").attr("href"), {
|
||||
type: "delete",
|
||||
success: function() {
|
||||
if (follow) location.href = $a.attr("href");
|
||||
}
|
||||
});
|
||||
$notif.remove();
|
||||
return false;
|
||||
});
|
||||
|
||||
$("form.request_analysis a").click(function() {
|
||||
$(this).parent().submit();
|
||||
});
|
||||
|
||||
var elem = document.createElement('audio');
|
||||
var canPlayAudio = !! elem.canPlayType && elem.canPlayType('audio/ogg; codecs="vorbis"');
|
||||
var $soundToggle = $('#sound_state');
|
||||
|
|
|
@ -13,6 +13,7 @@ div.board_wrap {
|
|||
}
|
||||
div.moves_wrap {
|
||||
margin-left: 532px;
|
||||
position: relative;
|
||||
}
|
||||
span.board_mark {
|
||||
position: absolute;
|
||||
|
@ -99,3 +100,19 @@ span.board_mark.horz {
|
|||
#GameText a.moveOn {
|
||||
background: #FFE73B;
|
||||
}
|
||||
#GameLastComment {
|
||||
position: absolute;
|
||||
top: 512px;
|
||||
margin-top: 1em;
|
||||
font-size: 1.4em;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
div.adv_chart {
|
||||
margin-top: 10px;
|
||||
height: 150px;
|
||||
width: 512px;
|
||||
}
|
||||
div.adv_chart iframe {
|
||||
height: 150px;
|
||||
}
|
||||
|
|
|
@ -592,7 +592,8 @@ div.locale_menu a.active
|
|||
|
||||
/* strong inactive gradient */
|
||||
div.hooks td.action,
|
||||
.button:hover
|
||||
.button:hover,
|
||||
div.notification
|
||||
{
|
||||
background: #ffffff;
|
||||
background: -moz-linear-gradient(center top , #ffffff, #c0c0c0) repeat scroll 0 0 #ffffff;
|
||||
|
@ -644,20 +645,24 @@ div.checkmateCaptcha input {
|
|||
width: 5em;
|
||||
}
|
||||
|
||||
div.game_more {
|
||||
margin-top: 20px;
|
||||
div.undergame_box {
|
||||
margin-top: 10px;
|
||||
width: 512px;
|
||||
box-shadow: 0 0 7px #d0d0d0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
line-height: 24px;
|
||||
}
|
||||
div.game_more a {
|
||||
div.undergame_box a {
|
||||
text-decoration: none;
|
||||
}
|
||||
div.game_more a:hover {
|
||||
div.undergame_box a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
div.undergame_box div.inner {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
div.game_more div.more_top {
|
||||
padding: 3px 10px;
|
||||
width: 492px;
|
||||
|
@ -685,8 +690,6 @@ div.game_more textarea {
|
|||
border-radius: 3px;
|
||||
}
|
||||
div.game_extra {
|
||||
padding: 10px 10px;
|
||||
width: 492px;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
div.game_extra div.body {
|
||||
|
@ -739,3 +742,23 @@ div.progressbar div {
|
|||
div.progressbar.flashy.green div {
|
||||
background: #90d050;
|
||||
}
|
||||
|
||||
div.notifications {
|
||||
width: 1005px;
|
||||
margin: 0 auto -10px auto;
|
||||
}
|
||||
div.notification {
|
||||
margin-left: 211px;
|
||||
width: 498px;
|
||||
border: 1px solid #404040;
|
||||
padding: 6px 8px 7px 8px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
div.notification a.close {
|
||||
text-decoration: none;
|
||||
float: right;
|
||||
}
|
||||
div.notification a.close:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ body.dark div.lichess_board_wrap,
|
|||
body.dark div.lichess_table .lichess_button,
|
||||
body.dark div.lichess_goodies div.box,
|
||||
body.dark div.lichess_table,
|
||||
body.dark div.game_more,
|
||||
body.dark div.undergame_box,
|
||||
body.dark div.clock,
|
||||
body.dark #GameText,
|
||||
body.dark #GameBoard table.boardTable,
|
||||
|
@ -52,7 +52,7 @@ body.dark .ui-widget-content,
|
|||
body.dark div.lichess_goodies div.box,
|
||||
body.dark div.lichess_table,
|
||||
body.dark div.lichess_separator,
|
||||
body.dark div.game_more,
|
||||
body.dark div.undergame_box,
|
||||
body.dark div.game_extra,
|
||||
body.dark div.clock,
|
||||
body.dark .button,
|
||||
|
@ -78,7 +78,8 @@ body.dark #lichess_message input,
|
|||
body.dark div.progressbar,
|
||||
body.dark form.translation_form div.messages,
|
||||
body.dark form.translation_form input,
|
||||
body.dark div.locale_menu a
|
||||
body.dark div.locale_menu a,
|
||||
body.dark div.adv_chart
|
||||
{
|
||||
border-color: #3e3e3e;
|
||||
}
|
||||
|
@ -111,7 +112,7 @@ body.dark #top span.new_messages {
|
|||
body.dark div.lichess_chat_inner,
|
||||
body.dark div.undertable_inner,
|
||||
body.dark div.lichess_goodies div.box,
|
||||
body.dark div.game_more,
|
||||
body.dark div.undergame_box,
|
||||
body.dark div.lichess_bot td,
|
||||
body.dark div.lichess_table,
|
||||
body.dark div.lichess_table_wrap div.clock,
|
||||
|
@ -125,6 +126,13 @@ body.dark div.undertable a.user_link {
|
|||
color: #808080;
|
||||
}
|
||||
|
||||
body.dark .user_link.white {
|
||||
background-position: 0 -288px;
|
||||
}
|
||||
body.dark .user_link.black {
|
||||
background-position: 0 -272px;
|
||||
}
|
||||
|
||||
body.dark div.hooks_wrap {
|
||||
box-shadow: 0 0 20px #444;
|
||||
background: rgba(10,10,10,0.8);
|
||||
|
@ -134,7 +142,8 @@ body.dark div.hooks_wrap {
|
|||
background: -ms-linear-gradient(top, rgba(33,33,33,0.9) 0%,rgba(15,15,15,0.5) 100%);
|
||||
background: -o-linear-gradient(top, rgba(33,33,33,0.9) 0%,rgba(15,15,15,0.5) 100%);
|
||||
}
|
||||
body.dark div.lichess_bot tr:nth-child(even) td {
|
||||
body.dark div.lichess_bot tr:nth-child(even) td
|
||||
{
|
||||
background: #303030;
|
||||
}
|
||||
body.dark div.new_posts li span {
|
||||
|
@ -251,6 +260,7 @@ body.dark .ui-state-default,
|
|||
body.dark div.content_box_top,
|
||||
body.dark div.hooks tr,
|
||||
body.dark a.translation_call,
|
||||
body.dark div.notification,
|
||||
body.dark div.locale_menu a
|
||||
{
|
||||
background: #3a3a3a;
|
||||
|
|
9
todo
9
todo
|
@ -26,3 +26,12 @@ 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
|
||||
legend feedback: http://en.lichess.org/forum/lichess-feedback/a-few-points-bugssuggestions#1
|
||||
http://en.lichess.org/forum/lichess-feedback/problems-with-yin-yang#1
|
||||
also translate websockets error message
|
||||
@someone = link to someone's profile
|
||||
|
||||
next deploy
|
||||
-----------
|
||||
db.analysis.ensureIndex({done:1})
|
||||
db.analysis.ensureIndex({date:-1})
|
||||
|
|
Loading…
Reference in a new issue