Add diagonal and metaboard checking.
This commit is contained in:
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
@@ -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
|
||||||
|
!*/
|
||||||
109
src/libmttt.nim
109
src/libmttt.nim
@@ -1,7 +1,39 @@
|
|||||||
import sets
|
import sets, strformat, options
|
||||||
|
|
||||||
include libmttt/types
|
type
|
||||||
include libmttt/checks
|
## 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] =
|
proc newBoard[T](initial: T): Board[T] =
|
||||||
## Create a new game board filled with the initial value
|
## Create a new game board filled with the initial value
|
||||||
@@ -24,10 +56,59 @@ proc newGame*(player1, player2: Player): GameState =
|
|||||||
GameState(
|
GameState(
|
||||||
players: [player1, player2],
|
players: [player1, player2],
|
||||||
currentPlayer: player1,
|
currentPlayer: player1,
|
||||||
result: rOpen,
|
result: mFree,
|
||||||
board: newMetaBoard(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
|
## Perform a check on a single sub board to see its result
|
||||||
|
|
||||||
# Create a seconded, transposed board.
|
# Create a seconded, transposed board.
|
||||||
@@ -37,13 +118,23 @@ proc checkBoard*(board: Board[Mark]): BoardResult =
|
|||||||
for y in 0 .. 2:
|
for y in 0 .. 2:
|
||||||
transposed[x][y] = board[y][x]
|
transposed[x][y] = board[y][x]
|
||||||
|
|
||||||
var states: HashSet[BoardResult]
|
var states: HashSet[Mark]
|
||||||
states.init()
|
states.init()
|
||||||
|
states.incl(checkDiagonals(board))
|
||||||
for b in [board, transposed]:
|
for b in [board, transposed]:
|
||||||
states.incl(checkRows(b))
|
states.incl(checkRows(b))
|
||||||
|
|
||||||
return selectResult(states)
|
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
|
## 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)
|
||||||
|
|||||||
@@ -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()
|
|
||||||
|
|
||||||
@@ -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}
|
|
||||||
"""
|
|
||||||
@@ -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
|
|
||||||
159
tests/testChecks.nim
Normal file
159
tests/testChecks.nim
Normal file
@@ -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)
|
||||||
0
tests/testMoves.nim
Normal file
0
tests/testMoves.nim
Normal file
Reference in New Issue
Block a user