Tic-Tac-Toe: A Childhood Favorite
- Yahvin Gali
- Apr 16, 2021
- 10 min read
Updated: Sep 19, 2021
To design a Tic Tac Toe Game that allows two players (one human and one computer) to alternate placing marks in a 3X3 board until it is recorded that one of the two players has won, or the game results in a tie.
History
Tic-Tac-Toe has been around for centuries. Dating back to 1300 BC, a predecessor to Tic-Tac-Toe was played in Ancient Egypt. Another early variation was scrawled all over Rome; terni lapilli (three pebbles at a time) which only allowed players three marks to place and move around to win. Fast-forward a millennium, and the concept of Tic-Tac-Toe is prevalent in the game three man's morris, where required three pieces to be in a row to win, and Picaria, a game of the Pleuboans.
Tic-tac-Toe is a game rich with history. With modern technology, just about anyone can design their own Tic-Tac-Toe game.
Intoduction
This project serves to design a fun and engaging Tic-tac-Toe game between a computer player and a human player. Players will win according to modern rules, which are:
Any number of pieces; marks will continue to be placed until someone wins.
Moves alternate; after one piece is placed, the turn moves to the other player.
Already placed pieces cannot be moved.
Wins occur from 3 pieces in a row, a column, or a diagonal.
If no one gets three in a pattern, then the game ends with a tie.
In order to better understand my explanation of the code, access the Github Repository for this project. I suggest reading this post in sections, since we have a lot to cover.
Play by the Book
I order to ensure these rules are enforced when playing the game, several exceptions must be created. These are:
IllegalMoveException: Notifies user that the space the user wants to occupy with a piece is already filled.
public IllegalMoveException() {
super("That space is already occupied.");
}
InvalidInputException: Occurs when user enters an impossible input, such as d4, when there are rows and columns from a-c and 1-3, respectively.
/**
* Default InvalidInputException.
*/
public InvalidInputException() {
super("Invalid input. Please enter a two-character move that " +
"indicates a row and a column. Example: A2");
}
/**
* Allows creation of an InvalidInputException with a custom message.
* @param message Message detailing the cause of the exception.
*/
public InvalidInputException(String message) {
super(message);
}
NullInputException: Occurs when user enters nothing as the response to a prompt.
public NullInputException() {
super("Null input received.");
}
Now that the rules are properly defined, they need to implemented in the play sequence of the Tic-Tac-Toe Game. However, in order for the game to be played, we need to have players.
Computer Vs. Man
Before straight out defining a classes for the computer and the user, an abstract class must be created called Player.
This was done in order to define ComputerPlayer (the class that manages methods of the computer) and HumanPlayer (which provides methods that define possible moves for the user) as extensions of a class. This technique, known as inheritance, allows classes to be "children" of a larger "parent" class in order to save time and effort when designing a program, since most of the code of the child classes is identical to the parent class.
In this case, the ComputerPlayer and HumanPlayer classes have the same capabilities as the Player class:
Accessing a Tic-Tac-Toe board
Placing a piece on the board
Having a name
These capabilities are mirrored in the abstract methods makeMove() and getName(), both of which will be overridden and better defined in other player classes. For example, in ComputerPlayer, this snippet of code is present:
@Override
public String getName() {
return "Computer";
}
Similarly, the makeMove() in ComputerPlayer is written as:
boolean turnDone = false;
boolean gameOver = false;
System.out.println("\nComputer's move: ");
while (!turnDone) {
/**
* Generate random row and col values related to the
* dimensions of the board grid. The computer will continue making
* random moves until one of them succeeds.
*/
int row = new Random().nextInt(3);
int col = new Random().nextInt(3);
try {
turnDone = board.move(row,col,mark);
turnDone = true;
if((board.isWin(mark) == true) || (board.isTie() == true)){
gameOver = true;
}
else gameOver = false;
return gameOver;
} catch (IllegalMoveException e) {
// Display nothing. If the move was illegal, the computer
// will try again.
}
}
return gameOver;
I hope the comments help explain the as to why the Random class was utilized - in order to randomize legal moves on the board by the computer.
The boolean variable gameOver will make more sense when we go over the board classes, although turnDone can be understood from the above code: once a legal move occurs, turnDone becomes True, and the whiel loop ends.
Lastly, we see our first occurrence of the exceptions being used. In this case the IllegalMoveException is used as a safety net for computer moves; hence the try and catch statements.
The HumanPlayer class was similar, although it generates moves via the user. The code is shown below:
boolean turnDone = false;
boolean gameOver = false;
while (!turnDone) {
System.out.print("\nPlease enter your move: ");
String input = scanner.nextLine();
try {
turnDone = board.move(input, mark);
turnDone = true;
if((board.isWin(mark) == true) || (board.isTie() == true)){
gameOver = true;
}
else gameOver = false;
return gameOver;
} catch (InvalidInputException iie) {
System.out.println(iie.getMessage());
} catch (IllegalMoveException ime) {
System.out.println(ime.getMessage());
}
}
return turnDone;
Here, a Scanner class object was used to read in the user's move, which resulted in the utilization of the InvalidInputException. Notice that a different "kind" of the move() method was used here (more on that later). Otherwise, this method is virtually the same as the makeMove() method of the ComputerPlayer class, which further explains why inheritance was used for the Player classes.
Constructing the Board
Lastly, two classes need to be created: one that creates the Tic Tac Toe board and specifies wins and ties, and another that runs the game and displays the board after each move.
The TicTacToeBoard class creates the 'board" which is a 3x3 array of type char. It also contains both variations of the move method, both of which take an index, check if that spot on board is filled with SPACE, and then, if no exceptions are thrown, place the piece there.
The first move method is utilized in ComputerPlayer class, where the row and column arguments are each randomly generated by the computer as numbers, and then translated to coordinates. For example, if the computer generates row = 1, col = 3, the move method is called as (1,3, 'X'). The array is then checked via the getSpace() method; if it is not filled with an empty string at [0][2], then the space is filled with 'X'. The code for the move(int row, int col, char mark) is shown below:
public boolean move(int row, int col, char mark) throws IllegalMoveException {
if (grid[row][col] != SPACE) throw new IllegalMoveException();
else grid[row][col] = mark;
if (isWin(mark)==true || isTie()==true) return true;
else return false;
}
The isWin() and isTie() methods check if the performed move results in a win, or a tie, respectively. If either occurs, then the method returns true; the method returns false. This is done in order to keep track of whether or not the game ends completely, so that the continuation of alternating turns is ended, and teh relate dout put is displayed.
The overridden version of move, which accepts only a coordinate input and a mark, is used in the HumanPlayer class. For this method, proper user input specifies the location of where the user wants to put a piece (a3 is the same as [0][2] in the array). In order to validate the user input, a thorough serious of "tests" must be performed, such as checking if the input is null:
if (input == null){
throw new NullInputException();
}
ruling out incorrect input via length:
if (input.length() != 2){
throw new InvalidInputException();
}
and checking if the input each and every possible option:
if((!(input.substring(0,1).equalsIgnoreCase("A")) &&
!(input.substring(0,1).equalsIgnoreCase("B")) &&
!(input.substring(0,1).equalsIgnoreCase("C"))) ||
(!(input.substring(1).equalsIgnoreCase("1")) &&
!(input.substring(1).equalsIgnoreCase("2")) &&
!(input.substring(1).equalsIgnoreCase("3")))){
throw new InvalidInputException();
}
using various system methods, such as length(), substring(), and equalsIgnoreCase().
Lastly, if the input passes all these tests, it must be converted to coordinates in the array that is the board for the game. The code can be seen here (line 67 - 86).
To Win or Lose
Since Tic-tac-Toe only ends when one person is declared a winner, or a Tie results, two methods must be created that check for each case, respectively. The two methods, isWin() and isTie() have been referenced several times in past sections. Now is the time to explain each method in detail.
A Win...
isWin() is relatively simple at first glance: it begins with a boolean variable gameHasBeenWon, set to false, a value which will change based upon the results of various tests. The tests include checking for a:
horizontal win
vertical win
diagonal win x2 (top-left to bottom right & bottom left to top right)
Each of the checks performed can be found here, starting from line 138 and ending at 181.
In essence, each check is performed by iterating through the board (the array) in a particular fashion using a combination of for loops.
For a horizontal win, each of the 3 rows is scanned via a nested for loop. Before iteration through each cell in a row begins, a Boolean term "win" is set to true; it will turn false if a pattern is not found. Then, each cell is compared to the desired char mark ('X' or 'O'). If the checked cell is filled with the mark, then the "scope" shifts to the right.
For a vertical win, each of the 3 columns is scanned via a nested for loop. Just like the horizontal scanner, "win" is set to true, and will turn false if a pattern is not found. Then, each cell is compared to the desired char mark ('X' or 'O'). If the checked cell is filled with the mark, then the "scope" shifts down in the same column.
This difference in iterations between the horizontal scanner and vertical scanner is done by interchanging the order of the for loops. For horizontal wins, the outside loop is sorting through rows, and the inside loop is sorting through columns. The exact opposite is done for vertical wins.
Lastly, each of the diagonal wins must be checked. In similar fashion, win is set to true. However, only one loop is utilized. The for loop statement for each is shown below:
for (int i = 0; i < 3; i++) {
if (grid[i][i] != mark) win = false;
}
for (int i = 0; i < 3; i++) {
if (grid[i][2-i] != mark) win = false;
}
Using one int variable, i, the loops checking diagonally rising values by substituting i as the both the x and y coordinates for the array.
If any of the checks turn out true, gameHasBeenWon is set to true and returned as the result of the method. If none turn out true, then false is returned instead. This entire entire process effectively checks for Wins in the board. But what about Ties?
A Tie is Still a Loss
The isTie() method is far more elementary compared to isWin(), since it heavily relies on the value of gameHasBeenWon after isWin() is run. If gameHasBeenWon is true, then false is returned since the game does not end in a tie, but a win instead. The only possibility isTie() needs to directly account for is the presence of spaces: if any spaces are detected, the method returns false, since a move is still possible. The code is shown below:
boolean isTie() {
// If game has already been recorded as won, then it cannot be a tie.
if (gameHasBeenWon) return false;
// If there are any spaces on the board, return false, because it is
// not a tie.
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 3; col++) {
if (grid[row][col] == SPACE) return false;
}
}
// If the game was not won, but all the spaces are full, it is a tie.
return true;
}
With this, the possibility of a win or a tie is accounted for. Now, the board must be displayed for the user to view.
A Game to be Watched
The last two methods of importance are display() and clearGrid(), the latter of which is self-explanatory - it clears the grid entirely, replacing each and every cell in the board with a space.
display() is also pretty straightforward - it displays the current state of the Tic-Tac-Toe board for the user to view, with the rows and columns labeled, and the marks all displayed within their respective positions after a move has occurred. This explanation also bleeds into the description of the TicTacToeRunner class, which basically implements all the methods in the TicTacToeBoard class, while adding a few more elements involving user input. An example run is shown below (User input is bolded):
Welcome to Tic Tac Toe.
***** New Game *****
You are player X.
You move first.
1 2 3
A | |
-----------
B | |
-----------
C | |
Please enter your move: a5
Invalid input. Please enter a two-character move that indicates a row and a column. Example: A2
Please enter your move: d4
Invalid input. Please enter a two-character move that indicates a row and a column. Example: A2
Please enter your move: a1
1 2 3
A X | |
-----------
B | |
-----------
C | |
Computer's move:
1 2 3
A X | | O
-----------
B | |
-----------
C | |
Please enter your move: b2
1 2 3
A X | | O
-----------
B | X |
-----------
C | |
Computer's move:
1 2 3
A X | | O
-----------
B | X |
-----------
C O | |
Please enter your move: c3
1 2 3
A X | | O
-----------
B | X |
-----------
C O | | X
Player wins!
Do you wish to play again (y/n)? n
Goodbye!
(Note: I am currently learning HTML so that I can adapt my Tic Tac Toe game and add it to this website. Keep an eye out for the embedded game in the near future!)
Final Thoughts
This was, by far, the most strenuous and lengthy project I have undertaken in my short history as programmer. However, I can say with a great deal of satisfaction that I the joy experienced while working on this project is second to none.
One very interesting feature that I had quite a bit of fun adding was choosing who goes first between the computer and human players. This was done by utilizing Math.Random() in what appears to be a simulated coin toss. If the value is 0 (a heads) the player is assigned the mark X, and goes first. If the value is 1 (a tails) the computer goes first, and the player is assigned the mark O. This element of randomness in deciding who makes a move first was so entertaining to both code and test.
This project has awakened within me a true, fiery passion for Computer Science. I now realize that what I do as a programmer is not just a hobby; it's the vocation that I aim to pursue in life. I will continue to engage in programming projects outside of my school course, and may even feature them on this website. As mentioned in the note above, I will be improving several of my programming projects on this website, especially this Tic-Tac-Toe Game. Look forward to the opportunity to play Tic-Tac-Toe right here, on this website. As a fair warning, the computer player can put up a mean fight, and (out of the nearly 500 tests) wins 3 out 4 times!
Comentarios