-
Notifications
You must be signed in to change notification settings - Fork 1
/
tictactoe_skeleton.py
166 lines (141 loc) · 5.64 KB
/
tictactoe_skeleton.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
class bcolors:
PINK = '\033[95m'
BLUE = '\033[94m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
END_COLOR = '\033[0m'
class Piece:
def __init__(self, symbol: str, color=None):
if type(symbol) != str:
raise ValueError(f"Symbol must be a string: {symbol}")
elif len(symbol) != 1:
raise ValueError(f"Symbol must be 1 letter: {symbol}")
self.symbol = symbol
self.color = color
def __str__(self) -> str:
"""Magic method for print(some_piece) and str(some_piece)"""
if self.color is not None:
return f'{self.color}{self.symbol}{bcolors.END_COLOR}'
return self.symbol
def __eq__(self, other) -> bool:
"""Magic method for checking equality (==)"""
return self.symbol == other.symbol
# A static method is one that does not use "self"
# Therefore, it does not need an instance to be used.
# We can therefore say Piece.EMPTY(), and we will get an
# empty piece. We can not say for example Board.play().
# This is because Board is a class, not an object. We can,
# however, say Board().play(). Board() creates an object,
# and we call play on that object.
@staticmethod
def EMPTY() -> 'Piece':
"""Represents and empty piece"""
return Piece(" ")
class Player:
def __init__(self, piece: Piece, color):
self.piece = piece
self.piece.color = color
self.color = color
def __str__(self) -> str:
return f'{self.color}{self.piece}{bcolors.END_COLOR}'
class Board:
def __init__(self):
# Update these pieces, to update the board! self._pieces[row][column] = some_piece
self._pieces = [
[Piece.EMPTY(), Piece.EMPTY(), Piece.EMPTY()],
[Piece.EMPTY(), Piece.EMPTY(), Piece.EMPTY()],
[Piece.EMPTY(), Piece.EMPTY(), Piece.EMPTY()],
]
def __str__(self):
str = "\n" * 5
str += "-------------"
for row in self._pieces:
# Even though _print_row is static, we can call it from inside the object.
# Not the other way around. Both self._print_row and Board._print_row is allowed.
str += self._print_row(row[0], row[1], row[2])
return str
@staticmethod
def _print_row(first, second, third):
str = "\n"
str += f"| {first} | {second} | {third} |\n"
str += "-------------"
return str
def update(self, row: int, column: int, piece: Piece):
self._pieces[row][column] = piece
def get_winner(self):
pieces = self._pieces
# Check row
for row in range(3):
if pieces[row][0] == pieces[row][1] == pieces[row][2] and pieces[row][0] != Piece.EMPTY():
return pieces[row][0]
# Check columns
for column in range(3):
if pieces[0][column] == pieces[1][column] == pieces[2][column] and pieces[0][column] != Piece.EMPTY():
return pieces[0][column]
# Check diagonal from top-left
if pieces[0][0] == pieces[1][1] == pieces[2][2] and pieces[0][0] != Piece.EMPTY():
return pieces[0][0]
# Check diagonal from bottom-left
if pieces[0][2] == pieces[1][1] == pieces[2][0] and pieces[0][2] != Piece.EMPTY():
return pieces[0][2]
return None
def is_valid_input(self, row, column):
# Check boundaries
if (0 <= row < 3) and (0 <= column < 3) and self._pieces[row][column] == Piece.EMPTY():
return True
return False
class Game:
def __init__(self, board: Board, player_one: Player, player_two: Player):
self.board = board
self.player_one = player_one
self.player_two = player_two
self.player_ones_turn = True
def play(self):
finished = False
while not finished:
print(self.board)
if self.player_ones_turn:
current_player = self.player_one
else:
current_player = self.player_two
row, column = self.ask_for_input(current_player)
self.board.update(row, column, current_player.piece)
winner = self.board.get_winner()
if winner is not None:
self.celebrate_victory(winner)
finished = True
self.player_ones_turn = not self.player_ones_turn
def ask_for_input(self, player: Player):
while True:
string = input(f"{player}'s turn (row column): ")
try:
row, column = string.strip().split(' ')
row, column = int(row), int(column)
if self.board.is_valid_input(row, column):
return row, column
except:
# IF PROGRAM CRASHES
pass
print("Invalid input. Please enter a row and column separated by space.")
def celebrate_victory(self, winner):
print(self.board)
print(f"Congratulations, {winner}!")
"""
This block is used when you want to execute some code in this file. For example if we
were to import this file into another file, using
>>> from tictactoe import Game
If we did not have the following block, the two lines would be run, even though we were
saying
$ python some_other_file.py
We do not want that! We only want to run these lines, when we are executing THIS file, that is
$ python tictactoe.py
In all other cases, we don't want to run the following lines.
"""
if __name__ == '__main__':
cross = Piece('X')
circle = Piece('O')
game = Game(Board(), Player(cross, bcolors.BOLD), Player(circle, bcolors.GREEN))
game.play()