From 93196c420088cc8a570c67132501e7645c313f90 Mon Sep 17 00:00:00 2001 From: Nocturn9x Date: Wed, 11 Jan 2023 14:23:30 +0100 Subject: [PATCH] Added initial work on tic tac toe --- .gitignore | 1 - src/game.nim | 107 +++++ src/player.nim | 96 ++++ src/util/matrix.nim | 1016 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1219 insertions(+), 1 deletion(-) create mode 100644 src/game.nim create mode 100644 src/player.nim create mode 100644 src/util/matrix.nim diff --git a/.gitignore b/.gitignore index 750bcf3..6196058 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ nimcache/ nimblecache/ htmldocs/ - diff --git a/src/game.nim b/src/game.nim new file mode 100644 index 0000000..69319b4 --- /dev/null +++ b/src/game.nim @@ -0,0 +1,107 @@ +# Copyright 2022 Mattia Giambirtone & All Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import util/matrix + + +type + TileKind* = enum + ## A tile enumeration kind + Empty = 0, + Self, + Enemy + GameStatus* = enum + ## A game status enumeration + Playing, + Win, + Lose, + Draw + TicTacToeGame* = ref object + map*: Matrix[int] + last: TileKind + + +proc newTicTacToe*: TicTacToeGame = + ## Creates a new TicTacToe object + new(result) + result.map = zeros[int]((3, 3)) + + +proc get*(self: TicTacToeGame): GameStatus = + ## Returns the game status + # Checks the rows + for _, row in self.map: + if all(row == newMatrix[int](@[1, 1, 1])): + return Win + elif all(row == newMatrix[int](@[2, 2, 2])): + return Lose + # Checks the columns + for _, col in self.map.transpose(): + if all(col == newMatrix[int](@[1, 1, 1])): + return Win + elif all(col == newMatrix[int](@[2, 2, 2])): + return Lose + # Checks the diagonals + if all(self.map.diag() == newMatrix[int](@[1, 1, 1])): + return Win + elif all(self.map.diag() == newMatrix[int](@[2, 2, 2])): + return Lose + # Top right diagonal (we flip the matrix left to right so + # that the diagonal gets shifted on the other side) + elif all(self.map.fliplr().diag() == newMatrix[int](@[1, 1, 1])): + return Win + elif all(self.map.fliplr().diag() == newMatrix[int](@[2, 2, 2])): + return Lose + # No check was successful and there's no empty slots: draw! + if not any(self.map == 0): + return Draw + # There are empty slots and no one won yet, we're still in game! + return Playing + + +proc `$`*(self: TicTacToeGame): string = + ## Stringifies self + for i, row in self.map: + for j, e in row: + if e == 0: + result &= "_" + elif e == 1: + result &= "X" + else: + result &= "O" + if j in 0..8: + result &= " " + if i < 2: + result &= "\n" + + +proc place*(self: TicTacToeGame, tile: TileKind, x, y: int) = + ## Places a tile onto the playing board + if TileKind(self.map[x, y]) == Empty: + self.map[x, y] = int(tile) + + +proc asGame*(self: Matrix[int]): TicTacToeGame = + ## Wraps a 3x3 matrix into a tris game + ## object + assert self.shape == (3, 3) + new(result) + result.map = self + + +proc build*(data: seq[int]): TicTacToeGame = + ## Builds a tris game from a flat + ## sequence + new(result) + result.map = newMatrixFromSeq[int](data, (3, 3)) diff --git a/src/player.nim b/src/player.nim new file mode 100644 index 0000000..59098de --- /dev/null +++ b/src/player.nim @@ -0,0 +1,96 @@ +# Copyright 2022 Mattia Giambirtone & All Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import game +import util/matrix + + +type + Move = ref object + ## A specific state in a + ## tic-tac-toe match, with + ## all of its children states + state: Matrix[int] + # Since tic-tac-toe is a rather + # simple game, we can generate all + # possible (legal) board configurations + # and then look up the current board when + # playing against the user in order to find + # the best move to win the match + next: array[9, Move] + parent: Move + depth: int + + +proc generateMoves*(map: Matrix[int], turn: TileKind, depth: int = 0): Move = + ## Generates the full tree of all + ## possible tic tac toe moves starting + ## from a given board state + new(result) + result.state = map.copy() + result.depth = depth + if result.state.asGame().get() != Playing: + # The game is over, no need to generate + # any more moves! + return + for row in 0.. temp.high(): + inc(col) + idx = col + + +proc newMatrixFromSeq*[T](data: seq[T], shape: tuple[rows, cols: int], order: MatrixOrder = RowMajor): Matrix[T] = + ## Creates a new matrix of the given shape from a flat + ## sequence + new(result) + new(result.data) + result.data[] = data + result.shape = shape + result.order = order + + +proc zeros*[T: int | float](shape: tuple[rows, cols: int], order: MatrixOrder = RowMajor): Matrix[T] = + ## Creates a new matrix of the given shape + ## filled with zeros + new(result) + new(result.data) + result.data[] = @[] + let size = shape.getSize() + result.shape = shape + when T is int: + for _ in 0.. added: + result.data[].delete(0) + result.shape.rows = 0 + result.shape.cols = added + result.order = RowMajor + + +proc sum*[T](self: MatrixView[T]): T = + ## Returns the sum of all elements + ## in the matrix view + var i = 0 + while i < self.shape.cols: + result += self[i] + inc(i) + + +proc copy*[T](self: Matrix[T]): Matrix[T] = + ## Creates a new copy of the given matrix + ## (copies the underlying data!) + new(result) + new(result.data) + result.data[] = self.data[] + result.shape = self.shape + result.order = self.order + + +proc dup*[T](self: Matrix[T]): Matrix[T] = + ## Creates a new shallow copy of the given + ## matrix, without copying the underlying + ## data + new(result) + result.data = self.data + result.shape = self.shape + result.order = self.order + + +proc copy*[T](self: MatrixView[T]): Matrix[T] = + ## Creates a new copy of the given matrix + ## view. Only the data of the chosen row is + ## copied + new(result) + new(result.data) + for e in self: + result.data[].add(e) + result.shape = self.shape + result.m = self.m + + +proc dup*[T](self: MatrixView[T]): MatrixView[T] = + ## Creates a new shallow copy of the given + ## matrix view, without copying the underlying + ## data + new(result) + result.m = self.m + result.shape = self.shape + result.row = self.row + +# matrix/scalar operations + +# Wrappers because builtins are not +# procvars +func add*[T](a, b: T): T = a + b +func sub*[T](a, b: T): T = a - b +func mul*[T](a, b: T): T = a * b +func divide*[T](a, b: T): T = a / b +func neg*[T](a: T): T = -a + +# Warning: These *all* perform copies of the underlying matrix! +proc `+`*[T](a: Matrix[T], b: T): Matrix[T] = a.copy().apply(add, b, axis= -1) +proc `+`*[T](a: T, b: Matrix[T]): Matrix[T] = b.copy().apply(add, a, axis= -1) + +proc `-`*[T](a: Matrix[T], b: T): Matrix[T] = a.copy().apply(sub, b, axis= -1) +proc `-`*[T](a: T, b: Matrix[T]): Matrix[T] = b.copy().apply(sub, a, axis= -1) +proc `-`*[T](a: Matrix[T]): Matrix[T] = a.copy().apply(neg, a, axis= -1) + +proc `*`*[T](a: Matrix[T], b: T): Matrix[T] = a.copy().apply(mul, b, axis = -1) +proc `*`*[T](a: T, b: Matrix[T]): Matrix[T] = b.copy().apply(mul, a, axis= -1) + +proc `/`*[T](a: Matrix[T], b: T): Matrix[T] = a.copy().apply(divide, b, axis= -1) +proc `/`*[T](a: T, b: Matrix[T]): Matrix[T] = b.copy().apply(divide, a, axis= -1) + + +# matrix/matrix operations. They produce a new matrix with the +# result of the operation + +proc `+`*[T](a, b: MatrixView[T]): Matrix[T] = + ## Performs the vector sum of the + ## given matrix views and returns a new + ## vector with the result + when not defined(release): + if a.shape.cols != b.shape.cols: # Basically if their length is different + raise newException(ValueError, &"incompatible argument shapes for addition") + new(result) + new(result.data) + result.shape = a.shape + result.order = RowMajor + result.data[] = newSeqOfCap[T](result.shape.getSize()) + for i in 0.. 0 and b.shape.rows > 0 and a.shape != b.shape: + raise newException(ValueError, &"incompatible argument shapes for addition") + elif (a.shape.rows == 0 or b.shape.rows == 0) and a.shape.cols != b.shape.cols: + raise newException(ValueError, &"incompatible argument shapes for addition") + if a.shape.rows == 0 and b.shape.rows == 0: + return a[0] + b[0] + new(result) + new(result.data) + result.data[] = newSeqOfCap[T](result.shape.getSize()) + result.shape = a.shape + result.order = RowMajor + if result.shape.rows > 1: + for row in 0.. 0 and b.shape.rows > 0 and a.shape.cols != b.shape.rows: + raise newException(ValueError, &"incompatible argument shapes for multiplication") + elif (a.shape.rows == 0 or b.shape.rows == 0) and a.shape.cols != b.shape.cols: + raise newException(ValueError, &"incompatible argument shapes for multiplication") + new(result) + new(result.data) + result.shape = (a.shape.rows, b.shape.cols) + result.order = RowMajor + result.data[] = newSeqOfCap[T](result.shape.getSize()) + if result.shape.rows > 1: + if a.shape.rows == b.shape.rows: + for row in 0..`*[T](a: Matrix[T], b: T): Matrix[bool] = + new(result) + new(result.data) + result.shape = a.shape + result.data[] = newSeqOfCap[bool](result.shape.getSize()) + for e in a.data[]: + result.data[].add(e > b) + + +proc `<=`*[T](a: Matrix[T], b: T): Matrix[bool] = + new(result) + new(result.data) + result.shape = a.shape + result.data[] = newSeqOfCap[bool](result.shape.getSize()) + for e in a.data[]: + result.data[].add(e <= b) + + +proc `>=`*[T](a: Matrix[T], b: T): Matrix[bool] = + new(result) + new(result.data) + result.shape = a.shape + result.data[] = newSeqOfCap[bool](result.shape.getSize()) + for e in a.data[]: + result.data[].add(e >= b) + + +proc `==`*[T](a: MatrixView[T], b: MatrixView[T]): Matrix[bool] = + when not defined(release): + if a.len() != b.len(): + raise newException(ValueError, "invalid shapes for comparison") + new(result) + new(result.data) + result.shape = a.shape + result.order = RowMajor + result.data[] = newSeqOfCap[bool](result.shape.getSize()) + var col = 0 + while col < result.shape.cols: + result.data[].add(a[col] == b[col]) + inc(col) + + +proc `==`*[T](a: Matrix[T], b: MatrixView[T]): Matrix[bool] = + when not defined(release): + if a.shape.cols != b.len() or a.shape.rows > 0: + raise newException(ValueError, "invalid shapes for comparison") + return a[0] == b + + +proc diag*[T](a: Matrix[T], offset: int = 0): Matrix[T] = + ## Returns the diagonal of the given + ## matrix starting at the given offset + if offset >= a.shape.cols: + return newMatrix[T](@[]) + var current = offset.ind2sub(a.shape) + var res = newSeqOfCap[T](a.shape.getSize()) + while current.row < a.shape.rows and current.col < a.shape.cols: + res.add(a.data[a.getIndex(current.row, current.col)]) + inc(current.row) + inc(current.col) + result = newMatrix(res) + + +proc fliplr*[T](self: Matrix[T]): Matrix[T] = + ## Flips each row in the matrix left + ## to right. A copy is returned + new(result) + result.shape = self.shape + result.order = self.order + new(result.data) + result.data[] = newSeqOfCap[T](self.shape.getSize()) + for row in self: + for i in countdown(row.len() - 1, 0, 1): + result.data[].add(row[i]) + + +proc `==`*[T](a, b: Matrix[T]): Matrix[bool] = + when not defined(release): + if a.shape != b.shape: + raise newException(ValueError, "can't compare matrices of different shapes") + new(result) + new(result.data) + result.shape = a.shape + result.order = RowMajor + result.data[] = newSeqOfCap[bool](result.shape.getSize()) + if a.shape.rows == 0: + result = a[0] == b[0] + for r in 0..`*[T](a, b: Matrix[T]): Matrix[bool] = + when not defined(release): + if a.shape != b.shape: + raise newException(ValueError, "can't compare matrices of different shapes") + new(result) + new(result.data) + result.shape = a.shape + result.order = RowMajor + result.data[] = newSeqOfCap[bool](result.shape.getSize()) + if a.shape.rows == 0: + result = a[0] > b[0] + for r in 0.. b[r, c]) + + +proc `>=`*[T](a, b: Matrix[T]): Matrix[bool] = + when not defined(release): + if a.shape != b.shape: + raise newException(ValueError, "can't compare matrices of different shapes") + new(result) + new(result.data) + result.shape = a.shape + result.order = RowMajor + result.data[] = newSeqOfCap[bool](result.shape.getSize()) + if a.shape.rows == 0: + result = a[0] >= b[0] + for r in 0..= b[r, c]) + + +proc `<=`*[T](a, b: Matrix[T]): Matrix[bool] = + when not defined(release): + if a.shape != b.shape: + raise newException(ValueError, "can't compare matrices of different shapes") + new(result) + new(result.data) + result.shape = a.shape + result.order = RowMajor + result.data[] = newSeqOfCap[bool](result.shape.getSize()) + if a.shape.rows == 0: + result = a[0] <= b[0] + for r in 0.. a +proc `*`*[T](a: Matrix[T], b: MatrixView[T]): Matrix[T] = b * a +proc `==`*[T](a: T, b: Matrix[T]): Matrix[bool] = b == a +proc `==`*[T](a: MatrixView[T], b: Matrix[T]): Matrix[bool] = b == a + + +proc toRowMajor*[T](self: Matrix[T], copy: bool = true): Matrix[T] = + ## Converts a column-major matrix to a + ## row-major one. Returns a copy unless + ## copy equals false + if self.order == RowMajor: + return self + if copy: + result = self.copy() + else: + result = self + result.order = RowMajor + for row in self: + for element in row: + self.data[].add(element) + + +proc toColumnMajor*[T](self: Matrix[T], copy: bool = true): Matrix[T] = + ## Converts a row-major matrix to a + ## column-major one + if self.order == ColumnMajor: + return self + if copy: + result = self.copy() + else: + result = self + self.order = ColumnMajor + let orig = self.data[] + self.data[] = @[] + var idx = 0 + var col = 0 + while col < self.shape.cols: + self.data[].add(orig[idx]) + idx += self.shape.cols + if idx > orig.high(): + inc(col) + idx = col + result = self + + +# Matrices and matrix views are iterable! + +iterator items*[T](self: Matrix[T]): MatrixView[T] = + if self.len() > 0: + for row in 0.. 0: + for column in 0.. 0: + return $(self[0]) + result &= "[" + for i, row in self: + result &= "[" + for j, e in row: + result &= $e + if j < self.shape.cols - 1: + result &= ", " + if i < self.shape.rows - 1: + result &= "], \n" + result &= " " + else: + result &= "]" + result &= "]" + + +proc dot*[T](self, other: Matrix[T]): Matrix[T] = + ## Computes the dot product of the two + ## input matrices + if self.shape.rows > 1 and other.shape.rows > 1: + when not defined(release): + if self.shape.rows != other.shape.cols: + raise newException(ValueError, &"incompatible argument shapes for dot product") + result = zeros[T]((self.shape.rows, other.shape.cols)) + var other = other.transpose() + for i in 0.. 1: + when not defined(release): + if self.shape.cols != other.shape.cols: + raise newException(ValueError, &"incompatible argument shapes for dot product") + result = zeros[T]((0, self.shape.rows)) + for i in 0.. 1: + return other.transpose().dot(self) + else: + return self * other + + +proc where*[T](cond: Matrix[bool], x, y: Matrix[T]): Matrix[T] = + ## Behaves like numpy.where() + when not defined(release): + if not (x.shape == y.shape and y.shape == cond.shape): + raise newException(ValueError, &"all inputs must be of equal shape for where()") + result = x.copy() + var + row = 0 + col = 0 + if cond.shape.rows == 0: + while col < cond.shape.cols: + if not cond[0, col]: + result[0, col] = y[0, col] + inc(col) + while row < cond.shape.rows: + while col < cond.shape.cols: + if not cond[row, col]: + result[row, col] = y[row, col] + inc(col) + inc(row) + col = 0 + + +proc where*[T](cond: Matrix[bool], x: Matrix[T], y: T): Matrix[T] = + ## Behaves like numpy.where, but with a constant + when not defined(release): + if not (x.shape == cond.shape): + raise newException(ValueError, &"all inputs must be of equal shape for where()") + result = x.copy() + var + row = 0 + col = 0 + if cond.shape.rows == 0: + while col < cond.shape.cols: + if not cond[0, col]: + result[0, col] = y + inc(col) + while row < cond.shape.rows: + while col < cond.shape.cols: + if not cond[row, col]: + result[row, col] = y + inc(col) + inc(row) + col = 0 + + +# Just a helper to avoid mistakes and so that x.where(x > 10, y) works as expected +proc where*[T](self: Matrix[T], cond: Matrix[bool], other: Matrix[T]): Matrix[T] {.inline.} = cond.where(self, other) +proc where*[T](self: Matrix[T], cond: Matrix[bool], other: T): Matrix[T] {.inline.} = cond.where(self, other) + + +proc max*[T](self: Matrix[T]): T = + ## Returns the largest element + ## into the matrix + var m: T = self[0, 0] + for row in self: + for element in row: + if m < element: + m = element + return m + + +proc argmax*[T](self: Matrix[T]): int = + ## Returns the index of largest element + ## into the matrix + var m: T = self[0, 0] + var + row = 0 + col = 0 + while row < self.shape.rows: + while col < self.shape.cols: + if self[row, col] > m: + m = self[row, col] + if self.shape.rows == 0: + while col < self.shape.cols: + if self[0, col] > m: + m = self[0, col] + inc(col) + return self.getIndex(row, col) + + +proc contains*[T](self: Matrix[T], e: T): bool = + ## Returns whether the matrix contains + ## the element e + for row in self: + for element in row: + if element == e: + return true + return false + + +when isMainModule: + import math + + proc pow(a, b: int): int = + return a ^ b + + var m = newMatrix[int](@[@[1, 2, 3], @[4, 5, 6]]) + var k = m.transpose() + assert k[2, 1] == m[1, 2], "transpose mismatch" + assert all(m.transpose() == k), "transpose mismatch" + assert k.sum() == m.sum(), "element sum mismatch" + assert all(k.sum(axis=1) == m.sum(axis=0)), "sum over axis mismatch" + assert all(k.sum(axis=0) == m.sum(axis=1)), "sum over axis mismatch" + var y = newMatrix[int](@[1, 2, 3, 4]) + assert y.sum() == 10, "element sum mismatch" + assert (y + y).sum() == 20, "matrix sum mismatch" + assert all(m + m == m * 2), "m + m != m * 2" + var z = newMatrix[int](@[1, 2, 3]) + assert (m * z).sum() == 46, "matrix multiplication mismatch" + assert all(z * z == z.apply(pow, 2, axis = -1, copy=true)), "matrix multiplication mismatch" + var x = newMatrix[int](@[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + assert (x < 5).where(x, x * 10).sum() == 360, "where mismatch" + assert all((x < 5).where(x, x * 10) == x.where(x < 5, x * 10)), "where mismatch" + assert x.max() == 9, "max mismatch" + assert x.argmax() == 10, "argmax mismatch" + assert all(newMatrix[int](@[12, 23]).dot(newMatrix[int](@[@[11, 22], @[33, 44]])) == newMatrix[int](@[891, 1276])) + assert all(newMatrix[int](@[@[1, 2, 3], @[2, 3, 4]]).dot(newMatrix[int](@[1, 2, 3])) == newMatrix[int](@[14, 20])) + assert all(m.diag() == newMatrix[int](@[1, 5])) + assert all(m.diag(1) == newMatrix[int](@[2, 6])) + assert all(m.diag(2) == newMatrix[int](@[3])) + assert m.diag(3).len() == 0 + var j = m.fliplr() + assert all(j.diag() == newMatrix[int](@[3, 5])) + assert all(j.diag(1) == newMatrix[int](@[2, 4])) + assert all(j.diag(2) == newMatrix[int](@[1]))