JTic‐Tac‐Toe - rFronteddu/general_wiki GitHub Wiki

Step 1: Tic Tac Toe in Java

import java.util.Scanner; // for input

public class TicTacToe {
    static char[][] board = {
        {' ', ' ', ' '},
        {' ', ' ', ' '},
        {' ', ' ', ' '}
    };

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        char currPlayer = 'X';
        boolean gameEnded = false;
        
        System.out.println("Welcome to TTT!");
        printBoard();

        while (!gameEnded) {
            System.out.println("Player " + currPlayer + ", enter your move (row and column: 0, 1, or 2): ");
            int row = sc.nextInt();
            int col = sc.nextInt();

            // validate row and col
            if (row < 0 || row > 2 || col < 0 || col > 2) {
                System.out.println("This position is off the board, try again");
                continue;
            }

            if (board[row][col] != ' ') {
                System.out.println("Cell already taken, try again");
                continue;
            }

            board[row][col] = currPlayer;
            printBoard();
            
            // check victory
            if (checkWinner(currPlayer)) {
                System.out.println(currPlayer + " won!");
                gameEnded = true;
            } else if (isBoardFull()) {
                // tie
                gameEnded = true;
            } else {
                // switch player
                currPlayer = (currPlayer == 'X') ? 'O' : 'X';
            }
        }
        sc.close();
    }

    static void printBoard() {
        System.out.println("-------------");
        for (int i = 0; i < 3; i++) {
            System.out.print("| ");
            for (int j = 0; j < 3; j++) {
                System.out.print(board[i][j] + " | ");
            }
            System.out.println();
            System.out.println("-------------");
        }
    }

    static boolean checkWinner(char player) {
        // row and col
        for (int i = 0; i < 3; i++) { 
            if ((board[i][0] == player && board[i][1] == player && board[i][2] == player) ||
               (board[0][i] == player && board[1][i] == player && board[2][i] == player)) return true;
        }
       // diagonal
       if ((board[0][0] == player && board[1][1] == player && board[2][2] == player) ||
               (board[0][2] == player && board[1][1] == player && board[2][0] == player)) return true;

       return false;
    }
 
    static boolean isBoardFull() {
       for (int i = 0; i < 3; i++) {
           for (int j = 0; j < 3; j++) {
               if(board[i][j] == ' ') return false;
           }
       }
       return true;
    }  
}

Step 2: AI with min/max algorithm (opponent never loses!)

