-
Notifications
You must be signed in to change notification settings - Fork 1
/
tictac.py
357 lines (289 loc) · 11.1 KB
/
tictac.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
# -----------------------------------------------------------------------------
# Name: tictac
# Purpose: Implement a game of Tic Tac Toe
#
# Author: Hin Cheung Matthew Wo
# Date: 03-09-2015
# -----------------------------------------------------------------------------
"""
Play tic tac toe of any board size with computer.
A GUI application that simulates a tic tac toe game, use mouse to play the
game with computer. Player and computer take turn on the same board.
"""
import tkinter
import random
class Game(object):
"""
Represents a tic tac toe game instance.
Argument:
parent (Tkinter.Frame): Root frame for the game to show
Attributes:
board (dictionary): representation of the game's board
status_line (tkinter.label): game status line
board_canvas (tkinter.canvas): game board canvas
Class variables:
BOARD_SIZE (int): Board size
DIMENSION (int): Board dimension
GRID_SIZE (int): Size of each grid, computed
COLOR_PLAYER (string): Grid color for player
COLOR_COMPUTER (string): Grid color for computer
VICTORY_HORIZONTAL (list): lines that makes horizontal victory
VICTORY_VERTICAL (list): lines that makes vertical victory
VICTORY_DIAGONAL (list): lines that makes diagonal victory
"""
# Game setting constants
BOARD_SIZE = 500 # Board size
DIMENSION = 3 # Board dimension
GRID_SIZE = BOARD_SIZE // DIMENSION # Size of each grid
COLOR_PLAYER = "magenta" # Grid color for player
COLOR_COMPUTER = "cyan" # Grid color for computer
# Victory constants, to be instantiated
VICTORY_HORIZONTAL = [] # Horizontal victory
VICTORY_VERTICAL = [] # Vertical victory
VICTORY_DIAGONAL = [] # Diagonal victory
@classmethod
def init_victories(cls):
"""
Initialize the victory lines constants, accessed by Game.VICTORY_*
Parameters:
None
Returns:
None
"""
cls.VICTORY_HORIZONTAL = list([[(k, line)
for k in range(Game.DIMENSION)]
for line in range(Game.DIMENSION)])
cls.VICTORY_VERTICAL = list([[(line, k)
for k in range(Game.DIMENSION)]
for line in range(Game.DIMENSION)])
cls.VICTORY_DIAGONAL = list([[(i, Game.DIMENSION-1-i)
for i in range(Game.DIMENSION-1, -1, -1)],
[(i, i)
for i in range(Game.DIMENSION)]])
def __init__(self, parent):
"""
First instantiation of the game
Parameters:
parent (Tkinter.Tk): root window of the game
Returns:
Game (Game): the game itself
"""
parent.title('Tic Tac Toe') # set the window's title
self.init_victories() # init game constants
# Init instance variables
self.board = dict()
# Setup GUI
top_frame = tkinter.Frame(parent)
top_frame.pack(side=tkinter.TOP)
center_frame = tkinter.Frame(top_frame)
center_frame.pack(side=tkinter.BOTTOM)
bottom_frame = tkinter.Frame(parent)
bottom_frame.pack(side=tkinter.BOTTOM)
# Create a canvas widget
board_canvas = tkinter.Canvas(center_frame,
width=Game.BOARD_SIZE,
height=Game.BOARD_SIZE)
# Create the restart button widget
restart_button = tkinter.Button(top_frame, text="Restart")
restart_button.bind("<Button-1>", self.restart)
restart_button.pack()
# Create a label widget for the win/lose message
status_line = tkinter.Label(bottom_frame, text="")
status_line.pack()
# Add references to instance for further updates
self.status_line = status_line
self.board_canvas = board_canvas
# Draw game board
self.initialize_game()
def initialize_game(self):
"""
Draws separation lines onto board canvas
Parameters:
None
Returns:
None
"""
# Draw separation lines
for i in range(1, Game.DIMENSION):
# Horizontal lines
self.board_canvas.create_line(0,
Game.GRID_SIZE * i,
Game.BOARD_SIZE,
Game.GRID_SIZE * i)
# Vertical lines
self.board_canvas.create_line(Game.GRID_SIZE * i,
0,
Game.GRID_SIZE * i,
Game.BOARD_SIZE)
self.board_canvas.bind("<Button-1>", self.play)
self.board_canvas.pack()
def restart(self, event):
"""
This method is invoked when the user clicks on the RESTART button.
Clears everything in the board canvas and draw new separation lines
Parameters:
event: Mouse event from Tkinter
Returns:
None
"""
self.board_canvas.delete("all") # Erase the canvas
self.board = dict() # clear model
self.update_status_line("") # clear status line
self.initialize_game() # invoke initialize_game
def play(self, event):
"""
Event handler for handling user clicked on a grid on the board
Parameters:
event (Tkinter.Event): mouse event captured on the game board
"""
p_column = int(event.x * (Game.DIMENSION/Game.BOARD_SIZE))
p_row = int(event.y * (Game.DIMENSION/Game.BOARD_SIZE))
if 0 <= p_column <= Game.DIMENSION - 1 \
and 0 <= p_row <= Game.DIMENSION - 1 \
and (p_column, p_row) not in self.board:
self.move((p_column, p_row), "P")
# Player made a move, check if won
if not self.check_game_ended():
move = self.gen_random_move()
self.move(move, "C")
self.check_game_ended()
def check_game(self):
"""
Check game status and returns the game if won or lost
Parameters:
None
Returns:
status (Boolean): returns True if player wins, False otherwise
"""
d_winner = self.line_winner(Game.VICTORY_DIAGONAL)
v_winner = self.line_winner(Game.VICTORY_VERTICAL)
h_winner = self.line_winner(Game.VICTORY_HORIZONTAL)
winner = [d_winner, v_winner, h_winner]
if "P" in winner:
return True
elif "C" in winner:
return False
def check_game_ended(self):
"""
Updates the game status and return the status of the game
Parameters:
None
Returns:
status (Boolean): returns True if game is ended, False otherwise
"""
# Check if player won and tie
end = False
check = self.check_game()
no_more_moves = self.no_more_moves()
if no_more_moves and not check:
self.update_status_line("It's a tie!")
end = True
elif check is not None:
self.board_canvas.unbind("<Button-1>")
end = True
if check:
self.update_status_line("You won!")
else:
self.update_status_line("You lost!")
return end
def gen_random_move(self, column=-1, row=-1):
"""
Recursive method that generates a valid move on the board
Parameters:
column (Optional integer): seed column
row (Optional integer): seed row
Returns:
move (Tuple): a valid move formatted as (column, row)
"""
move = (column, row)
return move if (column >= 0 and row >= 0) \
and (move not in self.board) \
else self.gen_random_move(
random.randint(0, Game.DIMENSION-1),
random.randint(0, Game.DIMENSION-1))
def move(self, move, player):
"""
Place a move on board. If move is valid, fill the grid.
Parameters:
move (Tuple): a move formatted as (column, row)
player (String): the name of player, "C" for computer, "P" for player
Returns:
None
"""
if 0 <= move[0] <= Game.DIMENSION and 0 <= move[1] <= Game.DIMENSION \
and move not in self.board:
self.board[move] = player
self.fill_grid(move[0], move[1], player)
def update_status_line(self, text):
"""
Change the text on the GUI
Parameters:
text (String): text to update in GUI
Returns:
None
"""
self.status_line.config(text=text)
def no_more_moves(self):
"""
Check if there is any move left on the board
Parameters:
None
Returns:
has_move (Boolean): True if there is move left, False otherwise
"""
return len(self.board) >= Game.DIMENSION ** 2
def line_winner(self, line):
"""
Returns the winner of the line if exists.
Parameters:
line (List): use Game.VICTORY_*Line* constants to represent the line
of moves
Returns:
winner (String): returns None unless there exists a winner in line,
then it will return either "P" for player or "C"
for computer
"""
for moves in line:
result = self.extract_values(moves)
if result and result.count(result[0]) == Game.DIMENSION:
return result[0]
def extract_values(self, arr):
"""
Extract a list of moves from the board and return their occupy status
Parameters:
arr (List): list of moves to be extracted from the board
e.g. [(0,0), (0,1), (0,2)]
Returns:
occupy_statuses (List): the status of the grids extracted from arr
e.g. ["C", "P", "P"]
"""
return [v for k, v in self.board.items() if k in arr]
def fill_grid(self, column, row, player):
"""
Fill the grid at column and row for player / computer
Parameters:
column (Integer): target's column
row (Row): target's row
Returns:
None
"""
grid_start = (column * Game.GRID_SIZE, row * Game.GRID_SIZE)
grid_end = (grid_start[0] + Game.GRID_SIZE,
grid_start[1] + Game.GRID_SIZE)
color = Game.COLOR_PLAYER
if player is "C":
color = Game.COLOR_COMPUTER
self.board_canvas.create_rectangle(grid_start[0],
grid_start[1],
grid_end[0],
grid_end[1],
fill=color)
def main():
# Instantiate a root window
root = tkinter.Tk()
# Instantiate a Game object
game = Game(root)
# Enter the main event loop
root.mainloop()
if __name__ == '__main__':
main()