Build Your First Python Game - Tic Tac Toe (Object-Oriented)
1. Understanding Object-Oriented Programming
Our Tic-Tac-Toe game is built using Object-Oriented Programming (OOP), which organizes code into classes and objects. Think of classes as blueprints and objects as the actual things built from those blueprints.
Why Use OOP for Tic-Tac-Toe?
- Organization: Each part of the game has its own responsibility
- Reusability: We can create multiple players or boards easily
- Maintainability: Changes to one class don’t break others
- Real-world modeling: Code mirrors how we think about the game
Game Structure Flow
Player Class (Represents each player) + 📋 Board Class (Manages the game grid) = 🎮 TicTacToe Class (Controls the entire game)
classDiagram
class Player {
+name
+symbol
}
class Board {
+grid
+display()
+make_move()
+check_winner()
}
class TicTacToe {
+board
+players
+current_player
+switch_player()
}
TicTacToe "1" --> "1" Board
TicTacToe "2" --> "1..2" Player
class Player:
def __init__(self, name, symbol):
self.name = name
self.symbol = symbol
# Let's test it by creating some players
player1 = Player("Alice", "X")
player2 = Player("Bob", "O")
print(f"Player 1: {player1.name} uses symbol '{player1.symbol}'")
print(f"Player 2: {player2.name} uses symbol '{player2.symbol}'")
Player 1: Alice uses symbol 'X'
Player 2: Bob uses symbol 'O'
2. The Player Class - Simple but Essential
The Player class is our simplest class, but it demonstrates key OOP concepts perfectly.
What just happened in the code above:
- __init__ method: The “constructor” that runs when creating a new player
- self parameter: Refers to the specific player object being created
- Instance variables: name and symbol are stored with each player
- Encapsulation: Each player keeps track of their own data
class Board:
def __init__(self):
self.grid = [" "] * 9 # Creates 9 empty spaces
print("New board created!")
print(f"Grid contents: {self.grid}")
# Test creating a board
test_board = Board()
New board created!
Grid contents: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
3. The Board Class - Where the Logic Lives
The Board class is the heart of our game logic. It manages the 3x3 grid and all game rules.
Board Initialization:
- self.grid = [” “] * 9: Creates a list with 9 empty spaces
- Why 9 spaces? We represent the 3x3 grid as positions 0-8
- Position mapping: User enters 1-9, we convert to 0-8 internally
flowchart TD
A[Player chooses position] --> B[Board.make_move]
B --> C{Is position valid?}
C -- No --> D[Show error]
C -- Yes --> E{Is spot empty?}
E -- No --> D
E -- Yes --> F[Place symbol]
F --> G[Update board]
class Board:
def __init__(self):
self.grid = [" "] * 9
def display(self):
print("\n")
print(" " + self.grid[0] + " | " + self.grid[1] + " | " + self.grid[2])
print("---+---+---")
print(" " + self.grid[3] + " | " + self.grid[4] + " | " + self.grid[5])
print("---+---+---")
print(" " + self.grid[6] + " | " + self.grid[7] + " | " + self.grid[8])
print("\n")
def display_reference(self):
reference = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
print("Board positions:\n")
print(" " + reference[0] + " | " + reference[1] + " | " + reference[2])
print("---+---+---")
print(" " + reference[3] + " | " + reference[4] + " | " + reference[5])
print("---+---+---")
print(" " + reference[6] + " | " + reference[7] + " | " + reference[8])
print("\n")
# Test the display methods
board = Board()
print("This shows the position numbers:")
board.display_reference()
print("This shows the current game state:")
board.display()
This shows the position numbers:
Board positions:
1 | 2 | 3
---+---+---
4 | 5 | 6
---+---+---
7 | 8 | 9
This shows the current game state:
| |
---+---+---
| |
---+---+---
| |
🎯 Notice the Method Responsibility
The Board class knows how to display itself. We don’t need external code to format the output - this is encapsulation in action!
class Board:
def __init__(self):
self.grid = [" "] * 9
def display(self):
print("\n")
print(" " + self.grid[0] + " | " + self.grid[1] + " | " + self.grid[2])
print("---+---+---")
print(" " + self.grid[3] + " | " + self.grid[4] + " | " + self.grid[5])
print("---+---+---")
print(" " + self.grid[6] + " | " + self.grid[7] + " | " + self.grid[8])
print("\n")
def is_full(self):
return " " not in self.grid
def make_move(self, position, symbol):
index = position - 1 # Convert 1-9 to 0-8
if index < 0 or index > 8:
print("Invalid position. Choose a number between 1 and 9.")
return False
if self.grid[index] != " ":
print("That spot is already taken. Try again.")
return False
self.grid[index] = symbol
return True
# Test the game logic
board = Board()
print("Testing valid move:")
result1 = board.make_move(5, "X") # Should work
print(f"Move successful: {result1}")
board.display()
print("Testing invalid move (same spot):")
result2 = board.make_move(5, "O") # Should fail
print(f"Move successful: {result2}")
print("Testing invalid position:")
result3 = board.make_move(10, "O") # Should fail
print(f"Move successful: {result3}")
4. Win Detection - The Smart Algorithm
The most complex part of our Board class is checking for winners. Let’s break down this algorithm:
Why this approach is brilliant:
- Data-driven: All winning combinations are stored as data, not hardcoded logic
- Scalable: Easy to modify for different board sizes
- Readable: The winning patterns are clearly visible
- Efficient: Checks all possibilities in a simple loop
flowchart TD
A[After each move] --> B[Check all win combinations]
B --> C{Any combination matches player symbol?}
C -- Yes --> D[Declare winner]
C -- No --> E{Board full?}
E -- Yes --> F[Declare tie]
E -- No --> G[Continue game]
class Board:
def __init__(self):
self.grid = [" "] * 9
def display(self):
print("\n")
print(" " + self.grid[0] + " | " + self.grid[1] + " | " + self.grid[2])
print("---+---+---")
print(" " + self.grid[3] + " | " + self.grid[4] + " | " + self.grid[5])
print("---+---+---")
print(" " + self.grid[6] + " | " + self.grid[7] + " | " + self.grid[8])
print("\n")
def make_move(self, position, symbol):
index = position - 1
if 0 <= index <= 8 and self.grid[index] == " ":
self.grid[index] = symbol
return True
return False
def check_winner(self, symbol):
win_combinations = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], # Rows
[0, 3, 6], [1, 4, 7], [2, 5, 8], # Columns
[0, 4, 8], [2, 4, 6] # Diagonals
]
print(f"Checking for winner with symbol '{symbol}'")
print(f"Win combinations to check: {win_combinations}")
for combo in win_combinations:
if (self.grid[combo[0]] == symbol and
self.grid[combo[1]] == symbol and
self.grid[combo[2]] == symbol):
print(f"WINNER! Found winning combination: {combo}")
return True
print("No winner found")
return False
# Test win detection
board = Board()
print("Setting up a winning scenario...")
board.make_move(1, "X") # Top left
board.make_move(2, "X") # Top middle
board.make_move(3, "X") # Top right - should be a win!
board.display()
is_winner = board.check_winner("X")
print(f"Is X the winner? {is_winner}")
5. The TicTacToe Class - The Game Controller
The TicTacToe class orchestrates everything. It’s the “conductor” of our game orchestra.
🎯 Key OOP Principle
Notice how each class has a single responsibility: Player stores player data, Board manages the grid, and TicTacToe controls game flow. This is called the Single Responsibility Principle!
class Player:
def __init__(self, name, symbol):
self.name = name
self.symbol = symbol
class Board:
def __init__(self):
self.grid = [" "] * 9
def display(self):
print("\n")
print(" " + self.grid[0] + " | " + self.grid[1] + " | " + self.grid[2])
print("---+---+---")
print(" " + self.grid[3] + " | " + self.grid[4] + " | " + self.grid[5])
print("---+---+---")
print(" " + self.grid[6] + " | " + self.grid[7] + " | " + self.grid[8])
print("\n")
def display_reference(self):
reference = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
print("Board positions:\n")
print(" " + reference[0] + " | " + reference[1] + " | " + reference[2])
print("---+---+---")
print(" " + reference[3] + " | " + reference[4] + " | " + reference[5])
print("---+---+---")
print(" " + reference[6] + " | " + reference[7] + " | " + reference[8])
print("\n")
def is_full(self):
return " " not in self.grid
def make_move(self, position, symbol):
index = position - 1
if index < 0 or index > 8:
print("Invalid position. Choose a number between 1 and 9.")
return False
if self.grid[index] != " ":
print("That spot is already taken. Try again.")
return False
self.grid[index] = symbol
return True
def check_winner(self, symbol):
win_combinations = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], # Rows
[0, 3, 6], [1, 4, 7], [2, 5, 8], # Columns
[0, 4, 8], [2, 4, 6] # Diagonals
]
for combo in win_combinations:
if (self.grid[combo[0]] == symbol and
self.grid[combo[1]] == symbol and
self.grid[combo[2]] == symbol):
return True
return False
class TicTacToe:
def __init__(self, player1, player2):
self.board = Board() # Composition: TicTacToe "has-a" Board
self.players = [player1, player2] # Stores both players
self.current_player = player1 # Tracks whose turn it is
def switch_player(self):
# Alternate between the two players
self.current_player = (
self.players[1] if self.current_player == self.players[0] else self.players[0]
)
print(f"Now it's {self.current_player.name}'s turn")
# Test the TicTacToe class setup
player1 = Player("Alice", "X")
player2 = Player("Bob", "O")
game = TicTacToe(player1, player2)
print(f"Game created! Current player: {game.current_player.name}")
game.switch_player()
game.switch_player()
6. Testing Class Collaboration
Let’s see how all our classes work together in a simplified game scenario:
Notice how the classes collaborate:
- TicTacToe manages the overall game flow
- Board validates moves and checks for winners
- Player provides the data needed for moves
flowchart TD
A[Start Game] --> B[Current Player's Turn]
B --> C[Player chooses position]
C --> D[Board.make_move]
D --> E{Move valid?}
E -- No --> B
E -- Yes --> F[Board.display]
F --> G[Check winner]
G -- Winner --> H[End Game]
G -- No winner --> I{Board full?}
I -- Yes --> J[End Game=Tie]
I -- No --> K[Switch Player]
K --> B
# Let's simulate a quick game to see all classes working together
print("=== SIMULATED GAME DEMO ===")
player1 = Player("Alice", "X")
player2 = Player("Bob", "O")
game = TicTacToe(player1, player2)
# Simulate some moves
moves = [
(1, "Alice plays position 1"),
(5, "Bob plays position 5"),
(2, "Alice plays position 2"),
(6, "Bob plays position 6"),
(3, "Alice plays position 3 - should win!")
]
print(f"Starting game: {player1.name} vs {player2.name}")
game.board.display_reference()
game.board.display()
for position, description in moves:
print(f"\n--- {description} ---")
# Make the move
success = game.board.make_move(position, game.current_player.symbol)
if success:
game.board.display()
# Check for winner
if game.board.check_winner(game.current_player.symbol):
print(f"🎉 {game.current_player.name} ({game.current_player.symbol}) WINS!")
break
# Check for tie
if game.board.is_full():
print("It's a tie!")
break
# Switch players
game.switch_player()
else:
print("Move failed, trying next move...")
✨ Key Takeaways
What makes this good Object-Oriented design:
- Separation of concerns: Each class has one clear job
- Encapsulation: Data and methods that work on that data are grouped together
- Composition: Complex objects are built from simpler ones
- Maintainability: You can modify the Board display without touching game logic
- Reusability: The Player class could be used in other games
- Readability: The code structure mirrors how we think about the game
🚀 Challenge Yourself
Try adding new features like score tracking, different board sizes, or AI players. Notice how the OOP structure makes these additions much easier!
# Try modifying the code above! Here are some ideas:
# 1. Add a method to Player class to track wins
class EnhancedPlayer(Player):
def __init__(self, name, symbol):
super().__init__(name, symbol)
self.wins = 0
def add_win(self):
self.wins += 1
print(f"{self.name} now has {self.wins} wins!")
# Test it
enhanced_player = EnhancedPlayer("Charlie", "Z")
enhanced_player.add_win()
enhanced_player.add_win()
# 2. What other enhancements can you think of?
# - Add a reset method to Board?
# - Track the number of moves?
# - Add different difficulty levels?
print("Your turn to experiment! Try modifying the classes above.")
🗂️ Flashcards
What is the Player class?
Answer:
A class that represents a player, storing their name and symbol (X or O).
🖥️ Try It Yourself
📝 Quick Check
What did you learn?