7.13.3. Tic-Tac-Toe

Tic-tac-toe (also known as noughts and crosses or Xs and Os) is a simple "paper and pencil" game. Two players alternate turns, with the first player marking an X on a 3-by-3 game board and the second marking an O. The goal is to make three of your marks in a row, blocking and preventing your opponent from doing the same.

Tic-tac-toe was one of the first (possibly the first) games used to explore artificial intelligence. As the Wikipedia link suggests, writing a computer program to play a perfect game is relatively simple (see the section on strategy). However, at this point in our study of C++, we use tic-tac-toe only to demonstrate two-dimensional arrays and how to pass them as function arguments.

The Tic-Tac-Toe Problem

Write a C++ program that allows two players to play tic-tac-toe.

Program Requirements:

  1. Use an array to represent the playing board
  2. Initialize the array to represent an empty board
  3. Display the game board prior to every move and at the end of the game
  4. prompt each player to move in their turn
    1. The player moves by entering a row and column number and the program marks the corresponding array element with either an X or an O
    2. The program validates each move by insuring that the row and column are inbounds and that the selected space is empty - if the player enters an invalid move, the program loops until a valid move is entered
    3. As a special case, if the player enters -1 for either the row or the column, the game ends
  5. After each move, test to see if there is a winner
    1. If there is a winner, the program displays the board, announces the winner, and ends
  6. Declare a draw and end when no moves remain

Solutions

Even when a program is small and simple, programmers often have a great deal of flexibility in how they choose to implement its various parts. As the size and complexity increase, so do the number of options. The tic-tac-toe example focuses on just four choices to illustrate that some choices only work on some operating systems, and some involve tradeoffs between different parts of the program.

One of the purposes of the tic-tac-toe example is to demonstrate how to pass two-dimensional arrays to functions. That implies the program must define at least one function beyond main. But functions have value far beyond demonstrating syntax and behavior. Functions help to focus our attention on what needs to be done rather than on how to do it. Functions help us manage the complexity of increasingly large programs: they allow us to focus on one small sub-problem - one that we are more likely to understand fully, from beginning to end - while temporarily ignoring the full problem. Our first choice is how to decompose the tic-tac-toe problem into a set of mutually interoperable functions.

The picture shows the solution functions as an inverted tree. Function main is at the top, calling init_board, display, get_move, and test_win, and test_borad calling test_win at the bottom.
Functional decomposition for the tic-tac-toe program. There are many ways to decompose a problem and two experienced software designers may decompose the same problem in different ways. Furthermore, both decompositions may work and may be "good" as judged by some abstract measure. And, even if the decompositions are similar, the function names may not be.
main
Defines the board array and other variables, sequences the function calls, and loops until the game ends
init_board
Initializes the game board
display
Displays the game board, including the marks made by the players
get_move
Alternates moves between players and gets the players moves as a row and column number
test_win
Calls test_board to see if either player has won; if a play has won, displays the winner and the final state of the game board and ends the program
test_board
Examines the rows, columns, and diagonals for three of the same marks in a row and returns the winning player, if there is one, as a character: 'X' or 'O'

The second choice we explore is how we draw the game board. We play the tic-tac-toe game on a 3×3 board. As we have done all semester, we will make the board using ASCII or text characters. But even within this constraint, we still have a choice to make. As a first choice, we may use the characters '|' and '-' to represent the vertical and horizontal lines and the '+' character to represent the intersection or cross lines. Although this approach works, it doesn't result in smooth, unbroken lines. A second approach is to use the drawing symbols that are available on some systems as extended ASCII codes.

A picture showing how the program draws a tic-tax-toe board with extended ASCII characters.
const char VERT = '|';
const char HORIZ = '-';
const char CROSS = '+';
const char VERT = (char)179;
const char HORIZ = (char)196;
const char CROSS = (char)197;
(a)(b)(c)
Drawing the tic-tac-toe game board.
  1. The tic-tac-toe game board drawn graphically so that the lines are unbroken.
  2. Symbolic constants used to draw the game board with ASCII characters. Although these values will work on all computers that support ASCII characters, they produce the broken lines reminiscent of printed characters.
  3. Symbolic constants for the extended ASCII drawing characters. Using these characters results in smooth lines that look more like they are drawn rather than printed. However, these characters are not supported by all computers. These values are known to work on Windows computers but may not work on others.
Our approach is to define symbolic constants for each drawing character set, making switching between the different sets easier.

The moves a player can make at any time during gameplay depends on the previous moves made by both players: once a player makes a mark on one of the nine spaces on the board, that space is no longer available. Our tic-tac-toe program will use a two-dimensional array to represent the game board, that is, to store each player's moves. However, the array may also optionally store the characters needed to divide the board into the nine spaces where the players may make their marks. Our third choice then is how to draw the game board. Our first option is storing the line-drawing characters in the game board array, making the display function quite simple. Our second choice is to store the moves in the array and draw the lines in the display function, making it more complex.

The board with the ASCII drawing characters in the array. The array storing only game data and the program drawing the board with output statements.
(a)(b)
Options for representing the moves and the game board. Overall, the size and complexity of both approaches are about the same. Version (a) places the complexity in the init_board function while version (b) shifts the complexity to the display function.
  1. Moves and board symbols are stored together in a 2D array. This version has the advantage that the display function is small and simple. However, the init_board function is larger and more complex, and while the size of both versions of test_board is is the same, the for-loops needed in this version are a little more complex.
  2. Moves only stored in the 2D array - the board is drawn by separate statements in the display function. This implementation results in a small, simple init_board function, but the display function is much larger and more complex.

The fourth and final choice only applies to the display function needed by the version of the program represented by Figure 3(b), and just shows that a given sub-problem can of often be solved in many ways. (The display function for version (a) is simple and structured very much like the multiplication table introduced in chapter 3.) The fence post problem, also introduced in chapter 3, describes a general problem where a loop must print a repeating pattern. In the tic-tac-toe program, the pattern is ar,0|ar,1|ar,2, where each ar,c is one element of row r of the array, and | is the vertical line that is part of the playing board. Using pseudo code, we can demonstrate three of the possible ways structuring the inner loop of the display function:

for col = 0, 1, and 2
    print a[row, col]
    if col < 2
        print '|'
print new line
print a[row, 0]
for col = 1 and 2
    print '|' and a[row, col]
print new line
for col = 0 and 1
    print a[row, col] and '|'
print a[row, 2] and new line
Pseudo code solutions for the tic-tac-toe display function. Each row of the 3(b) version of the display function represents an instance of the fence post problem, which can be solved in many ways. For each solution, row (the loop control variable for an outer loop), is held constant at 0, 1, or 2.

The fence post problem is more challenging when the number of repetitions is either large, variable, or both. This problem fixes the number of repetitions in each loop to three, which greatly simplifies the problem. The following versions of the tic-tac-toe program demonstrate a few of the many choices that computer scientists may make when solving a problem with a program.

Downloadable Code

ViewDownloadComments
ttt1.cpp ttt1.cpp moves and board characters together in the array (Figure 3(a))
ttt2.cpp ttt2.cpp moves only in the array (Figure 3(b)); has three versions of the display function:
  1. Does not use the fence post problem solution
  2. Used the fence post solution
  3. Uses the fact that the game board has exactly three rows and three columns to eliminate the for-loops altogether