Today we will deepen our knowledge of Java by creating a Tic-Tac-Toe game that runs from the command line (console). The process will consider working with arrays, as well as some aspects of object-oriented programming (non-static methods, non-static fields, constructor).

Arrays
When writing a game, an array is used, so let’s first take a look at what it is. Arrays store a set of variables of the same type. If a variable looks like a box, with a type, name and value written on the side, then the array is like a block of such boxes. Both the type and the name of the block are the same, and access to a particular box (value) occurs by the number (index).

In Java, arrays are objects, they are created using the new directive. When creating, we indicate the number of elements of the array or initialize it with a set of values. The code below illustrates both options:

class Arrays {
    public static void main(String[] args) {
        int[] arr = new int[5];
        int[] arrInit = {1, 2, 3, 4, 5};
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i * 2 + arrInit[i];
        }
        for (int a : arr) {
            System.out.println(a);
        }
    }
}

You can work with array elements as with ordinary variables, assigning the result of an expression to them and reading stored values. In this case, the index of the array element is indicated in square brackets. Indexing in Java starts from 0 (from zero). The first loop initializes the elements of the arr array using the values ​​from the arrInit array. Each array has a length field that contains the number of its elements. The second loop prints the elements of the array to the console using the second for option – without the loop counter.

Methods
In addition to main (), a class can contain other methods. As an example, consider a class with an add () method that computes and returns the sum of two values ​​passed as parameters. Pay attention to the int type that comes before the method name – this is the return type. The two variables in brackets are parameters. The collection of the name and parameters is called the signature of the method. The method is called by name, the values ​​to be passed are indicated in parentheses. In the method, they get into the variable parameters. The return command returns the result of the addition of these two variables and exits the method.

class MethodStatic {
    public static void main(String[] args) {
        int c = add(5, 6);
        System.out.println("5 + 6 = " + c);
    }
 
    static int add(int a, int b) {
        return a + b;
    }
}

The word static means that the method is static. If we refer to any method from a static method, then the callee must also be static. This is why add() is static – it is called from static main(). Using static methods is the exception rather than the rule, so let’s see how to make add() non-static.

The only solution is to create an object based on the class. And then call the method with a dot after the object name. In this case, the method can be non-static. The code below illustrates this.

class MethodNotStatic {
    public static void main(String[] args) {
        MethodNotStatic method = new MethodNotStatic();
        int c = method.add(5, 6);
        System.out.println("5 + 6 = " + c);
    }
 
    int add(int a, int b) {
        return a + b;
    }
}

Class fields
Variables exist only within the method where they are declared. And if you need a variable that is available in all methods of a class, then it’s time to use fields. Fields are declared like variables, specifying the type and name. But they are located not in methods, but directly in the body of the class. Like methods, they can be static or non-static. Non-static fields, like methods, are available only after the object is created.

class FieldExample {
    int a;
 
    public static void main(String[] args) {
        FieldExample field = new FieldExample();
        field.a = 12;
        System.out.println("a = " + field.a);
        System.out.println(field.getA());
        field.printA();
    }
 
    int getA() {
        return a;
    }
 
    void printA() {
        System.out.println(a);
    }
}

The above code illustrates how to work with a non-static int a field. It is customary to place descriptions of fields first in the class code, followed by descriptions of methods. We get the ability to access the field (write, read) only after the object is created. You can also see that this field is available in all non-static methods of the object, and in static main() – through a dot after the object name.

Tic-tac-toe. Class template


Let’s start writing the game code. Let’s start with a class template and defining the required fields. This is what the code below contains. The first two lines are class imports. First in the body of the class are descriptions of fields, then methods. The main() method is used to create an object (since fields and methods are non-static) and call the game() method with game logic.

import java.util.Random;
import java.util.Scanner;
 
class TicTacToe {
    final char SIGN_X = 'x';
    final char SIGN_O = 'o';
    final char SIGN_EMPTY = '.';
    char[][] table;
    Random random;
    Scanner scanner;
 
    public static void main(String[] args) {
        new TicTacToe().game();
    }
    TicTacToe() {
        
    }
    void game() {
        
    }
}

We use three symbolic constants as fields: SIGN_X, SIGN_O and SIGN_EMPTY. Their values ​​cannot be changed, this is indicated by the final modifier. The two-dimensional character array table will be our playing field. You will also need a random object to generate computer moves and a scanner for user input.

Method names are usually written with a lowercase letter. However, in the code we see the TicTacToe () method – is there a violation here? No, because this method is special and is called a constructor in object-oriented programming. The constructor is called immediately after the object is created. Its name, as you can see, must match the name of the class. We use a constructor to initialize the fields.

TicTacToe() {
    random = new Random();
    scanner = new Scanner(System.in);
    table = new char[3][3];
}

