Monday, December 01, 2025

Building an Intelligent Connect 4: From Classic Game to AI Opponent




Introduction


Connect 4, also known as "Four in a Row" or "4 Wins," is a timeless strategy game that has captivated players since its introduction in 1974. Despite its simple rules—be the first to connect four pieces in a row—the game offers surprising strategic depth. In this article, we'll explore the journey of implementing a modern version of Connect 4, complete with an AI opponent that can challenge even experienced players.


The Foundation: Understanding Connect 4


Before diving into code, it's crucial to understand what makes Connect 4 tick. The game is played on a 6×7 grid where players take turns dropping colored discs into columns. Gravity pulls each disc to the lowest available position in the chosen column. A player wins by creating a line of four consecutive discs horizontally, vertically, or diagonally.


What makes Connect 4 particularly interesting from a computational perspective is that it's a solved game—meaning that with perfect play, the first player can always force a win. This mathematical certainty provides an excellent foundation for implementing AI strategies.


Architecture Overview


Our implementation follows a modular design with three key components:


1. Game Logic Core: Handles board state, move validation, and win detection

2. AI Engine: Implements the minimax algorithm with position evaluation

3. User Interface: Provides both terminal and web-based interaction


The Game Logic Core


The heart of our implementation is the `Connect4` class, which manages the game state through a NumPy array. This choice of data structure offers several advantages:


self.board = np.zeros((self.rows, self.cols), dtype=int)


Using NumPy provides efficient array operations and makes it easy to extract rows, columns, and diagonals for win checking. The board representation uses:

- 0 for empty cells

- 1 for human player pieces

- 2 for AI player pieces


Move Validation and Execution


One of the critical aspects of any board game implementation is ensuring moves are valid. In Connect 4, this means:


1. The selected column must be within bounds (0-6)

2. The column must have at least one empty space


The `make_move` method handles this elegantly by scanning from the bottom up:


for row in range(self.rows - 1, -1, -1):

    if self.board[row][col] == 0:

        self.board[row][col] = player

        return True



This approach mimics the physical game where pieces fall to the lowest available position.


The AI Brain: Minimax Algorithm


The AI's intelligence comes from the minimax algorithm, a decision-making algorithm for turn-based games. The core idea is simple yet powerful: assume both players play optimally, and choose the move that maximizes your minimum guaranteed outcome.


Understanding Minimax


Minimax works by building a game tree where:

- Each node represents a game state

- Each edge represents a possible move

- Leaf nodes are evaluated with a scoring function


The algorithm alternates between maximizing (AI's turn) and minimizing (human's turn) layers, propagating scores up the tree.


Alpha-Beta Pruning


Pure minimax can be computationally expensive, examining every possible game state. Alpha-beta pruning optimizes this by eliminating branches that won't affect the final decision:


alpha = max(alpha, value)

if alpha >= beta:

    break  # Beta cutoff


This optimization can reduce the number of nodes examined from O(b^d) to O(b^(d/2)) in the best case, where b is the branching factor and d is the depth.


Position Evaluation


The quality of AI play heavily depends on how well it evaluates board positions. Our evaluation function considers multiple factors:


1. Center Column Control: Pieces in the center column have more winning possibilities

2. Window Scoring: Groups of 2-3 pieces with empty spaces score higher

3. Threat Assessment: Blocking opponent's potential wins



def evaluate_window(self, window: List[int], player: int) -> int:

    score = 0

    opponent = 3 - player

    

    if window.count(player) == 4:

        score += 100

    elif window.count(player) == 3 and window.count(0) == 1:

        score += 5

    elif window.count(player) == 2 and window.count(0) == 2:

        score += 2


User Interface Design


Terminal Interface


The terminal version prioritizes clarity and simplicity. Using Unicode box-drawing characters creates a clean, professional appearance:



  0   1   2   3   4   5   6

┌───┬───┬───┬───┬───┬───┬───┐

│   │   │   │   │   │   │   

├───┼───┼───┼───┼───┼───┼───┤

│   │   │ ● │ ○ │   │   │   

└───┴───┴───┴───┴───┴───┴───┘



The use of filled circles (● and ○) provides clear visual distinction between players.


Web Interface


The web version leverages modern CSS for an engaging visual experience:


- Responsive Design: Scales appropriately across devices

- Visual Feedback: Hover effects indicate valid moves

- Smooth Animations: CSS transitions make piece placement feel natural

- Color Psychology: Red and yellow pieces mirror the classic physical game


