study clock states - WIP - for #2851
parent
27d01eceaf
commit
45042c3512
|
@ -1 +1 @@
|
|||
Subproject commit 8cdd646452c375441313268094b7cc0e50a48d3a
|
||||
Subproject commit 4d1fe47307f82e9aadc00b1b46167f5ebcf52619
|
|
@ -24,6 +24,8 @@ case class Centis(value: Int) extends AnyVal with Ordered[Centis] {
|
|||
|
||||
object Centis {
|
||||
|
||||
implicit val centisIso = Iso.int[Centis](Centis.apply, _.value)
|
||||
|
||||
def apply(centis: Long): Centis = Centis {
|
||||
if (centis > Int.MaxValue) {
|
||||
lila.log("common").error(s"Truncating Centis! $centis")
|
||||
|
|
|
@ -101,13 +101,13 @@ final class PgnDump(
|
|||
white = moves.headOption filter (".." !=) map { san =>
|
||||
chessPgn.Move(
|
||||
san = san,
|
||||
timeLeft = clocks lift (index * 2 - clockOffset) map (_.roundSeconds)
|
||||
secondsLeft = clocks lift (index * 2 - clockOffset) map (_.roundSeconds)
|
||||
)
|
||||
},
|
||||
black = moves lift 1 map { san =>
|
||||
chessPgn.Move(
|
||||
san = san,
|
||||
timeLeft = clocks lift (index * 2 + 1 - clockOffset) map (_.roundSeconds)
|
||||
secondsLeft = clocks lift (index * 2 + 1 - clockOffset) map (_.roundSeconds)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -12,8 +12,8 @@ import lila.db.BSON.{ Reader, Writer }
|
|||
import lila.db.dsl._
|
||||
import lila.tree.Node.{ Shape, Shapes }
|
||||
|
||||
import lila.common.Iso
|
||||
import lila.common.Iso._
|
||||
import lila.common.{ Iso, Centis }
|
||||
|
||||
object BSONHandlers {
|
||||
|
||||
|
@ -23,6 +23,7 @@ object BSONHandlers {
|
|||
implicit val StudyNameBSONHandler = stringIsoHandler(Study.nameIso)
|
||||
implicit val ChapterIdBSONHandler = stringIsoHandler(Chapter.idIso)
|
||||
implicit val ChapterNameBSONHandler = stringIsoHandler(Chapter.nameIso)
|
||||
implicit val CentisBSONHandler = intIsoHandler(Centis.centisIso)
|
||||
|
||||
private implicit val PosBSONHandler = new BSONHandler[BSONString, Pos] {
|
||||
def read(bsonStr: BSONString): Pos = Pos.posAt(bsonStr.value) err s"No such pos: ${bsonStr.value}"
|
||||
|
@ -139,6 +140,7 @@ object BSONHandlers {
|
|||
comments = readComments(r),
|
||||
glyphs = r.getO[Glyphs]("g") | Glyphs.empty,
|
||||
crazyData = r.getO[Crazyhouse.Data]("z"),
|
||||
clock = r.getO[Centis]("l"),
|
||||
children = r.get[Node.Children]("n")
|
||||
)
|
||||
def writes(w: Writer, s: Node) = $doc(
|
||||
|
@ -152,6 +154,7 @@ object BSONHandlers {
|
|||
"co" -> w.listO(s.comments.list),
|
||||
"g" -> s.glyphs.nonEmpty,
|
||||
"z" -> s.crazyData,
|
||||
"l" -> s.clock,
|
||||
"n" -> (if (s.ply < Node.MAX_PLIES) s.children else Node.emptyChildren)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package lila.study
|
||||
|
||||
import chess.Pos
|
||||
import lila.common.Centis
|
||||
import lila.tree.Node.{ Shape, Shapes }
|
||||
|
||||
private[study] object CommentParser {
|
||||
|
@ -9,18 +10,47 @@ private[study] object CommentParser {
|
|||
private val circlesRemoveRegex = """\[\%csl[\s\r\n]+((?:\w{3}[,\s]*)+)\]""".r
|
||||
private val arrowsRegex = """(?s).*\[\%cal[\s\r\n]+((?:\w{5}[,\s]*)+)\].*""".r
|
||||
private val arrowsRemoveRegex = """\[\%cal[\s\r\n]+((?:\w{5}[,\s]*)+)\]""".r
|
||||
private val clockRegex = """(?s).*\[\%clk[\s\r\n]+([\d:]+)\].*""".r
|
||||
private val clockRemoveRegex = """\[\%clk[\s\r\n]+[\d:]+\]""".r
|
||||
|
||||
private type ShapesAndComment = (Shapes, String)
|
||||
case class ParsedComment(
|
||||
shapes: Shapes,
|
||||
clock: Option[Centis],
|
||||
comment: String
|
||||
)
|
||||
|
||||
def extractShapes(comment: String): ShapesAndComment =
|
||||
parseCircles(comment) match {
|
||||
case (circles, c2) => parseArrows(c2) match {
|
||||
case (arrows, c3) => (circles ++ arrows) -> c3
|
||||
def apply(comment: String): ParsedComment =
|
||||
parseShapes(comment) match {
|
||||
case (shapes, c2) => parseClock(c2) match {
|
||||
case (clock, c3) => ParsedComment(shapes, clock, c3)
|
||||
}
|
||||
}
|
||||
|
||||
private val clkRemoveRegex = """\[\%clk[\s\r\n]+[\d:]+\]""".r
|
||||
def removeClk(comment: String) = clkRemoveRegex.replaceAllIn(comment, "").trim
|
||||
private type ClockAndComment = (Option[Centis], String)
|
||||
|
||||
private def readCentis(hours: String, minutes: String, seconds: String) = for {
|
||||
h <- parseIntOption(hours)
|
||||
m <- parseIntOption(minutes)
|
||||
s <- parseIntOption(seconds)
|
||||
} yield Centis(h * 360000 + m * 6000 + s * 100)
|
||||
|
||||
private def parseClock(comment: String): ClockAndComment = comment match {
|
||||
case clockRegex(str) => (str.split(':') match {
|
||||
case Array(minutes, seconds) => readCentis("0", minutes, seconds)
|
||||
case Array(hours, minutes, seconds) => readCentis(hours, minutes, seconds)
|
||||
case _ => none
|
||||
}) -> clockRemoveRegex.replaceAllIn(comment, "").trim
|
||||
case _ => None -> comment
|
||||
}
|
||||
|
||||
private type ShapesAndComment = (Shapes, String)
|
||||
|
||||
private def parseShapes(comment: String): ShapesAndComment =
|
||||
parseCircles(comment) match {
|
||||
case (circles, comment) => parseArrows(comment) match {
|
||||
case (arrows, comment) => (circles ++ arrows) -> comment
|
||||
}
|
||||
}
|
||||
|
||||
private def parseCircles(comment: String): ShapesAndComment = comment match {
|
||||
case circlesRegex(str) =>
|
||||
|
|
|
@ -4,6 +4,7 @@ import chess.format.pgn.{ Glyph, Glyphs }
|
|||
import chess.format.{ Uci, UciCharPair, FEN }
|
||||
import chess.variant.Crazyhouse
|
||||
|
||||
import lila.common.Centis
|
||||
import lila.tree.Node.{ Shapes, Comment, Comments }
|
||||
|
||||
sealed trait RootOrNode {
|
||||
|
@ -28,6 +29,7 @@ case class Node(
|
|||
comments: Comments = Comments(Nil),
|
||||
glyphs: Glyphs = Glyphs.empty,
|
||||
crazyData: Option[Crazyhouse.Data],
|
||||
clock: Option[Centis],
|
||||
children: Node.Children
|
||||
) extends RootOrNode {
|
||||
|
||||
|
@ -223,6 +225,7 @@ object Node {
|
|||
fen = FEN(b.fen),
|
||||
check = b.check,
|
||||
crazyData = b.crazyData,
|
||||
clock = b.clock,
|
||||
children = Children(b.children.toVector map fromBranch)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import scalaz.Validation.FlatMap._
|
|||
|
||||
import chess.format.pgn.{ Tag, Glyphs, San, Dumper }
|
||||
import chess.format.{ Forsyth, FEN, Uci, UciCharPair }
|
||||
|
||||
import lila.common.Centis
|
||||
import lila.importer.{ ImportData, Preprocessed }
|
||||
import lila.tree.Node.{ Comment, Comments, Shapes }
|
||||
|
||||
|
@ -19,8 +21,8 @@ private object PgnImport {
|
|||
ImportData(pgn, analyse = none).preprocess(user = none).map {
|
||||
case prep @ Preprocessed(game, replay, result, initialFen, parsedPgn) =>
|
||||
val annotator = parsedPgn.tag("annotator").map(Comment.Author.External.apply)
|
||||
makeShapesAndComments(parsedPgn.initialPosition.comments, annotator) match {
|
||||
case (shapes, comments) =>
|
||||
parseComments(parsedPgn.initialPosition.comments, annotator) match {
|
||||
case (shapes, _, comments) =>
|
||||
val root = Node.Root(
|
||||
ply = replay.setup.turns,
|
||||
fen = initialFen | FEN(game.variant.initialFen),
|
||||
|
@ -80,13 +82,17 @@ private object PgnImport {
|
|||
}
|
||||
}
|
||||
|
||||
private def makeShapesAndComments(comments: List[String], annotator: Option[Comment.Author]): (Shapes, Comments) =
|
||||
comments.map(CommentParser.removeClk).foldLeft(Shapes(Nil), Comments(Nil)) {
|
||||
case ((shapes, comments), txt) => CommentParser.extractShapes(txt) match {
|
||||
case (s, c) => (shapes ++ s) -> (c.trim match {
|
||||
case "" => comments
|
||||
case com => comments + Comment(Comment.Id.make, Comment.Text(com), annotator | Comment.Author.Lichess)
|
||||
})
|
||||
private def parseComments(comments: List[String], annotator: Option[Comment.Author]): (Shapes, Option[Centis], Comments) =
|
||||
comments.foldLeft(Shapes(Nil), none[Centis], Comments(Nil)) {
|
||||
case ((shapes, clock, comments), txt) => CommentParser(txt) match {
|
||||
case CommentParser.ParsedComment(s, c, str) => (
|
||||
(shapes ++ s),
|
||||
c orElse clock,
|
||||
(str.trim match {
|
||||
case "" => comments
|
||||
case com => comments + Comment(Comment.Id.make, Comment.Text(com), annotator | Comment.Author.Lichess)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,8 +105,8 @@ private object PgnImport {
|
|||
val sanStr = moveOrDrop.fold(Dumper.apply, Dumper.apply)
|
||||
makeNode(game, rest, annotator) map { mainline =>
|
||||
val variations = makeVariations(rest, game, annotator)
|
||||
makeShapesAndComments(san.metas.comments, annotator) match {
|
||||
case (shapes, comments) => Node(
|
||||
parseComments(san.metas.comments, annotator) match {
|
||||
case (shapes, clock, comments) => Node(
|
||||
id = UciCharPair(uci),
|
||||
ply = game.turns,
|
||||
move = Uci.WithSan(uci, sanStr),
|
||||
|
@ -110,6 +116,7 @@ private object PgnImport {
|
|||
comments = comments,
|
||||
glyphs = san.metas.glyphs,
|
||||
crazyData = game.situation.board.crazyData,
|
||||
clock = clock,
|
||||
children = removeDuplicatedChildrenFirstNode {
|
||||
Node.Children {
|
||||
mainline.fold(variations)(_ :: variations).toVector
|
||||
|
|
|
@ -2,71 +2,114 @@ package lila.study
|
|||
|
||||
import org.specs2.mutable._
|
||||
import org.specs2.specification._
|
||||
import lila.tree.Node.Shapes
|
||||
|
||||
import lila.common.Centis
|
||||
import lila.tree.Node.Shape._
|
||||
import lila.tree.Node.Shapes
|
||||
|
||||
class CommentParserTest extends Specification {
|
||||
|
||||
val C = CommentParser
|
||||
|
||||
"remove clk" should {
|
||||
"parse comment" should {
|
||||
"empty" in {
|
||||
C.removeClk("") must_== ""
|
||||
C("").comment must_== ""
|
||||
}
|
||||
"without" in {
|
||||
C.removeClk("Hello there") must_== "Hello there"
|
||||
C("Hello there").comment must_== "Hello there"
|
||||
}
|
||||
"at start" in {
|
||||
C.removeClk("[%clk 10:40:33] Hello there") must_== "Hello there"
|
||||
C("[%clk 10:40:33] Hello there").comment must_== "Hello there"
|
||||
}
|
||||
"at end" in {
|
||||
C.removeClk("Hello there [%clk 10:40:33]") must_== "Hello there"
|
||||
C("Hello there [%clk 10:40:33]").comment must_== "Hello there"
|
||||
}
|
||||
"multiple" in {
|
||||
C.removeClk("Hello there [%clk 10:40:33][%clk 10:40:33]") must_== "Hello there"
|
||||
C("Hello there [%clk 10:40:33][%clk 10:40:33]").comment must_== "Hello there"
|
||||
}
|
||||
"new lines" in {
|
||||
C.removeClk("Hello there [%clk\n10:40:33]") must_== "Hello there"
|
||||
C("Hello there [%clk\n10:40:33]").comment must_== "Hello there"
|
||||
}
|
||||
}
|
||||
"parse clock" should {
|
||||
"empty" in {
|
||||
C("").clock must beNone
|
||||
}
|
||||
"without" in {
|
||||
C("Hello there").clock must beNone
|
||||
}
|
||||
"only" in {
|
||||
C("[%clk 10:40:33]").clock must_== Some(Centis(3843300))
|
||||
}
|
||||
"one hour" in {
|
||||
C("[%clk 1:40:33]").clock must_== Some(Centis(603300))
|
||||
}
|
||||
"at start" in {
|
||||
C("[%clk 10:40:33] Hello there").clock must_== Some(Centis(3843300))
|
||||
}
|
||||
"at end" in {
|
||||
C("Hello there [%clk 10:40:33]").clock must_== Some(Centis(3843300))
|
||||
}
|
||||
"in the middle" in {
|
||||
C("Hello there [%clk 10:40:33] something else").clock must_== Some(Centis(3843300))
|
||||
}
|
||||
"multiple" in {
|
||||
C("Hello there [%clk 10:40:33][%clk 10:40:33]").clock must_== Some(Centis(3843300))
|
||||
}
|
||||
"new lines" in {
|
||||
C("Hello there [%clk\n10:40:33]").clock must_== Some(Centis(3843300))
|
||||
}
|
||||
"no hours" in {
|
||||
C("Hello there [%clk 40:33] something else").clock must_== Some(Centis(243300))
|
||||
}
|
||||
}
|
||||
"parse shapes" should {
|
||||
"empty" in {
|
||||
C.extractShapes("") must_== (Shapes(Nil) -> "")
|
||||
C("") must_== C.ParsedComment(Shapes(Nil), None, "")
|
||||
}
|
||||
"without" in {
|
||||
C.extractShapes("Hello there") must_== (Shapes(Nil) -> "Hello there")
|
||||
C("Hello there") must_== C.ParsedComment(Shapes(Nil), None, "Hello there")
|
||||
}
|
||||
"at start" in {
|
||||
C.extractShapes("[%csl Gb4,Yd5,Rf6] Hello there") must beLike {
|
||||
case (shapes, "Hello there") => shapes.value must haveSize(3)
|
||||
C("[%csl Gb4,Yd5,Rf6] Hello there") must beLike {
|
||||
case C.ParsedComment(shapes, None, "Hello there") => shapes.value must haveSize(3)
|
||||
}
|
||||
}
|
||||
"at end" in {
|
||||
C.extractShapes("Hello there [%csl Gb4,Yd5,Rf6]") must beLike {
|
||||
case (shapes, "Hello there") => shapes.value must haveSize(3)
|
||||
C("Hello there [%csl Gb4,Yd5,Rf6]") must beLike {
|
||||
case C.ParsedComment(shapes, None, "Hello there") => shapes.value must haveSize(3)
|
||||
}
|
||||
}
|
||||
"multiple" in {
|
||||
C.extractShapes("Hello there [%csl Gb4,Yd5,Rf6][%cal Ge2e4,Ye2d4,Re2g4]") must beLike {
|
||||
case (shapes, "Hello there") => shapes.value must haveSize(6)
|
||||
C("Hello there [%csl Gb4,Yd5,Rf6][%cal Ge2e4,Ye2d4,Re2g4]") must beLike {
|
||||
case C.ParsedComment(shapes, None, "Hello there") => shapes.value must haveSize(6)
|
||||
}
|
||||
}
|
||||
"new lines" in {
|
||||
C.extractShapes("Hello there [%csl\nGb4,Yd5,Rf6]") must beLike {
|
||||
case (shapes, "Hello there") => shapes.value must haveSize(3)
|
||||
C("Hello there [%csl\nGb4,Yd5,Rf6]") must beLike {
|
||||
case C.ParsedComment(shapes, None, "Hello there") => shapes.value must haveSize(3)
|
||||
}
|
||||
}
|
||||
"multiple, one new line" in {
|
||||
C.extractShapes("Hello there [%csl\nGb4,Yd5,Rf6][%cal Ge2e4,Ye2d4,Re2g4]") must beLike {
|
||||
case (shapes, "Hello there") => shapes.value must haveSize(6)
|
||||
C("Hello there [%csl\nGb4,Yd5,Rf6][%cal Ge2e4,Ye2d4,Re2g4]") must beLike {
|
||||
case C.ParsedComment(shapes, None, "Hello there") => shapes.value must haveSize(6)
|
||||
}
|
||||
C.extractShapes("Hello there [%csl Gb4,Yd5,Rf6][%cal\nGe2e4,Ye2d4,Re2g4]") must beLike {
|
||||
case (shapes, "Hello there") => shapes.value must haveSize(6)
|
||||
C("Hello there [%csl Gb4,Yd5,Rf6][%cal\nGe2e4,Ye2d4,Re2g4]") must beLike {
|
||||
case C.ParsedComment(shapes, None, "Hello there") => shapes.value must haveSize(6)
|
||||
}
|
||||
}
|
||||
"multiple mess" in {
|
||||
C.extractShapes("Hello there [%csl \n\n Gb4,Yd5,Rf6][%cal\nGe2e4,Ye2d4,Re2g4]") must beLike {
|
||||
case (shapes, "Hello there") => shapes.value must haveSize(6)
|
||||
C("Hello there [%csl \n\n Gb4,Yd5,Rf6][%cal\nGe2e4,Ye2d4,Re2g4]") must beLike {
|
||||
case C.ParsedComment(shapes, None, "Hello there") => shapes.value must haveSize(6)
|
||||
}
|
||||
}
|
||||
}
|
||||
"parse all" should {
|
||||
"multiple shapes + clock" in {
|
||||
C("Hello there [%clk 10:40:33][%csl \n\n Gb4,Yd5,Rf6][%cal\nGe2e4,Ye2d4,Re2g4]") must beLike {
|
||||
case C.ParsedComment(shapes, clock, "Hello there") =>
|
||||
shapes.value must haveSize(6)
|
||||
clock must_== Some(Centis(3843300))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ class PgnDumpTest extends Specification {
|
|||
move = Uci.WithSan(Uci(uci).get, san),
|
||||
fen = FEN("<fen>"),
|
||||
check = false,
|
||||
clock = None,
|
||||
crazyData = None,
|
||||
children = children
|
||||
)
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
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 play.api.libs.json._
|
||||
import lila.common.Centis
|
||||
|
||||
sealed trait Node {
|
||||
def ply: Int
|
||||
|
@ -50,6 +52,7 @@ case class Root(
|
|||
glyphs: Glyphs = Glyphs.empty,
|
||||
children: List[Branch] = Nil,
|
||||
opening: Option[FullOpening] = None,
|
||||
clocks: Option[(Centis, Centis)] = None, // initial clock states for white & black
|
||||
crazyData: Option[Crazyhouse.Data]
|
||||
) extends Node {
|
||||
|
||||
|
@ -78,6 +81,7 @@ case class Branch(
|
|||
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 {
|
||||
|
||||
|
|
|
@ -348,7 +348,7 @@ object ApplicationBuild extends Build {
|
|||
libraryDependencies ++= provided(play.api)
|
||||
)
|
||||
|
||||
lazy val tree = project("tree", Seq(chess)).settings(
|
||||
lazy val tree = project("tree", Seq(common, chess)).settings(
|
||||
libraryDependencies ++= provided(play.api)
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue