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.")