Tic-tac-toe (also known as noughts and crosses or Xs and Os) is a simple "paper and pencil" game. Two players alternate turns; the first player marks an X on a 3-by-3 game board, and the second player marks an O. The goal of each player is to make three of their marks in a row, while blocking their 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, the text only uses the game to demonstrate two-dimensional arrays and how to pass them as function arguments.
Write a C++ program that allows two players to play tic-tac-toe.
Program Requirements:
Even when a program is small and simple, programmers often have a great deal of flexibility in how they choose to implement it, and that flexibility increases with the program's size and complexity. It's impractical to demonstrate the myriad variations, but it's beneficial to explore a few fundamental options. One goal of the tic-tac-toe example is to demonstrate the use of two-dimensional arrays. Therefore, the example begins with one possible way of distributing the program's tasks across various functions.s
Functions are a programming mainstay, helping programmers manage program complexity and eliminate duplicate code in a program. Arguably, their most significant contribution is allowing programmers to view problem solutions as logical operations, independent of their specific implementation details - they allow software developers to focus on what needs to be done rather than on how to do it. During implementation, functions allow programmers to focus on one small sub-problem at a time, while temporarily ignoring the full problem.
The second option the example explores is how to store and process the game data. As the tic-tac-toe problem appears in a chapter describing arrays, arrays are the expected storage technique, as specified in the program requirements. Opponents play the game on a 3×3 board. As we have done all semester, we draw the board with ASCII characters. Surprisingly, this constraint still provides two options. The program can use standard ASCII characters (codes 0-127). Following this approach, the program uses '|' and '-' to represent vertical and horizontal lines, and '+' to represent the intersection or cross lines. Although this approach works, it doesn't result in smooth, unbroken lines. A second approach uses the extended ASCII characters (codes 128-255), but different systems interpret these codes in various ways. Specifically, the program can use the extended ASCII drawing codes available on Windows systems.
![]() |
![]() |
const char VERT = '|'; const char HORIZ = '-'; const char CROSS = '+'; |
| (c) | ||
const char VERT = (char)179; const char HORIZ = (char)196; const char CROSS = (char)197; |
||
| (a) | (b) | (d) |
Requirement 3 specifies that players enter their moves as row and column numbers; the program validates the move and marks a valid move with an X or O. The moves players can make during their turn depend on the previous moves made by both players: once a player makes a mark on one of the board spaces, that space is no longer available. The program uses a two-dimensional array to capture the game's state after each move. To help players make valid, strategic moves, the program must display the game's current state - the spaces filled with Xs and Os - and the rows and columns a player can choose. How game developers implement the playing board affects the algorithms used to solve these problems. They can store the board drawing characters in the array, or embed the drawing characters as constants in the display function code.
| Drawing and Move Characters In board | Only Move Characters In board | |
|---|---|---|
|
|
|
| (a) | (b) | (c) |
if (row % 2) and if (col % 2). It also uses integer division to map loop control variables to the board labels:
cout << row / 2 << " ";
cout << col / 2;
board[row * 2][col * 2] = player;
Programs can always use the standard ASCII characters to draw the tic-tac-toe board. However, using extended drawing characters when available results in a more attractive game board. This paradox illustrates the broader problem of dealing efficiently with system-dependent code. Hybrid and interpreted languages solve this problem with language-specific operations that rely on a virtual machine or interpreter, ported to a specific system, to implement the system-dependant operations. Various C++ solutions are possible.
Programmers can maintain separate, parallel code files, each tailored for a specific system. However, the likelihood of failing to update a given file increases with a program's longevity, the number of its files, and the rate at which the files are modified. This approach is rare in authentic production settings. Many examples throughout the text include multiple options for performing an operation. Most of the options are commented out, leaving only one to complete the program. This approach is appropriate for small demonstration programs whose purpose is to illustrate various problem-solving strategies, but it suffers from the same likely errors as the first: at some time, someone will fail to switch the comments before releasing an updated version. Fortunately, a C and C++ offer a better approach implemented with preprocessor directives.
| Tic-Tac-Toe System Dependencies | Conditional Compilation Directives | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
#ifdef _MSC_BUILD
// extended ASCII - Windows only
const char VERT = (char)179;
const char HORIZ = (char)196;
const char CROSS = (char)197;
void clear() { system("cls"); }
#else
// basic ASCII - all systems
const char VERT = '|';
const char HORIZ = '-';
const char CROSS = '-';
void clear() { system("clear"); }
#endif |
| ||||||||||||||||||
| (a) | (b) |
Program Requirement 3 stipulates that the program must clear the screen before displaying the current game board. Historically, users connected a variety of terminals to computers, and each had a distinct ASCII control character sequence for clearing it. As a result, C++ doesn't have a built-in screen-clearing operation; instead, it uses a console command: "cls" or "clear." Modern versions of Windows open a console window with cmd or PowerShell, and "cls" clears either one. POSIX systems allow users to choose their terminal window (bash is a popular choice), and "clear" clears all of them, and also clears a PowerShell window. The C++ system library function passes its string argument to the console window, and the conditional directives use it to create a small function that clears the console.
| View | Download | Comments |
|---|---|---|
| ttt1.cpp | ttt1.cpp | The moves and board drawing characters are stored together in the array. All rows and columns are labeled. |
| ttt2.cpp | ttt2.cpp | The moves and board drawing characters together in the array. Only rows and columns corresponding to valid moves are labeled. |
| ttt3.cpp | ttt3.cpp | Only moves are stored in the array, and only rows and columns corresponding to valid moves are labeled. |