show advantage chart and fix mate analysis
This commit is contained in:
parent
21840e52fa
commit
a18f1292f9
|
@ -52,11 +52,9 @@ final class AnalyseFSM(
|
|||
whenUnhandled {
|
||||
case Event(analyse: Analyse, data) ⇒
|
||||
nextAnalyse(data enqueue Task(analyse, sender))
|
||||
case Event(Out(""), _) ⇒ stay
|
||||
case Event(Out(t), _) if t startsWith "id " ⇒ stay
|
||||
case Event(Out(t), _) if t startsWith "info " ⇒ stay
|
||||
case Event(Out(t), _) if t startsWith "option name " ⇒ stay
|
||||
case Event(Err(t), _) ⇒ { log.error(t); stay }
|
||||
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 {
|
||||
|
@ -83,6 +81,9 @@ final class AnalyseFSM(
|
|||
})
|
||||
}
|
||||
|
||||
def isNoise(t: String) =
|
||||
t.isEmpty || (t startsWith "id ") || (t startsWith "info ") || (t startsWith "option name ")
|
||||
|
||||
def onTermination() {
|
||||
process.destroy()
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ 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[Info] =
|
||||
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))
|
||||
|
|
36
app/analyse/AdvantageChart.scala
Normal file
36
app/analyse/AdvantageChart.scala
Normal file
|
@ -0,0 +1,36 @@
|
|||
package lila
|
||||
package analyse
|
||||
|
||||
import com.codahale.jerkson.Json
|
||||
|
||||
final class AdvantageChart(infos: List[Info]) {
|
||||
|
||||
val max = 10
|
||||
|
||||
def columns = AdvantageChart.columns
|
||||
|
||||
def rows = Json generate values.map(_.map(x ⇒ List(x._1, x._2)))
|
||||
|
||||
private lazy val values: List[Option[(String, Float)]] = (infos sliding 2 map {
|
||||
case info :: next :: Nil ⇒ (next.score, next.mate) match {
|
||||
case (Some(score), _) ⇒ Some(move(info, next) -> box(score.pawns))
|
||||
case (_, Some(mate)) ⇒ Some(move(info, next) -> box(info.color.fold(-mate, mate) * max))
|
||||
case _ ⇒ None
|
||||
}
|
||||
case _ ⇒ None
|
||||
}).toList.dropWhile(_.isEmpty).reverse.dropWhile(_.isEmpty).reverse
|
||||
|
||||
private def box(v: Float) = math.min(max, math.max(-max, v))
|
||||
|
||||
private def move(info: Info, next: Info) = info.color.fold(
|
||||
"%d. %s", "%d... %s"
|
||||
).format(info.turn, info.move.uci) + Advice(info, next).fold(" " + _.nag.symbol, "")
|
||||
|
||||
}
|
||||
|
||||
object AdvantageChart {
|
||||
|
||||
val columns = Json generate List(
|
||||
"string" :: "Move" :: Nil,
|
||||
"number" :: "Advantage" :: Nil)
|
||||
}
|
|
@ -2,7 +2,7 @@ package lila
|
|||
package analyse
|
||||
|
||||
import chess.{ Pos, Color, White, Black }
|
||||
import chess.format.UciMove
|
||||
import chess.format.{ UciMove, Nag }
|
||||
|
||||
case class Analysis(
|
||||
infos: List[Info],
|
||||
|
@ -11,43 +11,32 @@ case class Analysis(
|
|||
|
||||
def encode: String = infos map (_.encode) mkString Analysis.separator
|
||||
|
||||
lazy val advices: List[Advice] = (infos.zipWithIndex sliding 2 map {
|
||||
case (info, turn) :: (next, _) :: Nil ⇒ Advice(info, next, turn)
|
||||
case _ ⇒ None
|
||||
lazy val advices: List[Advice] = (infos sliding 2 map {
|
||||
case info :: next :: Nil ⇒ Advice(info, next)
|
||||
case _ ⇒ None
|
||||
}).toList.flatten
|
||||
|
||||
def scoreRows: List[Option[List[Any]]] = infos map { info ⇒
|
||||
info.score map { score ⇒
|
||||
List(info.move.uci, score.pawns)
|
||||
}
|
||||
}
|
||||
|
||||
lazy val advantageChart = new AdvantageChart(infos)
|
||||
}
|
||||
|
||||
sealed trait Advice {
|
||||
def severity: Severity
|
||||
def info: Info
|
||||
def next: Info
|
||||
def turn: Int
|
||||
def text: String
|
||||
|
||||
def color = Color(turn % 2 == 0)
|
||||
def fullMove = 1 + turn / 2
|
||||
|
||||
def ply = info.ply
|
||||
def turn = info.turn
|
||||
def color = info.color
|
||||
def nag = severity.nag
|
||||
}
|
||||
|
||||
sealed abstract class Nag(val code: Int)
|
||||
case object Blunder extends Nag(4)
|
||||
case object Mistake extends Nag(2)
|
||||
case object Inaccuracy extends Nag(6)
|
||||
|
||||
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, Blunder)
|
||||
case object CpMistake extends CpSeverity(-100, Mistake)
|
||||
case object CpInaccuracy extends CpSeverity(-50, Inaccuracy)
|
||||
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]) {
|
||||
|
@ -59,57 +48,56 @@ object CpSeverity {
|
|||
case class CpAdvice(
|
||||
severity: CpSeverity,
|
||||
info: Info,
|
||||
next: Info,
|
||||
turn: Int) extends Advice {
|
||||
next: Info) extends Advice {
|
||||
|
||||
def text = severity.nag.toString
|
||||
}
|
||||
|
||||
sealed abstract class MateSeverity(nag: Nag, val desc: String) extends Severity(nag: Nag)
|
||||
case object MateDelayed extends MateSeverity(Inaccuracy,
|
||||
case object MateDelayed extends MateSeverity(Nag.Inaccuracy,
|
||||
desc = "Not the quickest path to checkmate")
|
||||
case object MateLost extends MateSeverity(Mistake,
|
||||
case object MateLost extends MateSeverity(Nag.Mistake,
|
||||
desc = "Forced checkmate lost")
|
||||
case object MateCreated extends MateSeverity(Blunder,
|
||||
case object MateCreated extends MateSeverity(Nag.Blunder,
|
||||
desc = "Checkmate is now unavoidable")
|
||||
object MateSeverity {
|
||||
def apply(current: Option[Int], next: Option[Int], turn: Int): Option[MateSeverity] =
|
||||
(current, next, Color(turn % 2 == 0)).some collect {
|
||||
case (None, Some(n), White) if n < 0 ⇒ MateCreated
|
||||
case (None, Some(n), Black) if n > 0 ⇒ MateCreated
|
||||
case (Some(c), None, White) if c > 0 ⇒ MateLost
|
||||
case (Some(c), None, Black) if c < 0 ⇒ MateLost
|
||||
case (Some(c), Some(n), White) if c > 0 && c >= n ⇒ MateDelayed
|
||||
case (Some(c), Some(n), Black) if c < 0 && c <= n ⇒ MateDelayed
|
||||
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 >= c ⇒ MateDelayed
|
||||
}
|
||||
}
|
||||
case class MateAdvice(
|
||||
severity: MateSeverity,
|
||||
info: Info,
|
||||
next: Info,
|
||||
turn: Int) extends Advice {
|
||||
next: Info) extends Advice {
|
||||
|
||||
def text = severity.toString
|
||||
}
|
||||
|
||||
object Advice {
|
||||
|
||||
def apply(info: Info, next: Info, turn: Int): Option[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(negate(turn)(delta))
|
||||
} yield CpAdvice(severity, info, next, turn)
|
||||
severity ← CpSeverity(info.color.fold(delta, -delta))
|
||||
} yield CpAdvice(severity, info, next)
|
||||
} orElse {
|
||||
val mate = info.mate
|
||||
val nextMate = next.mate
|
||||
MateSeverity(mate, nextMate, turn) map { severity ⇒
|
||||
MateAdvice(severity, info, next, turn)
|
||||
}
|
||||
MateSeverity(
|
||||
mateChance(info, info.color),
|
||||
mateChance(next, info.color)) map { severity ⇒
|
||||
MateAdvice(severity, info, next)
|
||||
}
|
||||
}
|
||||
|
||||
private def negate(turn: Int)(v: Int) = (turn % 2 == 0).fold(v, -v)
|
||||
private def mateChance(info: Info, color: Color) =
|
||||
info.color.fold(info.mate, info.mate map (-_)) map { chance ⇒
|
||||
color.fold(chance, -chance)
|
||||
}
|
||||
}
|
||||
|
||||
object Analysis {
|
||||
|
@ -121,7 +109,9 @@ object Analysis {
|
|||
}
|
||||
|
||||
def decode(str: String): Valid[List[Info]] =
|
||||
(str.split(separator).toList map Info.decode).sequence
|
||||
(str.split(separator).toList.zipWithIndex map {
|
||||
case (info, index) ⇒ Info.decode(index + 1, info)
|
||||
}).sequence
|
||||
|
||||
def builder = new AnalysisBuilder(Nil)
|
||||
}
|
||||
|
@ -130,7 +120,7 @@ final class AnalysisBuilder(infos: List[Info]) {
|
|||
|
||||
def size = infos.size
|
||||
|
||||
def +(info: Info) = new AnalysisBuilder(info :: infos)
|
||||
def +(info: Int ⇒ Info) = new AnalysisBuilder(info(infos.size + 1) :: infos)
|
||||
|
||||
def done = new Analysis(infos.reverse.zipWithIndex map {
|
||||
case (info, turn) ⇒
|
||||
|
@ -139,11 +129,16 @@ final class AnalysisBuilder(infos: List[Info]) {
|
|||
}
|
||||
|
||||
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,
|
||||
|
@ -158,11 +153,12 @@ object Info {
|
|||
|
||||
private val separator = ","
|
||||
|
||||
def decode(str: String): Valid[Info] = str.split(separator).toList match {
|
||||
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,
|
||||
|
@ -174,10 +170,11 @@ object Info {
|
|||
moveString: String,
|
||||
bestString: String,
|
||||
score: Option[Int],
|
||||
mate: Option[Int]): Valid[Info] = for {
|
||||
mate: Option[Int]): Valid[Int ⇒ Info] = for {
|
||||
move ← UciMove(moveString) toValid "Invalid info move " + moveString
|
||||
best ← UciMove(bestString) toValid "Invalid info best " + bestString
|
||||
} yield Info(
|
||||
} yield ply ⇒ Info(
|
||||
ply = ply,
|
||||
move = move,
|
||||
best = best,
|
||||
score = score map Score.apply,
|
||||
|
|
|
@ -12,7 +12,7 @@ object Annotator {
|
|||
|
||||
private def annotateTurns(p: pgn.Pgn, advices: List[Advice]): pgn.Pgn =
|
||||
advices.foldLeft(p) {
|
||||
case (acc, advice) ⇒ acc.updateTurn(advice.fullMove, turn ⇒
|
||||
case (acc, advice) ⇒ acc.updateTurn(advice.turn, turn ⇒
|
||||
turn.update(advice.color, move ⇒
|
||||
move.copy(
|
||||
nag = advice.nag.code.some,
|
||||
|
@ -23,7 +23,7 @@ object Annotator {
|
|||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
@(pov: Pov, analysis: lila.analyse.Analysis)
|
||||
|
||||
<div
|
||||
class="adv_chart"
|
||||
title="White advantage"
|
||||
data-columns='[["string","Move"],["number", "Adv"]]'
|
||||
data-rows="@toJson(analysis.scoreRows)"></div>
|
|
@ -40,23 +40,28 @@ moreJs = moreJs) {
|
|||
<div id="GameLastComment"></div>
|
||||
</div>
|
||||
<textarea id="pgnText" readonly="readonly">@Html(pgn)</textarea>
|
||||
<div class="undergame_box game_analysis">
|
||||
@analysis.map { a =>
|
||||
@if(a.done) {
|
||||
@analyse.analysis(pov, a)
|
||||
} else {
|
||||
@a.fail.map { f =>
|
||||
@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</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}.getOrElse {
|
||||
<form action="@routes.Analyse.computer(gameId, color.name)" method="post">
|
||||
<button type="submit">Request a computer analysis</button>
|
||||
</form>
|
||||
}
|
||||
</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>
|
||||
|
|
|
@ -11,7 +11,7 @@ ai {
|
|||
}
|
||||
analyse {
|
||||
hash_size = 64
|
||||
movetime = 100
|
||||
movetime = 200
|
||||
url = "http://localhost:9000/ai/analyse/stockfish"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ trait Resolvers {
|
|||
}
|
||||
|
||||
trait Dependencies {
|
||||
val scalachess = "com.github.ornicar" %% "scalachess" % "2.8"
|
||||
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"
|
||||
|
@ -69,18 +69,7 @@ object ApplicationBuild extends Build with Resolvers with Dependencies {
|
|||
"lila.templating.Environment._",
|
||||
"lila.ui",
|
||||
"lila.http.Context",
|
||||
"com.github.ornicar.paginator.Paginator",
|
||||
"com.codahale.jerkson.Json.{ generate => toJson }"
|
||||
)
|
||||
//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")
|
||||
"com.github.ornicar.paginator.Paginator")
|
||||
)
|
||||
|
||||
lazy val cli = Project("cli", file("cli"), settings = buildSettings).settings(
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
function drawCharts() {
|
||||
|
||||
var light = $('body').hasClass('light');
|
||||
var bg = light ? '#ffffff' : '#2a2a2a';
|
||||
var bg = "transparent"; //light ? '#ffffff' : '#2a2a2a';
|
||||
var textcolor = {color: light ? '#848484' : '#a0a0a0'};
|
||||
var linecolor = {color: light ? '#9f9f9f' : '#505050'};
|
||||
var weak = light ? '#ccc' : '#3e3e3e';
|
||||
var strong = light ? '#909090' : '#707070';
|
||||
|
||||
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,7 +87,7 @@ 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}
|
||||
});
|
||||
});
|
||||
|
@ -96,13 +97,22 @@ function drawCharts() {
|
|||
var chart = new google.visualization.AreaChart(this);
|
||||
chart.draw(data, {
|
||||
width: 512,
|
||||
height: 400,
|
||||
height: 150,
|
||||
title: $(this).data('title'),
|
||||
titleTextStyle: textcolor,
|
||||
chartArea:{left:"5%",top:"5%",width:"78%",height:"90%"},
|
||||
titlePosition: "in",
|
||||
chartArea:{left:"0%",top:"0%",width:"100%",height:"100%"},
|
||||
backgroundColor: bg,
|
||||
vAxis: {textStyle: textcolor, gridlines: linecolor},
|
||||
legend: {textStyle: textcolor}
|
||||
vAxis: {
|
||||
maxValue: $(this).data('max'),
|
||||
minValue: -$(this).data('max'),
|
||||
baselineColor: strong,
|
||||
gridlines: {color:weak},
|
||||
minorGridlines: {color:weak}
|
||||
},
|
||||
legend: {position: "none"},
|
||||
axisTitlesPosition: "none",
|
||||
pointSize: 2
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -661,6 +661,15 @@ div.undergame_box a:hover {
|
|||
div.undergame_box div.inner {
|
||||
padding: 10px;
|
||||
}
|
||||
div.adv_chart {
|
||||
height: 150px;
|
||||
width: 512px;
|
||||
border-left: 1px solid #cecece;
|
||||
border-right: 1px solid #cecece;
|
||||
}
|
||||
div.adv_chart iframe {
|
||||
height: 150px;
|
||||
}
|
||||
div.game_analysis {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue