Search code examples
javaalgorithmminimax

TicTacToeAI with Minimax Algorithm


I have implemented Minimax Algorithm for Tic Tac Toe Game from GeeksForGeeks. I know how the Minimax Algorithm works but my code here doesn't work according to it. I have checked and checked for the things I might be doing wrong and also have debugged it. But it seems, I am not able to find it.

Please look into the algorithm, it would be much thankful for extra set of eyes and to find the incorrect part which I can't seem to find.

I have commented every part of the code that is helpful with Minimax Algorithm. I think it would be easy to catch up.

Please help me out here.

Thank you.

import java.util.Arrays;
import java.util.Scanner;

class Move {
    // row index
    protected int row;
    // column index
    protected int col;
    // exp is 'Our Expression'.
    protected char exp;
    // oppExp is 'Opponent's Expression'
    protected char oppExp;
}

class TicTacToe {

    private final char[][] arr = new char[3][3];

    // Move - to keep track of rowIndex and columnIndex
    // from the minimax algorithm. And to keep track of
    // 'X'/'O', who is our opponent for which the algorithm
    // should find moves for. 
    private final Move moves = new Move();

    TicTacToe() {
        // Initialising field
        for (int i = 0; i < 3; i++) {
            Arrays.fill(this.arr[i], ' ');
        }
    }

    public String[] whosePlaying(Scanner sc) {
        while (true) {
            System.out.print("Input command: ");
            // Getting input for players vs players
            String str = sc.nextLine();
            if (str.startsWith("start")) {
                String[] players = str.replaceAll("start", "").trim().split(" ");
                if (players.length == 2) {
                    if ((players[0].equals("user") || players[0].equals("ai")) &&
                            (players[1].equals("user") || players[1].equals("ai"))) {
                        return players;
                    }
                }
            }
            System.out.println("Bad parameters!");
        }
    }
    
    public void playUser (Scanner sc, char exp) {   // exp is expression('X' / 'O') for user

        // x is RowIndex
        // y is ColumnIndex
        // To get from the user
        int x, y;
        System.out.print("Enter the coordinates (According to Matrix, Space separated integers): ");

        while (true) {

            // try - catch for input that might not be number
            try {
                sc = new Scanner(System.in);
                x = sc.nextInt();
                y = sc.nextInt();
                break;
            } catch (Exception e) {
                System.out.println("You should enter numbers!");

                playUser(sc, exp);  // Ask again for coordinates
            }
        }

        if (x > 2 || y > 2 || x < 0 || y < 0) {
            System.out.println("Coordinates should be from 0 to 2!");

            playUser(sc, exp);  // Ask again for coordinates
        } else {
            // Check for availability.
            if (this.arr[x][y] == ' ') {
                this.arr[x][y] = exp;
                displayField(); // Displaying TicTacToe field after user's turn.
            } else {
                System.out.println("This cell is occupied! Choose another one!");

                playUser(sc, exp);  // Ask again for coordinates
            }
        }
    }

    public void playComputer (char exp) {
        System.out.println("Making move level \"AI\"");

        // Setting our expresssion that is X / O.
        moves.exp = exp;
        // Finding opponents expresssion that is X / O.
        if (moves.exp == 'X') moves.oppExp = 'O';
        else moves.oppExp = 'X';

        // Searching for the best move.
        searchBestMove();

        // Setting the best coordinates from the minimax algorithm
        // into the field with our expresssion.
        this.arr[moves.row][moves.col] = moves.exp;
        displayField(); // Displaying TicTacToe field after AI's turn.
    }
    
