Program: Tic-Tac-Toe

Tic-Tac-Toe, or noughts and crosses, is a game where you don’t need to hear the rules because everybody knows what it is. We’ll be coding a version that allows two people to play against other. First, we need to draw the gameboard. We’re dealing with a console window meaning we can only output characters but that doesn’t mean we can’t draw things (look up “ASCII Art”). We can make a crude gameboard out of characters like this:

O  |   | O
---+---+---
X  | X | O
---+---+---
   |   | X

The following program draws an empty gameboard just using print statements:

public class TicTacToe {
    public static void main(String[] args) {
        System.out.println("   |   |   ");
        System.out.println("---+---+---");
        System.out.println("   |   |   ");
        System.out.println("---+---+---");
        System.out.println("   |   |   ");
    }
}
   |   |  
---+---+---
   |   |  
---+---+---
   |   |  

It may look like magic but, again, it’s just characters printed in a way that looks like a gameboard. Take line 3, for instance. It is simply:

  • 3 spaces, 1 pipe, 3 spaces, 1 pipe, 3 spaces, resulting in: ”   |   |   “.

And line 4 is:

  • 3 dashes, 1 plus, 3 dashes, 1 plus, 3 dashes, resulting in: “—+—+—“.

Lines 5, 6, and 7 follow the same pattern.

Players need to be able to enter a position. For this, we’ll number each position. If you have a full-size keyboard then you’ll notice that the numpad resembles a Tic-Tac-Toe board, with 1 at the bottom-left and 9 at the top-right. Therefore, we’ll use this numbering scheme for our game. When the player enters a position, we need to somehow replace the corresponding space character with either an X or an O character. For example, if the player entered 5, the gameboard would need to look like this:

   |   |  
---+---+---
   | X |  
---+---+---
   |   |  

To accomplish this, the string for the middle row needs to be changed from

"   |   |   "

to

"   | X |   "

For this, an array is the ideal data structure for storing the state of the gameboard. We’ll use a char array that stores 9 characters, one for each position in the gameboard. Each element in the array will be one of three characters: a space, an X, or an O. Will we print out the array elements within the gameboard itself. This means every time the array changes, we can reprint the gameboard to reflect those changes. This will become clearer in a few moments. First, we’ll create the array.

import java.util.Arrays;

public class TicTacToe {
    public static void main(String[] args) {
        char[] posArr = new char[9];
        Arrays.fill(posArr, ' ');

        System.out.println("   |   |   ");
        System.out.println("---+---+---");
        System.out.println("   |   |   ");
        System.out.println("---+---+---");
        System.out.println("   |   |   ");
    }
}

On line 5, a new char array is created with 9 elements. It is stored in variable posArr (short for positions array). Line 6 uses the fill method of the Arrays class. This method sets every element in an array to a specified value. It takes two arguments; the first is an array and the second is a value to fill it with. We pass posArr as the first argument and ' ' (a space) as the second argument. In short, this means every element in posArr gets set to a ' ' character. Next, we can print all the elements in the appropriate positions in the gameboard. The most readable way is to use printf:

import java.util.Arrays;

public class TicTacToe {
    public static void main(String[] args) {
        char[] posArr = new char[9];
        Arrays.fill(posArr, ' ');

        System.out.printf(" %c | %c | %c \n", posArr[6], posArr[7], posArr[8]);
        System.out.printf("---+---+---\n");
        System.out.printf(" %c | %c | %c \n", posArr[3], posArr[4], posArr[5]);
        System.out.printf("---+---+---\n");
        System.out.printf(" %c | %c | %c \n", posArr[0], posArr[1], posArr[2]);
    }
}

Look at the code closely. Line 8 prints the string:

" %c | %c | %c \n"

It’s still the same string as before except the middle spaces are now %c and, as you know with printf, these things are simply placeholders for the arguments that follow. Therefore, the first %c will be replaced with posArr[6], the second %c with posArr[7], and the third %c with posArr[8]. (We use %c because it’s specifically for chars.) This means whatever characters we put in index 6, 7, or 8 will show in those positions in the gameboard. Lines 10 and 12 do exactly the same thing with the other elements in the array. To make this clear, let’s see what happens if we fill the array with a different character.

