study clock states - WIP - for #2851

pull/2872/head
Thibault Duplessis 2017-03-29 13:51:33 +02:00
parent 27d01eceaf
commit 45042c3512
11 changed files with 141 additions and 48 deletions

@ -1 +1 @@
Subproject commit 8cdd646452c375441313268094b7cc0e50a48d3a
Subproject commit 4d1fe47307f82e9aadc00b1b46167f5ebcf52619

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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