Connect Four is a two-player connection game where players take turns dropping colored discs into a vertically suspended 6x7 grid. The objective is to be the first to form a horizontal, vertical, or diagonal line of four of one's own discs.
AI can play Connect Four effectively using algorithms like Minimax with Alpha-Beta Pruning or Monte Carlo Tree Search (MCTS). Below, we explore these approaches and provide pseudocode in Java.
Minimax evaluates all possible moves to determine the best one, assuming the opponent plays optimally. Alpha-Beta Pruning optimizes this by ignoring branches that won’t affect the final decision.
MCTS builds a search tree by simulating random games, focusing on promising moves. It balances exploration and exploitation.
Function evaluateBoard(board):
score = 0
Add points for AI's strategic positions (center, higher rows, potential 3-in-a-row)
Subtract points for human's strategic positions
Return score (non-zero unless board is empty)
Function minimax(board, depth, alpha, beta, isMaximizing):
If board.isWin(PLAYER):
Return high positive score (favor quicker wins)
If board.isWin(HUMAN):
Return high negative score (favor later losses)
If board.isDraw():
Return 0
If depth == 0:
Return evaluateBoard(board)
If isMaximizing (AI's turn):
bestScore = negative infinity
For each valid column:
Simulate AI move in column
score = minimax(new board, depth-1, alpha, beta, false)
bestScore = max(bestScore, score)
Update alpha
If beta <= alpha:
Break (prune)
Return bestScore
Else (human's turn):
bestScore = positive infinity
For each valid column:
Simulate human move in column
score = minimax(new board, depth-1, alpha, beta, true)
bestScore = min(bestScore, score)
Update beta
If beta <= alpha:
Break (prune)
Return bestScore
Function findBestMove(board):
bestMove = null
bestScore = negative infinity
For each valid column:
Simulate AI move in column
score = minimax(new board, maxDepth, negative infinity, positive infinity, false)
If score > bestScore:
bestScore = score
bestMove = column
Return [bestMove, bestScore]
Class Node:
board = game state
move = column played
visits = visit count
totalScore = cumulative score
children = list of possible next states
parent = previous node
Function select(node):
While node has children and game not over:
Select child with highest exploration-exploitation score
Return selected node
Function expand(node):
For each valid move:
Create child node with simulated move
Add child to node's children
Function simulate(board):
While game not over:
Make random valid move
Return 1 if AI wins, -1 if human wins, 0 if draw
Function backpropagate(node, score):
While node exists:
Increment visits
Add score to totalScore
Move to parent
Function findBestMove(board):
root = new Node(board)
For each of iterations:
node = select(root)
If game not over:
expand(node)
node = first child or current node
score = simulate(node's board)
backpropagate(node, score)
bestChild = child with most visits
Return [bestChild.move, bestChild.averageScore * scalingFactor]
Connect Four is a simple yet strategically deep game. Human players can succeed with center control and trap-setting, while AI solutions like Minimax with Alpha-Beta Pruning or MCTS can achieve near-perfect play. The provided Java pseudocode outlines how to implement these AI strategies, which can be adapted into a full game implementation.
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Scanner;
// Main class implementing a Connect Four game with AI opponents using Minimax and MCTS algorithms
public class ConnectFourAI {
// Constants defining the game board dimensions and player symbols
private static final int ROWS = 6; // Number of rows in the Connect Four grid
private static final int COLS = 7; // Number of columns in the Connect Four grid
private static final char PLAYER = 'X'; // Symbol representing the AI player
private static final char HUMAN = 'O'; // Symbol representing the human player
private static final char EMPTY = '.'; // Symbol representing an empty cell
// Inner class representing the game board and its operations
static class Board {
char[][] grid; // 2D array representing the game board
char currentPlayer; // Tracks whose turn it is (HUMAN or PLAYER)
// Constructor: Initializes an empty board and sets human as the starting player
Board() {
grid = new char[ROWS][COLS];
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
grid[i][j] = EMPTY; // Fill board with empty cells
}
}
currentPlayer = HUMAN; // Human starts the game
}
// Creates a deep copy of the current board for simulation purposes
Board copy() {
Board newBoard = new Board();
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
newBoard.grid[i][j] = grid[i][j]; // Copy cell values
}
}
newBoard.currentPlayer = currentPlayer; // Copy current player
return newBoard;
}
// Returns a list of columns that are not full (valid for dropping a disc)
List getValidColumns() {
List validCols = new ArrayList<>();
for (int col = 0; col < COLS; col++) {
if (grid[0][col] == EMPTY) { // Check top row of each column
validCols.add(col);
}
}
return validCols;
}
// Drops a disc for the specified player in the given column
// Returns true if successful, false if column is full
boolean dropDisc(int col, char player) {
for (int row = ROWS - 1; row >= 0; row--) { // Start from bottom row
if (grid[row][col] == EMPTY) {
grid[row][col] = player; // Place disc
currentPlayer = (player == HUMAN) ? PLAYER : HUMAN; // Switch player
return true;
}
}
return false; // Column is full
}
// Checks if the specified player has four discs in a row (horizontal, vertical, or diagonal)
boolean isWin(char player) {
// Check horizontal lines
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS - 3; j++) {
if (grid[i][j] == player && grid[i][j + 1] == player &&
grid[i][j + 2] == player && grid[i][j + 3] == player) {
return true;
}
}
}
// Check vertical lines
for (int i = 0; i < ROWS - 3; i++) {
for (int j = 0; j < COLS; j++) {
if (grid[i][j] == player && grid[i + 1][j] == player &&
grid[i + 2][j] == player && grid[i + 3][j] == player) {
return true;
}
}
}
// Check diagonal (positive slope, bottom-left to top-right)
for (int i = 3; i < ROWS; i++) {
for (int j = 0; j < COLS - 3; j++) {
if (grid[i][j] == player && grid[i - 1][j + 1] == player &&
grid[i - 2][j + 2] == player && grid[i - 3][j + 3] == player) {
return true;
}
}
}
// Check diagonal (negative slope, top-left to bottom-right)
for (int i = 0; i < ROWS - 3; i++) {
for (int j = 0; j < COLS - 3; j++) {
if (grid[i][j] == player && grid[i + 1][j + 1] == player &&
grid[i + 2][j + 2] == player && grid[i + 3][j + 3] == player) {
return true;
}
}
}
return false; // No winning condition found
}
// Checks if the game is a draw (board full and no winner)
boolean isDraw() {
for (int col = 0; col < COLS; col++) {
if (grid[0][col] == EMPTY) { // If any column has space, not a draw
return false;
}
}
return !isWin(PLAYER) && !isWin(HUMAN); // Draw if no winner and board is full
}
// Checks if the game is over (win or draw)
boolean isGameOver() {
return isWin(PLAYER) || isWin(HUMAN) || isDraw();
}
// Prints the current state of the board to the console
void printBoard() {
System.out.println(" 0 1 2 3 4 5 6"); // Column numbers
for (int i = 0; i < ROWS; i++) {
System.out.print("|");
for (int j = 0; j < COLS; j++) {
System.out.print(grid[i][j] + "|"); // Print cell contents
}
System.out.println();
}
System.out.println("---------------"); // Board bottom border
}
}
// Inner class implementing the Minimax algorithm with Alpha-Beta pruning for AI moves
static class MinimaxAI {
private final int maxDepth; // Maximum depth for Minimax search
// Constructor: Sets the maximum depth for the Minimax algorithm
MinimaxAI(int maxDepth) {
this.maxDepth = maxDepth;
}
// Evaluates the board's state for non-terminal positions using a heuristic
private int evaluateBoard(Board board) {
int score = 0;
// Weights for columns, favoring central columns (more strategic)
int[] columnWeights = {1, 2, 3, 4, 3, 2, 1};
for (int col = 0; col < COLS; col++) {
for (int row = 0; row < ROWS; row++) {
if (board.grid[row][col] == PLAYER) {
score += columnWeights[col] * (ROWS - row); // Higher rows score more
} else if (board.grid[row][col] == HUMAN) {
score -= columnWeights[col] * (ROWS - row); // Penalize human positions
}
}
}
// Reward potential three-in-a-row opportunities (not blocked)
score += countPotentialThrees(board, PLAYER) * 50;
score -= countPotentialThrees(board, HUMAN) * 50;
return score == 0 ? 1 : score; // Avoid zero score unless board is empty
}
// Counts potential three-in-a-row opportunities for the specified player
private int countPotentialThrees(Board board, char player) {
int count = 0;
// Check horizontal
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS - 3; j++) {
int playerCount = 0, emptyCount = 0;
for (int k = 0; k < 4; k++) {
if (board.grid[i][j + k] == player) playerCount++;
else if (board.grid[i][j + k] == EMPTY) emptyCount++;
}
if (playerCount == 3 && emptyCount == 1) count++; // Three discs + one empty
}
}
// Check vertical
for (int i = 0; i < ROWS - 3; i++) {
for (int j = 0; j < COLS; j++) {
int playerCount = 0, emptyCount = 0;
for (int k = 0; k < 4; k++) {
if (board.grid[i + k][j] == player) playerCount++;
else if (board.grid[i + k][j] == EMPTY) emptyCount++;
}
if (playerCount == 3 && emptyCount == 1) count++;
}
}
// Check diagonal (positive slope)
for (int i = 3; i < ROWS; i++) {
for (int j = 0; j < COLS - 3; j++) {
int playerCount = 0, emptyCount = 0;
for (int k = 0; k < 4; k++) {
if (board.grid[i - k][j + k] == player) playerCount++;
else if (board.grid[i - k][j + k] == EMPTY) emptyCount++;
}
if (playerCount == 3 && emptyCount == 1) count++;
}
}
// Check diagonal (negative slope)
for (int i = 0; i < ROWS - 3; i++) {
for (int j = 0; j < COLS - 3; j++) {
int playerCount = 0, emptyCount = 0;
for (int k = 0; k < 4; k++) {
if (board.grid[i + k][j + k] == player) playerCount++;
else if (board.grid[i + k][j + k] == EMPTY) emptyCount++;
}
if (playerCount == 3 && emptyCount == 1) count++;
}
}
return count;
}
// Minimax algorithm with Alpha-Beta pruning to evaluate moves
// Returns the score of the best move for the current board state
int minimax(Board board, int depth, int alpha, int beta, boolean isMaximizing) {
// Terminal states: AI win, human win, draw, or max depth reached
if (board.isWin(PLAYER)) return 1000 - depth; // AI win, favor quicker wins
if (board.isWin(HUMAN)) return -1000 + depth; // Human win, favor later losses
if (board.isDraw()) return 0; // Draw
if (depth == 0) return evaluateBoard(board); // Evaluate non-terminal state
if (isMaximizing) { // AI's turn (maximizing score)
int maxEval = Integer.MIN_VALUE;
for (int col : board.getValidColumns()) {
Board newBoard = board.copy();
newBoard.dropDisc(col, PLAYER); // Simulate AI move
int eval = minimax(newBoard, depth - 1, alpha, beta, false);
maxEval = Math.max(maxEval, eval);
alpha = Math.max(alpha, eval); // Update alpha
if (beta <= alpha) break; // Prune branch
}
return maxEval;
} else { // Human's turn (minimizing score)
int minEval = Integer.MAX_VALUE;
for (int col : board.getValidColumns()) {
Board newBoard = board.copy();
newBoard.dropDisc(col, HUMAN); // Simulate human move
int eval = minimax(newBoard, depth - 1, alpha, beta, true);
minEval = Math.min(minEval, eval);
beta = Math.min(beta, eval); // Update beta
if (beta <= alpha) break; // Prune branch
}
return minEval;
}
}
// Finds the best column for the AI to play, along with its evaluated score
int[] findBestMove(Board board) {
int bestMove = -1;
int bestValue = Integer.MIN_VALUE;
for (int col : board.getValidColumns()) {
Board newBoard = board.copy();
newBoard.dropDisc(col, PLAYER); // Simulate AI move
int moveValue = minimax(newBoard, maxDepth, Integer.MIN_VALUE, Integer.MAX_VALUE, false);
if (moveValue > bestValue) {
bestValue = moveValue;
bestMove = col; // Update best move if score is higher
}
}
return new int[]{bestMove, bestValue}; // Return column and score
}
}
// Inner class implementing Monte Carlo Tree Search (MCTS) for AI moves
static class MCTSAI {
// Node class for the MCTS GrayscaleFilter: Represents a game state in the search tree
static class Node {
Board board; // Board state at this node
int visits; // Number of times this node has been visited
double totalScore; // Cumulative score from simulations
List children; // Child nodes (possible moves)
Node parent; // Parent node
int move; // Column played to reach this node
// Constructor: Initializes a node with a board state and move
Node(Board board, int move) {
this.board = board;
this.move = move;
this.visits = 0;
this.totalScore = 0;
this.children = new ArrayList<>();
}
}
private final int iterations; // Number of MCTS iterations
Random random = new Random(); // Random number generator for simulations
// Constructor: Sets the number of MCTS iterations
MCTSAI(int iterations) {
this.iterations = iterations;
}
// Selects a node for expansion using UCT (Upper Confidence Bound for Trees)
Node select(Node root) {
Node current = root;
while (!current.children.isEmpty() && !current.board.isGameOver()) {
final Node finalCurrent = current;
current = current.children.stream()
.max((a, b) -> Double.compare(
a.totalScore / (a.visits + 1) + Math.sqrt(2 * Math.log(finalCurrent.visits + 1) / (a.visits + 1)),
b.totalScore / (b.visits + 1) + Math.sqrt(2 * Math.log(finalCurrent.visits + 1) / (b.visits + 1))
)).orElse(current); // Select node with highest UCT value
}
return current;
}
// Expands a node by creating child nodes for all valid moves
void expand(Node node) {
for (int col : node.board.getValidColumns()) {
Board newBoard = node.board.copy();
newBoard.dropDisc(col, newBoard.currentPlayer); // Simulate move
Node child = new Node(newBoard, col);
child.parent = node;
node.children.add(child); // Add child to node
}
}
// Simulates a random game from the given board state until a terminal state
double simulate(Board board) {
Board simBoard = board.copy();
while (!simBoard.isGameOver()) {
List validCols = simBoard.getValidColumns();
int col = validCols.get(random.nextInt(validCols.size())); // Random move
simBoard.dropDisc(col, simBoard.currentPlayer);
}
if (simBoard.isWin(PLAYER)) return 1; // AI win
if (simBoard.isWin(HUMAN)) return -1; // Human win
return 0; // Draw
}
// Backpropagates the simulation result up the tree
void backpropagate(Node node, double score) {
while (node != null) {
node.visits++;
node.totalScore += score; // Update node statistics
node = node.parent;
}
}
// Finds the best move by running MCTS and selecting the most visited child
int[] findBestMove(Board board) {
Node root = new Node(board.copy(), -1);
for (int i = 0; i < iterations; i++) {
Node node = select(root); // Select a node
if (!node.board.isGameOver()) {
expand(node); // Expand if not terminal
node = node.children.isEmpty() ? node : node.children.get(0); // Use first child
}
double score = simulate(node.board); // Simulate from node
backpropagate(node, score); // Update tree with result
}
Node bestNode = root.children.stream()
.max((a, b) -> Integer.compare(a.visits, b.visits))
.orElse(null); // Select most visited child
int bestMove = bestNode != null ? bestNode.move : -1;
double bestScore = bestNode != null ? bestNode.totalScore / bestNode.visits : 0;
return new int[]{bestMove, (int)(bestScore * 1000)}; // Return move and scaled score
}
}
// Runs the game loop, allowing a human to play against the AI
public static void playGame(Scanner scanner) {
System.out.println("Welcome to Connect Four!");
System.out.println("Choose AI: 1 for Minimax, 2 for MCTS");
int aiChoice = scanner.nextInt();
int maxDepth = 7; // Default Minimax depth
int mctsIterations = 1000; // Default MCTS iterations
// Configure AI based on user choice
if (aiChoice == 1) {
System.out.println("Enter Minimax depth (1-10, recommended 7): ");
maxDepth = Math.max(1, Math.min(10, scanner.nextInt())); // Limit depth
} else {
System.out.println("Enter MCTS iterations (100-10000, recommended 1000): ");
mctsIterations = Math.max(100, Math.min(10000, scanner.nextInt())); // Limit iterations
}
Board board = new Board(); // Create new game board
MinimaxAI minimaxAI = new MinimaxAI(maxDepth); // Initialize Minimax AI
MCTSAI mctsAI = new MCTSAI(mctsIterations); // Initialize MCTS AI
// Main game loop
while (!board.isGameOver()) {
board.printBoard(); // Display board
if (board.currentPlayer == HUMAN) {
System.out.println("Your turn (enter column 0-6): ");
int col;
do {
col = scanner.nextInt();
if (!board.getValidColumns().contains(col)) {
System.out.println("Invalid move. Try again (0-6): ");
}
} while (!board.dropDisc(col, HUMAN)); // Get valid human move
} else {
System.out.println("AI's turn...");
// Choose AI move based on user selection
int[] moveAndScore = (aiChoice == 1) ? minimaxAI.findBestMove(board) : mctsAI.findBestMove(board);
int col = moveAndScore[0];
int score = moveAndScore[1];
System.out.println("AI plays column: " + col + " (Score: " + score + ")");
board.dropDisc(col, PLAYER); // AI makes move
}
}
// Display final board and result
board.printBoard();
if (board.isWin(HUMAN)) {
System.out.println("Congratulations! You win!");
} else if (board.isWin(PLAYER)) {
System.out.println("AI wins! Better luck next time.");
} else {
System.out.println("It's a draw!");
}
}
// Main method: Entry point for the program
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in); // Create scanner for input
playGame(scanner); // Start the game
scanner.close(); // Close scanner
}
}