Skip to content

Instantly share code, notes, and snippets.

@ltbringer
Last active September 9, 2018 20:01
Show Gist options
  • Save ltbringer/3527941e0b8715aed354568a762e638b to your computer and use it in GitHub Desktop.
Save ltbringer/3527941e0b8715aed354568a762e638b to your computer and use it in GitHub Desktop.
Tic tac toe board
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
import seaborn as sns
class Board(object):
"""
The environment for the reinforcement learning project.
It should:
- Have a matrix of size = NxN (which is basically N rows and N columns where N is a positive integer, greater than 0)
- I have taken N as 3, but that is not necessary.
- Initialize the matrix with zeroes.
- The values in the matrix will be represented by integers: 0, 1, 2.
- 0: empty cell represented by ' '.
- 1: cell occupied by symbol 'O'.
- 2: cell occupied by symbol 'X'.
- 'X' or 'O' can be chosen by the player at initialization step by providing a choice in `player_sym`, defaults to 'x'
- The other symbol will be chosen for the bot.
- Have a property of winner, initialized by None.
- Have a method to reset the board.
- Have a method to represent the board in a human friendly form using 'X', 'O' and ' ' instead of the respective integers 2, 1, and 0.
- Have a method which lets a user play by plotting a symbol of 'X' or 'O' only! anywhere within the matrix.
- Calculates if there is a winner after each symbol is plotted.
- A win is defined by any row, column or diagonal being filled with the same symbol, with the symbol as the winner.
- If there is a winner, prints a message for the same.
"""
def __init__(self, n=3, player_sym='x'):
"""
Constructor of the Board class, creates board objects.
- n(default=3) int: The number of rows and columns in the tic-tac-toe board.
- player_sym(default='x') str: The symbol chosen by a human player.
"""
self.board = None
self.reset_board(n)
self.stale = False
# Initalize the board
self.sym_o = {
'mark': 'O',
'value': 1
}
# Setup the 'O' symbol
self.sym_x = {
'mark': 'X',
'value': 2
}
# Setup the 'X' symbol
self.sym_empty = {
'mark': ' ',
'value': 0
}
# Setup the default ' ' Symbol
self.player_sym, self.bot_sym = (self.sym_x, self.sym_o) \
if player_sym.lower() == 'x' \
else (self.sym_o, self.sym_x)
# Ensure different symbols are assigned to the bot and the player.
self.winner = None
# Initialize the winner as None
def reset_board(self, n=3):
"""
params:
- n(default=3): int: The number of rows and columns in the tic-tac-toe board.
Clear the board when the game is to be restarted or a new game has to be started.
"""
self.board = np.zeros((n, n)).astype(int)
self.winner = None
def draw_char_for_item(self, item):
"""
Returns the string mapping of the integers in the matrix
which can be understood by, but is not equal to:
{
0: ' ',
1: 'O',
2: 'X'
}
(The exact mapping is present in the constructor)
params:
- item int: One of (1, 2, 0) representing the mark of the player, bot or empty.
return: str
"""
if item == self.sym_x.get('value'):
# If item = 2 (value of symbol x, return mark of symbol x viz: 'X')
return self.sym_x.get('mark')
elif item == self.sym_o.get('value'):
# If item = 1 (value of symbol o, return mark of symbol o viz: 'O')
return self.sym_o.get('mark')
else:
# Otherwise the cell must be empty, as only 1, 2 have 'O','X' mapped onto them.
return self.sym_empty.get('mark')
def draw_board(self):
"""
Prints a human friendly representation of the tic-tac-toe board
"""
elements_in_board = self.board.size
# Calculate the elements in the board
items = [
self.draw_char_for_item(self.board.item(item_idx))
for item_idx in range(elements_in_board)
]
# For each integer cell/element in the matrix, find the character mapped to it
# and store in a list.
board = """
{} | {} | {}
-----------
{} | {} | {}
-----------
{} | {} | {}
""".format(*items)
# The *items expand to N arguments where N is the number of elements in `items`,
# which is equal to the number of elements in the matrix, hence the string equivalent
# of the board
print(board)
def have_same_val(self, axis, item, item_x, item_y):
"""
Oh boy! without the documentation this would be just 12-14 lines of code.
Checks if a row(if axis = 0) of the board matrix has same values throughout.
or
Checks if a column(if axis = 1) of the board matrix has same values throughout.
This is useful to check if a row or column is filled up by the symbol which was added the latest.
params:
- axis int: The direction along which operations are to be performed. Can have a value of 0 or 1 only.
- 0 means row
- 1 means column
- item_x int: The row of the matrix in which item has been inserted.
- item_y int: The column of the matrix in which the item has been inserted.
- item int: The latest integer inserted into the matrix at row-index = item_x, and column-index = item_y.
"""
max_limit, _ = self.board.shape
# Get the number of rows in the board.
result = True
# Optimistic approach, assume the result to be true,
# unless proven wrong in the further steps.
row_idx = col_idx = 0
# set row_idx and col_idx iteration variables as 0
# they don't get used much, they are present for code readability.
main_idx, fixed_idx, ignore_idx = (col_idx, item_x, item_y) \
if axis == 0 \
else (row_idx, item_y, item_x)
# main_idx: Update this index each iteration of the loop.
# fixed_idx: Don't modify this index ever.
# ignore_idx: this is the index of the inserted element
# which doesn't need to be evaluated, so ignore.
# The if-else ensures weather to increment the row index
# or the column index according to the value of the axis.
while main_idx < max_limit:
# If the main_idx which starts at 0 is less than number of rows/cols in matrix.
if main_idx != ignore_idx:
# And main_idx is not equal to the index of the latest item inserted (ignore_idx)
# because for a fixed_index if we compare main_idx and ignore_idx it would give us the
# latest element added, which will be equal to itself.
# Learning algorithms are costly, ain't nobady got time fo that!
board_item = self.board[fixed_idx][main_idx] \
if axis == 0 \
else self.board[main_idx][fixed_idx]
# find the item(board_item) in the matrix
# corresponding to main_idx and the fixed_index.
# It should be an element in the same row or column depending on the axis.
if board_item != item or board_item == 0:
# If the board_item found is not equal to the latest item added
# or if the board item is 0, which is still not marked by bot or player,
# result is false as the function didn't find all
# values to be same across the row, or column.
# and exit the loop because a single-mismatch is sufficient
# to confirm that all elements are not same.
result = False
break
main_idx += 1
return result
def left_diagonal_has_same_values(self, item, item_x, item_y):
"""
params
- item_x int: The row of the matrix in which item has been inserted.
- item_y int: The column of the matrix in which the item has been inserted.
- item int: The latest integer inserted into the matrix at row-index = item_x, and column-index = item_y.
"""
i = j = 0
# set i, j to 0
result = True
# Optimistic approach, assume the result to be true,
# unless proven wrong in the further steps.
max_limit, _ = self.board.shape
# Get the number of rows in the board.
while i < max_limit:
# The row index i is sufficient as i and j are incremented
# by same factor resulting in same values (Either would do)
if i != item_x:
# Avoid checking for the latest item added as that's what we are comparing with
if self.board[i][j] != item or self.board[i][j] == 0:
# If the board_item found is not equal to the latest item added
# result is false as the function didn't find all
# values to be same across the row, or column.
# and exit the loop because a single-mismatch is sufficient
# to confirm that all elements are not same.
result = False
break
i += 1
j += 1
return result
def right_diagonal_has_same_values(self, item, item_x, item_y):
"""
params
- item_x int: The row of the matrix in which item has been inserted.
- item_y int: The column of the matrix in which the item has been inserted.
- item int: The latest integer inserted into the matrix at row-index = item_x, and column-index = item_y.
"""
result = True
max_limit, _ = self.board.shape
i = 0
j = max_limit - 1
while i < max_limit:
# The row index i is sufficient as i and j are incremented
# by same factor resulting in same values (Either would do)
if i != item_x:
# Avoid checking for the latest item added as that's what we are comparing with
if self.board[i][j] != item or self.board[i][j] == 0:
# If the board_item found is not equal to the latest item added
# result is false as the function didn't find all
# values to be same across the row, or column.
# and exit the loop because a single-mismatch is sufficient
# to confirm that all elements are not same.
result = False
break
i += 1
j -= 1
return result
def cols_have_same_values(self, item, item_x, item_y):
"""
Check if any of the columns have same values
params
- item_x int: The row of the matrix in which item has been inserted.
- item_y int: The column of the matrix in which the item has been inserted.
- item int: The latest integer inserted into the matrix at row-index = item_x, and column-index = item_y.
"""
axis = 1
return self.have_same_val(axis, item, item_x, item_y)
def rows_have_same_values(self, item, item_x, item_y):
"""
Check if any of the rows have same values
params
- item_x int: The row of the matrix in which item has been inserted.
- item_y int: The column of the matrix in which the item has been inserted.
- item int: The latest integer inserted into the matrix at row-index = item_x, and column-index = item_y.
"""
axis = 0
return self.have_same_val(axis, item, item_x, item_y)
def element_diagonal_has_same_value(self, item, item_x, item_y):
"""
Check if any of the diagonals have same values
params
- item_x int: The row of the matrix in which item has been inserted.
- item_y int: The column of the matrix in which the item has been inserted.
- item int: The latest integer inserted into the matrix at row-index = item_x, and column-index = item_y.
"""
max_limit, _ = self.board.shape
if item_x == item_y and item_x + item_y == max_limit - 1:
return self.left_diagonal_has_same_values(item, item_x, item_y) or \
self.right_diagonal_has_same_values(item, item_x, item_y)
if item_x == item_y:
# elements on the left diagonal have same row and column value.
return self.left_diagonal_has_same_values(item, item_x, item_y)
if item_x + item_y == max_limit - 1:
# elements on the right diagonal have sum of the row and column value as the same number.
return self.right_diagonal_has_same_values(item, item_x, item_y)
# Else, it is not either of the diagonals
return False
def is_game_over(self, player, item, item_x, item_y):
"""
Check if the game is over, which is defined by a row, column or diagonal having
the same values as the latest inserted integer `item`.
params
- item_x int: The row of the matrix in which item has been inserted.
- item_y int: The column of the matrix in which the item has been inserted.
- item int: The latest integer inserted into the matrix at row-index = item_x, and column-index = item_y.
"""
return self.cols_have_same_values(item, item_x, item_y) or \
self.rows_have_same_values(item, item_x, item_y) or \
self.element_diagonal_has_same_value(item, item_x, item_y)
def is_winning_move(self, player, item, item_x, item_y):
"""
Check if the last move was a winning move, which is defined by a row, column or diagonal having
the same values as the latest inserted integer `item`.
params
- item_x int: The row of the matrix in which item has been inserted.
- item_y int: The column of the matrix in which the item has been inserted.
- item int: The latest integer inserted into the matrix at row-index = item_x, and column-index = item_y.
"""
if self.is_game_over(player, item, item_x, item_y):
self.winner = player
return True
return False
def is_stale(self):
"""
Checks if there is no vacant space on the board
"""
x, y = np.where(self.board == 0)
if len(x) == 0 and len(y) == 0:
self.stale = True
return self.stale
def player_move(self, input_symbol, item_x, item_y):
"""
The method which facilitates insertion of values into the board matrix.
params:
- input_symbol: 'X' or 'O'
- item_x int: The row of the matrix in which item has been inserted.
- item_y int: The column of the matrix in which the item has been inserted.
"""
symbol = None
if input_symbol == self.sym_o.get('mark'):
# If 'O' was inserted
symbol = self.sym_o
elif input_symbol == self.sym_x.get('mark'):
# If 'X' was inserted
symbol = self.sym_x
else:
# invalid symbol
return
if self.board[item_x][item_y] == 0:
self.board[item_x][item_y] = symbol.get('value')
# insert the integer corresponding to the symbol in to the matrix.
self.draw_board()
# Show the board in a human friendly format for evaluation.
if self.is_winning_move(symbol.get('mark'), symbol.get('value'), item_x, item_y):
# If this move was a winning move, declare the symbol as the winner.
print('Winner is: {}'.format(self.winner))
return self.winner
elif self.is_stale():
print('Draw')
return 'draw'
def play(self, item_x, item_y):
"""
The method exposed to a human user
facilitates insertion of values into the board matrix.
params:
- input_symbol: 'X' or 'O'
- item_x int: The row of the matrix in which item has been inserted.
- item_y int: The column of the matrix in which the item has been inserted.
"""
max_limit, _ = self.board.shape
if item_x > max_limit - 1 or item_y > max_limit:
# If the row, column values dont' exist in the board matrix.
# Exit without inserting it into the board.
return
self.player_move(self.player_sym.get('mark'), item_x, item_y)
def bot_play(self, item_x, item_y):
"""
The method exposed to a bot
facilitates insertion of values into the board matrix.
params:
- input_symbol: 'X' or 'O'
- item_x int: The row of the matrix in which item has been inserted.
- item_y int: The column of the matrix in which the item has been inserted.
"""
max_limit, _ = self.board.shape
if item_x > max_limit - 1 or item_y > max_limit:
return
self.player_move(self.bot_sym.get('mark'), item_x, item_y)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment