diff --git a/taklib/src/main/scala/com/github/daenyth/taklib/Game.scala b/taklib/src/main/scala/com/github/daenyth/taklib/Game.scala index c3625b6..6b22c7f 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/Game.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/Game.scala @@ -193,6 +193,14 @@ class Game private (val size: Int, val rules: RuleSet, val history: NonEmptyList[(GameAction, Board)]) { + override def toString = { + def pretty(ga: GameAction) = ga match { + case _: StartGameWithBoard => "{New Game}" + case t: TurnAction => s"${t.player} ${t.ptn}" + } + s"" + } + private val reserveCount = rules.stoneCounts(size) def currentBoard: Board = history.head._2 def nextPlayer: Player = rules.expectedStoneColor(turnNumber) diff --git a/taklib/src/main/scala/com/github/daenyth/taklib/Implicits.scala b/taklib/src/main/scala/com/github/daenyth/taklib/Implicits.scala index 4bd1de3..84daa8f 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/Implicits.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/Implicits.scala @@ -28,7 +28,7 @@ object Implicits { } } - def >>?[U](f: T => (String \/ Parser[U])): Parser[U] = p.flatMap { x => + def >>=?[U](f: T => (String \/ Parser[U])): Parser[U] = p.flatMap { x => f(x) match { case -\/(err) => new Parser[U] { def apply(in: Input): ParseResult[U] = Failure(err, in) } @@ -48,6 +48,8 @@ object Implicits { t } } + + /** Use like `"foo" ???> (fooParser: Parser[Foo])` */ implicit class DebugParserOps(name: String) { def ???>[T](p: Parser[T]) = new DebugParser(name, p) } diff --git a/taklib/src/main/scala/com/github/daenyth/taklib/PtnParser.scala b/taklib/src/main/scala/com/github/daenyth/taklib/PtnParser.scala index 1e0d168..9f1489b 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/PtnParser.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/PtnParser.scala @@ -57,7 +57,12 @@ object PtnParser extends RegexParsers with RichParsing { Move(player, idx, direction, count, drops) } } - val turnAction: Parser[Player => TurnAction] = moveStones | playStone + + val infoMark: Parser[String] = "'{1,2}".r | "[!?]{1,2}".r + + val turnAction: Parser[Player => TurnAction] = (moveStones | playStone) ~ infoMark.? ^^ { + case action ~ _ => action + } val headerLine: Parser[(String, String)] = "[" ~ """\S+""".r ~ "\".*\"".r ~ "]" ^^ { case _ ~ headerKey ~ headerValue ~ _ => @@ -66,15 +71,19 @@ object PtnParser extends RegexParsers with RichParsing { val headers: Parser[PtnHeaders] = rep(headerLine).map(_.toMap) - val fullTurnLine: Parser[(Int, Player => TurnAction, Player => TurnAction)] = """\d+\.""".r ~ turnAction ~ turnAction ^^ { - case turnNumber ~ whiteAction ~ blackAction => - (turnNumber.dropRight(1).toInt, whiteAction, blackAction) - } + val comment: Parser[String] = """(?s)\{(.*?)\}""".r - val lastTurnLine: Parser[(Int, Player => TurnAction, Option[Player => TurnAction])] = """\d+\.""".r ~ turnAction ~ turnAction.? ^^ { - case turnNumber ~ whiteAction ~ blackAction => - (turnNumber.dropRight(1).toInt, whiteAction, blackAction) - } + val fullTurnLine: Parser[(Int, Player => TurnAction, Player => TurnAction)] = + """\d+\.""".r ~ turnAction ~ turnAction ~ comment.? ^^ { + case turnNumber ~ whiteAction ~ blackAction ~ _comment => + (turnNumber.dropRight(1).toInt, whiteAction, blackAction) + } + + val lastTurnLine: Parser[(Int, Player => TurnAction, Option[Player => TurnAction])] = + """\d+\.""".r ~ turnAction ~ turnAction.? ~ comment.? ^^ { + case turnNumber ~ whiteAction ~ blackAction ~ _comment => + (turnNumber.dropRight(1).toInt, whiteAction, blackAction) + } def gameHistory(startingTurn: Int, skipFirst: Boolean): Parser[Vector[Player => TurnAction]] = rep(fullTurnLine) ~ lastTurnLine.? ^^? { @@ -114,8 +123,6 @@ object PtnParser extends RegexParsers with RichParsing { }.leftMap(_.getMessage) } - val infoMark: Parser[String] = "'{1,2}".r | "[!?]{1,2}".r - val roadWin: Parser[RoadWin] = "R-0" ^^ { _ => RoadWin(White) } | "0-R" ^^ { _ => @@ -136,7 +143,7 @@ object PtnParser extends RegexParsers with RichParsing { } val gameEnd: Parser[GameEndResult] = roadWin | flatWin | resignation | draw - def ptn(ruleSet: RuleSet): Parser[(PtnHeaders, MoveResult[Game])] = headers >>? { gameHeaders => + def ptn(ruleSet: RuleSet): Parser[(PtnHeaders, MoveResult[Game])] = headers >>=? { gameHeaders => gameHeaders.get("TPS") match { case None => \/-(gameHistoryFromTurn(ruleSet, gameHeaders, 1, skipFirst = false, Game.ofSize)) diff --git a/taklib/src/test/resources/ptn/8x8.ptn b/taklib/src/test/resources/ptn/8x8.ptn new file mode 100644 index 0000000..296f284 --- /dev/null +++ b/taklib/src/test/resources/ptn/8x8.ptn @@ -0,0 +1,80 @@ +[Site "PlayTak.com"] +[Event "Online Play"] +[Date "2016.07.04"] +[Time "20:27:05"] +[Player1 "Turing"] +[Player2 "maron"] +[Clock ""] +[Result "R-0"] +[Size "8"] + +1. a1 d1 +2. d2 d3 +3. Cd4 c3 +4. e3 e4 +5. e5 Ce6 +6. d5 d6 +7. f5 f6 +8. g5 g6 +9. b6 c5 +10. Cc6 Sf4 +11. c4 b4 +12. b5 b3 +13. b7 b2 +14. e2 e7 +15. b8 Sa7 +16. d7 1e7<1 +17. c7 d8 +18. 1b5>1 1e4-1 +19. 1e2+1 1d3-1 +20. 3e3<3 1c3>1 +21. 1d4-1 Cd4 +22. b5 1a7>1 +23. c2 c3 +24. Se7 e4 +25. a5 1f4+1 +26. f2 e3 +27. e2 f4 +28. g3 g4 +29. h4 h5 +30. h3 h6 +31. g2 f3 +32. 1d1+1 1f3-1 +33. d1 f3 +34. Sf1 b1 +35. 2d3<2 1b4>1 +36. 3c3+3 1d4-1 +37. c3 2d3<2 +38. 3d2+12 Sd2 +39. 3d3>12 2f2+2 +40. Sd3 4f3>4 +41. f2 3g3>3 +42. h2 1e4-1 +43. Se4 2e3-2 +44. 1h4-1 2g3>2 +45. 1h2+1 Sg3 +46. 7h3+11212 1g3>1 +47. g3 2h3+2 +48. 1e4-1 3h4+12 +49. e4 4h6-13 +50. h2 1d2<1 +51. 5c4>11111 3c3+12 +52. 4h4<112 2f5-2 +53. 1e4>1 1e6-1 +54. 3f4<3 1f3+1 +55. b4 3c5-3 +56. 5e4>5 2e5-2 +57. e5 Sf5 +58. 1c6-1 1f5<1 +59. h4 4c4>4 +60. c4 c3 +61. e6 f7 +62. 1e7<1 1d6>1 +63. g7 1e4-1 +64. f5 2b7-2 +65. 2c5>11 g1 +66. h1 1g1>1 +67. g1 4h5<13 +68. 2e5>2 2g5-2 +69. 6f4>6 2h1<2 +70. 5f5<113 R-0 \ No newline at end of file diff --git a/taklib/src/test/resources/ptn/annot1.ptn b/taklib/src/test/resources/ptn/annot1.ptn new file mode 100644 index 0000000..9ff4de9 --- /dev/null +++ b/taklib/src/test/resources/ptn/annot1.ptn @@ -0,0 +1,78 @@ +[Site "PlayTak.com"] +[Event "Online Play"] +[Date "2016.07.25"] +[Time "21:49:41"] +[Player1 "Abyss"] +[Player2 "TakticianBot"] +[Result "R-0"] +[Size "5"] + +1. a5 a1 {Adjacent Corners opening} +2. b1 c1 {I edgecrawl, confident that the bot will play c1.} +3. b2 c5 {b2 seems the most principled follow up to b1--it extends the only + white path both northwards and eastwards. Having seen alphatak_bot's preferred +variation 3. ... b5, c5 was barely a surprise.} +4. b5 b3 +5. b4 d5 +6. c3 1b3+1 {Taktician's response caught me somewhat offguard: I had expected a +capture from the fifth rank, but I quickly saw that Taktician's move was more +interesting. The point is that if I mindlessly try and extend my road 2b4+ would +be unpleasant.} +7. Cc4 Cb3 {Both our caps come in, eyeing both the other's potential roads and +the b4 square, but mine comes a tempo ahead.} +8. 1c4<1 c2 {My capture also pins black's cap.} +9. d3 e3 {We both extend our east-west threats on the side where our cap lies, i.e. +I expand in the north while Taktician expands in the south.} +10. d4 e2 +11. e4 Sc4 +12. a3 d2 {In response to Taktician's c4 wall, I decide to pin the black cap two +ways with a3.} +13. a4! Sa2?! {Often, (especially when playing against bots) walls are only temporarily +defensive, and later come back to haunt you when they start hoovering up influence +in the late game. This wall, however, has no such ambitions. Once I abandon my +north-south threat (which will occur shortly) this wall becomes a huge thorn in +black's own side. Black can scarely ever break the stronghold on a1-a2 / b1-b2; +therefore black can never again make a serious threat on the 1st and 2nd ranks.} +14. e5 1d5-1 +15. d5 1e3<1 +16. 1d5-1 2d3+2 {I made a slight miscalculation that 17. e4< would be fine for me, +but here I realized that if 17. ... c4> I cannot ever crush the wall because +2b4>11 is 0-R after b3>. With that in mind, I must instead play the only +logical move Sd3.} +17. Sd3 3d4>3? {I expected 17 ... c4>, because I didn't see any breakthroughs +for me if black surrenders c4 in exchange for d4. In fact, having the wall on +d4 seems perhaps more annoying because of the same tactic that I mentioned on +move 16.} +18. 1d3+1 4e4+4 {We dance around a bit, maneuvering control of the big +white stack.} +19. e4 d5 +20. 2d4+2 5e5-5 +21. 1b4>1 1b3>1 {I decide to give up b4 and voluntarily move into a pin in hopes of +undermining black's north-south threat and eventually playing 2c4>11.} +22. b3 2b4-2 {I gladly take the space black leaves behind: b3. Note that after +2b4-2 b3 is de facto undefended, because if black ever plays 2c3< I can respond +with 2c4>11!! and black will gain a very soft stack while I gain a very hard +stack.} +23. b4 3b3+3 +24. 1d4>1 1a5>1 +25. Sb3 Se3 {I get just a little bit nervous after 24. ... 1a5>1, so I +decide to play a wall on b3 instead of a flat. This was perhaps an overreaction, +but it also more firmly challenges b4, which it will eventually win, so I don't +know which move was better.} +26. 5e4<5 1e3<1?? {26. ... 1e3<1 is perhaps the losing move. If Taktician wanted +a wall on d3, why not 26 ... Sd3 and keep the e file closed? As we shall see, +my e4 stack will be instrumental in dismantling black's threats once and for all.} +27. d1 1d3+1 +28. 1d1<1 d1 +29. 1b2>1 2c3-2 +30. e1 3c2-3 {With e1 I lay my penultimate flat and gain the power to end the game +whenever I wish. Black must now play with only the flat count in mind.} +31. 2e4-11! 4c1<4 {Black's response is forced.} +32. 1b3+1 1d2>1 +33. 4b4-13 2e2+2 +34. 1b4+1 1c5<1 {My cap is finally unpinned.} +35. 1c4>1 c3 +36. 5d4<14 1c3+1 +37. 1d4<1 d2 {As I will win next move on flats, Taktician doesn't bother blocking +the road threat.} +38. e4 {R-0} R-0 diff --git a/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala b/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala index 59363fb..b049c56 100644 --- a/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala +++ b/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala @@ -221,7 +221,14 @@ class GameTest case (action, n) if n % 2 == 0 => action(White) case (action, _) => action(Black) } - val game = actions.foldLeftM(Game.ofSize(6).value) { (game, action) => game.takeTurn(action) } + val game = actions.foldLeftM[MoveResult, Game](Game.ofSize(6).value) { (game, action) => game.takeTurn(action) } game should matchPattern { case GameOver(FlatWin(White), _) => () } } } + +class GamePtnTest extends FlatSpec with Matchers with DisjunctionValues { + + def roundTripPtn(g: Game): MoveResult[Game] = + Game.fromPtn(g.toPtn).value + +} diff --git a/taklib/src/test/scala/com/github/daenyth/taklib/PtnParserTest.scala b/taklib/src/test/scala/com/github/daenyth/taklib/PtnParserTest.scala index dfb0fd4..425bd0a 100644 --- a/taklib/src/test/scala/com/github/daenyth/taklib/PtnParserTest.scala +++ b/taklib/src/test/scala/com/github/daenyth/taklib/PtnParserTest.scala @@ -1,14 +1,23 @@ package com.github.daenyth.taklib +import com.github.daenyth.taklib.PtnParser.PtnHeaders import org.scalatest.{FlatSpec, Matchers} import org.typelevel.scalatest.DisjunctionValues import scala.io.Source +import scalaz.\/ -class PtnParserTest extends FlatSpec with Matchers with DisjunctionValues with MoveResultValues { - def readPtn(name: String) = Source.fromResource(s"ptn/$name.ptn").getLines.mkString("\n") +object PtnParserTest { + def readPtn(ptnFileName: String): String = + Source.fromResource(s"ptn/$ptnFileName.ptn").getLines.mkString("\n") + + def parsePtn(ptn: String): (String \/ (PtnHeaders, MoveResult[Game])) = + PtnParser.parseEither(PtnParser.ptn(DefaultRules), ptn) - def parsePtn(ptn: String) = PtnParser.parseEither(PtnParser.ptn(DefaultRules), ptn) +} + +class PtnParserTest extends FlatSpec with Matchers with DisjunctionValues with MoveResultValues { + import PtnParserTest._ "BoardIndex names" should "round trip ptn parsing" in { val size = 5 @@ -36,4 +45,20 @@ class PtnParserTest extends FlatSpec with Matchers with DisjunctionValues with M gameFromTps.turnNumber shouldEqual gameFromPtn.turnNumber gameFromTps.currentBoard shouldEqual gameFromPtn.currentBoard } + + "An 8x8 game" should "work" in { + val ptn = readPtn("8x8") + val (_, result: MoveResult[Game]) = parsePtn(ptn).value + result should matchPattern { + case GameOver(RoadWin(White), _) => () + } + } + + "A game with annotation comments" should "parse correctly" in { + val ptn = readPtn("annot1") + val (_, result) = parsePtn(ptn).value + result should matchPattern { + case GameOver(RoadWin(White), _) => () + } + } }