import java.util.Arrays;

public class TicTacToe {
    public static void main(String[] args) {
        char[] posArr = new char[9];
        Arrays.fill(posArr, '$');

        System.out.printf(" %c | %c | %c \n", posArr[6], posArr[7], posArr[8]);
        System.out.printf("---+---+---\n");
        System.out.printf(" %c | %c | %c \n", posArr[3], posArr[4], posArr[5]);
        System.out.printf("---+---+---\n");
        System.out.printf(" %c | %c | %c \n", posArr[0], posArr[1], posArr[2]);
    }
}
[Run]
 $ | $ | $
---+---+---
 $ | $ | $
---+---+---
 $ | $ | $

The only change is on line 6. It sets all nine elements in the array to a dollar sign character. Therefore, when lines 8, 10, and 12 print the gameboard, every %c is replaced with a dollar sign, resulting in the output shown above. As you may then imagine, by changing the proper array index to an ‘X’ or an ‘O’ when a player takes their turn, the gameboard will reflect this change, thus simulating a game of Tic-Tac-Toe. Let’s see this in action by allowing the user to enter a position from the keyboard.

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

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

        char[] posArr = new char[9];
        Arrays.fill(posArr, ' ');

        System.out.print("Enter a position: ");
        int pos = sc.nextInt();
        posArr[pos] = 'X';

        System.out.printf(" %c | %c | %c \n", posArr[6], posArr[7], posArr[8]);
        System.out.printf("---+---+---\n");
        System.out.printf(" %c | %c | %c \n", posArr[3], posArr[4], posArr[5]);
        System.out.printf("---+---+---\n");
        System.out.printf(" %c | %c | %c \n", posArr[0], posArr[1], posArr[2]);
    }
}

Lines 6 and lines 11-13 are new. Lines 6 creates a Scanner for input. Lines 11 asks the user to enter a position. Line 12 obtains the number the user enters (into pos). Line 13 sets index pos in the array to the character X. If we run the program and enter 5, we’d expect an X to go in the middle according to a numpad, but it goes into position 6 instead.

[Run]
Enter a position: 5
   |   |
---+---+---
   |   | X
---+---+---
   |   |

We can clearly see why this happens by looking at line 17. posArr[5] is on the right, not in the middle, so it makes sense that entering 5 would put an X in that location. The issue is that array indexes go from 0-8 but numpads go from 1-9. It’s an easy fix–just take 1 away from the player’s input compensate for the offset, like so:

pos[num - 1] = 'X';

Let’s also surround the relevant code with a loop so we can keep entering positions.

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

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

        char[] posArr = new char[9];
        Arrays.fill(posArr, ' ');
        
        while (true) {
            System.out.print("Enter a position: ");
            int pos = sc.nextInt();
            posArr[pos - 1] = 'X';

            System.out.printf(" %c | %c | %c \n", posArr[6], posArr[7], posArr[8]);
            System.out.printf("---+---+---\n");
            System.out.printf(" %c | %c | %c \n", posArr[3], posArr[4], posArr[5]);
            System.out.printf("---+---+---\n");
            System.out.printf(" %c | %c | %c \n", posArr[0], posArr[1], posArr[2]);
        }
    }
}

Line 12-20 are now encapsulated in a while loop, which is never-ending since the condition is true but this is fine because line 13 will always block the program for more input. Here is how the game plays now:

[Run]
Enter a position: 4
   |   |  
---+---+---
 X |   |  
---+---+---
   |   |  
Enter a position: 9
   |   | X
---+---+---
 X |   |  
---+---+---
   |   |  
Enter a position: 2
   |   | X
---+---+---
 X |   |  
---+---+---
   | X |  
Enter a position: 5
   |   | X
---+---+---
 X | X |  
---+---+---
   | X |  

