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:
Thibault Duplessis 2012-07-06 09:33:56 +02:00
commit 681213d981
94 changed files with 1584 additions and 431 deletions

View file

@ -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
```

View file

@ -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]]
}

View file

@ -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
}
}
}

View file

@ -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)),

View file

@ -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")
}

View file

@ -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")
}

View file

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

View file

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

View file

@ -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)
}

View 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
}

View 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()
}
}

View 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
}

View file

@ -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
}

View file

@ -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)
}

View 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)
}

View file

@ -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 {

View file

@ -18,6 +18,7 @@ final class Process(
}
def destroy() {
write("stop")
write("quit")
Thread sleep 300
process.destroy()

View file

@ -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)))
}

View file

@ -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
}

View file

@ -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

View 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)
}

View file

@ -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)
}

View 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
}

View 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
View 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))
}

View 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)
}
}

View 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
View 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
}

View 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)
}
}

View file

@ -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 {

View file

@ -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,

View file

@ -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"))
}

View file

@ -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,

View file

@ -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)
}

View 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))
}
}

View file

@ -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,

View file

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

View file

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

View file

@ -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"

View file

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

View file

@ -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

View file

@ -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,

View file

@ -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)
}

View file

@ -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,

View file

@ -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)
}

View file

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

View file

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

View file

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

View file

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

View 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)
}
}

View 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)
}

View 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(""))(_ + _)
}
}

View file

@ -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()

View file

@ -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 {

View file

@ -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))
}

View file

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

View file

@ -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

View file

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

View file

@ -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 (_ != ' ')

View file

@ -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)))
}

View file

@ -7,7 +7,7 @@ import play.api.templates.Html
trait AssetHelper {
val assetVersion = 51
val assetVersion = 52
def cssTag(name: String) = css("stylesheets/" + name)

View file

@ -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
}

View file

@ -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
}

View file

@ -2,7 +2,6 @@ package lila
package timeline
import com.novus.salat.annotations._
import com.mongodb.BasicDBList
case class Entry(
gameId: String,

View file

@ -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(

View file

@ -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,

View 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>

View file

@ -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>
}
}

View file

@ -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&#64;gmail.com</p>
<br />
<br />
<code>@ex.getMessage</code>

View file

@ -62,6 +62,9 @@
<span></span>
</a>
</div>
<div class="notifications">
@ctx.me.map(notifications(_))
</div>
<div class="content">
<div class="header">
<h1>

View 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"))

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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
View file

@ -0,0 +1,3 @@
#!/bin/sh
play -Dconfig.file=conf/application_ai_server.conf

View file

@ -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
View file

@ -0,0 +1,3 @@
#!/bin/sh
play -Dconfig.file=conf/application_dev.conf

View file

@ -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");
}

View file

@ -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");
}
}

View file

@ -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
}
}
}

View file

@ -0,0 +1,7 @@
include "application"
config_name = "production AI server"
ai {
server = true
client = false
}

16
conf/application_dev.conf Normal file
View 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
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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");
}

View file

@ -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() {

View file

@ -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');

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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
View file

@ -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})