/**
     * Implements the Minimax algorithm to determine the optimal move in Tic Tac Toe.
     * <p>
     * This recursive function explores all possible future moves and assigns a score to each:
     * <ul>
     *   <li>+10 → Computer (AI) wins</li>
     *   <li>-10 → Human player wins</li>
     *   <li>0 → Draw</li>
     * </ul>
     * The algorithm chooses the move that maximizes the AI’s score and minimizes the human’s.
     * A depth penalty is applied to favor faster wins and slower losses.
     * <p>
     * It loops over all empty positions, simulates a move, and recursively evaluates outcomes,
     * and returns the best score based on whether it’s maximizing (AI’s turn) or minimizing (Human’s turn).
     *
     * Minmax has exponential growth, at each step it checks all possible moves of the ai and 
     * the player (the game tree with b as branching factor 
     * (moves per turn and d as depth turns ahead to look => O(b^d)). 
     * The growth is actually factorial, but in big O notation it can be considered exponential. 
     * For TTT, at the start, there are 9 possible moves with at 
     * most 9 possible turns => 9^9. Possible ways to make it faster are pruning: ignore paths that 
     * have worse scores than the ones we are examining, so 
     * if player has score alpha, if ai score is below we can stop examining the path and vice versa, 
     * memoization (store already processed paths), limit 
     * depth and use an euristic to estmate what's left, evaluate independent branches/moves in parallel.
     *
     * @param board       The current game board represented as a 2D character array.
     * @param depth       The depth of the recursive call — used to adjust scores for quicker outcomes.
     * @param isMaximizing True if it's the AI’s turn (maximizing player), false if it's the human’s turn.
     * @param ai          The character representing the AI (e.g., 'O').
     * @param human       The character representing the human player (e.g., 'X').
     * @return An integer score representing the evaluated utility of the current game state.
     */
    static int minmax(char[][] board, int depth, boolean isMaximizing, char ai, char human) {
        if (checkWinner(ai)) return 10 - depth;
        if (checkWinner(human)) return depth - 10;
        if (isBoardFull()) return 0;

        int bestScore = isMaximizing ? Integer.MIN_VALUE : Integer.MAX_VALUE;
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (board[i][j] == ' ') {
                    board[i][j] = isMaximizing ? ai : human;
                    int score = minmax(board, depth + 1, !isMaximizing, ai, human);
                    board[i][j] = ' ';
                    if (isMaximizing)
                        bestScore = Math.max(bestScore, score);
                    else
                        bestScore = Math.min(bestScore, score);
                }
            }
        }
        return bestScore;
    }

    static int[] findBestMove(char ai, char human) {
        int bestScore = Integer.MIN_VALUE;
        int[] move = new int[2];
        
        for (int i = 0; i < 3; i++) {
            for(int j = 0; j < 3; j++) {
                if(board[i][j] == ' ') {
                    board[i][j] = ai;
                    int score = minmax(board, 0, false, ai, human);
                    board[i][j] = ' ';
                    if(score > bestScore) {
                        bestScore = score;
                        move[0] = i;
                        move[1] = j;
                    }
                }
            }
        }
        return move;
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        char human = 'X';
        char ai = 'O';
        
        System.out.println("Welcome to TTT (you = " + human + ", Computer = " + ai + ")");
        printBoard();

        while (true) {
            System.out.println("Your turn (row and column: 0, 1, or 2): ");
            int row = sc.nextInt();
            int col = sc.nextInt();

            // validate row and col
            if (row < 0 || row > 2 || col < 0 || col > 2) {
                System.out.println("This position is off the board, try again");
                continue;
            }

            if (board[row][col] != ' ') {
                System.out.println("Cell already taken, try again");
                continue;
            }

            board[row][col] = human;
            printBoard();
            
            // check victory
            if (checkWinner(human)) {
                System.out.println(human + " won!");
                break;
            } else if (isBoardFull()) {
                System.out.println("It's a draw!");
                break;
            } 
            // ai turn
            System.out.println("Computer's turn...");
            int[] bestMove = findBestMove(ai, human);
            board[bestMove[0]][bestMove[1]] = ai;
            printBoard();
           
            // check victory
            if (checkWinner(ai)) {
                System.out.println("AI won!");
                break;
            } else if (isBoardFull()) {
                System.out.println("It's a draw!");
                break;
            } 
        }
        sc.close();
    }

Add a random move at the start to make the AI feel less robotic

import java.util.Random;
// ....
// add a variable count to count the moves
if (moves == 1) { // human acted first, now is the first ai turn
    Random rand = new Random();
    int row, col;
    do {
        row = rand.nextInt(3);
        col = rand.nextInt(3);
    } while (board[row][col] != ' ');
    bestMove = new int[]{row, col};
} else {
    bestMove = findBestMove(ai, human); // Use Minimax after that
}

Difficulty levels

static int[] bestMove(Board board, char ai, char hu, Difficulty level) {
    switch (level) {
        case EASY:
            return randomMove(board); // random move
        case MEDIUM:
            // 50% chance random, 50% Minimax
            if (Math.random() < 0.5)
                return randomMove(board);
            else
                return minimaxMove(board, ai, hu);
        case HARD:
        default:
            return minimaxMove(board, ai, hu); // optimal move
    }
}

Undo/redo with command pattern

The idea is to use the command pattern to encapsulate a request as an object, thereby letting you parametrize clients with queues, requests, and operations. In TTT, each move can be represented as a command that knows how to execute the move, to undo it, and that can be restored from a history.

import java.util.Scanner;
import java.util.Stack;

class Main {
    static public class Board {
        private char[][] grid = new char[3][3];
    
