Tic Tac Toe is a classic paper and pencil game that is often used as an introductory project for beginning programmers learning a new language. It is relatively simple to code, but allows developers to practice key programming concepts like loops, functions, conditionals, and two-dimensional arrays.
In this comprehensive guide, we will walk through how to code a Tic Tac Toe game in Python using 2D lists to represent the game board. We will cover core topics like:
- Initializing the game board as a 3x3 grid using a 2D list
- Allowing two players to take turns placing X’s and O’s
- Checking for winner in rows, columns, and diagonals
- Handling turns, ties, and restarts
- Printing the game board after each turn
By the end, you will have a fully functional Tic Tac Toe game in Python that runs in the Jupyter notebook environment. This serves as an excellent introduction to foundational Python programming skills that can be built upon for more complex projects.
Overview of Tic Tac Toe Rules and Gameplay
Tic Tac Toe is a two player game played on a 3x3 grid. Each player takes turns placing their symbol, either X or O, on an open square on the grid. The first player to get 3 of their symbols in a row, either horizontally, vertically, or diagonally, wins the game. If all 9 squares are filled and no player has achieved 3 in a row, the game results in a tie.
Basic gameplay follows these rules:
- The game board is represented as a 3x3 grid (can use a 2D list in Python)
- There are two players, one is X and the other is O
- Players take turns placing X’s and O’s on open squares
- Once placed, X’s and O’s cannot be moved or removed
- The first player to get 3 of their symbols in a row wins
- If all squares are filled with no winner, the game is a tie
- Players can decide to play again for a new game
This straightforward setup makes Tic Tac Toe an ideal pedagogical example for teaching core programming techniques like nested lists, functions, and game state logic in Python.
Initializing the Game Board in Python
The first step is to initialize a blank Tic Tac Toe board in Python that we can print and allow players to place their X’s and O’s on.
Since Tic Tac Toe is played on a 3x3 grid, we can represent the board using a 2D list with 3 inner lists to represent the rows, and 3 elements in each inner list to represent the columns.
# Initialize empty 3x3 board
board = [
[" ", " ", " "],
[" ", " ", " "],
[" ", " ", " "]
]
This [Wikipedia page on Tic Tac Toe] provides a helpful conceptual visualization of the 3x3 grid numbering we will use:
1 | 2 | 3
---------
4 | 5 | 6
---------
7 | 8 | 9
Each inner list in our 2D board
list corresponds to a row, and each element within those inner lists corresponds to columns. This allows us to index directly into the appropriate row and column we want to place a move in later.
For better code organization, we can wrap this initialization step into a initialize_board()
function:
def initialize_board():
board = [
[" ", " ", " "],
[" ", " ", " "],
[" ", " ", " "]
]
return board
To test, we can call the function and print the initial blank board:
board = initialize_board()
print(board)
Which outputs:
[[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
Our blank Tic Tac Toe board is ready! Next we need to write the functions to print the board after each move.
Printing the Game Board
To display the current state of the board after each turn, we’ll write a print_board()
function that takes the board
2D list as input, loops through it, and prints each row & column with pipes and lines in between.
def print_board(board):
print("\n")
for row in board:
print(row[0] + " | " + row[1] + " | " + row[2])
print("\n")
To make this print formatting a bit clearer, we can also add divider lines in between each row:
def print_board(board):
print("\n")
for row in board:
print(row[0] + " | " + row[1] + " | " + row[2])
print(row[0] + " | " + row[1] + " | " + row[2])
print("\n")
Calling print_board()
after each move will redraw the board with the updated X
and O
placements.
Let’s test it by placing an X
in the center square and printing:
board[1][1] = 'X'
print_board(board)
This prints:
| |
| X |
| |
Perfect! Now we can dynamically print an updated board after each player’s move.
Checking for a Winner
The core game logic lies in being able to check if a player has won by getting 3 X’s or O’s in a row after each move. We can write a check_win()
function that takes in the current board
state and checks the rows, columns, and diagonals for a winner.
To check the rows, we can loop through each inner list in the board and see if any row has all X’s or O’s:
def check_win(board):
# Check rows
for row in board:
if row.count("X") == 3:
print("X wins!")
if row.count("O") == 3:
print("O wins!")
We can check the columns by looping through the column index and checking vertically:
# Check columns
for col in range(3):
if board[0][col] == board[1][col] == board[2][col] != " ":
print(board[0][col], "wins!")
Finally, we check the two diagonals corner to corner:
# Check diagonals
if board[0][0] == board[1][1] == board[2][2] != " ":
print(board[0][0], "wins!")
if board[0][2] == board[1][1] == board[2][0] != " ":
print(board[0][2], "wins!")
We can also check if the game was a tie if all squares are filled with no winner:
# Check for tie
if " " not in board[0] and " " not in board[1] and " " not in board[2]:
print("Tie!")
Our full check_win()
function now looks like:
def check_win(board):
# Check rows
for row in board:
if row.count("X") == 3:
print("X wins!")
if row.count("O") == 3:
print("O wins!")
# Check columns
for col in range(3):
if board[0][col] == board[1][col] == board[2][col] != " ":
print(board[0][col], "wins!")
# Check diagonals
if board[0][0] == board[1][1] == board[2][2] != " ":
print(board[0][0], "wins!")
if board[0][2] == board[1][1] == board[2][0] != " ":
print(board[0][2], "wins!")
# Check for tie
if " " not in board[0] and " " not in board[1] and " " not in board[2]:
print("Tie!")
We can test it by setting up a winning diagonal for X:
board[0][0] = "X"
board[1][1] = "X"
board[2][2] = "X"
check_win(board)
This will correctly print out:
X wins!
Our check_win()
function is now able to accurately detect all winning conditions after each move!
Handling Player Moves
Up until now, we’ve manually placed X’s and O’s on the board ourselves. To actually play, we need to implement logic for alternating turns between two players.
We’ll track whose turn it is with a current_player
variable that switches between “X” and “O”. Inside the game loop, we can prompt the current player to enter their move location from 1-9 based on our board numbering scheme earlier.
# Game loop
current_player = "X"
while True:
print_board(board)
print(f"Turn: {current_player}")
move = int(input("Enter your move 1-9: "))
# Make move on board
board[int((move-1)/3)][(move-1)%3] = current_player
# Check win or tie
winner = check_win(board)
# Switch players
if current_player == "X":
current_player = "O"
else:
current_player = "X"
We calculate the row and column indexes to update based on the user’s numeric move input, place their X or O on the board, check for a winner, and finally switch active players.
This allows us to naturally alternate moves between the two players until someone wins or a tie occurs.
To improve the user experience, we can add some validation to prevent selecting an already taken square:
# Inside player move prompt
if board[int((move-1)/3)][(move-1)%3] != " ":
print("Invalid move, already taken!")
continue
Now the game will properly handle player moves and alternate turns. Next we can implement restarts and game loops.
Restarting and Looping the Game
To allow players to continually play new games without having to restart the Python kernel, we can wrap our main game logic in a loop that prompts if players want to play again.
restart = "Y"
while restart == "Y":
# Main game loop and logic
restart = input("Play again? Y/N")
print("Thanks for playing!")
We initialize restart
to “Y” to enter the first game loop. Inside the loop we have our full game logic handling player moves, win conditions, and printing the board.
After a game finishes, we prompt if the player wants to play again, setting restart
based on their input. The outer loop continues as long as restart
remains “Y”, allowing quick resets for new games.
To fully reset the board on each new game, we can call our initialize_board()
function:
while restart == "Y":
board = initialize_board()
# Rest of game logic & loops
print("Thanks for playing!")
Now we have a full playable Tic Tac Toe game in Python complete with move handling, turn alternation, win condition checking, and restarts!
Putting It All Together
We can put together everything we’ve covered so far into a complete, playable Tic Tac Toe game in Python:
def is_space_empty(board, row, col):
return board[row][col] == " "
def initialize_board():
return [
[" ", " ", " "],
[" ", " ", " "],
[" ", " ", " "]
]
def print_board(board):
print("\n")
print(board[0][0] + " | " + board[0][1] + " | " + board[0][2])
print(board[1][0] + " | " + board[1][1] + " | " + board[1][2])
print(board[2][0] + " | " + board[2][1] + " | " + board[2][2])
print("\n")
def check_win(board):
# Check rows
for row in board:
if row.count("X") == 3:
return "X"
if row.count("O") == 3:
return "O"
# Check columns
for col in range(3):
if board[0][col] == board[1][col] == board[2][col] != " ":
return board[0][col]
# Check diagonals
if board[0][0] == board[1][1] == board[2][2] != " ":
return board[0][0]
if board[0][2] == board[1][1] == board[2][0] != " ":
return board[0][2]
# Check for tie
if " " not in board[0] and " " not in board[1] and " " not in board[2]:
return "Tie"
return ""
def player_move(board, current_player, icon):
if icon == "X":
number = 1
elif icon == "O":
number = 2
print("Your turn player {}".format(number))
while True:
choice = int(input("Enter your move (1-9): ").strip())
row = int((choice - 1) / 3)
col = (choice - 1) % 3
if not is_space_empty(board, row, col):
print()
print("That space is taken!")
else:
board[row][col] = icon
break
def choose_first_player():
print("Who wants to go first? (X/O)")
player_choice = input().strip().upper()
while player_choice not in ["X", "O"]:
print("Invalid choice. Please enter X or O")
player_choice = input().strip().upper()
return player_choice
def reset_board(board):
for row in range(3):
for col in range(3):
board[row][col] = " "
def main():
board = initialize_board()
current_player = choose_first_player()
while True:
print_board(board)
player_move(board, current_player, icon=current_player)
winner = check_win(board)
if winner != "":
print(f"{winner} wins! Game over.")
break
if current_player == "X":
current_player = "O"
else:
current_player = "X"
restart = input("Play again? (Y/N)")
if restart.upper() == "N":
print("Bye!")
return
reset_board(board)
current_player = choose_first_player()
if __name__ == "__main__":
main()
And that’s it! We now have a fully playable Tic Tac Toe game in Python complete with 2D lists to represent the board, alternating player moves, win condition checking, and restarts.
The full code can be run in a Jupyter notebook cell to allow playing the game interactively. This serves as a solid foundation for practicing core Python programming concepts.
Summary
In this comprehensive guide, we walked through how to code the classic paper and pencil game Tic Tac Toe in Python.
Key topics covered included:
- Initializing a 3x3 board as a 2D list data structure
- Printing the game board after each player’s move
- Checking all win conditions across rows, columns, and diagonals
- Alternating moves between two players, X and O
- Implementing game restarts and loops for continual play
The full code example provides a complete Tic Tac Toe game playable in Jupyter notebook that serves as a beginner-friendly introduction to foundational Python programming skills.
Some ways this game can be extended or improved on include:
- Adding AI or automated players
- Implementing graphical UI instead of printing text board
- Allowing network multiplayer over local networks or internet
- Tracking game statistics and wins
Overall, coding a basic game like Tic Tac Toe allows beginning Python programmers to get hands-on experience applying core language features like functions, loops, lists, and logic flow. The full code example above can serve as a template to build off for more advanced versions.