294 lines
9.5 KiB
Scala
294 lines
9.5 KiB
Scala
package lila.tree
|
|
|
|
import play.api.libs.json._
|
|
|
|
import chess.format.pgn.{ Glyph, Glyphs }
|
|
import chess.format.{ Uci, UciCharPair }
|
|
import chess.opening.FullOpening
|
|
import chess.Pos
|
|
import chess.variant.Crazyhouse
|
|
|
|
import chess.Centis
|
|
|
|
sealed trait Node {
|
|
def ply: Int
|
|
def fen: String
|
|
def check: Boolean
|
|
// None when not computed yet
|
|
def dests: Option[Map[Pos, List[Pos]]]
|
|
def drops: Option[List[Pos]]
|
|
def eval: Option[Eval]
|
|
def shapes: Node.Shapes
|
|
def comments: Node.Comments
|
|
def gamebook: Option[Node.Gamebook]
|
|
def glyphs: Glyphs
|
|
def children: List[Branch]
|
|
def opening: Option[FullOpening]
|
|
def comp: Boolean // generated by a computer analysis
|
|
def crazyData: Option[Crazyhouse.Data]
|
|
def addChild(branch: Branch): Node
|
|
def dropFirstChild: Node
|
|
def clock: Option[Centis]
|
|
|
|
// implementation dependent
|
|
def idOption: Option[UciCharPair]
|
|
def moveOption: Option[Uci.WithSan]
|
|
|
|
// who's color plays next
|
|
def color = chess.Color(ply % 2 == 0)
|
|
|
|
def mainlineNodeList: List[Node] =
|
|
dropFirstChild :: children.headOption.fold(List.empty[Node])(_.mainlineNodeList)
|
|
}
|
|
|
|
case class Root(
|
|
ply: Int,
|
|
fen: String,
|
|
check: Boolean,
|
|
// None when not computed yet
|
|
dests: Option[Map[Pos, List[Pos]]] = None,
|
|
drops: Option[List[Pos]] = None,
|
|
eval: Option[Eval] = None,
|
|
shapes: Node.Shapes = Node.Shapes(Nil),
|
|
comments: Node.Comments = Node.Comments(Nil),
|
|
gamebook: Option[Node.Gamebook] = None,
|
|
glyphs: Glyphs = Glyphs.empty,
|
|
children: List[Branch] = Nil,
|
|
opening: Option[FullOpening] = None,
|
|
clock: Option[Centis] = None, // clock state at game start, assumed same for both players
|
|
crazyData: Option[Crazyhouse.Data]
|
|
) extends Node {
|
|
|
|
def idOption = None
|
|
def moveOption = None
|
|
def comp = false
|
|
|
|
def addChild(branch: Branch) = copy(children = children :+ branch)
|
|
def prependChild(branch: Branch) = copy(children = branch :: children)
|
|
def dropFirstChild = copy(children = if (children.isEmpty) children else children.tail)
|
|
}
|
|
|
|
case class Branch(
|
|
id: UciCharPair,
|
|
ply: Int,
|
|
move: Uci.WithSan,
|
|
fen: String,
|
|
check: Boolean,
|
|
// None when not computed yet
|
|
dests: Option[Map[Pos, List[Pos]]] = None,
|
|
drops: Option[List[Pos]] = None,
|
|
eval: Option[Eval] = None,
|
|
shapes: Node.Shapes = Node.Shapes(Nil),
|
|
comments: Node.Comments = Node.Comments(Nil),
|
|
gamebook: Option[Node.Gamebook] = None,
|
|
glyphs: Glyphs = Glyphs.empty,
|
|
children: List[Branch] = Nil,
|
|
opening: Option[FullOpening] = None,
|
|
comp: Boolean = false,
|
|
clock: Option[Centis] = None, // clock state after the move is played, and the increment applied
|
|
crazyData: Option[Crazyhouse.Data]
|
|
) extends Node {
|
|
|
|
def idOption = Some(id)
|
|
def moveOption = Some(move)
|
|
|
|
def addChild(branch: Branch) = copy(children = children :+ branch)
|
|
def prependChild(branch: Branch) = copy(children = branch :: children)
|
|
def dropFirstChild = copy(children = if (children.isEmpty) children else children.tail)
|
|
|
|
def setComp = copy(comp = true)
|
|
}
|
|
|
|
object Node {
|
|
|
|
sealed trait Shape
|
|
object Shape {
|
|
type ID = String
|
|
type Brush = String
|
|
case class Circle(brush: Brush, orig: Pos) extends Shape
|
|
case class Arrow(brush: Brush, orig: Pos, dest: Pos) extends Shape
|
|
}
|
|
case class Shapes(value: List[Shape]) extends AnyVal {
|
|
def list = value
|
|
def ++(shapes: Shapes) = Shapes {
|
|
(value ::: shapes.value).distinct
|
|
}
|
|
}
|
|
object Shapes {
|
|
val empty = Shapes(Nil)
|
|
}
|
|
|
|
case class Comment(id: Comment.Id, text: Comment.Text, by: Comment.Author) {
|
|
def removeMeta = text.removeMeta map { t =>
|
|
copy(text = t)
|
|
}
|
|
}
|
|
object Comment {
|
|
case class Id(value: String) extends AnyVal
|
|
object Id {
|
|
def make = Id(scala.util.Random.alphanumeric take 4 mkString)
|
|
}
|
|
private val metaReg = """\[%[^\]]+\]""".r
|
|
case class Text(value: String) extends AnyVal {
|
|
def removeMeta: Option[Text] = {
|
|
val v = metaReg.replaceAllIn(value, "").trim
|
|
if (v.nonEmpty) Some(Text(v)) else None
|
|
}
|
|
}
|
|
sealed trait Author
|
|
object Author {
|
|
case class User(id: String, titleName: String) extends Author
|
|
case class External(name: String) extends Author
|
|
case object Lichess extends Author
|
|
case object Unknown extends Author
|
|
}
|
|
def sanitize(text: String) = Text {
|
|
text.trim.take(4000)
|
|
.replaceAll("""\r\n""", "\n") // these 3 lines dedup white spaces and new lines
|
|
.replaceAll("""(?m)(^ *| +(?= |$))""", "")
|
|
.replaceAll("""(?m)^$([\n]+?)(^$[\n]+?^)+""", "$1")
|
|
.replaceAll("\\{|\\}", "") // {} are reserved in PGN comments
|
|
}
|
|
}
|
|
case class Comments(value: List[Comment]) extends AnyVal {
|
|
def list = value
|
|
def findBy(author: Comment.Author) = list.find(_.by == author)
|
|
def set(comment: Comment) = Comments {
|
|
if (list.exists(_.by == comment.by)) list.map {
|
|
case c if c.by == comment.by => c.copy(text = comment.text)
|
|
case c => c
|
|
}
|
|
else list :+ comment
|
|
}
|
|
def delete(commentId: Comment.Id) = Comments {
|
|
value.filterNot(_.id == commentId)
|
|
}
|
|
def +(comment: Comment) = Comments(comment :: value)
|
|
|
|
def filterEmpty = Comments(value.filter(_.text.value.nonEmpty))
|
|
}
|
|
object Comments {
|
|
val empty = Comments(Nil)
|
|
}
|
|
|
|
case class Gamebook(deviation: Option[String], hint: Option[String]) {
|
|
private def trimOrNone(txt: Option[String]) = txt.map(_.trim).filter(_.nonEmpty)
|
|
def cleanUp = copy(
|
|
deviation = trimOrNone(deviation),
|
|
hint = trimOrNone(hint)
|
|
)
|
|
def nonEmpty = deviation.nonEmpty || hint.nonEmpty
|
|
}
|
|
|
|
// TODO copied from lila.game
|
|
// put all that shit somewhere else
|
|
private implicit val crazyhousePocketWriter: OWrites[Crazyhouse.Pocket] = OWrites { v =>
|
|
JsObject(
|
|
Crazyhouse.storableRoles.flatMap { role =>
|
|
Some(v.roles.count(role ==)).filter(0 <).map { count =>
|
|
role.name -> JsNumber(count)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
private implicit val crazyhouseDataWriter: OWrites[chess.variant.Crazyhouse.Data] = OWrites { v =>
|
|
Json.obj("pockets" -> List(v.pockets.white, v.pockets.black))
|
|
}
|
|
|
|
implicit val openingWriter: OWrites[chess.opening.FullOpening] = OWrites { o =>
|
|
Json.obj(
|
|
"eco" -> o.eco,
|
|
"name" -> o.name
|
|
)
|
|
}
|
|
|
|
private implicit val posWrites: Writes[Pos] = Writes[Pos] { p =>
|
|
JsString(p.key)
|
|
}
|
|
private implicit val shapeCircleWrites = Json.writes[Shape.Circle]
|
|
private implicit val shapeArrowWrites = Json.writes[Shape.Arrow]
|
|
implicit val shapeWrites: Writes[Shape] = Writes[Shape] {
|
|
case s: Shape.Circle => shapeCircleWrites writes s
|
|
case s: Shape.Arrow => shapeArrowWrites writes s
|
|
}
|
|
implicit val shapesWrites: Writes[Node.Shapes] = Writes[Node.Shapes] { s =>
|
|
JsArray(s.list.map(shapeWrites.writes))
|
|
}
|
|
implicit val glyphWriter: Writes[Glyph] = Json.writes[Glyph]
|
|
implicit val glyphsWriter: Writes[Glyphs] = Writes[Glyphs] { gs =>
|
|
Json.toJson(gs.toList)
|
|
}
|
|
|
|
implicit val clockWrites: Writes[Centis] = Writes { clock =>
|
|
JsNumber(clock.centis)
|
|
}
|
|
implicit val commentIdWrites: Writes[Comment.Id] = Writes { id =>
|
|
JsString(id.value)
|
|
}
|
|
implicit val commentTextWrites: Writes[Comment.Text] = Writes { text =>
|
|
JsString(text.value)
|
|
}
|
|
implicit val commentAuthorWrites: Writes[Comment.Author] = Writes[Comment.Author] {
|
|
case Comment.Author.User(id, name) => Json.obj("id" -> id, "name" -> name)
|
|
case Comment.Author.External(name) => JsString(s"${name.trim}")
|
|
case Comment.Author.Lichess => JsString("lichess")
|
|
case Comment.Author.Unknown => JsNull
|
|
}
|
|
implicit val commentWriter = Json.writes[Node.Comment]
|
|
private implicit val commentsWriter: Writes[Node.Comments] = Writes[Node.Comments] { s =>
|
|
JsArray(s.list.map(commentWriter.writes))
|
|
}
|
|
implicit val gamebookWriter = Json.writes[Node.Gamebook]
|
|
import Eval.JsonHandlers.evalWrites
|
|
|
|
def makeNodeJsonWriter(alwaysChildren: Boolean): Writes[Node] = Writes { node =>
|
|
import node._
|
|
val comments = node.comments.list.flatMap(_.removeMeta)
|
|
(
|
|
add("id", idOption.map(_.toString)) _ compose
|
|
add("uci", moveOption.map(_.uci.uci)) _ compose
|
|
add("san", moveOption.map(_.san)) _ compose
|
|
add("check", true, check) _ compose
|
|
add("eval", eval.filterNot(_.isEmpty)) _ compose
|
|
add("comments", comments, comments.nonEmpty) _ compose
|
|
add("gamebook", gamebook) _ compose
|
|
add("glyphs", glyphs.nonEmpty) _ compose
|
|
add("shapes", shapes.list, shapes.list.nonEmpty) _ compose
|
|
add("opening", opening) _ compose
|
|
add("dests", dests.map {
|
|
_.map {
|
|
case (orig, dests) => s"${orig.piotr}${dests.map(_.piotr).mkString}"
|
|
}.mkString(" ")
|
|
}) _ compose
|
|
add("drops", drops.map { drops =>
|
|
JsString(drops.map(_.key).mkString)
|
|
}) _ compose
|
|
add("clock", clock) _ compose
|
|
add("crazy", crazyData) _ compose
|
|
add("comp", true, comp) _ compose
|
|
add("children", children, alwaysChildren || children.nonEmpty)
|
|
)(Json.obj(
|
|
"ply" -> ply,
|
|
"fen" -> fen
|
|
))
|
|
}
|
|
|
|
implicit val defaultNodeJsonWriter: Writes[Node] =
|
|
makeNodeJsonWriter(alwaysChildren = true)
|
|
|
|
val minimalNodeJsonWriter: Writes[Node] =
|
|
makeNodeJsonWriter(alwaysChildren = false)
|
|
|
|
val partitionTreeJsonWriter: Writes[Node] = Writes { node =>
|
|
JsArray {
|
|
node.mainlineNodeList.map(minimalNodeJsonWriter.writes)
|
|
}
|
|
}
|
|
|
|
private def add[A](k: String, v: A, cond: Boolean)(o: JsObject)(implicit writes: Writes[A]): JsObject =
|
|
if (cond) o + (k -> writes.writes(v)) else o
|
|
|
|
private def add[A: Writes](k: String, v: Option[A]): JsObject => JsObject =
|
|
v.fold(identity[JsObject] _) { add(k, _, true) _ }
|
|
}
|