        public Board() {
            for (int i = 0; i < 3; i++)
                for (int j = 0; j < 3; j++)
                    grid[i][j] = ' ';
        }
    
        public boolean placeMark(int row, int col, char player) {
            if (grid[row][col] == ' ') {
                grid[row][col] = player;
                return true;
            }
            return false;
        }

        public void removeMark(int row, int col) {
            grid[row][col] = ' ';
        }
        
        public void print() {
            System.out.println("---------");
            for (int i = 0; i < 3; i++) {
                System.out.printf("| %c | %c | %c |%n", grid[i][0], grid[i][1], grid[i][2]);
                System.out.println("---------");
            }
        }
    }
    
    public interface Command {
        boolean execute();
        void undo();
    }
    
    static class MoveCommand implements Command {
        private final Board board;
        private final int row;
        private final int col;
        private final char player;
        private boolean executed = false;

        public MoveCommand(Board board, int row, int col, char player) {
            this.board = board;
            this.row = row;
            this.col = col;
            this.player = player;
        }

        @Override public boolean execute() {
            if (board.placeMark(row, col, player)) {
                executed = true;
                return true;
            } else {
                System.out.println("Invalid move!");
                return false;
            }
        }
        @Override public void undo() {
            if (executed) {
                board.removeMark(row, col);
                executed = false;
            }
        }
    }
    
    public static class CommandManager {
        private final Stack<Command> undoStack = new Stack<>();
        private final Stack<Command> redoStack = new Stack<>();

        public boolean executeCommand(Command command) {
            if (!command.execute()) {
                return false;
            }
            undoStack.push(command);
            redoStack.clear();
            return true;
        }
        
        public void undo() {
            // to undo it has to be the player turn, so I have to undo 
            if (undoStack.size() < 2) {
                System.out.println("Nothing to undo.");
                return;
            }

            // ai move
            Command command = undoStack.pop();
            command.undo();
            redoStack.push(command);
            // player move
            command = undoStack.pop();
            command.undo();
            redoStack.push(command);
        }
        
        public void redo() {
            if (redoStack.size() < 2) {
                System.out.println("Nothing to redo.");
                return;
            }
            // player move
            Command command = redoStack.pop();
            command.execute();
            undoStack.push(command);
            // ai move
            command = redoStack.pop();
            command.execute();
            undoStack.push(command);
        }
    }
    
    public static void main(String[] args) {
        Board board = new Board();
        CommandManager manager = new CommandManager();
        Scanner scanner = new Scanner(System.in);
        char hu = 'X';
        char ai = 'O';
        boolean humanTurn = true;
        while(true) {
            board.print();
            if (humanTurn) {
                System.out.println("Player turn, enter r/u to  redo/undo or row col to add a new move.");
                String input = scanner.nextLine().trim();
                if (input.equalsIgnoreCase("u")) {
                    manager.undo();
                    continue;
                } else if (input.equalsIgnoreCase("r")) {
                    manager.redo();
                    continue;
                }
                    
                try {
                    String[] parts = input.split(" ");
                    int row = Integer.parseInt(parts[0]);
                    int col = Integer.parseInt(parts[1]);
                    if (!manager.executeCommand(new MoveCommand(board, row, col, hu))) {
                        System.out.println("Invalid, try again..");
                    }
                    if (hasWon(ai)) {
                        System.out.println("AI has won!");
                        break;
                    }
                    if (boardFull()) {
                        System.out.println("It's a tie!");
                        break;
                    }
                    humanTurn = false;
                } catch (Exception e) {
                    System.out.println("Invalid, try again..");
                }
            } else {
                System.out.println("AI turn.. thinking...");
                int[] move = bestMove(ai, hu);
                manager.executeCommand(new MoveCommand(board, move[0], move[1], ai));
                if (hasWon(ai)) {
                    System.out.println("AI has won!");
                    break;
                }
                if(boardFull()) {
                    System.out.println("It's a tie!");
                    break;
                }
                // get best move execute it, check for tie victory
                humanTurn = true;
            }
        }
    }
}
⚠️ **GitHub.com Fallback** ⚠️