In this tutorial, we will build a simple Tic-Tac-Toe game using Python and PyQt5. This project will help you understand Object-Oriented Programming (OOP), GUI design, and event handling in PyQt5.
By the end, you’ll have a fully functional game where two players can compete in a classic Tic-Tac-Toe match using a sleek GUI.
This beginner-friendly project covers essential programming concepts, including:
-
Using PyQt5 to create an interactive GUI
-
Handling user interactions through button clicks
-
Implementing game logic using Python classes
-
Updating UI elements dynamically based on game state
- Utilizing OOP to organize your code
Let’s dive in!
Step 1: Setting Up the Project
Before we start coding, let’s set up our Python project:
1. Make sure Python is installed on your computer. If not, download it from the official Python website.
2. Open your favorite code editor or IDE.
3. Create a new Python file, for example, tic_tac_toe.py
.
Before we start coding, Install PyQt5 if you haven't already. You can install it using:
pip install PyQt5
Great, now, let's dive head first into our Python editor to get this build started.
Step 2: Understanding How the Game Works
Tic-Tac-Toe is played on a 3x3 grid. Two players take turns placing their marker (X or O) on an empty square. The game ends when:
-
A player gets three markers in a row, column, or diagonal (winning condition).
-
The board is completely filled without a winner (draw condition).
We’ll use PyQt5 to build a user-friendly interface with buttons representing the grid squares. Each button click will place an X or O on the board, and the game logic will determine the winner.
When we're done, we should have something that looks like this:
Step 3: Importing Required Modules
Now, let’s start coding in Python! First, we need three Python modules for this project:
import sys # For handling system operations
import random # For selecting the starting player randomly
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QGridLayout, QLabel, QVBoxLayout
Why Do We Use These Modules?
-
sys: Required for running the PyQt5 application.
-
random: We use Python random to select which player goes first.
-
PyQt5.QtWidgets: Provides the necessary GUI components.
Step 4: Creating the Class Skeleton
To follow good OOP design practices, we will first create the class skeleton and then gradually implement each method.
import sys
import random
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QGridLayout, QLabel, QVBoxLayout
class TicTacToe(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
pass
def play_turn(self, row, col):
pass
def check_winner(self):
pass
def is_board_full(self):
pass
def update_status(self):
pass
def reset_game(self):
pass
if __name__ == '__main__':
app = QApplication(sys.argv)
window = TicTacToe()
window.show()
sys.exit(app.exec_())
Explanation:
- We define the
TicTacToe
class as a subclass ofQWidget
. - The
__init__
method initializes the game and callsinit_ui()
. - Each function is defined as a placeholder (
pass
) to be implemented step by step. - The main block (
if __name__ == '__main__':
) ensures that the script runs as a standalone application.
Step 5: Designing the Game Layout
We will now implement init_ui()
to define the GUI layout using QGridLayout
and QVBoxLayout
.
def init_ui(self):
self.setWindowTitle('Tic-Tac-Toe')
self.setGeometry(100, 100, 300, 350)
self.grid_layout = QGridLayout()
self.buttons = [[QPushButton('') for _ in range(3)] for _ in range(3)]
for i in range(3):
for j in range(3):
self.buttons[i][j].setFixedSize(80, 80)
self.buttons[i][j].clicked.connect(lambda _, row=i, col=j: self.play_turn(row, col))
self.grid_layout.addWidget(self.buttons[i][j], i, j)
self.status_label = QLabel("Player X's Turn")
self.reset_button = QPushButton('Reset Game')
self.reset_button.clicked.connect(self.reset_game)
self.v_layout = QVBoxLayout()
self.v_layout.addWidget(self.status_label)
self.v_layout.addLayout(self.grid_layout)
self.v_layout.addWidget(self.reset_button)
self.setLayout(self.v_layout)
self.board = [['' for _ in range(3)] for _ in range(3)]
self.current_player = 'X' if random.randint(0, 1) == 1 else 'O'
self.update_status()
Breakdown:
- We set up the main window title and size.
- A 3x3 grid of
QPushButton
objects represents the Tic-Tac-Toe board. - A label (
QLabel
) displays the current player's turn. - A reset button (
QPushButton
) allows the game to be restarted. update_status()
sets the initial player turn randomly.
Step 6: Handling Player Moves
Now, let's implement play_turn()
to handle player moves when they click a button.
def play_turn(self, row, col):
if self.board[row][col] == '' and not self.check_winner():
self.board[row][col] = self.current_player
self.buttons[row][col].setText(self.current_player)
if self.check_winner():
self.status_label.setText(f'Player {self.current_player} Wins!')
elif self.is_board_full():
self.status_label.setText('Match Draw!')
else:
self.current_player = 'X' if self.current_player == 'O' else 'O'
self.update_status()
Explanation:
- The method first checks if the clicked cell is empty and if the game is ongoing.
- It updates the board and sets the button text to the current player’s symbol (
X
orO
) with an f-string. - It then checks for a winner or a draw.
- If the game is still in progress, the turn switches to the other player.
Step 7: Checking for a Winner
The check_winner()
function verifies if a player has won.
def check_winner(self):
for row in self.board:
if row.count(row[0]) == 3 and row[0] != '':
return True
for col in range(3):
if self.board[0][col] == self.board[1][col] == self.board[2][col] and self.board[0][col] != '':
return True
if self.board[0][0] == self.board[1][1] == self.board[2][2] and self.board[0][0] != '':
return True
if self.board[0][2] == self.board[1][1] == self.board[2][0] and self.board[0][2] != '':
return True
return False
Breakdown:
- The function checks for three consecutive symbols horizontally, vertically, and diagonally.
Step 8: Checking for a Draw
The is_board_full()
function determines if the board is completely filled.
def is_board_full(self):
return all(self.board[i][j] != '' for i in range(3) for j in range(3))
Explanation:
- It loops through the board via the Python range function, and returns
True
if no empty spaces are left.
Step 9: Resetting the Game
The reset_game()
function resets the board and buttons.
def reset_game(self):
self.board = [['' for _ in range(3)] for _ in range(3)]
for i in range(3):
for j in range(3):
self.buttons[i][j].setText('')
self.current_player = 'X' if random.randint(0, 1) == 1 else 'O'
self.update_status()
Explanation:
- Clears the board and resets all buttons.
- Chooses a new starting player randomly.
Final Code: Tic-Tac-Toe Game
import sys
import random
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QGridLayout, QLabel, QVBoxLayout
class TicTacToe(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('Tic-Tac-Toe')
self.setGeometry(100, 100, 300, 350)
self.grid_layout = QGridLayout()
self.buttons = [[QPushButton('') for _ in range(3)] for _ in range(3)]
for i in range(3):
for j in range(3):
self.buttons[i][j].setFixedSize(80, 80)
self.buttons[i][j].clicked.connect(lambda _, row=i, col=j: self.play_turn(row, col))
self.grid_layout.addWidget(self.buttons[i][j], i, j)
self.status_label = QLabel('Player X\'s Turn')
self.reset_button = QPushButton('Reset Game')
self.reset_button.clicked.connect(self.reset_game)
self.v_layout = QVBoxLayout()
self.v_layout.addWidget(self.status_label)
self.v_layout.addLayout(self.grid_layout)
self.v_layout.addWidget(self.reset_button)
self.setLayout(self.v_layout)
self.board = [['' for _ in range(3)] for _ in range(3)]
self.current_player = 'X' if random.randint(0, 1) == 1 else 'O'
self.update_status()
def play_turn(self, row, col):
if self.board[row][col] == '' and not self.check_winner():
self.board[row][col] = self.current_player
self.buttons[row][col].setText(self.current_player)
if self.check_winner():
self.status_label.setText(f'Player {self.current_player} Wins!')
elif self.is_board_full():
self.status_label.setText('Match Draw!')
else:
self.current_player = 'X' if self.current_player == 'O' else 'O'
self.update_status()
def check_winner(self):
for row in self.board:
if row.count(row[0]) == 3 and row[0] != '':
return True
for col in range(3):
if self.board[0][col] == self.board[1][col] == self.board[2][col] and self.board[0][col] != '':
return True
if self.board[0][0] == self.board[1][1] == self.board[2][2] and self.board[0][0] != '':
return True
if self.board[0][2] == self.board[1][1] == self.board[2][0] and self.board[0][2] != '':
return True
return False
def is_board_full(self):
return all(self.board[i][j] != '' for i in range(3) for j in range(3))
def update_status(self):
self.status_label.setText(f'Player {self.current_player}\'s Turn')
def reset_game(self):
self.board = [['' for _ in range(3)] for _ in range(3)]
for i in range(3):
for j in range(3):
self.buttons[i][j].setText('')
self.current_player = 'X' if random.randint(0, 1) == 1 else 'O'
self.update_status()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = TicTacToe()
window.show()
sys.exit(app.exec_())
Wrapping Up
Congratulations! You’ve built a Tic-Tac-Toe game using Python and PyQt5. This project demonstrated how to use PyQt5 layouts, event handling, and object-oriented programming.
Next steps:
- Add a simple AI opponent.
- Improve the UI with colors and styles.
- Track player scores across multiple rounds.
Happy coding!