Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions taklib/src/main/scala/com/github/daenyth/taklib/Game.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"<Game ${size}x$size lastMove=[${pretty(history.head._1)}] turn=$turnNumber>"
}

private val reserveCount = rules.stoneCounts(size)
def currentBoard: Board = history.head._2
def nextPlayer: Player = rules.expectedStoneColor(turnNumber)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand All @@ -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)
}
Expand Down
31 changes: 19 additions & 12 deletions taklib/src/main/scala/com/github/daenyth/taklib/PtnParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 ~ _ =>
Expand All @@ -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.? ^^? {
Expand Down Expand Up @@ -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" ^^ { _ =>
Expand All @@ -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))
Expand Down
80 changes: 80 additions & 0 deletions taklib/src/test/resources/ptn/8x8.ptn
Original file line number Diff line number Diff line change
@@ -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
78 changes: 78 additions & 0 deletions taklib/src/test/resources/ptn/annot1.ptn
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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

}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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), _) => ()
}
}
}