• 1Understanding Object-Oriented Programming
    • Why Use OOP for Tic-Tac-Toe?
      • 🎭 Player Class
      • 📋 Board Class
      • 🎮 TicTacToe Class
  • 2The Player Class - Simple but Essential
    • What just happened in the code above:
  • 3The Board Class - Where the Logic Lives
    • Board Initialization:
      • 🎯 Notice the Method Responsibility
  • 4Win Detection - The Smart Algorithm
    • Why this approach is brilliant:
  • 5The TicTacToe Class - The Game Controller
    • 🎯 Key OOP Principle
  • 6Testing Class Collaboration
    • Notice how the classes collaborate:
  • Key Takeaways
    • What makes this good Object-Oriented design:
      • 🚀 Challenge Yourself
  • 1Understanding Object-Oriented Programming

    🎮 Want to play the game right now?

    Try out the web version of the game before diving into the code!

    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

    🎭 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'
    

    2The 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: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
    

    3The 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}")
    
    Testing valid move:
    Move successful: True
    
    
       |   |  
    ---+---+---
       | X |  
    ---+---+---
       |   |  
    
    
    Testing invalid move (same spot):
    That spot is already taken. Try again.
    Move successful: False
    Testing invalid position:
    Invalid position. Choose a number between 1 and 9.
    Move successful: False
    

    4Win 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}")
    
    Setting up a winning scenario...
    
    
     X | X | X
    ---+---+---
       |   |  
    ---+---+---
       |   |  
    
    
    Checking for winner with symbol 'X'
    Win combinations to check: [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]
    WINNER! Found winning combination: [0, 1, 2]
    Is X the winner? True
    

    5The 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!

    import random
    from colorama import Fore, Style, init
    init(autoreset=True)  # Initialize colorama
    
    class Player:
        def __init__(self, name, symbol, is_ai=False):
            self.name = name
            self.symbol = symbol
            self.is_ai = is_ai
            self.wins = 0
            self.moves = []
        
        def add_win(self):
            self.wins += 1
            print(f"{Fore.GREEN}🏆 {self.name} now has {self.wins} wins!{Style.RESET_ALL}")
        
        def get_move(self, board):
            if not self.is_ai:
                return None
            # AI Logic
            return self._get_best_move(board)
        
        def _get_best_move(self, board):
            # First, try to win
            for i in range(9):
                if board.grid[i] == " ":
                    board.grid[i] = self.symbol
                    if board.check_winner(self.symbol):
                        board.grid[i] = " "  # Reset the move
                        return i + 1
                    board.grid[i] = " "  # Reset the move
            
            # Then, block opponent's win
            opponent_symbol = "O" if self.symbol == "X" else "X"
            for i in range(9):
                if board.grid[i] == " ":
                    board.grid[i] = opponent_symbol
                    if board.check_winner(opponent_symbol):
                        board.grid[i] = " "  # Reset the move
                        return i + 1
                    board.grid[i] = " "  # Reset the move
            
            # Try to get center
            if board.grid[4] == " ":
                return 5
            
            # Try corners
            corners = [0, 2, 6, 8]
            random.shuffle(corners)
            for corner in corners:
                if board.grid[corner] == " ":
                    return corner + 1
            
            # Take any available space
            for i in range(9):
                if board.grid[i] == " ":
                    return i + 1
    
    class Board:
        def __init__(self):
            self.grid = [" "] * 9
            self.move_history = []
    
        def display(self):
            print("\n")
            colors = {" ": Style.RESET_ALL, "X": Fore.RED, "O": Fore.BLUE}
            for i in range(0, 9, 3):
                print(" " + colors[self.grid[i]] + self.grid[i] + Style.RESET_ALL + " | " + 
                      colors[self.grid[i+1]] + self.grid[i+1] + Style.RESET_ALL + " | " + 
                      colors[self.grid[i+2]] + self.grid[i+2] + Style.RESET_ALL)
                if i < 6:
                    print("---+---+---")
            print("\n")
    
        def display_reference(self):
            print(f"{Fore.YELLOW}Board positions:{Style.RESET_ALL}\n")
            reference = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
            for i in range(0, 9, 3):
                print(" " + reference[i] + " | " + reference[i+1] + " | " + reference[i+2])
                if i < 6:
                    print("---+---+---")
            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(f"{Fore.RED}Invalid position. Choose a number between 1 and 9.{Style.RESET_ALL}")
                return False
            if self.grid[index] != " ":
                print(f"{Fore.RED}That spot is already taken. Try again.{Style.RESET_ALL}")
                return False
            self.grid[index] = symbol
            self.move_history.append((position, symbol))
            return True
    
        def undo_last_move(self):
            if self.move_history:
                position, _ = self.move_history.pop()
                self.grid[position - 1] = " "
                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
            ]
            return any(all(self.grid[i] == symbol for i in combo) for combo in win_combinations)
    
    class TicTacToe:
        def __init__(self, player1, player2):
            self.board = Board()
            self.players = [player1, player2]
            self.current_player = player1
            self.game_history = []
            self.moves_count = 0
        
        def switch_player(self):
            self.current_player = self.players[1] if self.current_player == self.players[0] else self.players[0]
            if not self.current_player.is_ai:
                print(f"{Fore.CYAN}Now it's {self.current_player.name}'s turn{Style.RESET_ALL}")
        
        def play_turn(self):
            self.moves_count += 1
            if self.current_player.is_ai:
                position = self.current_player.get_move(self.board)
                print(f"{Fore.YELLOW}AI {self.current_player.name} chooses position {position}{Style.RESET_ALL}")
            else:
                try:
                    position = int(input(f"{self.current_player.name}, enter position (1-9) or 0 to undo: "))
                    if position == 0:
                        if self.board.undo_last_move():
                            self.switch_player()
                            self.moves_count -= 2
                            print(f"{Fore.YELLOW}Move undone!{Style.RESET_ALL}")
                        return True
                except ValueError:
                    print(f"{Fore.RED}Please enter a number!{Style.RESET_ALL}")
                    return True
            
            if self.board.make_move(position, self.current_player.symbol):
                self.board.display()
                
                if self.board.check_winner(self.current_player.symbol):
                    print(f"{Fore.GREEN}🎉 {self.current_player.name} WINS in {self.moves_count} moves!{Style.RESET_ALL}")
                    self.current_player.add_win()
                    self.game_history.append({
                        'winner': self.current_player.name,
                        'moves': self.moves_count
                    })
                    return False
                
                if self.board.is_full():
                    print(f"{Fore.YELLOW}It's a tie!{Style.RESET_ALL}")
                    self.game_history.append({
                        'winner': 'Tie',
                        'moves': self.moves_count
                    })
                    return False
                
                self.switch_player()
                return True
            return True
    
        def display_stats(self):
            print(f"\n{Fore.CYAN}=== Game Statistics ==={Style.RESET_ALL}")
            for player in self.players:
                print(f"{player.name}: {player.wins} wins")
            print(f"Total games played: {len(self.game_history)}")
            if self.game_history:
                avg_moves = sum(game['moves'] for game in self.game_history) / len(self.game_history)
                print(f"Average moves per game: {avg_moves:.1f}")
    
    # Test the enhanced game with AI
    player1 = Player("Human", "X")
    player2 = Player("AI", "O", is_ai=True)
    game = TicTacToe(player1, player2)
    
    print(f"{Fore.GREEN}=== Welcome to Advanced Tic-Tac-Toe! ==={Style.RESET_ALL}")
    game.board.display_reference()
    
    while True:
        game.board.display()
        if not game.play_turn():
            game.display_stats()
            play_again = input(f"\n{Fore.YELLOW}Play again? (y/n): {Style.RESET_ALL}").lower()
            if play_again != 'y':
                break
            game.board = Board()
            game.moves_count = 0
            print(f"\n{Fore.GREEN}=== New Game ==={Style.RESET_ALL}")
            game.board.display_reference()
    
    === Welcome to Advanced Tic-Tac-Toe! ===
    Board positions:
    Board positions:
    
    
    
     1 | 2 | 3
    ---+---+---
     4 | 5 | 6
    ---+---+---
     7 | 8 | 9
    
    
    
    
     1 | 2 | 3
    ---+---+---
     4 | 5 | 6
    ---+---+---
     7 | 8 | 9
    
    
    
    
       |   |  
    ---+---+---
       |   |  
    ---+---+---
       |   |  
    ---+---+---   |   |  
    ---+---+---
       |   |  
    
    
    
       |   |  
    
    
    
    
    
    
     X |   |  
     X |   |  
    ---+---+---
       |   | 
    ---+---+---
       |   |  
    ---+---+---
       
    ---+---+---
       |   |  
    
    
    
     |   |  
    
    
    
    
     X |   |  
    ---+---+---
    
     X |   |  
    ---+---+---
       |   |  
       |   |  
    ---+---+---
       |  ---+---+---
       |   |  
    
    
    AI AI chooses position 5
    
     |  
    
    
    AI AI chooses position 5
    
    
     X |   |  
    
     X |   |  
    ---+---+---
       | O |  
    ---+---+---
    ---+---+---
       | O |  
    ---+---+---
       |     |   |  
    
    
     |  
    
    
    Now it's Human's turn
    
    
    Now it's Human's turn
    
    
     X |   |  
    ---+---+---
     X |   |  
    ---+---+---
       | O   | O |  
     |  
    ---+---+---
       |   |  ---+---+---
       |   |  
    
    
    
    
    
    
    
     X |   | X
    
    
     X |   | X
    ---+---+---
       | O | ---+---+---
       | O |  
    ---+---+---
       |   
    ---+---+---
       |   |  
    
    
    
    
     X |   | X
     |  
    
    
    
    
     X |   | X
    ---+---+---
       | O |  ---+---+---
       | O |  
    ---+---+---
       |   | 
    ---+---+---
       |   |  
    
    
    AI AI chooses position 2
    
    
     
    
    
    AI AI chooses position 2
    
    
     X | O | X
    ---+---+--- X | O | X
    ---+---+---
       | O |  
    
       | O |  
    ---+---+---
    ---+---+---
       |     |   |  
    
    
     |  
    
    
    Now it's Human's turn
    
    
     X | O | X
    ---+---+---
       | ONow it's Human's turn
    
    
     X | O | X
    ---+---+---
       | O |  
    ---+---+---
       |   |  
    ---+---+---
       |   |  
    
    
     |  
    
    
    That spot is already taken. Try again.
    
    
     X | OThat spot is already taken. Try again.
    
    
     X | O | X
    ---+---+---
       |  | X
    ---+---+---
       | O |  
    ---+---+---
      O |  
    ---+---+---
       |   |  
    
    
     |   |  
    
    
    That spot is already taken. Try again.
    
    
     X | OThat spot is already taken. Try again.
    
    
     X | O | X
    ---+---+---
       | X
    ---+---+---
       | O |  
    ---+---+---
      | O |  
    ---+---+---
       |   |  
    
    
      |   |  
    
    
    That spot is already taken. Try again.
    
    
     X | That spot is already taken. Try again.
    
    
     X | O | X
    ---+---+---
    O | X
    ---+---+---
       | O   | O |  
    ---+---+---
       |  
    ---+---+---
       |   |   |   |  
    
    
    
    
    
    That spot is already taken. Try again.
    
    
     XThat spot is already taken. Try again.
    
    
     X | O | X
    ---+---+---
       | O | X
    ---+---+---
       | O |  
    ---+---+---
      | O |  
    ---+---+---
       |   |  
    
    
      |   |  
    
    
    That spot is already taken. Try again.
    
    
     X | OThat spot is already taken. Try again.
    
    
     X | O | X
    ---+---+---
       |  | X
    ---+---+---
       | O |  
    ---+---+---
       |   | O |  
    ---+---+---
       |   |  
    
    
     
    
    
    Please enter a number!
    
    
     X | O | XPlease enter a number!
    
    
     X | O | X
    ---+---+---
       | O | 
    ---+---+---
       | O |  
    ---+---+---
     
    ---+---+---
       |   |  
    
    
       |   |  
    
    
    
    
     X | O | X
    
     X | O | X
    ---+---+---
      
    ---+---+---
       |  | O |  
    ---+---+---O |  
    ---+---+---
       | 
       |   | X
    
      | X
    
    
    
    
    
    
     X | 
     X | O | X
    O | X
    ---+---+---
       | ---+---+---
       | O |  O |  
    ---+---+---
     
    ---+---+---
       |   | X
      |   | X
    
    
    AI AI chooses position 8
    
    
    
    
    AI AI chooses position 8
    
    
     X | O X | O | X
    ---+---+---
       | X
    ---+---+---
       | O | O |  
    ---+---+---
     |  
    ---+---+---
        | O | X
    
      | O | X
    
    
    🎉 AI WINS in 12 moves!
    🏆 AI now has 1 wins!
    
    === Game Statistics ===
    
    🎉 AI WINS in 12 moves!
    🏆 AI now has 1 wins!
    
    === Game Statistics ===
    Human: 0 wins
    AI: 1 wins
    Total games played: 1
    Average moves per game: 12.0
    Human: 0 wins
    AI: 1 wins
    Total games played: 1
    Average moves per game: 12.0
    
    class AIPlayer(Player):
        def __init__(self, name, symbol):
            super().__init__(name, symbol)
            self.is_ai = True
        
        def get_move(self, board):
            """
            Implements the AI strategy in this order:
            1. Win if possible
            2. Block opponent's winning move
            3. Take center
            4. Take corner
            5. Take any available space
            """
            # Try to win
            winning_move = self._find_winning_move(board, self.symbol)
            if winning_move is not None:
                return winning_move
            
            # Block opponent's winning move
            opponent_symbol = "O" if self.symbol == "X" else "X"
            blocking_move = self._find_winning_move(board, opponent_symbol)
            if blocking_move is not None:
                return blocking_move
            
            # Take center if available
            if board.grid[4] == " ":
                return 5
            
            # Take a corner if available
            corners = [0, 2, 6, 8]
            import random
            random.shuffle(corners)  # Add randomness to corner selection
            for corner in corners:
                if board.grid[corner] == " ":
                    return corner + 1
            
            # Take any available space
            for i in range(9):
                if board.grid[i] == " ":
                    return i + 1
        
        def _find_winning_move(self, board, symbol):
            """Check all possible moves to find a winning move"""
            for i in range(9):
                if board.grid[i] == " ":
                    # Try move
                    board.grid[i] = symbol
                    if board.check_winner(symbol):
                        board.grid[i] = " "  # Reset the move
                        return i + 1
                    board.grid[i] = " "  # Reset the move
            return None
    

    6Testing 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...")
    
    === SIMULATED GAME DEMO ===
    Starting game: Alice vs Bob
    Board positions:
    
     1 | 2 | 3
    ---+---+---
     4 | 5 | 6
    
    Starting game: Alice vs Bob
    Board positions:
    
     1 | 2 | 3
    ---+---+---
     4 | 5 | 6
    ---+---+------+---+---
     7 | 8 | 9
    
    
    
    
     7 | 8 | 9
    
    
    
    
    
       |   |     |   |  
    ---+---+---
       |   |  
    ---+---+---
       |   |  
    ---+---+---
    
    ---+---+---
       |     |   |  
    
     |  
    
    
    
    --- Alice plays position 1 ---
    
    
    --- Alice plays position 1 ---
    
    
     
    
     X |   |  
    ---+---+---X |   |  
    ---+---+---
    
       |   |     |   |  
    
    ---+---+---
       | ---+---+---
       |   |  
    
    
    Now it's Bob's turn
    
    --- Bob plays position 5 ---  |  
    
    
    Now it's Bob's turn
    
    --- Bob plays position 5 ---
    
    
    
    
    
     X |   |  
    ---+---+---
       | O |   X |   |  
    ---+---+---
       | O |  
    ---+---+---
       |   | 
    ---+---+---
       |   |  
    
    
    Now it's Alice's turn
    
    --- Alice plays position 2 ---
     
    
    
    Now it's Alice's turn
    
    --- Alice plays position 2 ---
    
    
     X | X |  
    
     X | X |  
    ---+---+---
       | O |  
    ---+---+---
    ---+---+---
       | O |  
    ---+---+---
       |   |  
    
    
    Now it's Bob's turn
    
    --- Bob plays position 6 ---
    
    
     X
       |   |  
    
    
    Now it's Bob's turn
    
    --- Bob plays position 6 ---
    
    
     X | X |  
    ---+---+---
       | O | O
    ---+---+--- | X |  
    ---+---+---
       | O | O
    ---+---+---
       |   |  
    
    
    Now it's Alice's turn
    
    --- Alice plays position 3 - should win! ---
    
    
     
       |   |  
    
    
    Now it's Alice's turn
    
    --- Alice plays position 3 - should win! ---
    
    
     X | X | X
    ---+---+---
       | O | X | X | X
    ---+---+---
       | O | O
    ---+---+---
      O
    ---+---+---
       |   |  
    
    
    🎉 Alice (X) WINS!
     |   |  
    
    
    🎉 Alice (X) WINS!
    

    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.")
    
    Charlie now has 1 wins!
    Charlie now has 2 wins!
    Your turn to experiment! Try modifying the classes above.
    
    Charlie now has 2 wins!
    Your turn to experiment! Try modifying the classes above.
    
    # Let's play a game against the AI!
    print("=== Playing against AI ===")
    
    # Create players
    human = Player("Human", "X")
    ai = AIPlayer("AI", "O")
    
    # Create game
    game = TicTacToe(human, ai)
    print("\nGame board positions:")
    game.board.display_reference()
    
    while True:
        # Human's turn
        while True:
            try:
                position = int(input("Enter your move (1-9): "))
                if 1 <= position <= 9 and game.board.make_move(position, human.symbol):
                    break
                print("Invalid move, try again.")
            except ValueError:
                print("Please enter a number between 1-9")
        
        game.board.display()
        
        # Check if human won
        if game.board.check_winner(human.symbol):
            print("🎉 You win!")
            break
        
        # Check for tie
        if game.board.is_full():
            print("It's a tie!")
            break
        
        # AI's turn
        print("\nAI is thinking...")
        ai_move = ai.get_move(game.board)
        print(f"AI chooses position {ai_move}")
        game.board.make_move(ai_move, ai.symbol)
        game.board.display()
        
        # Check if AI won
        if game.board.check_winner(ai.symbol):
            print("🤖 AI wins!")
            break
        
        # Check for tie again
        if game.board.is_full():
            print("It's a tie!")
            break
    
    # Ask to play again
    play_again = input("\nPlay again? (y/n): ").lower()
    if play_again == 'y':
        print("\nStarting new game...")
    
    === Playing against AI ===
    
    Game board positions:
    Board positions:
    
    
    Game board positions:
    Board positions:
    
     1 | 2 | 3
    ---+---+---
     4 | 5 | 6
    ---+---+---
    
     1 | 2 | 3
    ---+---+---
     4 | 5 | 6
    ---+---+---
     7 | 8 | 9
    
    
     7 | 8 | 9
    
    
    Please enter a number between 1-9
    Please enter a number between 1-9
    Please enter a number between 1-9
    Please enter a number between 1-9
    Please enter a number between 1-9
    Please enter a number between 1-9
    Please enter a number between 1-9
    Please enter a number between 1-9
    Please enter a number between 1-9
    Please enter a number between 1-9
    
    
     X |   |  
    ---+---+---
    
    
     X |   |  
    ---+---+---
       |   |  
       |   |  
    ---+---+---
      ---+---+---
       |   |  |   |  
    
    
    
    AI is thinking...
    AI chooses position 5
     
    
    
    
    AI is thinking...
    AI chooses position 5
    
    
     X |   |  
    
     X |   |  
    ---+---+---
       | O
    ---+---+---
       | O |  
    ---+---+---
      |  
    ---+---+---
       |   |  
    
    
      |   |  
    
    
    
    
     X |   | X
    
     X |   | X
    ---+---+---
       | O | 
    ---+---+---
       | O |  
    ---+---+---
       |  
    ---+---+---
       |   |  
    
    
    
    AI is thinking...
    AI chooses position 2
    
    
      |  
    
    
    
    AI is thinking...
    AI chooses position 2
    
    
     X | O |  X | O | X
    ---+---+---
       | O |  
    ---+---+---X
    ---+---+---
       | O |  
    ---+---+---
       |   |  
    
       |   |  
    
    
    
    
    That spot is already taken. Try again.
    Invalid move, try again.
    That spot is already taken. Try again.
    Invalid move, try again.
    
    
     X | O | X
    
    
     X | O | X
    ---+---+---
       | O |  ---+---+---
       | O |  
    ---+---+---
     X |   | 
    ---+---+---
     X |   |  
    
    
    
    AI is thinking...
    AI chooses position 8
    
    
     X |  
    
    
    
    AI is thinking...
    AI chooses position 8
    
    
     X | O | X
    ---+---+---
      O | X
    ---+---+---
       | O |  
    ---+---+---
      | O |  
    ---+---+---
     X | O |  
    
    
    🤖 AI wins!
    X | O |  
    
    
    🤖 AI wins!