7.14.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 programmers 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 before 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 ensuring 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 the player enters a valid move
    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. The main function is at the top, calling init_board, display, get_move, and test_win, and test_borad is 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, getting their 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 is drawn with graphic characters, producing an attractive board - the lines are unbroken - but some systems do not support these characters.
  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 are not as attractive, producing broken lines.
  3. Symbolic constants for the extended ASCII graphic drawing characters. Using these characters results in smooth lines that look more drawn than printed. These values 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 depend 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. Game 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 the same, the for-loops needed in this version are a little more complex.
  2. On the moves are stored in the 2D array - separate statements in the display function draw the board. 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 Figure 3(b) display function, demonstrating that a given sub-problem often has many solutions. (The display function for version (a) is simple and structured very much like the multiplication table introduced in chapter 3.) The fence post problem, introduced in chapter 3, is a general problem where a loop generally performs two operations but only performs one during the first or last iteration. Examples typically 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 of 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. We can view each row of the 3(b) version's display function as a fence post problem. For each solution, row (the loop control variable for an unseen outer loop) is held constant at 0, 1, or 2.

The fence post problem is more challenging when the number of repetitions is large, variable, or both. This problem fixes the number of repetitions in each loop to three, greatly simplifying the problem. The following versions of the tic-tac-toe program demonstrate a few of the many choices 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