Tic Tac Toe implemented in Python

I tried to do a tic tac toe game, so, to base myself in something, I searched through multiple tic tac toe codes and took some ideas from them to implement on my code. Here's the result and I would like anyone to make an analisys of it and tell me your opinion about it!

It's almost completely funcional, the only thing that needs to be fixed is the A.I. because there isn't any of it there, it just chooses a random number to place X or O. Later I'll try to build it by myself.

# Simple TIC TAC TOE game
import random
import time

board = {1: ' ', 2: ' ', 3: ' ',
4: ' ', 5: ' ', 6: ' ',
7: ' ', 8: ' ', 9: ' '}
winning_combinations = ((1, 2, 3), (4, 5, 6), (7, 8, 9),  # Horizontals
(1, 4, 7), (2, 5, 8), (3, 6, 9),  # Verticals
(1, 5, 9), (3, 5, 7))             # Diagonals

#  Displays the visual output in the terminal
def print_board():
print(board[1] + '|' + board[2] + '|' + board[3])
print('-+-+-')
print(board[4] + '|' + board[5] + '|' + board[6])
print('-+-+-')
print(board[7] + '|' + board[8] + '|' + board[9])
print('\n')

#  Defines who starts the game by chance
def who_starts():
sides = ['X', 'O']
print('The game is starting.')
print('Flipping coin...')
time.sleep(1)
if random.random() > 0.5:
print('The computer won.\n')
computer_side = random.choice(sides)
sides.remove(computer_side)
player_side = sides[0]
print(f"The computer has chosen to be {computer_side}.")
print(f"You will be {player_side[0]}.")
return player_side, computer_side, False  # This boolean will be used later on play()
else:
print('You won.\n')
while True:
player_side = input('Choose your side: ').upper()
if player_side not in sides:
print('You can only choose X or O\n')
else:
sides.remove(player_side)
computer_side = sides[0]
print(f'You have chosen to be {player_side}.')
print(f'The computer will be {computer_side}.')
return player_side, computer_side, True  # This boolean will be used later on play()

#  Allows the player place X or O in the board. The 'side' means whether X or O
def player_place(side):
check_if_full()
occupied = False
while not occupied:
try:
local = int(input('Choose somewhere to place it (1-9): '))
if local not in range(1, 10):
print('Invalid number. ')
else:
if board[local] != ' ':
print('This place is already occupied. \n')
else:
board[local] = side
print_board()
time.sleep(0.5)
check_win(side, 'O') if side == 'X' else check_win(side, 'X')
occupied = True
except ValueError:
print('Enter a number! ')

#  Allows the (dumb!) A.I. to place X or O in the board. The 'side' means whether X or O
def ai_place(side):
check_if_full()
local = random.randint(1, 9)
while board[local] != ' ':
local = random.randint(1, 9)
board[local] = side
time.sleep(1)
print_board()
time.sleep(0.5)
check_win('O', side) if side == 'X' else check_win('X', side)

#  Analyses winning patters in the board, if it detects it, it will end the game
def check_win(player_side, computer_side):
for combination in winning_combinations:
player_index = 0
computer_index = 0
for integer in combination:
if board[integer] == player_side:
player_index += 1
if player_index == 3:
print('You won!. GAME OVER')
exit()
elif board[integer] == computer_side:
computer_index += 1
if computer_index == 3:
print('You lost!, the AI defeated you. GAME OVER')
exit()

#  Checks if the board is full
def check_if_full():
count = 0
for i in range(1, 10):
if board[i] != ' ':
count += 1
if count == 9:
print('GAME OVER')
exit()

#  Executes the game with a loop based on the boolean value of the variable 'value'
def play():
player, computer, value = who_starts()
while True:
if value is True:
player_place(player), ai_place(computer)  # The player won, so will start first
else:
ai_place(computer), player_place(player)  # The computer won, starting first

#  Driver code
if __name__ == '__main__':
play()

$$$$


2 Answers

The final edited code is posted at the bottom.

1. You should just use a Python list to represent the board, as it looks like you are using the dictionary as a list anyway. Dictionaries are useful when you need to have a unique value map to another value. In this situation, you can just use the user's input to access a value in a list. Since lists start at index 0, we also need to update the winning combinations list as well:
board = [' '] * 9  # Creates a list with 9 items, where each item is ' '.
winning_combinations = ((0, 1, 2), (3, 4, 5), (6, 7, 8),  # Horizontals
(0, 3, 6), (1, 4, 7), (2, 5, 8),  # Verticals
(0, 4, 8), (2, 4, 6))             # Diagonals


We can then update the check_if_full to be the following:

def check_if_full():
if ' ' not in board:
print('GAME OVER')
exit()

