Simulate a tic-tac-toe game
Requirements
- A player is picked at random as a first player.
- The two players plays tic tac toe game.
- The game ends when the board is full or either one of the player wins.
- Return the board status and result after end of the game.
Player Class
class Player:
def __init__(self, name, symbol):
self.name = name
self.symbol = symbol
def pick_available_grid(self, available_position):
random_index = random.choice(available_position.keys())
return random_index
Board Class
class Board:
def __init__(self):
self.grid = [None]*9
self.available_position = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0}
def update_board(self, index, symbol):
self.grid[index-1] = symbol
del self.available_position[index]
def check_if_board_full(self):
len(self.available_position) == 0
def check_for_win_condition(self,symbol):
if (self.grid[0]==symbol and self.grid[1]==symbol and self.grid[2]==symbol):
return True
if (self.grid[3]==symbol and self.grid[4]==symbol and self.grid[5]==symbol):
return True
if (self.grid[6]==symbol and self.grid[7]==symbol and self.grid[8]==symbol):
return True
#vertical grid check
if (self.grid[0]==symbol and self.grid[3]==symbol and self.grid[6]==symbol):
return True
if (self.grid[1]==symbol and self.grid[4]==symbol and self.grid[7]==symbol):
return True
if (self.grid[2]==symbol and self.grid[5]==symbol and self.grid[8]==symbol):
return True
#diagonal grid check
if (self.grid[0]==symbol and self.grid[4]==symbol and self.grid[8]==symbol):
return True
if (self.grid[2]==symbol and self.grid[4]==symbol and self.grid[6]==symbol):
return True
Game Class
class Game:
global SYMBOL
SYMBOL = ['X', 'O']
def __init__(self):
self.board = Board()
self.players = []
self.result = None
def set_players(self):
player1 = Player('akanksha', SYMBOL[0])
player2 = Player('akanksha', SYMBOL[1])
self.players.append(player1)
self.players.append(player2)
def set_current_player(self, current_player=None):
current_player = self.players[1] if current_player == self.players[0] else self.players[0]
return current_player
def set_result(self):
if self.board.check_for_win_condition(self.players[0].symbol):
self.result = self.players[0]
elif self.board.check_for_win_condition(self.players[1].symbol):
self.result = self.players[1]
elif self.board.check_if_board_full():
self.result = 'Draw'
return self.result
def play_game(self):
current_player = self.set_current_player()
while(not self.set_result()):
available_position = self.board.available_position
grid_position = current_player.pick_available_grid(available_position)
self.board.update_board(grid_position, current_player.symbol)
current_player = self.set_current_player(current_player)
def end_game(self):
print(self.board.grid)
if self.result == 'Draw':
print('This game is a draw')
else:
print('player', self.result.name, "with symbol", self.result.symbol, "wins")
def start_game(self):
self.set_players()
self.play_game()
self.set_result()
self.end_game()
Main Program
class MainProgram:
def main():
print("Welcome to TicTacToe Game")
game = Game()
game.start_game()
main()
I am new to oops
, how can I improve this?
Questions:
- Should I create a separate class for
symbol
? - Should
check_for_win_condition
be included inGame
instead ofBoard
class? - Should
self.available_position
be used or it should be method which computes available cells fromgrid
1 Answer 1
Honestly, for the symbol, I'd just go with using uppercase characters (X and O) since the pieces don't really have any special functionality at this stage, and it's good to keep some simplicity with earlier projects.
I'd put the win condition checking into the game class. If you want to reuse the board class in the future, it's a good idea for you to avoid having any implementation-specific code (i.e. functionality that's specific to Tic-Tac-Toe) in the class - when you're dealing with Object Orientied Programming, you have to consider SOLID principles.
In terms of self.available_positions
, I'd forego it entirely. You already have a structure which keeps track of the board, so adding a second structure to keep track of available positions is largely redundant - because one is just the inverse of the other (i.e. self.available_positions
are the null elements in self.grid
), and duplication is wasting memory and can lead to error (it's also good practice to have a single source of truth).
In terms of code, it's a good start, but I have a few suggested modifications for the Board class:
class Board:
def __init__(self):
self.grid = {1: '', 2: '', 3: '', 4: '', 5: '', 6: '', 7: '', 8: '', 9: }
# Board starts with 9 available positions, and we'll decrement it by one.
# I've changed to this from the self.available_position dictionary because you were
# essentially maintaining two structures to maintain the progress of the game,
# and having a single integer that we decrement is easier.
self.remaining_turns = 9
def update_board(self, index, symbol):
self.grid[index-1] = symbol
# Decrease the number of available turns after placing a piece.
self.remaining_turns -= 1
# Probably worth checking for 0 remaining turns after performing this decrement.
def check_for_win_condition(self,symbol):
# This if statement will be entered if any of the functions return true
if check_diagonal(symbol) or check_horizontal(symbol) or check_vertical(symbol):
return True
def check_horizontal(self, symbol):
# 0,3,6 refer to the starting elements in your vertical grid check, the additions
# refer to the offsets to get to the other elements in the line
for element in [0, 3, 6]:
if self.grid[element]==self.grid[element+1]==self.grid[element+2]==symbol:
return True
def check_vertical(self, symbol):
# 0,1,2 refer to the starting elements in your vertical grid check, the additions
# refer to the offsets to get to the other elements in the line
for element in [0, 1, 2]:
if self.grid[element]==self.grid[element+3]==self.grid[element+6]==symbol:
return True
def check_diagonal(self, symbol):
# You can chain equality checks
if (self.grid[0]==self.grid[4]==self.grid[8]==symbol) or (self.grid[2]==self.grid[4]==self.grid[6]==symbol):
return True
It's just something I typed up quickly, and I haven't had the opportunity to test it yet. My biggest issue was with the check_for_win_condition
function - you were repeating a lot of the same checks (just altering the element reference for the grid).
In terms of language choice; Python is great - it's versatile, and it's intuitive - however, go with Python 3. Python 2 is end of life in 2020 - which means it will no longer have support from the Python developers/maintainers, but Python 3 is in active development, and it has a number of improvements and new features.
-
\$\begingroup\$ Thanks for answering this. I need a clarification on one point:
self.grid = {1: '', 2: '', 3: '', 4: '', 5: '', 6: '', 7: '', 8: '', 9: }
How are players going to find the available_position. Is it by looping through grid and picking up the keys which have empty string as value and then picking a random one among them. If it so then is it not better to have a available_position, it is a trade-off between time and space. \$\endgroup\$Akanksha– Akanksha2019年10月07日 07:07:14 +00:00Commented Oct 7, 2019 at 7:07 -
\$\begingroup\$ Basically, when the user says where they want to place their piece, you can run a small function to check whether the element at that index is an empty string (i.e. if
self.grid == '':
you can place the piece). If the element at the specified index isn’t an empty string, someone has already placed a piece there, so you should prompt the player to pick another place. \$\endgroup\$OngGab– OngGab2019年10月07日 11:29:25 +00:00Commented Oct 7, 2019 at 11:29 -
\$\begingroup\$ Instead of initializing grid as
dict
won'tlist
be a better data structure? \$\endgroup\$Akanksha– Akanksha2020年10月15日 19:35:33 +00:00Commented Oct 15, 2020 at 19:35