Game logic
The game logic is located in the game () method and is based on an infinite while loop. Below in the code snippet, the sequence of actions is described through comments:

// initialize the table
while (true) {
     // human move
     // check: if a person wins or a draw:
     // report and exit the loop
     // computer move
     // check: if the computer wins or a draw:
     // report and exit the loop
}

When writing working code, each action – for example, “human move”, “computer move”, “check” – we will replace with a call to the appropriate method. In case of a winning or a draw situation (all cells of the table are filled), exit the loop using break, ending the game.

void game() {
    initTable();
    while (true) {
        turnHuman();
        if (checkWin(SIGN_X)) {
            System.out.println("YOU WIN!");
            break;
        }
        if (isTableFull()) {
            System.out.println("Sorry, DRAW!");
            break;
        }
        turnAI();
        printTable();
        if (checkWin(SIGN_O)) {
            System.out.println("AI WIN!");
            break;
        }
        if (isTableFull()) {
            System.out.println("Sorry, DRAW!");
            break;
        }
    }
    System.out.println("GAME OVER.");
    printTable();
}

Implementing Helper Methods
It’s time to write the code for the methods called in game (). The very first, initTable (), provides initial initialization of the game table, filling its cells with “empty” characters. The outer loop, with a counter int row, selects rows, and the inner one, with a counter int col, iterates over the cells in each row.

void initTable() {
    for (int row = 0; row < 3; row++)
        for (int col = 0; col < 3; col++)
            table[row][col] = SIGN_EMPTY;
}

You will also need a method that displays the current state of the game table printTable ().

void printTable() {
    for (int row = 0; row < 3; row++) {
        for (int col = 0; col < 3; col++)
            System.out.print(table[row][col] + " ");
        System.out.println();
    }
}

In the turnHuman () method, which allows the user to make a move, we use the nextInt () method of the scanner object to read two integers (cell coordinates) from the console. Pay attention to how the do-while loop is used: the coordinate request is repeated if the user specifies the coordinates of an invalid cell (the table cell is busy or does not exist). If everything is in order with the cell, the SIGN_X symbol – “cross” is entered there.

void turnHuman() {
    int x, y;
    do {
        System.out.println("Enter X and Y (1..3):");
        x = scanner.nextInt() - 1;
        y = scanner.nextInt() - 1;
    } while (!isCellValid(x, y));
    table[y][x] = SIGN_X;
}

The validity of a cell is determined by the isCellValid () method. It returns a boolean value: true – if the cell is free and exists, false – if the cell is busy or incorrect coordinates are specified.

boolean isCellValid(int x, int y) {
    if (x < 0 || y < 0 || x >= 3|| y >= 3)
        return false;
    return table[y][x] == SIGN_EMPTY;
}

The turnAI () method is similar to the turnHuman () method using a do-while loop. Only the coordinates of the cell are not read from the console, but are generated randomly using the nextInt (3) method of the random object. The number 3, passed as a parameter, is a delimiter. Thus, random integers from 0 to 2 are generated (within the indices of the game table array). And the isCellValid () method again allows us to select only free cells to enter the SIGN_O sign – “zero” in them.

void turnAI() {
    int x, y;
    do {
        x = random.nextInt(3);
        y = random.nextInt(3);
    } while (!isCellValid(x, y));
    table[y][x] = SIGN_O;
}

It remains to add the last two methods – a win check and a draw check. The checkWin () method checks the game table for a “winning three” – three identical signs in a row, vertically or horizontally (in a loop), as well as along two diagonals. The sign to be checked is specified as the char dot parameter, due to which the method is universal – you can check the victory by both “crosses” and “zeroes”. Boolean true is returned on victory, false otherwise.

boolean checkWin(char dot) {
    for (int i = 0; i < 3; i++)
        if ((table[i][0] == dot && table[i][1] == dot &&
                         table[i][2] == dot) ||
                (table[0][i] == dot && table[1][i] == dot &&
                                  table[2][i] == dot))
            return true;
        if ((table[0][0] == dot && table[1][1] == dot &&
                  table[2][2] == dot) ||
                    (table[2][0] == dot && table[1][1] == dot &&
                      table[0][2] == dot))
            return true;
    return false;
}

The isTableFull () method in a nested double loop loops through all the cells of the game table and, if they are all occupied, returns true. If at least one cell is still free, false is returned.

boolean isTableFull() {
    for (int row = 0; row < 3; row++)
        for (int col = 0; col < 3; col++)
            if (table[row][col] == SIGN_EMPTY)
                return false;
    return true;
}

Now it remains to collect all these methods inside TicTacToe. The sequence of their placement in the class body is not important. And then you can try to play tic-tac-toe with the computer.

Leave a Reply