As you can see, every time a number is entered, an X is placed in the correct position. For instance, on the first go, the player entered 4, which gets stored in variable pos on line 13. Therefore line 14 reads:

posArr[4 - 1] = 'X';

4 – 1 is 3 so posArr[3] gets set to 'X'. Then, when it comes to printing out the gameboard on lines 16-20, every %c is replaced with a value from the array. The values are all spaces (due to line 9) except for posArr[3] which now contains an 'X'. This is why we see an X in the middle left position since line 18 replaces the first %c with posArr[3].

Next, let’s make sure the game alternates between X’s and O’s. One easy way to do this is to declare a char variable called turn that switches between X and O after every turn.

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

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

        char[] posArr = new char[9];
        Arrays.fill(posArr, ' ');

        char turn = 'X';

        while (true) {
            System.out.printf("Enter a position (%c): ", turn);
            int pos = sc.nextInt();
            posArr[pos - 1] = turn;

            System.out.printf(" %c | %c | %c \n", posArr[6], posArr[7], posArr[8]);
            System.out.printf("---+---+---\n");
            System.out.printf(" %c | %c | %c \n", posArr[3], posArr[4], posArr[5]);
            System.out.printf("---+---+---\n");
            System.out.printf(" %c | %c | %c \n", posArr[0], posArr[1], posArr[2]);

            turn = (turn == 'X') ? 'O' : 'X';
        }
    }
}

On line 11, variable turn is declared. If turn contains an X, that means its X’s turn and if turn contains an O, that means it’s O’s turn. To start, it contains an ‘X’. Line 14 asks the user to enter a position and also prints turn in brackets as this informs the players whose turn it is. When the player enters a position, line 16, like before, sets the relevant index in posArr to turn (X). Then the gameboard is printed. Then, line 24 sets turn to the opposite character (‘O’ if turn is ‘X’ and ‘X’ if turn is ‘O’). Therefore, turn now contains an ‘O’. The process then repeats—line 14 asks the user for a position again, line 16 sets the relevant index in posArr to turn (‘O’ this time), the gameboard is printed again and line 24 sets turn back to ‘X’, ad infinitum. One problem is that either player can place a piece in a position that’s already occupied. We can see this in the sample run below in the last two turns:

[Run]
Enter a position (X): 5
   |   |  
---+---+---
   | X |  
---+---+---
   |   |  
Enter a position (O): 4
   |   |  
---+---+---
 O | X |  
---+---+---
   |   |  
Enter a position (X): 1
   |   |  
---+---+---
 O | X |  
---+---+---
 X |   |  
Enter a position (O): 9
   |   | O
---+---+---
 O | X |  
---+---+---
 X |   |  
Enter a position (X): 9
   |   | X
---+---+---
 O | X |  
---+---+---
 X |   |  

O chose position 9, then X chose position 9, which overwrote O. To stop this from happening we can use a loop to keep asking the player for a position until they enter a valid one:

char turn = 'X';

