working computer analysis
This commit is contained in:
parent
7249322a45
commit
81fa54c6f7
|
@ -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]) =
|
||||
|
|
|
@ -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])
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue