From 2e527a79ac9d4078bd34f5dd21c7321a4e429034 Mon Sep 17 00:00:00 2001 From: luxick Date: Mon, 28 Oct 2019 19:06:46 +0100 Subject: [PATCH] Add diagonal and metaboard checking. --- .gitignore | 42 +++++++++++ src/libmttt.nim | 109 +++++++++++++++++++++++++--- src/libmttt/checks.nim | 42 ----------- src/libmttt/types.nim | 39 ---------- tests/test1.nim | 49 ------------- tests/testChecks.nim | 159 +++++++++++++++++++++++++++++++++++++++++ tests/testMoves.nim | 0 7 files changed, 301 insertions(+), 139 deletions(-) create mode 100644 .gitignore delete mode 100644 src/libmttt/checks.nim delete mode 100644 src/libmttt/types.nim delete mode 100644 tests/test1.nim create mode 100644 tests/testChecks.nim create mode 100644 tests/testMoves.nim diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65a32ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ + +# Created by https://www.gitignore.io/api/nim,visualstudiocode +# Edit at https://www.gitignore.io/?templates=nim,visualstudiocode + +### Nim ### +nimcache/ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history + +# End of https://www.gitignore.io/api/nim,visualstudiocode + + +# Created by https://www.gitignore.io/api/nim,visualstudiocode +# Edit at https://www.gitignore.io/?templates=nim,visualstudiocode + +### Nim ### +nimcache/ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# Ignore all +* + +# Unignore all with extensions +!*.* + +# Unignore all dirs +!*/ \ No newline at end of file diff --git a/src/libmttt.nim b/src/libmttt.nim index c04b3fe..748f404 100644 --- a/src/libmttt.nim +++ b/src/libmttt.nim @@ -1,7 +1,39 @@ -import sets +import sets, strformat, options -include libmttt/types -include libmttt/checks +type + ## A game board is a simple 2 dimensional array. + ## Markers of any type can be placed inside the cells + Board*[T] = array[3, array[3, T]] + + 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 + + GameState* = ref object + ## Contains all state for a pint in the game + board*: Board[Board[Mark]] + players*: array[2, Player] + currentPlayer*: Player + currentBoard*: Option[(int, int)] + result*: Mark + turn*: int + +proc `$`*(player: 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} + """ proc newBoard[T](initial: T): Board[T] = ## Create a new game board filled with the initial value @@ -24,10 +56,59 @@ proc newGame*(player1, player2: Player): GameState = GameState( players: [player1, player2], currentPlayer: player1, - result: rOpen, + result: mFree, board: newMetaBoard(mFree)) -proc checkBoard*(board: Board[Mark]): BoardResult = +################################################### +# Board Checking +################################################### + +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(mPlayer1): + return mPlayer1 + if states.contains(mPlayer2): + return mPlayer2 + if states.contains(mFree): + return mFree + return mDraw + +proc checkRow(row: array[3, Mark]): Mark = + var tokens = toHashSet(row) + # A player has won + if tokens.len == 1 and not tokens.contains(mFree): + return tokens.pop() + # The row is full + if tokens.len == 2 and not tokens.contains(mFree): + return mDraw + # There are still cells free in this row + return mFree + +proc checkRows(board: Board[Mark]): Mark = + ## Iterate over all rows of this board and return the result. + var states: HashSet[Mark] + states.init() + for row in board: + states.incl(checkRow(row)) + return states.selectResult() + +proc checkDiagonals(board: Board[Mark]): Mark = + var states: HashSet[Mark] + states.init() + var topToBottom: array[3, Mark] + var bottomToTop: array[3, Mark] + for x in 0 .. 2: + topToBottom[x] = board[x][x] + bottomToTop[x] = board[2-x][x] + states.incl(checkRow(topToBottom)) + states.incl(checkRow(bottomToTop)) + return states.selectResult() + +proc checkBoard*(board: Board[Mark]): Mark = ## Perform a check on a single sub board to see its result # Create a seconded, transposed board. @@ -37,13 +118,23 @@ proc checkBoard*(board: Board[Mark]): BoardResult = for y in 0 .. 2: transposed[x][y] = board[y][x] - var states: HashSet[BoardResult] - states.init() + var states: HashSet[Mark] + states.init() + states.incl(checkDiagonals(board)) for b in [board, transposed]: states.incl(checkRows(b)) + return selectResult(states) -proc checkBoard*(board: Board[Board[Mark]]): BoardResult = +proc checkBoard*(board: Board[Board[Mark]]): Mark = ## Perform a check on a metaboard to see the overall game result - rOpen + + # This temporary board will hold the intermediate results from each sub board + var subResults = newBoard(mFree) + var states: HashSet[Mark] + states.init() + for x in 0 .. 2: + for y in 0 .. 2: + subResults[x][y] = checkBoard(board[x][y]) + return checkBoard(subResults) diff --git a/src/libmttt/checks.nim b/src/libmttt/checks.nim deleted file mode 100644 index ea74fcd..0000000 --- a/src/libmttt/checks.nim +++ /dev/null @@ -1,42 +0,0 @@ -import sets - -import types - -proc selectResult(states: HashSet[BoardResult]): BoardResult = - ## Analyse a set of results and select the correct one - if states.contains(rPlayer1) and - states.contains(rPlayer2): - raise newException(Exception, "Both players cannot win at the same time") - - if states.contains(rPlayer1): - return rPlayer1 - if states.contains(rPlayer2): - return rPlayer2 - if states.contains(rOpen): - return rOpen - return rDraw - -proc checkRow(row: array[3, Mark]): BoardResult = - var tokens = toHashSet(row) - # A player has won - if tokens.len == 1 and not tokens.contains(mFree): - let rowResult = tokens.pop() - if rowResult == mPlayer1: - return rPlayer1 - else: - return rPlayer2 - # The row is full - if tokens.len == 2 and not tokens.contains(mFree): - return rDraw - # There are still cells free in this row - else: - return rOpen - -proc checkRows(board: Board[Mark]): BoardResult = - ## Iterate over all rows of this board and return the result. - var states: HashSet[BoardResult] - states.init() - for row in board: - states.incl(checkRow(row)) - return states.selectResult() - \ No newline at end of file diff --git a/src/libmttt/types.nim b/src/libmttt/types.nim deleted file mode 100644 index 88e4ef8..0000000 --- a/src/libmttt/types.nim +++ /dev/null @@ -1,39 +0,0 @@ -import strformat -type - ## A game board is a simple 2 dimensional array. - ## Markers of any type can be placed inside the cells - Board*[T] = array[3, array[3, T]] - - 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 - - BoardResult* = enum - rPlayer1, ## The first player has won the board - rPlayer2, ## The second player has won the board - rDraw, ## There is no winner. The board has ended in a draw - rOpen ## The game on this board is still ongoing - - GameState* = ref object - ## Contains all state for a pint in the game - board*: Board[Board[Mark]] - players*: array[2, Player] - currentPlayer*: Player - result*: BoardResult - turn*: int - -proc `$`*(player: 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} - """ \ No newline at end of file diff --git a/tests/test1.nim b/tests/test1.nim deleted file mode 100644 index 84e3900..0000000 --- a/tests/test1.nim +++ /dev/null @@ -1,49 +0,0 @@ -# This is just an example to get you started. You may wish to put all of your -# tests into a single file, or separate them into multiple `test1`, `test2` -# etc. files (better names are recommended, just make sure the name starts with -# the letter 't'). -# -# To run these tests, simply execute `nimble test`. - -import unittest - -import libmttt - -suite "Test the board result checker": - setup: - var - player1 = Player(name: "Max") - player2 = Player(name: "Adam") - state: GameState = newGame(player1, player2) - - test "row checking": - state.board[0][0] = [ - [mFree, mFree, mFree], - [mPlayer1, mPlayer1, mPlayer1], - [mFree, mFree, mFree] - ] - check checkBoard(state.board[0][0]) == rPlayer1 - - test "column checking": - state.board[0][0] = [ - [mPlayer2, mFree, mPlayer1], - [mPlayer2, mPlayer1, mFree], - [mPlayer2, mPlayer1, mFree] - ] - check checkBoard(state.board[0][0]) == rPlayer2 - - test "check for draw": - state.board[0][0] = [ - [mPlayer1, mPlayer2, mPlayer1], - [mPlayer2, mPlayer1, mPlayer1], - [mPlayer2, mPlayer1, mPlayer2] - ] - check checkBoard(state.board[0][0]) == rDraw - - test "check for open board": - state.board[0][0] = [ - [mPlayer1, mPlayer2, mFree], - [mPlayer1, mFree, mPlayer1], - [mFree, mPlayer1, mPlayer2] - ] - check checkBoard(state.board[0][0]) == rOpen \ No newline at end of file diff --git a/tests/testChecks.nim b/tests/testChecks.nim new file mode 100644 index 0000000..b05b078 --- /dev/null +++ b/tests/testChecks.nim @@ -0,0 +1,159 @@ +# This is just an example to get you started. You may wish to put all of your +# tests into a single file, or separate them into multiple `test1`, `test2` +# etc. files (better names are recommended, just make sure the name starts with +# the letter 't'). +# +# To run these tests, simply execute `nimble test`. + +import unittest + +import libmttt + +suite "Test the board result checker": + setup: + var + player1 = Player(name: "Max") + player2 = Player(name: "Adam") + state: GameState = newGame(player1, player2) + + test "winning row": + state.board[0][0] = [ + [mFree, mFree, mFree], + [mPlayer1, mPlayer1, mPlayer1], + [mFree, mFree, mFree] + ] + check checkBoard(state.board[0][0]) == mPlayer1 + + test "winning column": + state.board[0][0] = [ + [mPlayer2, mFree, mPlayer1], + [mPlayer2, mPlayer1, mFree], + [mPlayer2, mPlayer1, mFree] + ] + check checkBoard(state.board[0][0]) == mPlayer2 + + test "winning diagonals": + state.board[0][0] = [ + [mFree, mFree, mPlayer2], + [mFree, mPlayer2, mFree], + [mPlayer2, mFree, mFree] + ] + check(checkBoard(state.board[0][0]) == mPlayer2) + + state.board[0][0] = [ + [mPlayer1, mPlayer2, mPlayer2], + [mPlayer2, mPlayer1, mPlayer1], + [mPlayer2, mFree, mPlayer1] + ] + check(checkBoard(state.board[0][0]) == mPlayer1) + + test "board is a draw": + state.board[0][0] = [ + [mPlayer1, mPlayer2, mPlayer1], + [mPlayer2, mPlayer1, mPlayer1], + [mPlayer2, mPlayer1, mPlayer2] + ] + check checkBoard(state.board[0][0]) == mDraw + + test "board is open": + state.board[0][0] = [ + [mPlayer1, mPlayer2, mFree], + [mPlayer1, mFree, mPlayer1], + [mFree, mPlayer1, mPlayer2] + ] + check checkBoard(state.board[0][0]) == mFree + + test "free inital metaboard": + check checkBoard(state.board) == mFree + + test "winning metaboard row": + state.board[1][0] = [ + [mPlayer1, mPlayer2, mFree], + [mPlayer1, mPlayer1, mPlayer1], + [mFree, mPlayer1, mPlayer2] + ] + state.board[1][1] = [ + [mFree, mFree, mPlayer1], + [mFree, mPlayer1, mFree], + [mPlayer1, mFree, mFree] + ] + state.board[1][2] = [ + [mFree, mFree, mPlayer1], + [mFree, mFree, mPlayer1], + [mFree, mFree, mPlayer1] + ] + check checkBoard(state.board) == mPlayer1 + + test "winning metaboard column": + state.board[0][1] = [ + [mPlayer1, mPlayer2, mFree], + [mPlayer1, mPlayer1, mPlayer1], + [mFree, mPlayer1, mPlayer2] + ] + state.board[1][1] = [ + [mFree, mFree, mFree], + [mPlayer1, mPlayer1, mPlayer1], + [mFree, mFree, mFree] + ] + state.board[2][1] = [ + [mPlayer1, mFree, mFree], + [mPlayer1, mFree, mFree], + [mPlayer1, mFree, mFree] + ] + check checkBoard(state.board) == mPlayer1 + + test "winning metaboard diagonal": + state.board[0][0] = [ + [mPlayer2, mPlayer2, mFree], + [mPlayer2, mPlayer2, mPlayer2], + [mFree, mPlayer2, mPlayer2] + ] + state.board[1][1] = [ + [mFree, mFree, mFree], + [mPlayer2, mPlayer2, mPlayer2], + [mFree, mFree, mFree] + ] + state.board[2][2] = [ + [mPlayer2, mFree, mFree], + [mPlayer2, mFree, mFree], + [mPlayer2, mFree, mFree] + ] + check checkBoard(state.board) == mPlayer2 + + test "winning metaboard with some boards in draw": + let winner = [ + [mFree, mFree, mFree], + [mPlayer2, mPlayer2, mPlayer2], + [mFree, mFree, mFree] + ] + let drawn = [ + [mPlayer2, mPlayer2, mPlayer1], + [mPlayer1, mPlayer1, mPlayer2], + [mPlayer2, mPlayer1, mPlayer2] + ] + state.board[0][0] = winner + state.board[1][1] = winner + state.board[2][2] = winner + state.board[1][0] = drawn + state.board[2][0] = drawn + check checkBoard(state.board) == mPlayer2 + + test "metaboard is drawn": + let drawn = [ + [mPlayer2, mPlayer2, mPlayer1], + [mPlayer1, mPlayer1, mPlayer2], + [mPlayer2, mPlayer1, mPlayer2] + ] + for x in 0 .. 2: + for y in 0 .. 2: + state.board[x][y] = drawn + check checkBoard(state.board) == mDraw + + test "illegal situation: both players win board": + state.board[1][1] = [ + [mPlayer1, mPlayer1, mPlayer1], + [mPlayer2, mPlayer2, mPlayer2], + [mFree, mFree, mFree] + ] + expect(Exception): + discard checkBoard(state.board) \ No newline at end of file diff --git a/tests/testMoves.nim b/tests/testMoves.nim new file mode 100644 index 0000000..e69de29