lila/modules/analyse/src/main/model.scala

157 lines
4.7 KiB
Scala

package lila.analyse
import chess.{ Pos, Color, White, Black }
import chess.format.{ UciMove, Nag }
private[analyse] 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
}
private[analyse] sealed abstract class Severity(val nag: Nag)
private[analyse] sealed abstract class CpSeverity(val delta: Int, nag: Nag) extends Severity(nag)
private[analyse] case object CpBlunder extends CpSeverity(-300, Nag.Blunder)
private[analyse] case object CpMistake extends CpSeverity(-100, Nag.Mistake)
private[analyse] case object CpInaccuracy extends CpSeverity(-50, Nag.Inaccuracy)
private[analyse] 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
}
}
private[analyse] case class CpAdvice(
severity: CpSeverity,
info: Info,
next: Info) extends Advice {
def text = severity.nag.toString
}
private[analyse] sealed abstract class MateSeverity(nag: Nag, val desc: String) extends Severity(nag: Nag)
private[analyse] 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))
private[analyse] case object MateLost extends MateSeverity(Nag.Mistake,
desc = "Lost forced checkmate sequence")
private[analyse] case object MateCreated extends MateSeverity(Nag.Blunder,
desc = "Checkmate is now unavoidable")
private[analyse] 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)
}
}
private[analyse] case class MateAdvice(
severity: MateSeverity,
info: Info,
next: Info) extends Advice {
def text = severity.toString
}
private[analyse] 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)
}
}
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)
}
private[analyse] 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))
}