Performance Considerations


Computational Complexity


Connect 4 has approximately 4.5 × 10^12 possible game positions. Our AI needs to balance:

- Search Depth: Deeper searches yield better play but take longer

- Response Time: Players expect moves within 1-2 seconds

- Memory Usage: Storing game trees can quickly exhaust memory


Optimization Strategies


1. Move Ordering: Evaluating center columns first improves alpha-beta pruning

2. Transposition Tables: Caching evaluated positions avoids redundant calculations

3. Iterative Deepening: Provides good moves quickly while calculating better ones


Challenges and Solutions


Challenge 1: Preventing Infinite Loops


Early implementations sometimes trapped the AI in analysis paralysis. Solution: Implement time limits and iterative deepening.


Challenge 2: Balancing Difficulty


An AI that always wins isn't fun. We addressed this by:

- Making search depth configurable

- Adding slight randomization to move selection

- Implementing different difficulty levels


Challenge 3: State Management in Web Version


Managing game state across HTTP requests required careful design:

- Server maintains authoritative game state

- Client sends only move intentions

- Server validates all moves before applying


Future Enhancements


The current implementation provides a solid foundation for several exciting enhancements:


1. Machine Learning Integration: Train a neural network on game histories

2. Opening Book: Pre-computed optimal moves for common opening sequences

3. Multiplayer Support: WebSocket integration for real-time play

4. Tournament Mode: Track statistics and implement ELO ratings

5. Mobile App: Native implementations for iOS and Android


## Lessons Learned


Building this Connect 4 implementation reinforced several software engineering principles:


1. Separation of Concerns: Keeping game logic, AI, and UI independent enables flexibility

2. Test-Driven Development: Unit tests for win detection caught edge cases early

3. User Experience Matters: Small touches like hover effects significantly improve engagement

4. Performance vs. Perfection: Sometimes "good enough" AI is better than perfect but slow AI


Conclusion


Implementing Connect 4 with an AI opponent demonstrates how classic games can be enhanced with modern technology. The combination of traditional game theory (minimax), optimization techniques (alpha-beta pruning), and contemporary web technologies creates an engaging experience that honors the original while adding new dimensions.


The beauty of Connect 4 lies not just in its simplicity but in the depth that emerges from that simplicity. Our implementation captures this essence while showcasing how artificial intelligence can create challenging, enjoyable opponents for human players.


Whether you're a game developer looking for inspiration, a student learning about AI algorithms, or simply someone who enjoys a good game of Connect 4, this implementation provides both entertainment and educational value. The code is open for modification and enhancement—perhaps your contribution will be the next evolution in this classic game's digital journey.


Happy connecting, and may your fours be ever in a row!


The Actual Code


Terminal Version (Python)


import numpy as np

import random

from typing import List, Tuple, Optional


