Hunter Liu's Website

16. Week 8 Thursday: Tic-Tac-Toe, continued

≪ 15. Week 8 Tuesday: Tic-Tac-Toe | Table of Contents | 17. Week 9 Tuesday: Review! ≫

I ended up not being able to complete the full Tic-Tac-Toe program as planned on Tuesday; thus I anticipate that roughly the first half of our discussion will be spent filling in the member functions of the Board struct. Please refer to the preceeding notes to get caught up.

After we’ve finished making the game functional, we should start tying up the loose ends I mentioned towards the end of the preceeding notes. Let’s focus on the problem of input validation: first, we need to ensure that the user is entering integers when we ask them for a row and a column. When a malicious user decides not to do so, the game gets totally stuck and repeatedly prompts the user for their next input! (Think carefully about what’s happening in the input buffer and what causes this.) Let’s focus on this issue for now.

Currently, our input prompting and receiving occurs in the following five lines of code, somewhere deep in the main function:

1cout << "It's player " << marker << "'s turn!" << endl;
2cout << "Which row? ";
3int row; cin >> row;
4cout << "Which column? ";
5int col; cin >> col;

We ought to turn this into a function that prompts and receives the row and column from a player, perhaps called input_move.

Question 1.

What should the parameters and return type of input_move be?

I think this is a tricky choice to make. First, one should certainly provide char marker as a parameter, as the function needs to prompt either player X or player O to make a move. There’s nothing else the behaviour of the function depends upon, so it seems like this should be the only parameter.

But then what’s the return type? We have to somehow get the data of two integers back to main, i.e. the row and the column the user has input.

There are ways around this — you could, for instance, take two int&’s as parameters and have input_move directly modify those two references. This function would then have a void return type. You could also define a struct comprised of nothing but two integer variables and make that the return type (this is a rather large hammer for a rather small fly, though).

The option I’ll vie for is to make a function that writes a prompt (e.g. What row? ), waits for an input, and repeats the prompt indefinitely while the user has not entered valid integer input. We then make two separate function calls, one for the row and one for the column. One side effect is that we no longer have to pass in the marker as a parameter. The function signature would therefore be

1int input_integer(string prompt); 

This function will print out the prompt repeatedly until the user enters an integer input, then return that integer.

This function will go with the main function. This is not a member function of the Board struct — the game board isn’t the one asking the user for an input, after all. In fact, the board has nothing to do with getting input from the user! (Validating the input row/column for legality within the game is a different story, though.)

This function signature goes at the top of main.cpp (or whatever file contains your main function), and we’ll provide an implementation at the bottom. Your main.cpp file should now look something like this:

 1#include <iostream> 
 2#include <string> 
 3
 4#include "Board.hpp" 
 5
 6using namespace std; 
 7
 8/** 
 9 * Prompts a user for an integer input using the provided prompt, 
10 * repeatedly prompting the user until a valid integer input is 
11 * received. The valid integer input is then returned.
12 */ 
13int input_integer(string prompt); 
14
15int main() {
16    /* omitted for brevity */ 
17} 
18
19int input_integer(string prompt) {
20    // TODO...
21    return 1; 
22} 

Note that we now should include the string library since we’ve used the string class in our new function. Our old input system can be rewritten as follows:

1cout << "It's player " << marker << "'s turn!" << endl;
2int row = input_integer("Which row? "); 
3int col = input_integer("Which column? "); 

Compiling this works, though now the game no longer waits for user input. Let’s put our old skills with the input buffer to use and implement the input_integer function. The pseudocode is as follows:

To check that the user hasn’t screwed up the input buffer, we can use the cin.fail() function. I am hoping that this is relatively straightforward logic:

 1int input_integer(string prompt) {
 2    // repeats indefinitely... 
 3    while(true) { 
 4        // prompt the user and receive input. 
 5        cout << prompt; 
 6        int input; cin >> input; 
 7
 8        // if cin.fail() is TRUE, then the user 
 9        // really messed up. if it's false, then 
10        // we can return input now. 
11        if(!cin.fail()) {
12            return input; 
13        } 
14    } // end of while(true)  
15
16    return 0; // so our compiler doesn't warn us 
17} 

Now our game has functioning input validation! The reason we want to make this into a function is because we are calling the function more than once. Without this function, we would have to repeat the above logic and while loop multiple times throughout our program, which is undesirable.

So the user can no longer make our input processing go crazy, but they can still play illegal moves. At best, they can overwrite their moral and principled opponents’ moves; at worst, they can crash the program. So we need a way to check if a given row/column pair is a valid spot for a move: it needs to actually be on the board, and it can’t be an occupied space already.

Clearly, this behaviour depends on the state of the game: which row/column pairs are acceptable depends on which grid squares have already been filled in! Thus, we should make a new member function in the Board struct, called is_legal_move. It should receive a row and column and return a boolean, representing whether or not the given row and column are acceptable. Within board.hpp, inside the Board struct’s definition, we should add the function signature:

1/** 
2 * Determines whether or not the given row/column pair is a 
3 * legal move in the current Tic Tac Toe board. 
4 */ 
5bool is_legal_move(int row, int col); 

Likewise, we should add an implementation in board.cpp, and we can fill it in rather easily. First, we need to check that row and col are both between 1 and 3. Then, we need to check that the row and column they refer to is not a space.

 1bool Board::is_legal_move(int row, int col) {
 2    // check that both row and col are in bounds. 
 3    if(row < 1 || row > 3 || col < 1 || col > 3) {
 4        return false; 
 5    } 
 6
 7    // now just return true if the grid square is a space 
 8    // and false otherwise! no if statement needed (: 
 9    return board.at(row - 1).at(col - 1) == ' '; 
10} 

We can build and compile the program now, but we haven’t actually implemented anything! In the main function, we need to adjust the input loop slightly, as follows:

In code, it might look like:

 1cout << "It's player " << marker << "'s turn!" << endl;
 2int row, col; // need these out here because of scope! 
 3while(true) {
 4    row = input_integer("Which row? "); 
 5    col = input_integer("Which column? "); 
 6
 7    // if the move is illegal, issue a warning. 
 8    if(!board.is_valid_move(row, col)) {
 9        cout << "That's not a legal move!" << endl; 
10    } 
11
12    // otherwise, exit the loop. 
13    else { 
14        break; 
15    } 
16} 

And now we have a fully functional game of Tic Tac Toe!

I would still encourage you to try to extend this program in other ways. Can you adapt this code so that two players can play in a “first to three” format or “best two out of three” format? Can you get a list of players to play a “king of the hill” format? Can you implement a computerised opponent using the minimax algorithm (this is a challenge)?