Your Board
class is handling too much at present. It's managing the following items:
- The board itself
- Whose move it is
- Making moves for either player
- Printing the board
- Playing the game
- Displaying game status
Let's boil it down to the Board
being just a board. It carries the state of what moves have been put where, which really can just be shown on the board itself. To combine showing what moves have been put where, let's initialize your dictionary to contain None
s for values, that way it's really easy to check what has been taken:
class Board:
def __init__(self):
# a dictionary comprehension here makes this much more compact
self.board = {i: None for i in range(1, 10)}
Printing
Next, your print_board()
really could just be the __str__
dunder method, allowing you to print(board)
. This gets a little funky because of the dictionary:
class Board:
~snip~
def __str__(self):
rows = []
it = iter((v or str(k) for k, v in self.board.items()))
while True:
try:
rows.append('|'.join((next(it) for _ in range(3))))
except:
break
return '\n'.join(rows)
A little explanation. Because we've set v
to None
by default, you can use or
to either use the value set on the board if a player has chosen that spot, or the string value of the key, which is what you were using before.
None or '1'
'1'
'X' or '1'
'X'
Next, I am using an iterator to break up the keys into sets of 3:
d = {k: None for k in range(1, 10)}
it = iter((v or str(k) for k, v in d.items())
print([next(it) for _ in range(3)]
['1', '2', '3']
Then joining them together with a pipe character:
'|'.join(row)
'1|2|3'
And last, newline-joining the rows.
Now instead of using board.print_board()
you can just print(board)
Checking for Player Turn
This shouldn't be part of the Board
class. You can implement this as a for
loop using itertools.cycle
over the player pieces:
from itertools import cycle
for move in cycle('XO'):
print(move)
X
O
X
O
...
Ideally, this would be wrapped in either a Game
class or in a main
function that actually plays the game:
def main():
board = Board()
# You haven't implemented this, but let's pretend you have
you = Player(name='player1', marker='X')
cpu = Player(name='player2', marker='Y', is_cpu=True)
# you can just cycle the players themselves
for player in cycle((you, cpu)):
# play game here
...
Making Moves
The board can check for constraints on a chosen move, I think using the __setitem__
and __getitem__
methods could do nicely here:
class Board:
~snip~
def __getitem__(self, move):
try:
return self.board[move]
except KeyError as e:
spots = ', '.join(self.board)
raise ValueError(
"Didn't find that move on the board! "
f"Expected one of {spots}"
)
def __setitem__(self, move, marker):
check = self.board.get(move, 'Invalid')
if check is not None:
remaining = ', '.join((k for k, v in self.board.items() if not v))
raise ValueError(
f"Got {check}. Either move is not valid or spot is taken. "
f"Expected one of {remaining}"
)
self.board[move] = marker
This way, you can very easily just set the marker of your choice on the board directly:
board = Board()
board[1] = 'X'
board[1] = 'O'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in __setitem__
ValueError: Got 1. Either move is not valid or spot is taken.
Expected one of 2, 3, 4, 5, 6, 7, 8, 9
# it also catches bad values
board['thing'] = 'X'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in __setitem__
ValueError: Got 'thing'. Either move is not valid or spot is taken.
Expected one of 2, 3, 4, 5, 6, 7, 8, 9
Now, you can very easily take a turn in a function not related to the board at all. Going back to the cycle
idea:
# Not a perfect function but it gets the point across
def player_move(marker, board):
while True:
choice = int(input("Which move would you like from {board}? "))
try:
board[choice] = marker
except:
print('try again')
else:
break
return board
for marker in cycle('XO'):
if marker == 'O':
choice = random.choice([k for k, v in board.board if v is None])
board[choice] = marker
continue
board = player_move(marker)
Playing the game
Since you haven't implemented a Player
class, we can easily simulate one using a collections.namedtuple
:
from collections import namedtuple
Player = namedtuple('Player', ['name', 'marker'])
player1 = Player('player1', 'X')
player2 = Player('player2', 'O')
# Which can be easily extended to looping:
for name, marker in cycle((player1, player2)):
# play game
Now, to play the game, we can make a quick function to represent the player choice:
def player_move(marker, board):
while True:
choice = int(input(f"Choose a move from the board: {board} "))
try:
board[choice] = marker
# remember the errors in __getitem__ and __setitem__?
# you can now catch these as your "wrong choice"
except ValueError as e:
print(f"Invalid choice: {e}")
print("Try again")
else:
break
return board
Now, the main loop just looks like:
def main():
player1 = Player('player1', 'X')
player2 = Player('player2', 'O')
for name, marker in cycle((player1, player2)):
# might as well say whose turn it is
print(f"It's {name}'s turn! You are marker {marker}")
board = player_turn(marker, board)
# check for a win condition of some sort here
if is_win:
print(f"{name} won!")
break
Checking Possible Wins
Fortunately, you've enumerated all of the win conditions. Let's make that a class variable on the board, since it's invariant:
class Board:
possible_wins = [(1,2,3), (4,5,6), (7,8,9), (1,4,7), (2,5,8), (3,6,9), (1,5,9), (7,5,3)]
However, you loop through all of the conditions and sum to see if any conditions have been met. Use your same loop and just short-circuit when you find a triple that has all three elements the same value:
class Board:
~snip~
def is_win(self, marker):
# check rows
for coords in self.possible_wins:
# all three values must be the same value
if all(self[coord] == marker for coord in coords):
return True
return False
Checking for a stalemate
It's easy to check for a stalemate, all of the values need to be non-null:
class Board:
~snip~
def is_stalemate(self):
# any None values will cause this to return False
return all(self.board.values())
And in your game:
def main():
~snip~
if board.is_win():
...
elif board.is_stalemate():
print("Stalemate! No winners this time!")
break
Putting it all together
Your board class now looks like:
from itertools import cycle
from collections import namedtuple
Player = namedtuple('Player', ['name', 'marker'])
class Board:
possible_wins = [
(1,2,3),
(4,5,6),
(7,8,9),
(1,4,7),
(2,5,8),
(3,6,9),
(1,5,9),
(7,5,3)
]
def __init__(self):
# a dictionary comprehension here makes this much more compact
self.board = {i: None for i in range(1, 10)}
def __str__(self):
rows = []
it = iter((v or str(k) for k, v in self.board.items()))
while True:
try:
rows.append('|'.join((next(it) for _ in range(3))))
except:
break
return '\n'.join(rows)
def __getitem__(self, move):
try:
return self.board[move]
except KeyError as e:
spots = ', '.join(self.board)
raise ValueError(
"Didn't find that move on the board! "
f"Expected one of {spots}"
)
def __setitem__(self, move, marker):
check = self.board.get(move, 'Invalid')
if check is not None:
remaining = ', '.join((k for k, v in self.board.items() if not v))
raise ValueError(
f"Got {check}. Either move is not valid or spot is taken. "
f"Expected one of {remaining}"
)
self.board[move] = marker
def is_win(self, marker):
# check rows
for coords in self.possible_wins:
# all three values must be the same value
if all(self[coord] == marker for coord in coords):
return True
return False
def is_stalemate(self):
return all(self.board.values())
And playing the game now looks like:
def player_move(marker, board):
while True:
choice = int(input(f"Choose a move from the board: {board} "))
try:
board[choice] = marker
except ValueError as e:
print(f"Invalid choice: {e}")
print("Try again")
else:
break
return board
def main():
board = Board()
player1 = Player('player1', 'X')
player2 = Player('player2', 'O')
for name, marker in cycle((player1, player2)):
print(f"{name}'s turn! You are {marker}")
board = player_move(board=board, marker=marker)
if board.is_win(marker):
print(f"{name} wins!")
break
elif board.is_stalemate():
print('No winners, stalemate!')
break