-
Notifications
You must be signed in to change notification settings - Fork 0
/
tictactoe_nice.py
427 lines (320 loc) · 14 KB
/
tictactoe_nice.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
421
422
423
424
425
426
427
"""
tictactoe.py
A simple two-player command-line implementation of Tic-Tac-Toe.
https://github.com/ActiveState/code/tree/master/recipes/Python/576661_TicTacToe
Author: Aseem Kishore
No license... free to use, etc.
Documentation
=============
For this implementation, I numbered the rows 1, 2 and 3, and lettered the
columns A, B and C. Here are two example displays I can show the user:
A B C A B C
1 | | 1 / /
---+---+--- or ---/---/---
2 | | 2 / /
---+---+--- ---/---/---
3 | | 3 / /
Using this representation, I refer to each square as its (row, col) position.
The row must always be one of the integers 1, 2, or 3, and the col must always
be one of the strings 'A', 'B' or 'C'.
As for the actual implementation of the board, there are a few choices. One is
to simply use nine variables, like A1 = ..., A2 = ..., etc. But this isn't
clean, and it will make the input cumbersome (e.g. a bunch of if-elif cases to
decide which variable to change, based on what the user entered.). A better
option is to use a list of 9 elements. The 9 can be numbered in any way, e.g.
top-bottom, left-to-right, so board[6] would be the equivalent of (2, 'C').
What I chose to go with was a list of three sub-lists, where each sub-list
represents a row and contains three elements. So, board[1][2] is the new
equivalent of (2, 'C').
This implementation is not trivial. I can't easily recognize that board[1][2]
is the same as the user's (2, 'C'). Plus, my rows are numbered 1-3, and index
values always begin at 0, so there's the potential for an accidental bug. For
these reasons, it's always good to use *abstraction* -- hiding the actual
implementation, and instead using simple values everywhere rather than actual
implementation values (in this case, board[1][2] is an implementation value).
To abstract this away, I have a few functions which take care of converting
between conceptual values like square 2C and implementation values like
board[1][2]:
- square(row, col) takes a row and a col and makes a (row, col) tuple out of
them. If I use this function everywhere instead of directly making tuples,
I enforce that if I ever want to change the implementation (without having
to change the conceptual values like square 2C), I can do so here without
having to change all the places in my code where I directly made a tuple.
- square_row(square) and square_col(col) take care of indexing the tuple and
returning the respective values. Again, if I use these functions everywhere
instead of directly writing square[0] and square[1], I have the flexibility
to do things like change squares' implementation, e.g. from (row, col)
tuples to (col, row) tuples, by only changing these functions.
- get_square(square) and set_square(square) are the only two functions which
actually index board. Both functions convert the row 1-3 to an index 0-2,
then convert the column 'A'-'C' to an index 0-2. It's EXTREMELY important
that I never index the board directly ANYWHERE else.
With these functions, I have taken care of a huge potential for bugs. Now, I
no longer have to worry about recognizing that the square (2, 'C') is actually
board[1][2]. I can just use the abstract concept of squares everywhere.
I used the same abstraction idea for the graphics/display. All the functions
dealing with displaying the board are in one area. Moreover, I have multiple
functions (for multiple ideas), but only one function would actually be
considered "visible" or "public", and the rest are "private" or "hidden".
This visible function doesn't do anything on its own, only the hidden ones do.
But, the visible function serves as a middleman -- it calls one of the hidden
ones. By doing this, I am able to easily switch the style of display by only
changing one line in the visible function. Take a look to better understand.
The rest of the code should be understandable -- it's all stuff you've seen
before. If you have any questions, feel free to email me. Enjoy!
"""
from random import randint
# +++ Constants +++ #
EMPTY = ' ' # the value of an empty square
PL_1 = 'x' # player 1's mark
PL_2 = 'o' # player 2's mark
# these just make it easier to keep referring to 'A', 'B' and 'C'
A = 'A'
B = 'B'
C = 'C'
# +++ State variables +++ #
board = [[EMPTY, EMPTY, EMPTY], # board is initially all empty squares,
[EMPTY, EMPTY, EMPTY], # implemented as a list of rows,
[EMPTY, EMPTY, EMPTY]] # three rows with three squares each
current_player = randint(1, 2) # randomly choose starting player
# +++ Coordinate system functions +++ #
def square(row, col):
"""
Squares are represented as tuples of (row, col).
Rows are numbered 1 thru 3, cols 'A' thru 'C'.
"""
return (row, col)
def square_row(square):
"""
These two functions save us the hassle of using
Index values in our code, e.g. square[0]...
"""
return square[0]
def square_col(square):
"""
From this point on, i should never directly use
Tuples when working with squares.
"""
return square[1]
def get_square(square):
"""Return the value of the given square."""
row_i = square_row(square) - 1 # from values of 1-3 to values of 0-2
col_i = ord(square_col(square)) - ord(A) # ord yields the ASCII number
# note how this and set_square are the ONLY
# functions which directly use board!
return board[row_i][col_i]
def set_square(square, mark):
"""Set the value of the given square."""
row_i = square_row(square) - 1
col_i = ord(square_col(square)) - ord(A)
# note how this and get_square are the ONLY
# functions which directly use board!
board[row_i][col_i] = mark
def get_row(row):
"""Return the given row as a list of three values."""
return [get_square((row, A)), get_square((row, B)), get_square((row, C))]
def get_column(col):
"""Return the given column as a list of three values."""
return [get_square((1, col)), get_square((2, col)), get_square((3, col))]
def get_diagonal(corner_square):
"""
Return the diagonal that includes the given corner square.
Only (1, A), (1, C), (3, A) and (3, C) are corner squares.
"""
if corner_square == (1, A) or corner_square == (3, C):
return [get_square((1, A)), get_square((2, B)), get_square((3, C))]
else:
return [get_square((1, C)), get_square((2, B)), get_square((3, A))]
# +++ Game logic functions +++ #
def get_mark(player):
"""Return the mark of the given player (1 or 2)."""
if player == 1:
return PL_1
else:
return PL_2
def all_squares_filled():
"""Return True iff all squares have been filled."""
for row in range(1, 4): # range(1, 4) returns the list [1, 2, 3]
if EMPTY in get_row(row):
return False # this row contains an empty square, we know enough
return True # no empty squares found, all squares are filled
def player_has_won(player):
"""Return True iff the given player (1 or 2) has won the game."""
# we need to check if there are three of the player's marks in a row,
# so we'll keep comparing against a list of three in a row.
MARK = get_mark(player)
win = [MARK, MARK, MARK]
# first check horizontal rows
if get_row(1) == win or get_row(2) == win or get_row(3) == win:
return True
# no horizontal row, let's try vertical rows
if get_column(A) == win or get_column(B) == win or get_column(C) == win:
return True
# no vertical either, let's try the diagonals
if get_diagonal((1, A)) == win or get_diagonal((1, C)) == win:
return True
return False # none of the above, player hasn't won
# +++ Display functions +++ #
# Display idea 1 -- straight representation
#
# A B C
#
# 1 | |
# ---+---+---
# 2 | |
# ---+---+---
# 3 | |
#
def board_straight():
"""Return a straight string representation of the board."""
# for ease, we'll define all the squares as constants
A1, A2, A3 = get_square((1, A)), get_square((2, A)), get_square((3, A))
B1, B2, B3 = get_square((1, B)), get_square((2, B)), get_square((3, B))
C1, C2, C3 = get_square((1, C)), get_square((2, C)), get_square((3, C))
lines = []
lines.append("")
lines.append(" " + A + " " + B + " " + C + " ")
lines.append(" ")
lines.append("1 " + A1 + " | " + B1 + " | " + C1 + " ")
lines.append(" ---+---+---")
lines.append("2 " + A2 + " | " + B2 + " | " + C2 + " ")
lines.append(" ---+---+---")
lines.append("3 " + A3 + " | " + B3 + " | " + C3 + " ")
lines.append("")
return '\n'.join(lines) # the '\n' represents a newline
# Display idea 2 -- slanted representation
#
# A B C
#
# 1 / /
# ---/---/---
# 2 / /
# ---/---/---
# 3 / /
#
def board_slanted():
"""Return a slanted string representation of the board."""
# for ease, we'll define all the squares as constants
A1, A2, A3 = get_square((1, A)), get_square((2, A)), get_square((3, A))
B1, B2, B3 = get_square((1, B)), get_square((2, B)), get_square((3, B))
C1, C2, C3 = get_square((1, C)), get_square((2, C)), get_square((3, C))
lines = []
lines.append("")
lines.append(" " + A + " " + B + " " + C + " ")
lines.append(" ")
lines.append(" 1 " + A1 + " / " + B1 + " / " + C1 + " ")
lines.append(" ---/---/--- ")
lines.append(" 2 " + A2 + " / " + B2 + " / " + C2 + " ")
lines.append(" ---/---/--- ")
lines.append("3 " + A3 + " / " + B3 + " / " + C3 + " ")
lines.append("")
return '\n'.join(lines) # the '\n' represents a newline
# And now the flexibility of being able to choose either style with one change!
# This is the power of abstraction -- we abstracted away the task of drawing.
def draw_board():
"""Print the string representation of the board in its current state."""
# this is the only line we'd have to change.
# if you want to change it, just uncomment one line and comment the other!
board_style = board_slanted
# board_style = board_straight
print(board_style())
# +++ Game functions +++ #
def reset_board():
"""Set all empty values to clear the board."""
for row in (1, 2, 3):
for col in (A, B, C):
set_square(square(row, col), EMPTY)
def play_game():
"""This function does the actual playing."""
# we need the global statement to change variables
# that are defined OUTSIDE of the current function
global current_player
reset_board()
current_player = randint(1, 2)
print("Tic-Tac-Toe!")
print()
player1_name = input("Player 1, what is your name? ")
player2_name = input("Player 2, what is your name? ")
def get_name(player):
"""Quick helper function to print a player's name."""
if player == 1:
return player1_name
else:
return player2_name
print()
print("Welcome, %s and %s!" % (player1_name, player2_name))
print("%s will be %s, and %s will be %s." % (
player1_name, PL_1, player2_name, PL_2))
print("By random decision, %s will go first." % get_name(current_player))
print()
# let's wait for them to press enter to start playing
input("[Press enter when ready to play.] ")
draw_board()
while not all_squares_filled():
choice = input("%s, which square? (e.g. 2B, 2b, B2 or b2) " %
get_name(current_player))
if len(choice) != 2:
print("That's not a square. Please enter a square like b2, or 3C.")
print()
continue
if choice[0] not in ["1", "2", "3"] and \
choice[0].upper() not in [A, B, C]:
print("The first character must be a row (1, 2 or 3) "
"or column (A, B or C).")
print()
continue
if choice[1] not in ["1", "2", "3"] and \
choice[1].upper() not in [A, B, C]:
print("The second character must be a row (1, 2 or 3) "
"or column (A, B or C).")
print()
continue
if choice[0] in ["1", "2", "3"] and choice[1] in ["1", "2", "3"]:
print("You entered two rows! Please enter a row "
"and a column (A, B or C).")
print()
continue
if choice[0].upper() in [A, B, C] and choice[1].upper() in [A, B, C]:
print("You entered two columns! Please enter a row (1, 2 or 3) "
"and a column.")
print()
continue
# now we have a row and a column, figure out which is which
if choice[0] in ["1", "2", "3"]:
row = int(choice[0])
col = choice[1].upper()
else:
row = int(choice[1])
col = choice[0].upper()
choice = square(row, col) # make this into a (row, col) tuple
if get_square(choice) != EMPTY:
print("Sorry, that square is already marked.")
print()
continue
# if we're here, then it's a valid square, so mark it
set_square(choice, get_mark(current_player))
draw_board()
if player_has_won(current_player):
print("Congratulations %s -- you win!" % get_name(current_player))
print()
break
if all_squares_filled():
print("Cats game! %s and %s draw." % (player1_name, player2_name))
print()
break
# now switch players
current_player = 3 - current_player # sets 1 to 2 and 2 to 1
print("GAME OVER")
print()
# +++ Main program code +++ #
if __name__ == "__main__":
keep_playing = True
while keep_playing:
play_game()
again = input("Play again? (y/n) ").lower()
print()
print()
print()
if again != "y":
keep_playing = False
print("Thanks for playing!")