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: 3 additions & 5 deletions takcli/src/main/scala/com/github/daenyth/takcli/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ object Main {
}

def runGameTurn(g: Game): Task[MoveResult[Game]] =
printGame(g) *> runAction(g.nextPlayer).map(g.takeTurn)

def runAction(nextPlayer: Player): Task[TurnAction] = promptAction.map(_(nextPlayer))
printGame(g) *> promptAction.map(g.takeTurn)

def printGame(g: Game) = Task {
val nextPlayInfo = g.turnNumber match {
Expand Down Expand Up @@ -70,15 +68,15 @@ object Main {
g.currentBoard.boardPositions.map(_.reverse).transpose.map(_.map(_.toTps).mkString("\t")).mkString("\n")
}

def promptAction: Task[Player => TurnAction] =
def promptAction: Task[TurnAction] =
Task(StdIn.readLine("Your Move?\n > "))
.flatMap { input =>
if (input == null) { throw CleanExit } else
PtnParser
.parseEither(PtnParser.turnAction, input)
.fold(
err => Task.fail(PtnParseError(err)),
toAction => Task.now(toAction)
Task.now
)
}
.handleWith {
Expand Down
16 changes: 9 additions & 7 deletions taklib/src/main/scala/com/github/daenyth/taklib/Board.scala
Original file line number Diff line number Diff line change
Expand Up @@ -62,35 +62,37 @@ case class Board(size: Int, boardPositions: BoardLayout) {
BoardIndex(rank + 1, file + 1) -> boardPositions(rank)(file)
}.toVector

def applyAction(action: TurnAction): MoveResult[Board] = action match {
def applyAction(actingPlayer: Player, action: TurnAction): MoveResult[Board] = action match {
case PlayStone(at, stone) =>
stackAt(at).flatMap {
case s if s.nonEmpty => InvalidMove(s"A stack already exists at ${at.name}")
case _ =>
val stack = Stack.of(stone)
val stack = Stack.of(stone(actingPlayer))
val newPositions = setStackAt(boardPositions, at, stack)
OkMove(Board(size, newPositions))
}
case m: Move => doMoveAction(m)
}

def applyActions(actions: Seq[TurnAction]): MoveResult[Board] =
def applyActions(actions: Seq[(Player, TurnAction)]): MoveResult[Board] =
actions.headOption.fold[MoveResult[Board]](InvalidMove("Tried to apply an empty seq of actions")) {
a => applyActions(a, actions.tail: _*)
}

@tailrec
final def applyActions(a: TurnAction, as: TurnAction*): MoveResult[Board] =
final def applyActions(pa: (Player, TurnAction), pas: (Player, TurnAction)*): MoveResult[Board] = {
val (actingPlayer, a) = pa
// Explicit match instead of map/flatmap to appease @tailrec
applyAction(a) match {
applyAction(actingPlayer, a) match {
case i: InvalidMove => i
case o: GameOver => o
case s @ OkMove(newState) =>
as.toList match {
case s@OkMove(newState) =>
pas.toList match {
case Nil => s
case nextMove :: moreMoves => newState.applyActions(nextMove, moreMoves: _*)
}
}
}

private[taklib] def doMoveAction(m: Move): MoveResult[Board] = {
@tailrec
Expand Down
19 changes: 8 additions & 11 deletions taklib/src/main/scala/com/github/daenyth/taklib/Game.scala
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,11 @@ object DefaultRules extends RuleSet {
case None =>
Option(InvalidMove(s"Cannot move empty Stack at ${m.from}"))
case Some(controller) =>
(controller === action.player)
val player = game.nextPlayer
(controller === player)
.orElse(
InvalidMove(
s"${action.player} cannot move stack controlled by $controller at ${m.from}"
s"$player cannot move stack controlled by $controller at ${m.from}"
)
)
}
Expand All @@ -124,15 +125,9 @@ object DefaultRules extends RuleSet {
}
}

val playerOwnsStone: GameRule = { (game, action) =>
(action.player == game.nextPlayer)
.orElse(InvalidMove(s"${action.player} is not the correct color for this turn"))
}

override val rules: List[GameRule] = List(
actionIndexIsValid,
actingPlayerControlsStack,
playerOwnsStone
actingPlayerControlsStack
)

override val stoneCounts: Map[Int, (Int, Int)] = Map(
Expand Down Expand Up @@ -196,7 +191,9 @@ class Game private (val size: Int,
override def toString = {
def pretty(ga: GameAction) = ga match {
case _: StartGameWithBoard => "{New Game}"
case t: TurnAction => s"${t.player} ${t.ptn}"
case t: TurnAction =>
val lastPlayer = nextPlayer.fold(White, Black) // Take the opposite player
s"$lastPlayer ${t.ptn}"
}
s"<Game ${size}x$size lastMove=[${pretty(history.head._1)}] turn=$turnNumber>"
}
Expand All @@ -207,7 +204,7 @@ class Game private (val size: Int,

def takeTurn(action: TurnAction): MoveResult[Game] =
rules.check(this, action).getOrElse {
currentBoard.applyAction(action).flatMap { nextState =>
currentBoard.applyAction(nextPlayer, action).flatMap { nextState =>
val newHistory = (action, nextState) <:: history
val game = new Game(size, turnNumber + 1, rules, newHistory)
game.winner match {
Expand Down
29 changes: 13 additions & 16 deletions taklib/src/main/scala/com/github/daenyth/taklib/Move.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@ import scalaz.Monad
sealed trait GameAction
case class StartGameWithBoard(board: Board) extends GameAction
sealed trait TurnAction extends GameAction {
def player: Player // TODO maybe get rid of this - makes ptn parsing hard

def ptn: String = this match {
case PlayFlat(_, at) => at.name
case PlayStanding(_, at) => s"S${at.name}"
case PlayCapstone(_, at) => s"C${at.name}"
case Move(_, from, direction, count, drops) =>
case PlayFlat(at) => at.name
case PlayStanding(at) => s"S${at.name}"
case PlayCapstone(at) => s"C${at.name}"
case Move(from, direction, count, drops) =>
// Omit count+drops if moving whole stack or one piece
val num =
drops match {
Expand All @@ -29,24 +27,23 @@ sealed trait TurnAction extends GameAction {
}
}
object PlayStone {
def unapply(p: PlayStone): Option[(BoardIndex, Stone)] =
def unapply(p: PlayStone): Option[(BoardIndex, Player => Stone)] =
Some((p.at, p.stone))
}
sealed trait PlayStone extends TurnAction {
def at: BoardIndex
def stone: Stone
def stone: Player => Stone
}
case class PlayFlat(player: Player, at: BoardIndex) extends PlayStone {
val stone = FlatStone(player)
case class PlayFlat(at: BoardIndex) extends PlayStone {
val stone = FlatStone.apply _
}
case class PlayStanding(player: Player, at: BoardIndex) extends PlayStone {
val stone = StandingStone(player)
case class PlayStanding(at: BoardIndex) extends PlayStone {
val stone = StandingStone.apply _
}
case class PlayCapstone(player: Player, at: BoardIndex) extends PlayStone {
val stone = Capstone(player)
case class PlayCapstone(at: BoardIndex) extends PlayStone {
val stone = Capstone.apply _
}
case class Move(player: Player,
from: BoardIndex,
case class Move(from: BoardIndex,
direction: MoveDirection,
count: Option[Int],
drops: Option[Vector[Int]])
Expand Down
38 changes: 11 additions & 27 deletions taklib/src/main/scala/com/github/daenyth/taklib/PtnParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,10 @@ object PtnParser extends RegexParsers with RichParsing {
val file = str.charAt(1).toString.toInt
BoardIndex(rank, file)
}
val playFlat: Parser[Player => PlayFlat] =
boardIndex ^^ { idx => player =>
PlayFlat(player, idx)
}
val playStanding: Parser[Player => PlayStanding] =
"S".r ~ boardIndex ^^ {
case (_ ~ idx) =>
player =>
PlayStanding(player, idx)
}
val playCapstone: Parser[Player => PlayCapstone] =
"C".r ~ boardIndex ^^ {
case (_ ~ idx) =>
player =>
PlayCapstone(player, idx)
}
val playStone: Parser[Player => PlayStone] =
playFlat | playStanding | playCapstone
val playFlat: Parser[PlayFlat] = boardIndex ^^ { idx => PlayFlat(idx) }
val playStanding: Parser[PlayStanding] = "S".r ~ boardIndex ^^ { case _ ~ idx => PlayStanding(idx) }
val playCapstone: Parser[PlayCapstone] = "C".r ~ boardIndex ^^ { case _ ~ idx => PlayCapstone(idx) }
val playStone: Parser[PlayStone] = playFlat | playStanding | playCapstone

val moveDirection: Parser[MoveDirection] = "[-+<>]".r ^^ {
case "-" => Down
Expand All @@ -45,22 +31,21 @@ object PtnParser extends RegexParsers with RichParsing {
case ">" => Right
}

val moveStones: Parser[Player => Move] = {
val moveStones: Parser[Move] = {
val count = "[12345678]".r ^^ { _.toInt }
val drops = "[12345678]+".r ^^ { _.toVector.map(_.toString.toInt) }
(count.? ~ boardIndex ~ moveDirection ~ drops.?) ^^ {
case (count: Option[Int]) ~
(idx: BoardIndex) ~
(direction: MoveDirection) ~
(drops: Option[Vector[Int]]) =>
player =>
Move(player, idx, direction, count, drops)
Move(idx, direction, count, drops)
}
}

val infoMark: Parser[String] = "'{1,2}".r | "[!?]{1,2}".r

val turnAction: Parser[Player => TurnAction] = (moveStones | playStone) ~ infoMark.? ^^ {
val turnAction: Parser[TurnAction] = (moveStones | playStone) ~ infoMark.? ^^ {
case action ~ _ => action
}

Expand All @@ -73,24 +58,24 @@ object PtnParser extends RegexParsers with RichParsing {

val comment: Parser[String] = """(?s)\{(.*?)\}""".r

val fullTurnLine: Parser[(Int, Player => TurnAction, Player => TurnAction)] =
val fullTurnLine: Parser[(Int, TurnAction, 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])] =
val lastTurnLine: Parser[(Int, TurnAction, Option[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]] =
def gameHistory(startingTurn: Int, skipFirst: Boolean): Parser[Vector[TurnAction]] =
rep(fullTurnLine) ~ lastTurnLine.? ^^? {
case fullturns ~ lastTurn =>
var nextTurnNumber = startingTurn
val iter = fullturns.iterator
val history = new VectorBuilder[Player => TurnAction]
val history = new VectorBuilder[TurnAction]
\/.fromTryCatchNonFatal {
while (iter.hasNext) {
val (turnNumber, whiteAction, blackAction) = iter.next()
Expand Down Expand Up @@ -185,7 +170,6 @@ object PtnParser extends RegexParsers with RichParsing {
initialGame <- getInitialGame(size, ruleSet)
} yield {
val finalGame = history.zipWithIndex
.map { case (a, i) => (a(ruleSet.expectedStoneColor(i + 1)), i) }
.foldLeftM[MoveResult, Game](initialGame) {
case (game, (action, actionIdx)) =>
game.takeTurn(action).noteInvalid(r => s"(Move #${actionIdx + 1}) $r")
Expand Down
34 changes: 14 additions & 20 deletions taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ class GameTest

"A board with 5 stones in a row" should "have a road win" in {
val board = Board.ofSize(5)
val roadBoard = board.applyActions((1 to 5).map(n => PlayFlat(White, BoardIndex(1, n))))
val roadBoard = board.applyActions((1 to 5).map(n => White -> PlayFlat(BoardIndex(1, n))))
val game = Game.fromBoard(roadBoard.value)
game.winner.value shouldBe RoadWin(White)
}

"Four flats and a capstone" should "have a road win" in {
val board = Board.ofSize(5)
val moves = (1 to 4).map(n => PlayFlat(Black, BoardIndex(1, n))) ++ Vector(
PlayCapstone(Black, BoardIndex(1, 5))
val moves = (1 to 4).map(n => Black -> PlayFlat(BoardIndex(1, n))) ++ Vector(
Black -> PlayCapstone(BoardIndex(1, 5))
)
val roadBoard = board.applyActions(moves)
val game = Game.fromBoard(roadBoard.value)
Expand All @@ -69,8 +69,8 @@ class GameTest

"Four flats and a standing stone" should "not be a win" in {
val board = Board.ofSize(5)
val moves = (1 to 4).map(n => PlayFlat(Black, BoardIndex(1, n))) ++ Vector(
PlayStanding(Black, BoardIndex(1, 5))
val moves = (1 to 4).map(n => Black -> PlayFlat(BoardIndex(1, n))) ++ Vector(
Black -> PlayStanding(BoardIndex(1, 5))
)
val roadBoard = board.applyActions(moves)
val game = Game.fromBoard(roadBoard.value)
Expand All @@ -79,21 +79,21 @@ class GameTest

"A player" should "be able to move a stack they control" in {
val i = BoardIndex(1, 1)
val board = Board.ofSize(5).applyAction(PlayFlat(White, i)).value
val board = Board.ofSize(5).applyAction(White, PlayFlat(i)).value
val game = Game.fromBoard(board)
DefaultRules.actingPlayerControlsStack(game, Move(White, i, Right, None, None)) shouldBe None
DefaultRules.actingPlayerControlsStack(game, Move(i, Right, None, None)) shouldBe None
}

"A player" should "not be able to move a stack they don't control" in {
val i = BoardIndex(1, 1)
val board = Board.ofSize(5).applyAction(PlayFlat(Black, i)).value
val board = Board.ofSize(5).applyAction(Black, PlayFlat(i)).value
val game = Game.fromBoard(board)
DefaultRules.actingPlayerControlsStack(game, Move(White, i, Right, None, None)) shouldBe 'nonEmpty
DefaultRules.actingPlayerControlsStack(game, Move(i, Right, None, None)) shouldBe 'nonEmpty
}

"The first move" should "be taken with a black flatstone" in {
val game = Game.ofSize(5).value
val result = game.takeTurn(PlayFlat(Black, BoardIndex(1, 1)))
val result = game.takeTurn(PlayFlat(BoardIndex(1, 1)))
result shouldBe an[OkMove[_]]
}

Expand All @@ -105,8 +105,8 @@ class GameTest

"A game's tps" should "round trip to the same game" in {
val game1 = (for {
a <- Game.ofSize(5).value.takeTurn(PlayFlat(Black, BoardIndex(1, 1)))
b <- a.takeTurn(PlayFlat(White, BoardIndex(5, 1)))
a <- Game.ofSize(5).value.takeTurn(PlayFlat(BoardIndex(1, 1)))
b <- a.takeTurn(PlayFlat(BoardIndex(5, 1)))
} yield b).value
val tps = game1.toTps
val game2 = Game.fromTps(tps).value
Expand All @@ -117,7 +117,7 @@ class GameTest

"A long game" should "be playable without a problem" in {
// https://www.playtak.com/games/153358/view
val maybeMoves: Vector[String \/ (Player => TurnAction)] = Vector(
val maybeMoves: Vector[String \/ TurnAction] = Vector(
"a6",
"a1",
"c3",
Expand Down Expand Up @@ -214,13 +214,7 @@ class GameTest
"5c1>14",
"e3"
).map { PtnParser.parseEither(PtnParser.turnAction, _) }
val toActions: Vector[Player => TurnAction] = maybeMoves.sequenceU.value
val actions = toActions.zipWithIndex.map {
case (action, 0) => action(Black)
case (action, 1) => action(White)
case (action, n) if n % 2 == 0 => action(White)
case (action, _) => action(Black)
}
val actions: Vector[TurnAction] = maybeMoves.sequenceU.value
val game = actions.foldLeftM[MoveResult, Game](Game.ofSize(6).value) { (game, action) => game.takeTurn(action) }
game should matchPattern { case GameOver(FlatWin(White), _) => () }
}
Expand Down
Loading