working computer analysis

This commit is contained in:
Thibault Duplessis 2012-06-25 23:18:07 +02:00
parent 7249322a45
commit 81fa54c6f7
13 changed files with 144 additions and 79 deletions

View file

@ -6,15 +6,10 @@ import analyse.Info
object AnalyseParser {
def apply(lines: List[String]): String Valid[Info] =
moveString for {
move Uci parseMove moveString toValid "Invalid move " + moveString
bestString findBestMove(lines) toValid "Analysis bestmove not found"
best Uci parseMove bestString toValid "Invalid bestmove " + bestString
} yield Info(
move = move,
best = best,
cp = findCp(lines),
mate = findMate(lines))
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]) =

View file

@ -35,7 +35,10 @@ final class Analyser(
_ analysisRepo.progress(id, userId)
initialFen gameRepo initialFen id
analysis generator()(game, initialFen)
_ analysis.fold(_ io(), analysisRepo.done(id, _))
_ analysis.fold(
analysisRepo.fail(id, _),
analysisRepo.done(id, _)
)
} yield analysis,
io(!!("No such game " + id): Valid[Analysis])
)

View file

@ -8,35 +8,56 @@ case class Analysis(infos: List[Info], done: Boolean) {
def encode: String = infos map (_.encode) mkString Analysis.separator
lazy val richInfos = {
var prevCp = 0
infos.zipWithIndex map {
case (info, index) {
RichInfo(index, 0, info)
}
}
}
def of(color: Color) = PlayerAnalysis(
color = color,
richInfos filter (_.color == color))
lazy val advices: List[Advice] = (infos.zipWithIndex sliding 2 map {
case (info, turn) :: (next, _) :: Nil Advice(info, next, turn)
case _ None
}).toList.flatten
}
case class PlayerAnalysis(color: Color, richInfos: List[RichInfo]) {
sealed abstract class Severity(val delta: Int)
case object Blunder extends Severity(-300)
case object Mistake extends Severity(-100)
case object Inaccuracy extends Severity(-50)
object Severity {
val all = List(Inaccuracy, Mistake, Blunder)
def apply(delta: Int): Option[Severity] = all.foldLeft(none[Severity]) {
case (_, severity) if severity.delta > delta severity.some
case (acc, _) acc
}
}
def cps = richInfos map { ri ri.turn -> ri.cp }
case class Advice(
severity: Severity,
info: Info,
next: Info,
turn: Int) {
def color = Color(turn % 2 == 0)
def fullMove = 1 + turn / 2
}
object Advice {
def apply(info: Info, next: Info, turn: Int): Option[Advice] = for {
cp info.score map (_.centipawns)
nextCp next.score map (_.centipawns)
color = Color(turn % 2 == 0)
delta = nextCp - cp
severity Severity(color.fold(delta, -delta))
} yield Advice(severity, info, next, turn)
}
object Analysis {
private val separator = " "
def apply(str: String, done: Boolean) = decode(str) map { infos =>
def apply(str: String, done: Boolean) = decode(str) map { infos
new Analysis(infos, done)
}
def decode(str: String): Valid[List[Info]] =
(str.split(separator).toList map Info.decode).sequence
(str.split(separator).toList map Info.decode).sequence
def builder = new AnalysisBuilder(Nil)
}
@ -47,22 +68,29 @@ final class AnalysisBuilder(infos: List[Info]) {
def +(info: Info) = new AnalysisBuilder(info :: infos)
def done = new Analysis(infos.reverse, true)
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(
move: (Pos, Pos),
best: (Pos, Pos),
cp: Option[Int],
score: Option[Score],
mate: Option[Int]) {
def encode: String = List(
Uci makeMove move,
Uci makeMove best,
encode(cp),
encode(score map (_.centipawns)),
encode(mate)
) mkString Info.separator
def showMove = showPoss(move)
def showBest = showPoss(best)
private def showPoss(poss: (Pos, Pos)) = poss._1.key + poss._2.key
private def encode(oa: Option[Any]): String = oa.fold(_.toString, "_")
}
@ -71,25 +99,37 @@ object Info {
private val separator = ","
def decode(str: String): Valid[Info] = str.split(separator).toList match {
case moveString :: bestString :: cpString :: mateString :: Nil for {
move Uci parseMove moveString toValid "Invalid move " + moveString
best Uci parseMove bestString toValid "Invalid best " + bestString
} yield Info(
move = move,
best = best,
cp = parseIntOption(cpString),
mate = parseIntOption(mateString)
case moveString :: bestString :: cpString :: mateString :: Nil Info(
moveString, bestString, parseIntOption(cpString), parseIntOption(mateString)
)
case _ !!("Invalid encoded info " + str)
}
def apply(
moveString: String,
bestString: String,
score: Option[Int],
mate: Option[Int]): Valid[Info] = for {
move Uci parseMove moveString toValid "Invalid info move " + moveString
best Uci parseMove bestString toValid "Invalid info best " + bestString
} yield Info(
move = move,
best = best,
score = score map Score.apply,
mate = mate)
}
case class RichInfo(turn: Int, delta: Int, info: Info) {
case class Score(centipawns: Int) {
def color = Color(turn % 2 == 0)
def pawns: Float = centipawns / 100f
def showPawns: String = "%.2f" format pawns
def move = info.move
def best = info.best
def cp = info.cp
def mate = info.mate
def percent: Int = math.round(box(0, 100,
50 + (pawns / 10) * 50
))
def negate = Score(-centipawns)
private def box(min: Float, max: Float, v: Float) =
math.min(max, math.max(min, v))
}

View file

@ -12,9 +12,14 @@ final class AnalysisRepo(collection: MongoCollection) {
def done(id: String, a: Analysis) = io {
collection.update(
DBObject("_id" -> id),
$set(
"done" -> true,
"encoded" -> a.encode)
$set("done" -> true, "encoded" -> a.encode)
)
}
def fail(id: String, err: Failures) = io {
collection.update(
DBObject("_id" -> id),
$set("fail" -> err.shows)
)
}

View file

@ -23,8 +23,11 @@ object Analyse extends LilaController {
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) "Computer analysis complete"
case Left(e) println(e.getMessage)
case Right(a) a.fold(
err println("Computer analysis failure: " + err.shows),
_ println("Computer analysis succeeded for game " + id)
)
}
Redirect(routes.Analyse.replay(id, color))
}

View file

@ -99,7 +99,7 @@ final class CoreEnv private (application: Application, val settings: Settings) {
gameRepo = game.gameRepo,
userRepo = user.userRepo,
mongodb = mongodb.apply _,
() => ai.ai().analyse _)
() ai.ai().analyse _)
lazy val bookmark = new lila.bookmark.BookmarkEnv(
settings = settings,

View file

@ -1,3 +1,24 @@
@(pov: Pov, analysis: lila.analyse.Analysis)
@analysis.of(pov.color).cps.mkString(",")
<div class="inner">
<table>
<thead>
<tr>
<th>Severity</th>
<th>Move</th>
<th>Score</th>
<th>Better</th>
</tr>
</thead>
<body>
@analysis.advices.map { a =>
<tr>
<td>@a.severity.toString</td>
<td>@a.fullMove.@a.color.fold("", "..") @a.info.showMove</td>
<td>@a.info.score.map(_.showPawns) ⇒ @a.next.score.map(_.showPawns)</td>
<td>@a.info.showBest</td>
</tr>
}
</body>
</table>
</div>

View file

@ -12,19 +12,6 @@
}
@underchat = {
@analysis.map { a =>
@if(a.done) {
<div class="analysis">
@analyse.analysis(pov, a))
</div>
} else {
<p>Computer analysis in progress</p>
}
}.getOrElse {
<form action="@routes.Analyse.computer(gameId, color.name)" method="post">
<button type="submit">Request a computer analysis</button>
</form>
}
<div class="watchers">
@trans.spectators() <span class="list"></span>
</div>
@ -51,6 +38,15 @@ moreJs = moreJs) {
</div>
</div>
<textarea id="pgnText" readonly="readonly">@Html(pgn)</textarea>
@analysis.map { a =>
<div class="undergame_box game_analysis">
@a.done.fold(analyse.analysis(pov, a), Html("<div class='inner'>Computer analysis in progress</div>"))
</div>
}.getOrElse {
<form action="@routes.Analyse.computer(gameId, color.name)" method="post">
<button type="submit">Request a computer analysis</button>
</form>
}
@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>

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

@ -10,8 +10,7 @@ ai {
url = "http://localhost:9000/ai/play/stockfish"
}
analyse {
hash_size = 62
movetime = 200
hash_size = 128
url = "http://localhost:9000/ai/analyse/stockfish"
}
}

View file

@ -644,7 +644,7 @@ div.checkmateCaptcha input {
width: 5em;
}
div.game_more {
div.undergame_box {
margin-top: 20px;
width: 512px;
box-shadow: 0 0 7px #d0d0d0;
@ -652,12 +652,21 @@ div.game_more {
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_analysis {
font-family: monospace;
}
div.game_analysis table {
width: 100%;
}
div.game_more div.more_top {
padding: 3px 10px;
width: 492px;
@ -685,8 +694,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 {

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

4
todo
View file

@ -25,7 +25,3 @@ show lobby chat to anon (and rated games?)
or show empty chat
game chat box css issue http://en.lichess.org/forum/lichess-feedback/problem-with-the-chat-box#1
show last move on miniboards
next deploy
-----------
change treehugger password