-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathboard.py
420 lines (353 loc) · 17.1 KB
/
board.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
import numpy as np
from utils import log
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
log('is game stale? ', self.stale)
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)