1. You're print_board function is fine, but it can written in a more concise way. Consider:
def print_board():
# We store each row string here. Usually if you are joining many strings
# together, it's more efficient to append them to a list then join them.
# Here it doesn't matter, but keeps things short and concise.
output = []
# Because the length of each row is constant, we can just step by the length of each row in our loop.
for row_start in range(0, len(board), 3):
# Nicer way to creating the row string, without string concatenation
output.append("{}|{}|{}\n".format(board[row_start], board[row_start + 1], board[row_start + 2]))
output.append("-+-+-\n")
# Joins the strings in the list.
print("".join(output))


If you don't like this way, your original function is fine. Here it is, but with updated accesses to the board.

def print_board():
print(board[0] + '|' + board[1] + '|' + board[2])
print('-+-+-')
print(board[3] + '|' + board[4] + '|' + board[5])
print('-+-+-')
print(board[6] + '|' + board[7] + '|' + board[8])
print('\n')

1. It is best practice to make sure your functions only do one thing when possible. This makes it easier to reason what a function does because it keeps your functions shorter and you know exactly what it should be doing. So your who_starts function should only decide who starts the game, it shouldn't choose who is assigned to what side and also ask for user input. You should separate it into two functions who_starts and assign_sides. I will rename who_starts to starts_first:
def assign_sides():
sides = ['X', 'O']
player_side = None
comp_side = None

if random.random() > 0.5:
# Computer wins
print('The computer won.\n')
comp_side = random.choice(sides)
sides.remove(comp_side)
player_side = sides[0]
print(f"The computer has chosen to be {comp_side}.")
print(f"You will be {player_side[0]}.")
else:
print('You won.\n')
while True:
player_side = input('Choose your side: ').upper()
if player_side not in sides:
print('You can only choose X or O\n')
else:
sides.remove(player_side)
comp_side = sides[0]
print(f'You have chosen to be {player_side}.')
print(f'The computer will be {comp_side}.')
break

return player_side, comp_side

# Chooses who goes first. If True, the player goes first. Otherwise the computer
# goes first.
def starts_first():
# You can actually just have this in the play function, but to be explicit
#we'll make it its own function.
return random.random() > 0.5

# This is what play will look like after our changes
def play():
player, computer = assign_sides()
user_goes_first = starts_first()
while True:
if user_goes_first is True:
# The player won, so will start first
player_place(player), ai_place(computer)
else:
# The computer won, starting first
ai_place(computer), player_place(player)

1. You can simplify your check_win function. Instead of checking if either the user or AI wins after a move has been made, you just have to check if the current player has one after their move has been made. So if the user makes their move, then we only check if they have won. This same logic applies to the AI. We then get the following adjustments:
def player_place(side):
check_if_full()
while True:
try:
local = int(input('Choose somewhere to place it (1-9): ')) - 1
if local not in range(0, 9):
print('Invalid number. ')
else:
if board[local] != ' ':
print('This place is already occupied. \n')
else:
board[local] = side
print_board()
time.sleep(0.5)
if did_win(side):
print("You won!")
exit()
break
except ValueError:
print('Enter a number! ')

def ai_place(side):
check_if_full()
local = random.randint(0, len(board) - 1)
while board[local] != ' ':
local = random.randint(0, len(board) - 1)
board[local] = side
time.sleep(1)
print_board()
time.sleep(0.5)

if did_win(side):
print("Computer won! Game over")
exit()

def did_win(side):
# The following is called tuple unpacking. Because each item in
# winning_combinations is a tuple, we can simply unpack it by
# writing (square1, square2, square3), where each variable
# represents the value in the tuple. We can then use these directly
# without having to go tuple[index]. This makes for digestible code.
for (square1, square2, square3) in winning_combinations:
row = [board[square1], board[square2], board[square3]]
# all checks whether all items in an iterable are the same. The argument
# given is a list comprehension (which just returns a list). Thus we are
# checking if every item in the row is equal to the current player's side.
if all(item == side for item in row):
return True
return False


If anything is unclear, feel free to comment.

Final Code:

# Simple TIC TAC TOE game
import random
import time

board = [' '] * 9
winning_combinations = ((0, 1, 2), (3, 4, 5), (6, 7, 8),  # Horizontals
(0, 3, 6), (1, 4, 7), (2, 5, 8),  # Verticals
(0, 4, 8), (2, 4, 6))             # Diagonals

#  Displays the visual output in the terminal
def print_board():
output = []
for row_end in range(0, len(board), 3):
output.append("{}|{}|{}\n".format(
board[row_end], board[row_end + 1], board[row_end + 2])
)
output.append("-+-+-\n")
print("".join(output))

# Assigns each player to either 'X' or 'O'
def assign_sides():
sides = ['X', 'O']
player_side = None
comp_side = None

