show advantage chart and fix mate analysis

This commit is contained in:
Thibault Duplessis 2012-07-04 00:34:13 +02:00
parent 21840e52fa
commit a18f1292f9
11 changed files with 136 additions and 96 deletions

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@ ai {
}
analyse {
hash_size = 64
movetime = 100
movetime = 200
url = "http://localhost:9000/ai/analyse/stockfish"
}
}

View file

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

View file

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

View file

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