while (true) {
    int pos;
    do {
        System.out.printf("Enter a position (%c): ", turn);
        pos = sc.nextInt();
    } while (posArr[pos - 1] != ' ');

    posArr[pos - 1] = turn;

    System.out.printf(" %c | %c | %c \n", posArr[6], posArr[7], posArr[8]);
    System.out.printf("---+---+---\n");

A do-while loop has been added (lines 6-9) that encapsulates the input request to the player. Like before, line 7 asks for a position and line 8 obtains it. But this time, on line 9, the position is checked for validity in the do-while loop’s condition. It works by checking that the position in the array is not equal to ' ' (an empty space). If true, that means the space is occupied (presumably by an ‘X’ or ‘O’) and the do-while loop repeats, asking for another position. This can be seen in the following run:

[Run]
Enter a position (X): 5
   |   |  
---+---+---
   | X |  
---+---+---
   |   |  
Enter a position (O): 5
Enter a position (O): 7
 O |   |  
---+---+---
   | X |  
---+---+---
   |   |  
Enter a position (X): 5
Enter a position (X): 7
Enter a position (X): 7
Enter a position (X): 6
 O |   |  
---+---+---
   | X | X
---+---+---
   |   |  
Enter a position (O):

Here, X enters 5, then O enters 5 afterwards. This position is taken so O is asked for a position again and enters 7, which is accepted. Then on the next turn, X enters 5, which is occupied by X already so it’s invalid. X then enters 7 (twice), which is occupied by O so it’s also invalid. X then enters 6, which is accepted since it’s a space. Also, note that the declaration of variable pos has to be outside the loop (line 5) so that it can still be used on lines 9 and 11.

Next, we want the program to check for a winner and end the game when a player has won. To do this, after every turn, the program will need to check every row, column, and diagonal to check for a line of three X’s or O’s. First, let’s look at some code that just checks the top row:

    System.out.printf(" %c | %c | %c \n", posArr[6], posArr[7], posArr[8]);
    System.out.printf("---+---+---\n");
    System.out.printf(" %c | %c | %c \n", posArr[3], posArr[4], posArr[5]);
    System.out.printf("---+---+---\n");
    System.out.printf(" %c | %c | %c \n", posArr[0], posArr[1], posArr[2]);

    if (posArr[6] == turn && posArr[7] == turn && posArr[8] == turn) {
        System.out.println(turn + " is the WINNER!");
        break;
    }

    turn = (turn == 'X') ? 'O' : 'X';
}

Lines 7-10 are an if statement. The condition is composed of three, almost identical expressions separated by two && operators. These three expressions check if a particular position has the same character in it. For example, if X just went, then the condition would be interpreted as:

posArr[6] == 'X' && posArr[7] == 'X' && posArr[8] == 'X'

If all three expressions are true, that means X occupies the top row and line 8 prints “X is the WINNER” and line 9 breaks out of the loop, which ends the program.

That’s the top row accounted for but what about the rest? We need to write a condition that covers all possibilities. Prepare yourself…

while (true) {
    int pos;
    do {
        System.out.printf("Enter a position (%c): ", turn);
        pos = sc.nextInt();
    } while (posArr[pos - 1] != ' ');

    posArr[pos - 1] = turn;

    System.out.printf(" %c | %c | %c \n", posArr[6], posArr[7], posArr[8]);
    System.out.printf("---+---+---\n");
    System.out.printf(" %c | %c | %c \n", posArr[3], posArr[4], posArr[5]);
    System.out.printf("---+---+---\n");
    System.out.printf(" %c | %c | %c \n", posArr[0], posArr[1], posArr[2]);

    if (posArr[6] == turn && posArr[7] == turn && posArr[8] == turn
            || posArr[3] == turn && posArr[4] == turn && posArr[5] == turn
            || posArr[0] == turn && posArr[1] == turn && posArr[2] == turn
            || posArr[6] == turn && posArr[3] == turn && posArr[0] == turn
            || posArr[7] == turn && posArr[4] == turn && posArr[1] == turn
            || posArr[8] == turn && posArr[5] == turn && posArr[2] == turn
            || posArr[6] == turn && posArr[4] == turn && posArr[2] == turn
            || posArr[0] == turn && posArr[4] == turn && posArr[8] == turn) {
        System.out.println(turn + " is the WINNER!");
        break;
    }

    turn = (turn == 'X') ? 'O' : 'X';
}

This may look confusing but take some time to understand it. The condition now spans lines 16 to 23 and covers all possible win conditions. It has been split over multiple lines because it’s more readable this way—every line is checking a different row, column, or diagonal (and if it were on a single line, it’d take up a metre length of screen space). The entire condition is basically the first line copied and pasted over and over with each line checking different positions/indexes. There’s eight ways you can win in Tic-Tac-Toe, hence there’s eight lines in the condition. For instance, line 20 checks indices 7, 4, 1. On the numpad that’s 8, 5, 2, i.e. the middle column. Line 23 checks 0, 4, 8 (numpad: 1, 5, 9) i.e. the ascending diagonal. Notice that every line is separated by OR operators (||) meaning if any of the eight expressions are true, the whole condition results in true and the player wins. Note that it only checks the player whose turn it is and does not check both players. So, if X just had their turn, it would only check if X has three in a line. If O just had their turn, it would only check if O has three in a line. You can only win on your turn anyway so checking both players would be redundant.