    // Start of Minimax Algorithm -  Contains all methods needed for the algorithm
    private void searchBestMove() {
        int bestVal = Integer.MIN_VALUE;
        moves.row = -1;
        moves.col = -1;
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (this.arr[i][j] == ' ') {
                    this.arr[i][j] = moves.exp;
                    int moveVal = minimax(0, false);
                    this.arr[i][j] = ' ';

                    if (moveVal > bestVal) {
                        moves.row = i;
                        moves.col = j;
                        bestVal = moveVal;
                    }
                }
            }
        }
    }

    private int minimax(int depth, boolean isMax) {
        int score = checkGetScore();

        if (score == 10 || score == -10) return score;
        if (!checkForSpace()) return 0;

        int best;
        if (isMax) {
            best = Integer.MIN_VALUE;
            for (int i = 0; i < 3; i++) {
                for (int j = 0; j < 3; j++) {
                    if (this.arr[i][j] == ' ') {
                        this.arr[i][j] = moves.exp;
                        best = Math.max(best, minimax(depth + 1, false));
                        this.arr[i][j] = ' ';
                    }
                }
            }
        } else {
            best = Integer.MAX_VALUE;
            for (int i = 0; i < 3; i++) {
                for (int j = 0; j < 3; j++) {
                    if (this.arr[i][j] == ' ') {
                        this.arr[i][j] = moves.oppExp;
                        best = Math.min(best, minimax(depth + 1, true));
                        this.arr[i][j] = ' ';
                    }
                }
            }
        }
        return best;
    }

    // Check for the score if the AI wins in the upcoming move
    // This method helps AI to assign score for each way in the game while searching.
    private int checkGetScore() {
        // For Rows
        for (int i = 0; i < 3; i++) {
            if (this.arr[i][0] == moves.exp && this.arr[i][1] == moves.exp && this.arr[i][2] == moves.exp) {
                return 10;
            } else if (this.arr[i][0] == moves.oppExp && this.arr[i][1] == moves.oppExp && this.arr[i][2] == moves.oppExp) {
                return -10;
            }
        }
        // For Cols
        for (int i = 0; i < 3; i++) {
            if (this.arr[0][i] == moves.exp && this.arr[1][i] == moves.exp && this.arr[2][i] == moves.exp) {
                return 10;
            } else if (this.arr[0][i] == moves.oppExp && this.arr[1][i] == moves.oppExp && this.arr[2][i] == moves.oppExp) {
                return -10;
            }
        }
        // For Diagonals
        if (this.arr[0][0] == moves.exp && this.arr[1][1] == moves.exp && this.arr[2][2] == moves.exp) {
            return 10;
        } else if (this.arr[0][0] == moves.oppExp && this.arr[1][1] == moves.oppExp && this.arr[2][2] == moves.oppExp) {
            return -10;
        } else if (this.arr[0][2] == moves.exp && this.arr[1][1] == moves.exp && this.arr[2][0] == moves.exp) {
            return 10;
        } else if (this.arr[0][2] == moves.oppExp && this.arr[1][1] == moves.oppExp && this.arr[2][0] == moves.oppExp) {
            return -10;
        }

        return 0;
    }
    // End of Minimax Algoritm

    // Displays results of Win / Tie by checking Rows, Columns and Diagonals.
    public boolean displayResult() {
        int valR = checkRows();
        int valC = checkCols();
        int diag = checkDiag();
        if (diag == 1 || diag == 3 || valR == 3 || valC == 3) {
            System.out.println("X wins");
            return true;
        } else if (diag ==  2 || diag == 4 || valR == 2 || valC == 2) {
            System.out.println("O wins");
            return true;
        } else if ((diag == 0 || valC == 1 || valR == 1) && checkForSpace()) {
            System.out.println("Draw");
            return true;
        }
        return false;
    }

    // Prints the TicTacToe field
    public void displayField () {
        System.out.println("---------");
        for (char[] a : this.arr) {
            System.out.print("| ");
            for (char b : a) {
                System.out.print(b + " ");
            }
            System.out.println("|");
        }
        System.out.println("---------");
    }
    
    // Checks for the availability of space
    // in the TicTacToe field.
    private boolean checkForSpace() {
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (this.arr[i][j] == ' ') {
                    return false;
                }
            }
        }
        return true;
    }

    // Checks the Diagonals of the field
    private int checkDiag() {
        if (this.arr[0][0] == 'X' && this.arr[1][1] == 'X' && this.arr[2][2] == 'X') {
            return 1;
        } else if (this.arr[0][0] == 'O' && this.arr[1][1] == 'O' && this.arr[2][2] == 'O') {
            return 2;
        } else if (this.arr[0][2] == 'X' && this.arr[1][1] == 'X' && this.arr[2][0] == 'X') {
            return 3;
        } else if (this.arr[0][2] == 'O' && this.arr[1][1] == 'O' && this.arr[2][0] == 'O') {
            return 4;
        } else {
            return 0;
        }
    }

    // Checks the Rows of the field
    private int checkRows () {
        int cntX = 0,
                cntO = 0;
        for (int i = 0; i < 3; i++) {
            if (this.arr[i][0] == 'X' && this.arr[i][1] == 'X' && this.arr[i][2] == 'X') {
                cntX++;
            } else if (this.arr[i][0] == 'O' && this.arr[i][1] == 'O' && this.arr[i][2] == 'O') {
                cntO++;
            }
        }

        if (cntX == 1) {
            return 3;
        } else if (cntO == 1) {
            return 2;
        } else {
            return 1;
        }
    }

    // Checks the Columns of the field
    private int checkCols () {
        int cntX = 0,
                cntO = 0;
        for (int i = 0; i < 3; i++) {
            if (this.arr[0][i] == 'X' && this.arr[1][i] == 'X' && this.arr[2][i] == 'X') {
                cntX++;
            } else if (this.arr[0][i] == 'O' && this.arr[1][i] == 'O' && this.arr[2][i] == 'O') {
                cntO++;
            }
        }

        if (cntX == 1) {
            return 3;
        } else if (cntO == 1) {
            return 2;
        } else {
            return 1;
        }
    }

}