if random.random() > 0.5:
# Computer wins
print('The computer won.\n')
comp_side = random.choice(sides)
sides.remove(comp_side)
player_side = sides[0]
print(f"The computer has chosen to be {comp_side}.")
print(f"You will be {player_side[0]}.")
else:
print('You won.\n')
while True:
player_side = input('Choose your side: ').upper()
if player_side not in sides:
print('You can only choose X or O\n')
else:
sides.remove(player_side)
comp_side = sides[0]
print(f'You have chosen to be {player_side}.')
print(f'The computer will be {comp_side}.')
break

return player_side, comp_side

# Chooses who goes first. If True, the player goes first. Otherwise the computer goes first.
def starts_first():
# You can actually just have this in the play function, but to be explicit we'll make it its own function.
return random.random() > 0.5

#  Allows the player place X or O in the board. The 'side' means whether X or O
def player_place(side):
check_if_full()
while True:
try:
local = int(input('Choose somewhere to place it (1-9): ')) - 1
if local not in range(0, 9):
print('Invalid number. ')
else:
if board[local] != ' ':
print('This place is already occupied. \n')
else:
board[local] = side
print_board()
time.sleep(0.5)
if did_win(side):
print("You won!")
exit()
break
except ValueError:
print('Enter a number! ')

def ai_place(side):
check_if_full()
local = random.randint(0, len(board) - 1)
while board[local] != ' ':
local = random.randint(0, len(board) - 1)
board[local] = side
time.sleep(1)
print_board()
time.sleep(0.5)

if did_win(side):
print("Computer won! Game over")
exit()

def did_win(side):
# The following is called tuple unpacking. Because each item in
# winning_combinations is a tuple, we can simply unpack it by
# writing (square1, square2, square3), where each variable
# represents the value in the tuple. We can then use these directly
# without having to go tuple[index]. This makes for digestible code.
for (square1, square2, square3) in winning_combinations:
row = [board[square1], board[square2], board[square3]]
# all checks whether all items in an iterable are the same. The argument
# given is a list comprehension (which just returns a list). Thus we are
# checking if every item in the row is equal to the current player's side.
if all(item == side for item in row):
return True
return False

def check_if_full():
if ' ' not in board:
print('GAME OVER')
exit()

def play():
player, computer = assign_sides()
user_goes_first = starts_first()
while True:
if user_goes_first is True:
player_place(player), ai_place(computer)
else:
ai_place(computer), player_place(player)

#  Driver code
if __name__ == '__main__':
play()
$$$$

• thank you for fixing my code, now it looks much more professional. I hadn't understood some parts of the it, but thankfully you put some comments on it, which helped a lot the understanding. Aug 23, 2020 at 22:54
• About the print_board() func. I'd say it looks a bit confusing for me, because it uses many functions such as append(), join() and format(). But as I become better at coding I should use more of them to make it smaller rather than using only one, like print(), and repeating it many times, right? Aug 23, 2020 at 23:06

Variables and logic 1

I think it would be more standard to call the sides 0 and 1 and use those numbers to represent them.

If you do, you can simplify the side choosing code a bit, for example if the computer chooses computer_side = 1 then you do player_side = 1 - computer_side and vice versa. If the computer was 1 then player becomes 0 and it works both ways.

Logic 2

check_win('O', side) if side == 'X' else check_win('X', side)

This is unnecessarily complex and you don't need two different cases for it. After each move, you just need to check if anybody wins, since only one player can win after any move (by the game rules).

All you need to keep track of is which side the player is, and in the check_win function you check both sides if either wins, then act accordingly.

Logic 3

def ai_place(side):
check_if_full()


It seems very unintuitive for me to check if the board is full before placing and then ending the game. I would check if the board is full after each move, not before.

It's also related so the Single Responsibility Principle . A function called ai_place should only handle ai_placing, and it should already be taken care of that the board is not full (by checking that somewhere else, before calling this function).

Naming

def play():
player, computer, value = who_starts()


Since value represents "should the player start?" then it should be named something that indicates that too, for example player_starts is a fine name.

That also makes the next line if player_starts: which is very readable, rather than if value is True:

Other

if value is True:
player_place(player), ai_place(computer)  # The player won, so will start first
else:
ai_place(computer), player_place(player)  # The computer won, starting first


You are "abusing" tuple notation here for no reason. A comma between two expressions creates a tuple. Just put these on one line each since you're not actually using the tuple for anything.

if value is True:
player_place(player)
ai_place(computer)  # The player won, so will start first
else:
ai_place(computer)
player_place(player)  # The computer won, starting first

• thank you for fixing the readability! It's cleaner now Aug 23, 2020 at 22:56