[Run]
Enter a position (X): 5
   |   |  
---+---+---
   | X |  
---+---+---
   |   |  
Enter a position (O): 3
   |   |  
---+---+---
   | X |  
---+---+---
   |   | O
Enter a position (X): 1
   |   |   
---+---+---
   | X |  
---+---+---
 X |   | O
Enter a position (O): 6
   |   |  
---+---+---
   | X | O
---+---+---
 X |   | O
Enter a position (X): 9
   |   | X
---+---+---
   | X | O
---+---+---
 X |   | O
X is the WINNER!

Note
If you think that manually checking every single possibility is a bit of a crude way of programming, then you’re right, if only partially. For a different game that uses a bigger board, a more algorithmic approach would probably be required but since Tic-Tac-Toe is only a 3×3 board, a manual, hardcoded check will suffice.

The last thing the game needs to do is account for a draw. One way would be to loop through every element in the array after every turn and check if they’re all filled. If they are, then the game must have ended in a draw. Another way is to create a variable that starts at 1 and increases by 1 every turn. If it reaches 9 and nobody has won, then the game must have ended in a draw. We’ll go down the variable route. The following program is the final version of the game, with some comments describing each part.

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

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

        // Create and fill array with empty spaces
        char[] posArr = new char[9];
        Arrays.fill(posArr, ' ');

        // turn determines whose turn it is and turnNumber is the turn number
        char turn = 'X';
        int turnNumber = 1;

        // The game loop
        while (true) {

            // Obtain input
            int pos;
            do {
                System.out.printf("Enter a position (%c): ", turn);
                pos = sc.nextInt();
            } while (posArr[pos - 1] != ' ');

            // Modify array
            posArr[pos - 1] = turn;

            // Display the gameboard
            System.out.printf(" %c | %c | %c \n", posArr[6], posArr[7], posArr[8]);
            System.out.printf("---+---+---\n");
            System.out.printf(" %c | %c | %c \n", posArr[3], posArr[4], posArr[5]);
            System.out.printf("---+---+---\n");
            System.out.printf(" %c | %c | %c \n", posArr[0], posArr[1], posArr[2]);

            // Check if player whose turn it is has won
            if (posArr[6] == turn && posArr[7] == turn && posArr[8] == turn
                    || posArr[3] == turn && posArr[4] == turn && posArr[5] == turn
                    || posArr[0] == turn && posArr[1] == turn && posArr[2] == turn
                    || posArr[6] == turn && posArr[3] == turn && posArr[0] == turn
                    || posArr[7] == turn && posArr[4] == turn && posArr[1] == turn
                    || posArr[8] == turn && posArr[5] == turn && posArr[2] == turn
                    || posArr[6] == turn && posArr[4] == turn && posArr[2] == turn
                    || posArr[0] == turn && posArr[4] == turn && posArr[8] == turn) {
                System.out.println(turn + " is the WINNER!");
                break;
            }

            // Check for a draw
            if (turnNumber >= 9) {
                System.out.println("It's a DRAW!");
                break;
            }
            // Increase turn number
            turnNumber++;

            // Change turns
            turn = (turn == 'X') ? 'O' : 'X';
        }
    }
}

Line 12 creates variable turnNumber, which is set to 1. After every turn, line 55 increments turnNumber by 1 so we know how many turns have gone. Just before this, line 50 checks to see if turnNumber is 9 (or more, just in case). If it’s less than 9, then it does nothing and the game continues. On the other hand, if turnNumber is 9 (or more), then it must be a draw and line 51 prints “It’s a DRAW!” and line 52 breaks. This causes the loop to end as well as the program.

Leave a Reply

Your email address will not be published. Required fields are marked *