From 26462ecab7b80d5e75c9a558a2c16b97d8917da6 Mon Sep 17 00:00:00 2001 From: Gavin Bisesi Date: Thu, 20 Apr 2017 21:24:58 -0400 Subject: [PATCH] Remove TurnAction.player Close #9 --- .../com/github/daenyth/takcli/Main.scala | 8 ++-- .../com/github/daenyth/taklib/Board.scala | 16 ++++---- .../com/github/daenyth/taklib/Game.scala | 19 ++++------ .../com/github/daenyth/taklib/Move.scala | 29 +++++++------- .../com/github/daenyth/taklib/PtnParser.scala | 38 ++++++------------- .../com/github/daenyth/taklib/GameTest.scala | 34 +++++++---------- .../com/github/daenyth/taklib/MoveTest.scala | 35 ++++++++--------- .../github/daenyth/tpsserver/TpsServer.scala | 3 +- 8 files changed, 75 insertions(+), 107 deletions(-) diff --git a/takcli/src/main/scala/com/github/daenyth/takcli/Main.scala b/takcli/src/main/scala/com/github/daenyth/takcli/Main.scala index d9f1978..74f7878 100644 --- a/takcli/src/main/scala/com/github/daenyth/takcli/Main.scala +++ b/takcli/src/main/scala/com/github/daenyth/takcli/Main.scala @@ -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 { @@ -70,7 +68,7 @@ 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 @@ -78,7 +76,7 @@ object Main { .parseEither(PtnParser.turnAction, input) .fold( err => Task.fail(PtnParseError(err)), - toAction => Task.now(toAction) + Task.now ) } .handleWith { diff --git a/taklib/src/main/scala/com/github/daenyth/taklib/Board.scala b/taklib/src/main/scala/com/github/daenyth/taklib/Board.scala index d4f156c..b5ac08c 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/Board.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/Board.scala @@ -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 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 6b22c7f..2a7b4f9 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/Game.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/Game.scala @@ -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}" ) ) } @@ -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( @@ -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"" } @@ -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 { diff --git a/taklib/src/main/scala/com/github/daenyth/taklib/Move.scala b/taklib/src/main/scala/com/github/daenyth/taklib/Move.scala index 1875004..9fbad13 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/Move.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/Move.scala @@ -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 { @@ -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]]) 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 9f1489b..ed2000e 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/PtnParser.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/PtnParser.scala @@ -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 @@ -45,7 +31,7 @@ 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.?) ^^ { @@ -53,14 +39,13 @@ object PtnParser extends RegexParsers with RichParsing { (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 } @@ -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() @@ -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") 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 b049c56..e245770 100644 --- a/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala +++ b/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala @@ -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) @@ -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) @@ -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[_]] } @@ -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 @@ -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", @@ -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), _) => () } } diff --git a/taklib/src/test/scala/com/github/daenyth/taklib/MoveTest.scala b/taklib/src/test/scala/com/github/daenyth/taklib/MoveTest.scala index fd0b083..8e5d01a 100644 --- a/taklib/src/test/scala/com/github/daenyth/taklib/MoveTest.scala +++ b/taklib/src/test/scala/com/github/daenyth/taklib/MoveTest.scala @@ -8,15 +8,12 @@ class MoveTest extends FlatSpec with Matchers with MoveResultValues { val board = Board.ofSize(5) val idx = BoardIndex(1, 1) val neighbor = idx.neighbor(Right) - val finalBoard = board.applyActions( - PlayFlat(White, idx), - PlayFlat(Black, neighbor), - Move(White, idx, Right, None, None) - ) val result = for { - b <- finalBoard - a1 <- b.stackAt(idx) - a2 <- b.stackAt(neighbor) + a <- board.applyAction(White, PlayFlat(idx)) + b <- a.applyAction(Black, PlayFlat(neighbor)) + finalBoard <- b.applyAction(White, Move(idx, Right, None, None)) + a1 <- finalBoard.stackAt(idx) + a2 <- finalBoard.stackAt(neighbor) } yield (a1, a2) val (a1, a2) = result.value a1 shouldBe Stack.empty @@ -27,8 +24,8 @@ class MoveTest extends FlatSpec with Matchers with MoveResultValues { val board = Board.ofSize(5) val idx = BoardIndex(1, 1) val result = board.applyActions( - PlayFlat(White, idx), - Move(White, idx, Left, Some(1), Some(Vector(1))) + White -> PlayFlat(idx), + White -> Move(idx, Left, Some(1), Some(Vector(1))) ) result shouldBe an[InvalidMove] } @@ -38,10 +35,10 @@ class MoveTest extends FlatSpec with Matchers with MoveResultValues { val j = i.neighbor(Right) val board = Board .ofSize(5) - .applyActions(PlayStanding(White, i), PlayCapstone(Black, j)) + .applyActions(White -> PlayStanding(i), Black -> PlayCapstone(j)) .value val result = for { - afterMove <- board.applyAction(Move(Black, j, Left, None, None)) + afterMove <- board.applyAction(Black, Move(j, Left, None, None)) stack <- afterMove.stackAt(i) } yield stack result.value shouldEqual Stack(Vector(FlatStone(White), Capstone(Black))) @@ -54,19 +51,19 @@ class MoveTest extends FlatSpec with Matchers with MoveResultValues { val board = Board .ofSize(5) .applyActions( - PlayStanding(White, i), - PlayFlat(White, j), - PlayCapstone(Black, k), - Move(Black, k, Left, None, None) + White -> PlayStanding(i), + White -> PlayFlat(j), + Black -> PlayCapstone(k), + Black -> Move(k, Left, None, None) ) .value - val result = board.applyAction(Move(Black, j, Left, None, None)) + val result = board.applyAction(Black, Move(j, Left, None, None)) result shouldBe an[InvalidMove] } "An existing stack" should "prevent a new stack from being played at that space" in { val idx = BoardIndex(1, 1) - val board = Board.ofSize(5).applyAction(PlayFlat(White, idx)).value - board.applyAction(PlayFlat(White, idx)) shouldBe an[InvalidMove] + val board = Board.ofSize(5).applyAction(White, PlayFlat(idx)).value + board.applyAction(White, PlayFlat(idx)) shouldBe an[InvalidMove] } } diff --git a/tpsserver/src/main/scala/com/github/daenyth/tpsserver/TpsServer.scala b/tpsserver/src/main/scala/com/github/daenyth/tpsserver/TpsServer.scala index 52a17a9..b3019e0 100644 --- a/tpsserver/src/main/scala/com/github/daenyth/tpsserver/TpsServer.scala +++ b/tpsserver/src/main/scala/com/github/daenyth/tpsserver/TpsServer.scala @@ -23,8 +23,7 @@ object TpsServer { .leftMap(err => s"Move: $err") .validationNel (gameE |@| moveE) apply { - case (game, toAction) => - game.takeTurn(toAction(game.nextPlayer)) + case (game, action) => game.takeTurn(action) } }