public class MinimaxAlgorithm {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        TicTacToe tic = new TicTacToe();

        // For game to start specify players
        // Eg: start user ai    <-- user is X and user chance comes first.
        // Eg: start ai user    <-- ai is X and ai chance comes first.
        // Eg: start ai ai      <-- ai vs ai
        // Eg: user user        <-- user vs user
        String[] players = tic.whosePlaying(sc);

        // Displays the empty field of TicTacToe
        tic.displayField();

        // turnX = true. X's turn
        // turnX = false. O's turn
        boolean turnX = true;

        while (true) {

            // Alternate player turns
            // First chance of X, than O.
            if (turnX) {
                switch (players[0]) {
                    case "ai":
                        tic.playComputer('X');
                        break;
                    default:
                        tic.playUser(sc, 'X');
                        break;
                }
                turnX = false;
            } else {
                switch (players[1]) {
                    case "ai":
                        tic.playComputer('O');
                        break;
                    default:
                        tic.playUser(sc, 'O');
                        break;
                }
                turnX = true;
            }

            // displayresult() - Checks if the game is over by anyone winning or tie.
            // If someone wins or ties the "check" becomes true and finishes the game.
            // Checks after each move made by the players.
            boolean check = tic.displayResult();
            if (check) break;
        }
        sc.close();
    }
}

The blue arrow indicates where the mistake happened. The green marking specifies how it should have been. Working

CONSOLE:

Input command: start user ai
---------
|       |
|       |
|       |
---------
Enter the coordinates (According to Matrix, Space separated integers): 1 1
---------
|       |
|   X   |
|       |
---------
Making move level "AI"
---------
| O     |
|   X   |
|       |
---------
Enter the coordinates (According to Matrix, Space separated integers): 0 2
---------
| O   X |
|   X   |
|       |
---------
Making move level "AI"
---------
| O O X |    <-- Explanation: The AI made its move on [0, 1] 
|   X   |                    but it should have did on [2, 0]
|       |                    which will block my win on right diagonal.
---------
Enter the coordinates (According to Matrix, Space separated integers): 2 0
---------
| O O X |
|   X   |
| X     |
---------
X wins

Solution

  • There a mismatch between your function checkForSpace() and its use in minimax(). If there is space left you return false and if you get a false in minimax() you stop the search as if there is a tie. You need to invert the boolean in checkForSpace() or remove the logical not in minimax().

    You should propably rename checkForSpace() and other function that return a boolean. From The Art of Readable Code (Dustin Boswell and Trevor Foucher):

    When picking a name for a boolean variable or a function that returns a boolean, be sure it’s clear what true and false really mean.

    Here’s a dangerous example:

    bool read_password = true;
    

    Depending on how you read it (no pun intended), there are two very different interpretations:

    • We need to read the password

    • The password has already been read

    In this case, it’s best to avoid the word "read," and name it need_password or user_is_authenticated instead.

    In general, adding words like is, has, can, or should can make booleans more clear.

    For example, a function named SpaceLeft() sounds like it might return a number. If it were meant to return a boolean, a better name would be HasSpaceLeft().

    Finally, it’s best to avoid negated terms in a name. For example, instead of:

    bool disable_ssl = false;
    

    it would be easier to read (and more compact) to say:

    bool use_ssl = true;