diff --git a/src/libmttt.nim b/src/libmttt.nim index b73331d..aba04c2 100644 --- a/src/libmttt.nim +++ b/src/libmttt.nim @@ -1,25 +1,25 @@ -import sets, strformat, options, sequtils +import sets, options, sequtils type - ## A game board is a simple 2 dimensional array. + ## A game board is a 2 dimensional array. ## Markers of any type can be placed inside the cells Board*[T] = array[3, array[3, T]] - ## A Position in a board + ## A Position on a board Coordinate* = tuple[x: int, y:int] Player* = ref object mark*: Mark name*: string - Mark* = enum - mPlayer1, ## The mark of the fist player - mPlayer2, ## The mark of the second player - mFree, ## This mark signals that a cell is empty - mDraw ## Special mark to indicate the game has ended in a draw + Mark* {.pure.} = enum + Player1, ## The mark of the fist player + Player2, ## The mark of the second player + Free, ## This mark signals that a cell is empty + Draw ## Special mark to indicate the game has ended in a draw GameState* = ref object - ## Contains all state for a pint in the game + ## Contains all state for a game board*: Board[Board[Mark]] players*: array[2, Player] currentPlayer*: Player @@ -28,20 +28,11 @@ type turn*: int IllegalMoveError* = object of Exception + IIlegalStateError* = object of Exception -proc `$`*(player: Player): string = - $player.name - -proc `$`*(player: ref Player): string = - $player.name - -proc `$`*(game: GameState): string = - &""" - Game is in turn {$game.turn} - Players are: '{$game.players[0]}' and '{$game.players[1]}' - Current player: '{$game.currentPlayer}' - Game state is: {$game.result} - """ +################################################### +# Constructors +################################################### proc newBoard[T](initial: T): Board[T] = ## Create a new game board filled with the initial value @@ -51,23 +42,15 @@ proc newBoard[T](initial: T): Board[T] = [initial, initial, initial] ] -proc newMetaBoard[T](val: T): Board[Board[T]] = - ## Create the meta board, composed out of 9 normal boards. - [ - [newBoard(val), newBoard(val), newBoard(val)], - [newBoard(val), newBoard(val), newBoard(val)], - [newBoard(val), newBoard(val), newBoard(val)] - ] - -proc newGame*(player1: var Player, player2: var Player): GameState = - ## Create a new game board - player1.mark = mPlayer1 - player2.mark = mPlayer2 +proc newGameState*(player1: var Player, player2: var Player): GameState = + ## Initializes a new game state with the passed players + player1.mark = Mark.Player1 + player2.mark = Mark.Player2 GameState( players: [player1, player2], currentPlayer: player1, - result: mFree, - board: newMetaBoard(mFree)) + result: Mark.Free, + board: newBoard(newBoard(Mark.Free))) ################################################### # Board Checking @@ -75,28 +58,29 @@ proc newGame*(player1: var Player, player2: var Player): GameState = proc selectResult(states: HashSet[Mark]): Mark = ## Analyse a set of results and select the correct one - if states.contains(mPlayer1) and - states.contains(mPlayer2): - raise newException(Exception, "Both players cannot win at the same time") + if states.contains(Mark.Player1) and + states.contains(Mark.Player2): + raise newException(IIlegalStateError, "Both players cannot win at the same time") - if states.contains(mPlayer1): - return mPlayer1 - if states.contains(mPlayer2): - return mPlayer2 - if states.contains(mFree): - return mFree - return mDraw + if states.contains(Mark.Player1): + return Mark.Player1 + if states.contains(Mark.Player2): + return Mark.Player2 + if states.contains(Mark.Free): + return Mark.Free + return Mark.Draw proc checkRow(row: array[3, Mark]): Mark = + ## Evaluates a single row of a board var tokens = toHashSet(row) # A player has won - if tokens.len == 1 and not tokens.contains(mFree): + if tokens.len == 1 and not tokens.contains(Mark.Free): return tokens.pop() # The row is full - if tokens.len == 2 and not tokens.contains(mFree): - return mDraw + if tokens.len == 2 and not tokens.contains(Mark.Free): + return Mark.Draw # There are still cells free in this row - return mFree + return Mark.Free proc checkRows(board: Board[Mark]): Mark = ## Iterate over all rows of this board and return the result. @@ -109,13 +93,13 @@ proc checkRows(board: Board[Mark]): Mark = proc checkDiagonals(board: Board[Mark]): Mark = var states: HashSet[Mark] states.init() - var topToBottom: array[3, Mark] - var bottomToTop: array[3, Mark] + var criss: array[3, Mark] # Top left to bottom right cell + var cross: array[3, Mark] # Bottom left to top right cell for x in 0 .. 2: - topToBottom[x] = board[x][x] - bottomToTop[x] = board[2-x][x] - states.incl(checkRow(topToBottom)) - states.incl(checkRow(bottomToTop)) + criss[x] = board[x][x] + cross[x] = board[2-x][x] + states.incl(checkRow(criss)) + states.incl(checkRow(cross)) return states.selectResult() proc checkBoard*(board: Board[Mark]): Mark = @@ -123,7 +107,7 @@ proc checkBoard*(board: Board[Mark]): Mark = # Create a seconded, transposed board. # This way 'checkRows' can be used to check the columns - var transposed = newBoard(mFree); + var transposed = newBoard(Mark.Free); for x in 0 .. 2: for y in 0 .. 2: transposed[x][y] = board[y][x] @@ -140,7 +124,7 @@ proc checkBoard*(board: Board[Board[Mark]]): Mark = ## Perform a check on a metaboard to see the overall game result # This temporary board will hold the intermediate results from each sub board - var subResults = newBoard(mFree) + var subResults = newBoard(Mark.Free) var states: HashSet[Mark] states.init() for x in 0 .. 2: @@ -161,7 +145,7 @@ proc makeMove*(state: GameState, cell: Coordinate): GameState = let board = state.currentBoard.get() var currBoard = state.board[board.x][board.y] - if currBoard[cell.x][cell.y] != mFree: + if currBoard[cell.x][cell.y] != Mark.Free: raise newException(IllegalMoveError, "Chosen cell is not free") state.board[board.x][board.y][cell.x][cell.y] = state.currentPlayer.mark @@ -169,11 +153,11 @@ proc makeMove*(state: GameState, cell: Coordinate): GameState = state.result = checkBoard(state.board) # Exit early. The game has ended. - if state.result != mFree: + if state.result != Mark.Free: return state let nextBoard = checkBoard(state.board[cell.x][cell.y]) - if nextBoard == mFree: + if nextBoard == Mark.Free: state.currentBoard = cell.some() else: state.currentBoard = none(Coordinate) @@ -184,7 +168,7 @@ proc makeMove*(state: GameState, cell: Coordinate): GameState = return state proc makeMove*(state: GameState, cell: Coordinate, boardChoice: Coordinate): GameState = - if checkBoard(state.board[boardChoice.x][boardChoice.y]) != mFree: + if checkBoard(state.board[boardChoice.x][boardChoice.y]) != Mark.Free: raise newException(IllegalMoveError, "Player must choose an open board to play in") if state.currentBoard.isSome: raise newException(IllegalMoveError, "Player does not have free choice for board") diff --git a/tests/testChecks.nim b/tests/testChecks.nim index 4b5539f..5c12f79 100644 --- a/tests/testChecks.nim +++ b/tests/testChecks.nim @@ -14,146 +14,146 @@ suite "Test the board result checker": var player1 = Player(name: "Adam") player2 = Player(name: "Eve") - game: GameState = newGame(player1, player2) + game: GameState = newGameState(player1, player2) test "winning row": game.board[0][0] = [ - [mFree, mFree, mFree], - [mPlayer1, mPlayer1, mPlayer1], - [mFree, mFree, mFree] + [Mark.Free, Mark.Free, Mark.Free], + [Mark.Player1, Mark.Player1, Mark.Player1], + [Mark.Free, Mark.Free, Mark.Free] ] - check checkBoard(game.board[0][0]) == mPlayer1 + check checkBoard(game.board[0][0]) == Mark.Player1 test "winning column": game.board[0][0] = [ - [mPlayer2, mFree, mPlayer1], - [mPlayer2, mPlayer1, mFree], - [mPlayer2, mPlayer1, mFree] + [Mark.Player2, Mark.Free, Mark.Player1], + [Mark.Player2, Mark.Player1, Mark.Free], + [Mark.Player2, Mark.Player1, Mark.Free] ] - check checkBoard(game.board[0][0]) == mPlayer2 + check checkBoard(game.board[0][0]) == Mark.Player2 test "winning diagonals": game.board[0][0] = [ - [mFree, mFree, mPlayer2], - [mFree, mPlayer2, mFree], - [mPlayer2, mFree, mFree] + [Mark.Free, Mark.Free, Mark.Player2], + [Mark.Free, Mark.Player2, Mark.Free], + [Mark.Player2, Mark.Free, Mark.Free] ] - check(checkBoard(game.board[0][0]) == mPlayer2) + check(checkBoard(game.board[0][0]) == Mark.Player2) game.board[0][0] = [ - [mPlayer1, mPlayer2, mPlayer2], - [mPlayer2, mPlayer1, mPlayer1], - [mPlayer2, mFree, mPlayer1] + [Mark.Player1, Mark.Player2, Mark.Player2], + [Mark.Player2, Mark.Player1, Mark.Player1], + [Mark.Player2, Mark.Free, Mark.Player1] ] - check(checkBoard(game.board[0][0]) == mPlayer1) + check(checkBoard(game.board[0][0]) == Mark.Player1) test "board is a draw": game.board[0][0] = [ - [mPlayer1, mPlayer2, mPlayer1], - [mPlayer2, mPlayer1, mPlayer1], - [mPlayer2, mPlayer1, mPlayer2] + [Mark.Player1, Mark.Player2, Mark.Player1], + [Mark.Player2, Mark.Player1, Mark.Player1], + [Mark.Player2, Mark.Player1, Mark.Player2] ] - check checkBoard(game.board[0][0]) == mDraw + check checkBoard(game.board[0][0]) == Mark.Draw test "board is open": game.board[0][0] = [ - [mPlayer1, mPlayer2, mFree], - [mPlayer1, mFree, mPlayer1], - [mFree, mPlayer1, mPlayer2] + [Mark.Player1, Mark.Player2, Mark.Free], + [Mark.Player1, Mark.Free, Mark.Player1], + [Mark.Free, Mark.Player1, Mark.Player2] ] - check checkBoard(game.board[0][0]) == mFree + check checkBoard(game.board[0][0]) == Mark.Free test "free inital metaboard": - check checkBoard(game.board) == mFree + check checkBoard(game.board) == Mark.Free test "winning metaboard row": game.board[1][0] = [ - [mPlayer1, mPlayer2, mFree], - [mPlayer1, mPlayer1, mPlayer1], - [mFree, mPlayer1, mPlayer2] + [Mark.Player1, Mark.Player2, Mark.Free], + [Mark.Player1, Mark.Player1, Mark.Player1], + [Mark.Free, Mark.Player1, Mark.Player2] ] game.board[1][1] = [ - [mFree, mFree, mPlayer1], - [mFree, mPlayer1, mFree], - [mPlayer1, mFree, mFree] + [Mark.Free, Mark.Free, Mark.Player1], + [Mark.Free, Mark.Player1, Mark.Free], + [Mark.Player1, Mark.Free, Mark.Free] ] game.board[1][2] = [ - [mFree, mFree, mPlayer1], - [mFree, mFree, mPlayer1], - [mFree, mFree, mPlayer1] + [Mark.Free, Mark.Free, Mark.Player1], + [Mark.Free, Mark.Free, Mark.Player1], + [Mark.Free, Mark.Free, Mark.Player1] ] - check checkBoard(game.board) == mPlayer1 + check checkBoard(game.board) == Mark.Player1 test "winning metaboard column": game.board[0][1] = [ - [mPlayer1, mPlayer2, mFree], - [mPlayer1, mPlayer1, mPlayer1], - [mFree, mPlayer1, mPlayer2] + [Mark.Player1, Mark.Player2, Mark.Free], + [Mark.Player1, Mark.Player1, Mark.Player1], + [Mark.Free, Mark.Player1, Mark.Player2] ] game.board[1][1] = [ - [mFree, mFree, mFree], - [mPlayer1, mPlayer1, mPlayer1], - [mFree, mFree, mFree] + [Mark.Free, Mark.Free, Mark.Free], + [Mark.Player1, Mark.Player1, Mark.Player1], + [Mark.Free, Mark.Free, Mark.Free] ] game.board[2][1] = [ - [mPlayer1, mFree, mFree], - [mPlayer1, mFree, mFree], - [mPlayer1, mFree, mFree] + [Mark.Player1, Mark.Free, Mark.Free], + [Mark.Player1, Mark.Free, Mark.Free], + [Mark.Player1, Mark.Free, Mark.Free] ] - check checkBoard(game.board) == mPlayer1 + check checkBoard(game.board) == Mark.Player1 test "winning metaboard diagonal": game.board[0][0] = [ - [mPlayer2, mPlayer2, mFree], - [mPlayer2, mPlayer2, mPlayer2], - [mFree, mPlayer2, mPlayer2] + [Mark.Player2, Mark.Player2, Mark.Free], + [Mark.Player2, Mark.Player2, Mark.Player2], + [Mark.Free, Mark.Player2, Mark.Player2] ] game.board[1][1] = [ - [mFree, mFree, mFree], - [mPlayer2, mPlayer2, mPlayer2], - [mFree, mFree, mFree] + [Mark.Free, Mark.Free, Mark.Free], + [Mark.Player2, Mark.Player2, Mark.Player2], + [Mark.Free, Mark.Free, Mark.Free] ] game.board[2][2] = [ - [mPlayer2, mFree, mFree], - [mPlayer2, mFree, mFree], - [mPlayer2, mFree, mFree] + [Mark.Player2, Mark.Free, Mark.Free], + [Mark.Player2, Mark.Free, Mark.Free], + [Mark.Player2, Mark.Free, Mark.Free] ] - check checkBoard(game.board) == mPlayer2 + check checkBoard(game.board) == Mark.Player2 test "winning metaboard with some boards in draw": let winner = [ - [mFree, mFree, mFree], - [mPlayer2, mPlayer2, mPlayer2], - [mFree, mFree, mFree] + [Mark.Free, Mark.Free, Mark.Free], + [Mark.Player2, Mark.Player2, Mark.Player2], + [Mark.Free, Mark.Free, Mark.Free] ] let drawn = [ - [mPlayer2, mPlayer2, mPlayer1], - [mPlayer1, mPlayer1, mPlayer2], - [mPlayer2, mPlayer1, mPlayer2] + [Mark.Player2, Mark.Player2, Mark.Player1], + [Mark.Player1, Mark.Player1, Mark.Player2], + [Mark.Player2, Mark.Player1, Mark.Player2] ] game.board[0][0] = winner game.board[1][1] = winner game.board[2][2] = winner game.board[1][0] = drawn game.board[2][0] = drawn - check checkBoard(game.board) == mPlayer2 + check checkBoard(game.board) == Mark.Player2 test "metaboard is drawn": let drawn = [ - [mPlayer2, mPlayer2, mPlayer1], - [mPlayer1, mPlayer1, mPlayer2], - [mPlayer2, mPlayer1, mPlayer2] + [Mark.Player2, Mark.Player2, Mark.Player1], + [Mark.Player1, Mark.Player1, Mark.Player2], + [Mark.Player2, Mark.Player1, Mark.Player2] ] for x in 0 .. 2: for y in 0 .. 2: game.board[x][y] = drawn - check checkBoard(game.board) == mDraw + check checkBoard(game.board) == Mark.Draw test "illegal situation: both players win board": game.board[1][1] = [ - [mPlayer1, mPlayer1, mPlayer1], - [mPlayer2, mPlayer2, mPlayer2], - [mFree, mFree, mFree] + [Mark.Player1, Mark.Player1, Mark.Player1], + [Mark.Player2, Mark.Player2, Mark.Player2], + [Mark.Free, Mark.Free, Mark.Free] ] expect(Exception): discard checkBoard(game.board) \ No newline at end of file diff --git a/tests/testMoves.nim b/tests/testMoves.nim index 2e05403..b9abc8b 100644 --- a/tests/testMoves.nim +++ b/tests/testMoves.nim @@ -7,21 +7,21 @@ suite "Test the move procedures": var player1 = Player(name: "Adam") player2 = Player(name: "Eve") - game: GameState = newGame(player1, player2) + game: GameState = newGameState(player1, player2) test "Inital state": check game.currentPlayer == player1 check game.players == [player1, player2] check game.turn == 0 check game.currentBoard.isNone - check game.result == mFree + check game.result == Mark.Free test "First move": game = game.makeMove((1, 1), (1, 1)) check game.currentPlayer == player2 check game.turn == 1 check game.currentBoard == (1, 1).some() - check game.result == mFree + check game.result == Mark.Free test "Second move": game = game.makeMove((1, 1), (1, 1)) @@ -29,7 +29,7 @@ suite "Test the move procedures": check game.currentPlayer == player1 check game.turn == 2 check game.currentBoard == (0, 0).some() - check game.result == mFree + check game.result == Mark.Free test "Move on wrong board": game = game.makeMove((1, 1), (1, 1)) @@ -47,39 +47,39 @@ suite "Test the move procedures": test "Player 1 wins the game": let winner = [ - [mPlayer1, mFree, mFree], - [mPlayer1, mFree, mFree], - [mPlayer1, mFree, mFree] + [Mark.Player1, Mark.Free, Mark.Free], + [Mark.Player1, Mark.Free, Mark.Free], + [Mark.Player1, Mark.Free, Mark.Free] ] game.board[0][0] = winner game.board[1][1] = winner game.board[2][2] = [ - [mPlayer1, mFree, mFree], - [mFree, mFree, mFree], - [mPlayer1, mFree, mFree] + [Mark.Player1, Mark.Free, Mark.Free], + [Mark.Free, Mark.Free, Mark.Free], + [Mark.Player1, Mark.Free, Mark.Free] ] - check checkBoard(game.board) == mFree + check checkBoard(game.board) == Mark.Free game = game.makeMove((1, 0), (2, 2)) - check game.result == mPlayer1 + check game.result == Mark.Player1 check game.currentPlayer == player1 test "The game ends in a draw": let drawn = [ - [mPlayer2, mPlayer2, mPlayer1], - [mPlayer1, mPlayer1, mPlayer2], - [mPlayer2, mPlayer1, mPlayer2] + [Mark.Player2, Mark.Player2, Mark.Player1], + [Mark.Player1, Mark.Player1, Mark.Player2], + [Mark.Player2, Mark.Player1, Mark.Player2] ] for x in 0 .. 2: for y in 0 .. 2: game.board[x][y] = drawn game.board[2][2] = [ - [mPlayer2, mPlayer2, mPlayer1], - [mPlayer1, mPlayer1, mPlayer2], - [mPlayer2, mFree, mPlayer2] + [Mark.Player2, Mark.Player2, Mark.Player1], + [Mark.Player1, Mark.Player1, Mark.Player2], + [Mark.Player2, Mark.Free, Mark.Player2] ] - check checkBoard(game.board) == mFree + check checkBoard(game.board) == Mark.Free game = game.makeMove((2, 1), (2, 2)) - check game.result == mDraw + check game.result == Mark.Draw check game.currentPlayer == player1 \ No newline at end of file