class Connect4:

    def __init__(self):

        self.rows = 6

        self.cols = 7

        self.board = np.zeros((self.rows, self.cols), dtype=int)

        self.current_player = 1  # 1 for human, 2 for AI

        

    def display_board(self):

        """Display the current board state"""

        print("\n  0   1   2   3   4   5   6")

        print("┌───┬───┬───┬───┬───┬───┬───┐")

        

        for row in range(self.rows):

            print("│", end="")

            for col in range(self.cols):

                if self.board[row][col] == 0:

                    print("   ", end="│")

                elif self.board[row][col] == 1:

                    print(" ● ", end="│")  # Human player

                else:

                    print(" ○ ", end="│")  # AI player

            print()

            if row < self.rows - 1:

                print("├───┼───┼───┼───┼───┼───┼───┤")

        print("└───┴───┴───┴───┴───┴───┴───┘")

        

    def is_valid_move(self, col: int) -> bool:

        """Check if a column has space for a new piece"""

        return col >= 0 and col < self.cols and self.board[0][col] == 0

    

    def make_move(self, col: int, player: int) -> bool:

        """Place a piece in the specified column"""

        if not self.is_valid_move(col):

            return False

        

        # Find the lowest empty row

        for row in range(self.rows - 1, -1, -1):

            if self.board[row][col] == 0:

                self.board[row][col] = player

                return True

        return False

    

    def check_win(self, player: int) -> bool:

        """Check if the specified player has won"""

        # Check horizontal

        for row in range(self.rows):

            for col in range(self.cols - 3):

                if all(self.board[row][col + i] == player for i in range(4)):

                    return True

        

        # Check vertical

        for row in range(self.rows - 3):

            for col in range(self.cols):

                if all(self.board[row + i][col] == player for i in range(4)):

                    return True

        

        # Check diagonal (top-left to bottom-right)

        for row in range(self.rows - 3):

            for col in range(self.cols - 3):

                if all(self.board[row + i][col + i] == player for i in range(4)):

                    return True

        

        # Check diagonal (bottom-left to top-right)

        for row in range(3, self.rows):

            for col in range(self.cols - 3):

                if all(self.board[row - i][col + i] == player for i in range(4)):

                    return True

        

        return False

    

    def is_board_full(self) -> bool:

        """Check if the board is completely filled"""

        return np.all(self.board != 0)

    

    def get_valid_moves(self) -> List[int]:

        """Get list of valid column moves"""

        return [col for col in range(self.cols) if self.is_valid_move(col)]

    

    def evaluate_window(self, window: List[int], player: int) -> int:

        """Evaluate a window of 4 positions"""

        score = 0

        opponent = 3 - player

        

        if window.count(player) == 4:

            score += 100

        elif window.count(player) == 3 and window.count(0) == 1:

            score += 5

        elif window.count(player) == 2 and window.count(0) == 2:

            score += 2

            

        if window.count(opponent) == 3 and window.count(0) == 1:

            score -= 4

            

        return score

    

    def score_position(self, player: int) -> int:

        """Calculate a score for the current board position"""

        score = 0

        

        # Score center column

        center_array = [int(i) for i in list(self.board[:, self.cols // 2])]

        center_count = center_array.count(player)

        score += center_count * 3

        

        # Score horizontal

        for row in range(self.rows):

            row_array = [int(i) for i in list(self.board[row, :])]

            for col in range(self.cols - 3):

                window = row_array[col:col + 4]

                score += self.evaluate_window(window, player)

        

        # Score vertical

        for col in range(self.cols):

            col_array = [int(i) for i in list(self.board[:, col])]

            for row in range(self.rows - 3):

                window = col_array[row:row + 4]

                score += self.evaluate_window(window, player)

        

        # Score diagonal

        for row in range(self.rows - 3):

            for col in range(self.cols - 3):

                window = [self.board[row + i][col + i] for i in range(4)]

                score += self.evaluate_window(window, player)

        

        for row in range(self.rows - 3):

            for col in range(self.cols - 3):

                window = [self.board[row + 3 - i][col + i] for i in range(4)]

                score += self.evaluate_window(window, player)

        

        return score

    

    def minimax(self, depth: int, alpha: float, beta: float, maximizing_player: bool) -> Tuple[Optional[int], int]:

        """Minimax algorithm with alpha-beta pruning"""

        valid_moves = self.get_valid_moves()

        is_terminal = self.check_win(1) or self.check_win(2) or len(valid_moves) == 0

        

        if depth == 0 or is_terminal:

            if is_terminal:

                if self.check_win(2):  # AI wins

                    return (None, 100000000000000)

                elif self.check_win(1):  # Human wins

                    return (None, -100000000000000)

                else:  # Draw

                    return (None, 0)

            else:  # Depth is 0

                return (None, self.score_position(2))

        

        if maximizing_player:

            value = -float('inf')

            best_col = random.choice(valid_moves)

            

            for col in valid_moves:

                # Make a copy of the board

                temp_board = self.board.copy()

                self.make_move(col, 2)

                new_score = self.minimax(depth - 1, alpha, beta, False)[1]

                self.board = temp_board

                

                if new_score > value:

                    value = new_score

                    best_col = col

                    

                alpha = max(alpha, value)

                if alpha >= beta:

                    break

                    

            return best_col, value

        

        else:  # Minimizing player

            value = float('inf')

            best_col = random.choice(valid_moves)

            

            for col in valid_moves:

                # Make a copy of the board

                temp_board = self.board.copy()

                self.make_move(col, 1)

                new_score = self.minimax(depth - 1, alpha, beta, True)[1]

                self.board = temp_board

                

                if new_score < value:

                    value = new_score

                    best_col = col

                    

                beta = min(beta, value)

                if alpha >= beta:

                    break

                    

            return best_col, value

    

    def get_ai_move(self, difficulty: int = 5) -> int:

        """Get the AI's move using minimax algorithm"""

        col, _ = self.minimax(difficulty, -float('inf'), float('inf'), True)

        return col

    

    def play(self):

        """Main game loop"""

        print("Welcome to Connect 4!")

        print("You are ● and the AI is ○")

        print("Enter a column number (0-6) to place your piece")

        

        while True:

            self.display_board()

            

            if self.current_player == 1:  # Human turn

                try:

                    col = int(input("\nYour turn! Enter column (0-6): "))

                    if not self.make_move(col, 1):

                        print("Invalid move! Try again.")

                        continue

                except (ValueError, KeyboardInterrupt):

                    print("\nGame ended by user.")

                    break

            else:  # AI turn

                print("\nAI is thinking...")

                col = self.get_ai_move()

                self.make_move(col, 2)

                print(f"AI placed in column {col}")

            

            # Check for win

            if self.check_win(self.current_player):

                self.display_board()

                if self.current_player == 1:

                    print("\n🎉 Congratulations! You won!")

                else:

                    print("\n😔 AI wins! Better luck next time!")

                break

            

            # Check for draw

            if self.is_board_full():

                self.display_board()

                print("\n🤝 It's a draw!")

                break

            

            # Switch players

            self.current_player = 3 - self.current_player


if __name__ == "__main__":

    game = Connect4()

    game.play()

```





Web Version (Flask + HTML/CSS/JavaScript)


 `app.py` (Flask Backend)

from flask import Flask, render_template, jsonify, request

import numpy as np

from connect4 import Connect4  # Import the Connect4 class from above


app = Flask(__name__)

game = None


@app.route('/')

def index():

    return render_template('index.html')


@app.route('/new_game', methods=['POST'])

def new_game():

    global game

    game = Connect4()

    return jsonify({

        'board': game.board.tolist(),

        'message': 'New game started! Your turn.'

    })


@app.route('/make_move', methods=['POST'])

def make_move():

    global game

    if game is None:

        return jsonify({'error': 'No game in progress'}), 400

    

    data = request.json

    col = data.get('column')

    

    # Human move

    if not game.make_move(col, 1):

        return jsonify({'error': 'Invalid move'}), 400

    

    # Check if human won

    if game.check_win(1):

        return jsonify({

            'board': game.board.tolist(),

            'winner': 'human',

            'message': '🎉 Congratulations! You won!'

        })

    

    # Check for draw

    if game.is_board_full():

        return jsonify({

            'board': game.board.tolist(),

            'draw': True,

            'message': '🤝 It\'s a draw!'

        })

    

    # AI move

    ai_col = game.get_ai_move()

    game.make_move(ai_col, 2)

    

    # Check if AI won

    if game.check_win(2):

        return jsonify({

            'board': game.board.tolist(),

            'winner': 'ai',

            'message': '😔 AI wins! Better luck next time!',

            'ai_move': ai_col

        })

    

    # Check for draw after AI move

    if game.is_board_full():

        return jsonify({

            'board': game.board.tolist(),

            'draw': True,

            'message': '🤝 It\'s a draw!',

            'ai_move': ai_col

        })

    

    return jsonify({

        'board': game.board.tolist(),

        'ai_move': ai_col,

        'message': 'Your turn!'

    })


if __name__ == '__main__':

    app.run(debug=True)



`templates/index.html`


<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Connect 4 - AI Game</title>

    <style>

        body {

            font-family: Arial, sans-serif;

            display: flex;

            justify-content: center;

            align-items: center;

            min-height: 100vh;

            margin: 0;

            background-color: #2c3e50;

            color: white;

        }

        

        .container {

            text-align: center;

        }

        

        h1 {

            margin-bottom: 20px;

        }

        

        .board {

            display: inline-block;

            background-color: #3498db;

            padding: 10px;

            border-radius: 10px;

            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);

        }

        

        .row {

            display: flex;

        }

        

        .cell {

            width: 60px;

            height: 60px;

            margin: 5px;

            background-color: #34495e;

            border-radius: 50%;

            cursor: pointer;

            transition: background-color 0.3s;

            position: relative;

        }

        

        .cell:hover {

            background-color: #4a5f7a;

        }

        

        .cell.player1 {

            background-color: #e74c3c;

        }

        

        .cell.player2 {

            background-color: #f1c40f;

        }

        

        .cell.disabled {

            cursor: not-allowed;

        }

        

        .message {

            margin: 20px 0;

            font-size: 18px;

            min-height: 30px;

        }

        

        button {

            background-color: #27ae60;

            color: white;

            border: none;

            padding: 10px 20px;

            font-size: 16px;

            border-radius: 5px;

            cursor: pointer;

            transition: background-color 0.3s;

        }

        

        button:hover {

            background-color: #2ecc71;

        }

        

        .column-numbers {

            display: flex;

            margin-bottom: 5px;

        }

        

        .column-number {

            width: 60px;

            margin: 5px;

            text-align: center;

            font-weight: bold;

        }

    </style>

</head>

<body>

    <div class="container">

        <h1>Connect 4 - Play Against AI</h1>

        <div class="message" id="message">Click "New Game" to start!</div>

        

        <div class="board" id="board">

            <div class="column-numbers">

                <div class="column-number">0</div>

                <div class="column-number">1</div>

                <div class="column-number">2</div>

                <div class="column-number">3</div>

                <div class="column-number">4</div>

                <div class="column-number">5</div>

                <div class="column-number">6</div>

            </div>

        </div>

        

        <div style="margin-top: 20px;">

            <button onclick="newGame()">New Game</button>

        </div>

    </div>

    

    <script>

        let gameActive = false;

        let board = [];

        

        function createBoard() {

            const boardElement = document.getElementById('board');

            // Clear existing board except column numbers

            const rows = boardElement.querySelectorAll('.row');

            rows.forEach(row => row.remove());

            

            for (let row = 0; row < 6; row++) {

                const rowElement = document.createElement('div');

                rowElement.className = 'row';

                

                for (let col = 0; col < 7; col++) {

                    const cell = document.createElement('div');

                    cell.className = 'cell';

                    cell.dataset.row = row;

                    cell.dataset.col = col;

                    cell.onclick = () => makeMove(col);

                    rowElement.appendChild(cell);

                }

                

                boardElement.appendChild(rowElement);

            }

        }

        

        function updateBoard(boardData) {

            board = boardData;

            const cells = document.querySelectorAll('.cell');

            

            cells.forEach(cell => {

                const row = parseInt(cell.dataset.row);

                const col = parseInt(cell.dataset.col);

                const value = board[row][col];

                

                cell.className = 'cell';

                if (value === 1) {

                    cell.classList.add('player1');

                } else if (value === 2) {

                    cell.classList.add('player2');

                }

                

                if (!gameActive) {

                    cell.classList.add('disabled');

                }

            });

        }

        

        async function newGame() {

            try {

                const response = await fetch('/new_game', {

                    method: 'POST',

                    headers: {

                        'Content-Type': 'application/json'

                    }

                });

                

                const data = await response.json();

                gameActive = true;

                updateBoard(data.board);

                document.getElementById('message').textContent = data.message;

            } catch (error) {

                console.error('Error starting new game:', error);

            }

        }

        

        async function makeMove(column) {

            if (!gameActive) return;

            

            try {

                const response = await fetch('/make_move', {

                    method: 'POST',

                    headers: {

                        'Content-Type': 'application/json'

                    },

                    body: JSON.stringify({ column: column })

                });

                

                if (!response.ok) {

                    const error = await response.json();

                    document.getElementById('message').textContent = error.error || 'Invalid move!';

                    return;

                }

                

                const data = await response.json();

                updateBoard(data.board);

                document.getElementById('message').textContent = data.message;

                

                if (data.winner || data.draw) {

                    gameActive = false;

                }

            } catch (error) {

                console.error('Error making move:', error);

            }

        }

        

        // Initialize board on page load

        createBoard();

    </script>

</body>

</html>




Installation and Usage


Terminal Version:

1. Save the first code block as `connect4.py`

2. Install numpy: `pip install numpy`

3. Run: `python connect4.py`


Web Version:

1. Install Flask: `pip install flask numpy`

2. Create the following structure:

  

   connect4_web/

   ├── app.py

   ├── connect4.py (the terminal version code)

   └── templates/

       └── index.html

   

3. Run: `python app.py`

4. Open browser to `http://localhost:5000`


Features


- AI Opponent: Uses minimax algorithm with alpha-beta pruning

- Difficulty Adjustment: Can modify the `difficulty` parameter in `get_ai_move()`

- Visual Board: Clear representation of the game state

- Win Detection: Checks for horizontal, vertical, and diagonal wins

- Draw Detection: Recognizes when the board is full

- Responsive Web Interface: Clean, modern design with hover effects


The AI evaluates positions based on:

- Center column control

- Potential winning combinations

- Blocking opponent's winning moves

- Creating multiple threats


You can adjust the AI difficulty by changing the depth parameter in the `get_ai_move()` method. Higher values make the AI stronger but slower.



No comments: