CS202 Lecture Notes - Classes, Header/Source/Object/Executable Files

Overview, Compilation and Structure

This is a big lecture. My goal with this is to show you a typical well-structured Unix directory which includes a C++ class, and a suite of programs that make use of it. The class that we are going to implement is a game player for Tic-Tac-Toe. Before going into the code, let's talk about our directory structure. We have four directories: In this lecture, we define a class called Tic_Tac_Toe in the header file include/tic_tac_toe.hpp. This defines the methods and variables in the class. However, it does not implement the methods. That is done in src/tic_tac_toe.cpp. When we compile src/tic_tac_toe.cpp, it will be to the object file obj/tic_tac_toe.o.

Since src/tic_tac_toe.cpp does not have a main(), it is not a complete program. I have three different programs that have main()'s, and make use of the Tic_Tac_Toe class. These are:

Now, each .cpp file will have the line:

#include "tic_tac_toe.hpp"

That means that every .cpp file has the same specification of the Tic_Tac_Toe class. To compile each source file into an object file, we will do the following, using src/tic_tac_toe.cpp as an example:

UNIX> g++ -std=c++98 -Wall -Wextra -Iinclude -c -o obj/tic_tac_toe.o src/tic_tac_toe.cpp
UNIX> Let's go over each part of this: When we want to create an executable, we link together the object files, where one of them has a main(), and the others contain implementations of everything that is used. For example, when we want to make an executable from src/ttt_tester.cpp, we do:
UNIX> g++ -std=c++98 -Wall -Wextra -Iinclude -o bin/ttt_tester obj/ttt_tester.o obj/tic_tac_toe.o
UNIX> 
Since we don't include the -c flag, it makes the executable. The main() is defined in src/ttt_tester.o. It uses the Tic_Tac_Toe class, so we include src/tic_tac_toe.o, which implements all of the methods of the class. If we don't include src/tic_tac_toe.o, then we'll get a compiler error, saying that methods have not been implemented:
UNIX> g++ -std=c++98 -Wall -Wextra -Iinclude -o bin/ttt_tester obj/ttt_tester.o 
Undefined symbols for architecture x86_64:
  "Tic_Tac_Toe::Clear_Game()", referenced from:
      _main in ttt_tester.o
  "Tic_Tac_Toe::Make_Move(char, unsigned long, unsigned long)", referenced from:
      _main in ttt_tester.o
  "Tic_Tac_Toe::Tic_Tac_Toe()", referenced from:
      _main in ttt_tester.o
  "Tic_Tac_Toe::Game_State() const", referenced from:
      _main in ttt_tester.o
  "Tic_Tac_Toe::Board_String() const", referenced from:
      _main in ttt_tester.o
  "Tic_Tac_Toe::Print() const", referenced from:
      _main in ttt_tester.o
  "Tic_Tac_Toe::Stats(std::__1::vector >&) const", referenced from:
      _main in ttt_tester.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
UNIX> 
Now, the file makefile automates the compilation. If you type "make clean", then it will remove all of the object files and executables. Then, if you type "make" or "make all", it will make all of the object files and executables.
UNIX> make clean
rm -f obj/* bin/*
UNIX> make
g++ -std=c++98 -Wall -Wextra -Iinclude -c -o obj/ttt_tester.o src/ttt_tester.cpp
g++ -std=c++98 -Wall -Wextra -Iinclude -c -o obj/tic_tac_toe.o src/tic_tac_toe.cpp
g++ -std=c++98 -Wall -Wextra -Iinclude -o bin/ttt_tester obj/ttt_tester.o obj/tic_tac_toe.o
g++ -std=c++98 -Wall -Wextra -Iinclude -c -o obj/ttt_player.o src/ttt_player.cpp
g++ -std=c++98 -Wall -Wextra -Iinclude -o bin/ttt_player obj/ttt_player.o obj/tic_tac_toe.o
g++ -std=c++98 -Wall -Wextra -Iinclude -c -o obj/ttt_random.o src/ttt_random.cpp
g++ -std=c++98 -Wall -Wextra -Iinclude -o bin/ttt_random obj/ttt_random.o obj/tic_tac_toe.o
UNIX> 
Let me draw a picture of what is going on. This shows how the source files all include the include file, how they are compiled to object files, and which object files are linked together to make which executables:


The header file: include/tic_tac_toe.hpp

Let's take a look at the header file in include/tic_tac_toe.hpp. It defines a class with a constructor and six public methods. The comments tell you what the methods do. It also defines four protected variables. We'll talk more about them later.

#pragma once
 
#include <vector>
#include <string>

class Tic_Tac_Toe
{
  public:
    Tic_Tac_Toe();                    /* Constructor */
    void Clear_Game();                /* Turn the current board into an empty board */
    char Game_State() const;          /* Return the state of the game:
                                           'B' = beginning of game
                                           'X' = X's turn
                                           'O' = O's turn
                                           'x' = Game is over and X has won.
                                           'o' = Game is over and O has won.
                                           'd' = Game is over and it's a draw. */

    char Make_Move(char xo, size_t row, size_t col);  /* This does the move.  
                                                         xo must be 'X' or 'O'.
                                                         Returns 'E' on an error.
                                                         Otherwise it returns resulting game state. */

    void Print() const;                         /* Prints the board. */
    std::string Board_String() const;           /* Returns a 9-character string of X's, O's and -'s */
    void Stats(std::vector <int> &xod) const;   /* Sets xod to a three element vector: 
                                                   X wins, O wins, draws */

  protected:
    std::vector <std::string> Board;    /* The board */
    char State;                         /* Game state -- same values as Game_State() above */
    std::vector <int> X_O_D;            /* The three stats */
    int Open_Squares;                   /* The number of open squares on the board. */
};

First, you'll note that the methods are public. That means that anyone who creates an instance of the class can use the methods. The methods give you enough power to run tic-tac-toe games, and keep track of the winners. Second, you should see the use of the const keyword. Here it is put at the end of any method that doesn't change the class. For example, Print() prints the game board, but doesn't change anything. That allows the compiler to double-check that indeed your implementation doesn't change anything, and it helps you find bugs. Third, there's no "using namespace std" in the header file, so when I need to use things that are part of the "std" namespace, I need to put "std::" in front of them. You see that I've done this with vector and string. This is good practice, because some programmers don't want to have "using namespace std" in their code, and this way, you don't force them to do so. You can always put it in your own source (.cpp) files. Fourth, the variables are all protected. That means that they can only be accessed by the methods of the class, and not by any other code. Thus, the only way that you can use the class is through its methods.

Last, I have no executable code in the header file. This is something in which I believe firmly -- header files should have no executable code. That includes having a constructor that sets default values. That should be in the implementation.


Starting to implement the class: src/tic_tac_toe_1.cpp

Having defined my class, I need to implement and test it. You need to resist the temptation to do this all in one shot, even with a class that's as easy as this one. Here's what I did to implement the class. The first thing I did was write src/tic_tac_toe_1.cpp. This simply has dummy methods for every method in the class. I want to write some testing code next, and this allows me to compile and make sure that at least it's calling stuff correctly. Put another way, it allows me to test my header to make sure that it's correct and usable.

/* This implementation file simply has dummy methods for every method in the class.
   It allows me to write a testing program and have it compile.  Then, I'll start
   to implement the methods. */

#include "tic_tac_toe.hpp"
using namespace std;

Tic_Tac_Toe::Tic_Tac_Toe() {}

void Tic_Tac_Toe::Clear_Game() {}

void Tic_Tac_Toe::Print() const {}

char Tic_Tac_Toe::Game_State() const { return '-'; }

string Tic_Tac_Toe::Board_String() const { return "-"; }

void Tic_Tac_Toe::Stats(vector <int> &xod) const 
{ 
  xod.resize(3, 0); 
}

char Tic_Tac_Toe::Make_Move(char xo, size_t row, size_t col) 
{ 
  (void) xo;        // These statements shut the compiler up about not using the parameters.
  (void) row;
  (void) col;
  return 'E'; 
}

You can see that I've put "using namespace std" here -- that's because I'm happy to use the "std" namespace, and make my code more readable.

I've added a "make develop" to my makefile, which is where I specify how to compile the code that I'm using while I'm writing the program. I won't use it yet -- here, I'll simply type "make obj/tic_tac_toe_1.o", because I've configured that to just compile src/tic_tac_toe_1.cpp:

UNIX> make obj/tic_tac_toe_1.o
g++ -std=c++98 -Wall -Wextra -Iinclude -c -o obj/tic_tac_toe_1.o src/tic_tac_toe_1.cpp
UNIX> 

The testing program: src/ttt_tester.cpp

Next, I wrote my testing program, which is in src/ttt_tester.cpp. I write a lot of testing programs like this -- pretty much for every class that I create. The structure is simple -- it reads lines of text and converts them to vectors of words. It processes one line at a time, using the first word as a command. You can see the commands below in the procedure print_commands(). What you see is that there is a command to test each method of the class.

#include "tic_tac_toe.hpp"
#include <iostream>
#include <sstream>
#include <vector>
#include <cstdio>
#include <string>
using namespace std;

/* It's good to put this in a procedure at the beginning of your file -- that
   way you know where it is for reference, while you're writing the program. */

void print_commands()
{
  cout << "usage: ttt_tester -- commands on stdin." << endl;
  cout << endl;
  cout << "commands:" << endl;
  cout << "  C            - Clear game state." << endl;
  cout << "  GS           - Print the game state char." << endl;
  cout << "  P            - Print the board." << endl;
  cout << "  BS           - Print the Board String." << endl;
  cout << "  S            - Print stats." << endl;
  cout << "  M X/O R C    - Move X or O to space at row R, col C." << endl;
  cout << "  Q            - Quit." << endl;
  cout << "  ?            - Print commands." << endl;
}


int main()
{
  string s, line;         // I use these to read a line of text and turn it into a 
  vector <string> sv;     // vector of strings (which is in sv).
  istringstream ss;

  Tic_Tac_Toe ttt;        // Here's my tic-tac-toe game.
  vector <int> stats;     // This is for when I call ttt.Stats()

  int row, col;           // These are for the ttt.Make_Move(xo, row, col) call.
  char xo;

  while (1) {
 
    /* Print a prompt, and read in a line. */

    cout << "TTT> ";
    cout.flush();
    if (!getline(cin, line)) return 0;

    /* Use a stringstream to turn the line into a vector of words. */

    sv.clear();
    ss.clear();
    ss.str(line);
    while (ss >> s) sv.push_back(s);

    /* Ignore blank lines and lines that start with the pound sign. */

    if (sv.size() == 0 || sv[0][0] == '#') {

    /* Handle the simple commands: */

    } else if (sv[0] == "P") {
      ttt.Print();
    } else if (sv[0] == "C") {
      ttt.Clear_Game();
    } else if (sv[0] == "GS") {
      printf("%c\n", ttt.Game_State());
    } else if (sv[0] == "BS") {
      printf("%s\n", ttt.Board_String().c_str());

    /* Stats */

    } else if (sv[0] == "S") {
      ttt.Stats(stats);
      printf("X Wins: %4d\n", stats[0]);
      printf("O Wins: %4d\n", stats[1]);
      printf("Draws:  %4d\n", stats[2]);

    /* Make a move.  
       You'll note that I'm not using a stringstream for row/col.  This code
       is simpler, and since row and col have to be between 0 and 2, it works
       just fine.  You'll note that I'm not doing much error-checking here.
       That will be handled in Make_Move() which returns 'E' if it is called
       incorrectly.  */

    } else if (sv[0] == "M") {
      if (sv.size() != 4) {
        printf("Usage M X/O row col\n");
      } else {
        xo = sv[1][0];
        row = sv[2][0] - '0';
        col = sv[3][0] - '0';
        printf("Result of move: %c\n", ttt.Make_Move(xo, row, col));
      }

    /* Quit, print commands or a bad command. */

    } else if (sv[0] == "Q") {
      return 0;
    } else if (sv[0] == "?") {
      print_commands();
    } else {
      printf("Unknown command %s\n", sv[0].c_str());
    }
  }
}

The program is really simple, and it lets me write tic_tac_toe.cpp incrementally, by writing a few methods at a time, and then testing. This compiles with src/tic_tac_toe_1.cpp, and now you can "test" it. Of course, it doesn't really do anything, but at least you have everything compiling together are are ready to start implementing. In my makefile, I have this compile with src/tic_tac_toe_1.cpp to make the executable bin/ttt_tester_1:

UNIX> make clean
rm -f obj/* bin/*
UNIX> make bin/ttt_tester_1
g++ -std=c++98 -Wall -Wextra -Iinclude -c -o obj/ttt_tester.o src/ttt_tester.cpp
g++ -std=c++98 -Wall -Wextra -Iinclude -c -o obj/tic_tac_toe_1.o src/tic_tac_toe_1.cpp
g++ -std=c++98 -Wall -Wextra -Iinclude -o bin/ttt_tester_1 obj/ttt_tester.o obj/tic_tac_toe_1.o
UNIX> 
I'll run it, and it won't do much, but it lets me see that the methods are being called:
UNIX> bin/ttt_tester_1
TTT> ?
usage: ttt_tester -- commands on stdin.

commands:
  C            - Clear game state.
  GS           - Print the game state char.
  P            - Print the board.
  BS           - Print the Board String.
  S            - Print stats.
  M X/O R C    - Move X or O to space at row R, col C.
  Q            - Quit.
  ?            - Print commands.
TTT> C                                        # call ttt.Clear()
TTT> GS                                       # call ttt.Game_State()
-
TTT> P                                        # call ttt.Print()
TTT> BS                                       # call ttt.Board_String()
-
TTT> S                                        # call ttt.Stats()
X Wins:    0
O Wins:    0
Draws:     0
TTT> M - - -                                  # call ttt.Make_Move()
Result of move: E
TTT> Q
UNIX> 

Implementing the easy methods: src/tic_tac_toe_2.cpp

In src/tic_tac_toe_2.cpp, I implement all of the methods, except for Make_Move(). These are all really straightforward, and you can read what they do in their comments:

/* In this program, I implement the easy methods, which is all of the methods besides Make_Move() */

#include "tic_tac_toe.hpp"
#include <iostream>
using namespace std;

/* The constructor calls Clear_Game() to set up the empty board.
   It also creates the X_O_D vector and sets its entries to zero. */

Tic_Tac_Toe::Tic_Tac_Toe() 
{
  Clear_Game();
  X_O_D.resize(3, 0);
}

/* Clear_Game() creates an empty board with all dashes.  It sets the game state
   to 'B', for "Beginning", and sets the number of open squares to 9, since 
   all of the squares are empty. */

void Tic_Tac_Toe::Clear_Game() 
{
  State = 'B';
  Board.clear();
  Board.push_back("---");
  Board.push_back("---");
  Board.push_back("---");
  Open_Squares = 9;
}

/* Print() is simple, printing out the Board, one row per line. */

void Tic_Tac_Toe::Print() const 
{
  size_t i;

  for (i = 0; i < Board.size(); i++) cout << Board[i] << endl;
}

/* Game_State() is also simple, simply returning the State variable. */

char Tic_Tac_Toe::Game_State() const 
{
  return State; 
}

/* Board_String() concatenates the three rows of the Board together to make a single
   string without any newlines. */

string Tic_Tac_Toe::Board_String() const 
{ 
  string rv;

  rv = Board[0] + Board[1] + Board[2];
  return rv;
}

/* State() just copies X_O_D to its argument, which is a reference parameter. */

void Tic_Tac_Toe::Stats(vector <int> &xod) const 
{ 
  xod = X_O_D;
}

/* Make_Move() is still unwritten. */

char Tic_Tac_Toe::Make_Move(char xo, size_t row, size_t col) 
{
  (void) xo;
  (void) row;
  (void) col;
  return '.'; 
}

We can go ahead and test to see if these all work -- again, they are pretty simple, and since you can't make a move, they don't ever change. The makefile will compile this and src/ttt_tester.o into bin/ttt_tester_2:

UNIX> make clean
rm -f obj/* bin/*
UNIX> make bin/ttt_tester_2
g++ -std=c++98 -Wall -Wextra -Iinclude -c -o obj/ttt_tester.o src/ttt_tester.cpp
g++ -std=c++98 -Wall -Wextra -Iinclude -c -o obj/tic_tac_toe_2.o src/tic_tac_toe_2.cpp
g++ -std=c++98 -Wall -Wextra -Iinclude -o bin/ttt_tester_2 obj/ttt_tester.o obj/tic_tac_toe_2.o
UNIX> bin/ttt_tester_2
TTT> P
---
---
---
TTT> GS
B
TTT> S
X Wins:    0
O Wins:    0
Draws:     0
TTT> BS
---------
TTT> C
TTT> P
---
---
---
TTT> Q
UNIX> 

Implementing and testing Make_Move()

Make_Move() is the hard procedure. It is implemented in src/tic_tac_toe.cpp -- all of the other methods are copied over from src/tic_tac_toe_2.cpp. The implementation is explained in the comments. The code to test whether the caller has won feels a little clunky, but it is at least straightforward:

/* Make_Move() goes through the following steps:

   - Error check the arguments
   - Update the Board and Open_Squares
   - Test to see if whoever called Make_Move() has now won the game.  If so,
     update the stats and set the State to 'x' or 'o'.
   - Otherwise, test to see if the game is a Draw, and set its state to 'D'.
   - Finally, if the game isn't over, set the game state to 'X' or 'O' 
     to indicate whose turn it is. */

char Tic_Tac_Toe::Make_Move(char xo, size_t row, size_t col) 
{
  bool win;                  // This is used to record whether the caller has won the game.
 
  /* Error Check */

  if (xo != 'X' && xo != 'O') return 'E';
  if (xo == 'X' && State != 'B' && State != 'X') return 'E';
  if (xo == 'O' && State != 'B' && State != 'O') return 'E';
  if (row >= 3) return 'E';
  if (col >= 3) return 'E';
  if (Board[row][col] != '-') return 'E';

  /* Update the Board and decrement the number of open squares */

  Board[row][col] = xo;
  Open_Squares--;

  /* Test to see if whoever calls Make_Move has won.  The tests go in the following order:
       - Check to see if the move completed a row.
       - Check to see if the move completed a column.
       - Check to see if the move completed the \ diagonal
       - Check to see if the move completed the / diagonal */

  if (Board[row][0] == Board[row][1] && Board[row][0] == Board[row][2]) {
    win = true;
  } else if (Board[0][col] == Board[1][col] && Board[0][col] == Board[2][col]) {
    win = true;
  } else if (row == col && Board[0][0] == Board[1][1] && Board[0][0] == Board[2][2]) {
    win = true;
  } else if (row+col == 2 && Board[0][2] == Board[1][1] && Board[2][0] == Board[1][1]) {
    win = true;
  } else {
    win = false;
  }

  /* If the player won the game, update the stats and state accordingly. */

  if (win) {
    if (xo == 'X') {
      State = 'x';
      X_O_D[0]++;
    } else if (xo == 'O') {
      State = 'o';
      X_O_D[1]++;
    }

  /* Otherwise, if the game is a draw, then update the stats and state accordingly. */

  } else if (Open_Squares == 0) {
    State = 'D';
    X_O_D[2]++;

  /* Otherwise, set the State to whoever's turn it is. */

  } else if (xo == 'X') {
    State = 'O';
  } else {
    State = 'X';
  }

  /* Finally, return the state. */

  return State;
}

We can now compile bin/ttt_tester, and test the program:

UNIX> make clean
rm -f obj/* bin/*
UNIX> make bin/ttt_tester
g++ -std=c++98 -Wall -Wextra -Iinclude -c -o obj/ttt_tester.o src/ttt_tester.cpp
g++ -std=c++98 -Wall -Wextra -Iinclude -c -o obj/tic_tac_toe.o src/tic_tac_toe.cpp
g++ -std=c++98 -Wall -Wextra -Iinclude -o bin/ttt_tester obj/ttt_tester.o obj/tic_tac_toe.o
UNIX> bin/ttt_tester
TTT> M Fred Binky Luigi                   # Error checking
Result of move: E
TTT> M X 5 3
Result of move: E
TTT> M X 1 1                              # Put an X in the middle square (remember, 0-indexing)
Result of move: O
TTT> M O 0 1                              # Put an O into the top middle square
Result of move: X
TTT> P                                    # Print the board
-O-
-X-
---
TTT> M X 2 2                              # Put an X in bottom right square
Result of move: O
TTT> M O 0 0                              # Put an O in the top left square
Result of move: X
TTT> P                                    # Print the board
OO-
-X-
--X
TTT> M X 0 2                              # Put an X in top right square.  O is in trouble.
Result of move: O
TTT> M O 0 2                              # Error check putting an O into a square with an X
Result of move: E
TTT> M O 1 2                              # Put an O in the middle right square.
Result of move: X
TTT> P                                    # Print the board
OOX
-XO
--X
TTT> M X 2 0                              # X will now win the game.
Result of move: x                         # This is confirmed by the state of 'x'
TTT> P
OOX
-XO
X-X
TTT> GS                                   # Confirm the game state
x
TTT> S                                    # Show the stats.
X Wins:    1
O Wins:    0
Draws:     0
TTT> Q
UNIX> 
You can see that the testing program is a pain to use to play the game, but it's good at testing. It's a good idea here to test all of Make_Move() -- best thing is to create a file that has commands, and then confirm that running the program on the file gives you the output that you want. Let me give you a simple example that plays the same game as above, but just prints out the board strings that result. The program sed is really useful here to strip out those "TTT>" strings, and to remove the lines that say "Result of move".

The input file is in data/test_game_input_1.txt

# This plays the game where X wins on the left-to-right diagonal:
M X 1 1
BS
M O 0 1
BS
M X 2 2
BS
M O 0 0
BS
M X 0 2
BS
M O 1 2
BS
M X 2 0
BS
GS
P

When we run it, the output is pretty clunky:

UNIX> bin/ttt_tester < data/test_game_input_1.txt 

TTT> TTT> Result of move: O
TTT> ----X----
TTT> Result of move: X
TTT> -O--X----
TTT> Result of move: O
TTT> -O--X---X
TTT> Result of move: X
TTT> OO--X---X
TTT> Result of move: O
TTT> OOX-X---X
TTT> Result of move: X
TTT> OOX-XO--X
TTT> Result of move: x
TTT> OOX-XOX-X
TTT> x
TTT> OOX
-XO
X-X
TTT> UNIX>
However, piping it through a few sed commands makes it pretty clean:
UNIX> bin/ttt_tester < test_game_input_1.txt | sed 's/TTT> //' | sed '/Result/d'
----X----
-O--X----
-O--X---X
OO--X---X
OOX-X---X
OOX-XO--X
OOX-XOX-X
x
OOX
-XO
X-X

UNIX> 

A more natural game player -- src/ttt_player.cpp

In src/ttt_player.cpp, I have a program that runs a command-line version of the game which is a lot less cumbersome than src/ttt_tester.cpp. It starts by having 'X' start the game, and then it alternates 'X' and 'O' starting the subsequent games. It's a pretty simple program, and leverages the Game_State() method so that it doesn't even have to keep track of whose turn it is. Please read the comments for explanation.

/* This program runs a more natural command-line version of tic-tac-toe than
   src/ttt_tester.cpp.  Note how it makes use of the game state to help it
   play the game. */

#include "tic_tac_toe.hpp"
#include <iostream>
#include <cstdio>
#include <sstream>
#include <vector>
#include <string>
using namespace std;

int main()
{
  Tic_Tac_Toe ttt;                  // The game player
  vector <int> stats;               // This is for reading the stats
  size_t row, col;                  // These are entered on standard input for Make_Move()
  char start;                       // 'X' or 'O' for who starts the game
  char turn;                        // 'X' or 'O' for whose turn it is
  char state;                       // The game state.
  
  /* Set it up so that 'X' plays the first game.  
     We will alternate this between games. */

  start = 'X';

  while (1) {
 
    /* Get the game state, and print the board. */

    state = ttt.Game_State();
    cout << endl;
    ttt.Print();
    cout << endl;

    /* If we're playing the game, then figure out whose turn it is,
       and then get the player's move from standard input. */

    if (state == 'B' || state == 'X' || state == 'O') {
      turn = (state == 'B') ? start : state;
      cout << turn  << "'s Move: ";
      cout.flush();
      if (!(cin >> row >> col)) return 0;

      /* If the move is illegal, then we print an error message.
         Then, regardless of whether the move was legal or illegal,
         we're simply going to go to the top of the while loop.
         If there was an error, we'll simply repeat this code.
         Otherwise, the game will move on. */

      if (ttt.Make_Move(turn, row, col) == 'E') {
        cout << endl << "Bad input -- try again, please." << endl;
      }
        
    /* Otherwise, the game is over.  We're going to do the following things:
        - Print the winner (or whether it's a draw).
        - Print the stats.
        - Start a new game
        - Set the starting player to the other player. */

    } else {
      if (state == 'D') {
        printf("Draw\n");
      } else {
        printf("%c Wins!\n", state + ('A' - 'a'));   // This converts the lower-case to upper-case.
      }

      ttt.Stats(stats);
      printf("Stats: X:%d O:%d D:%d\n", stats[0], stats[1], stats[2]);

      ttt.Clear_Game();
      start = (start == 'X') ? 'O' : 'X';
    }
  }
}

Let's compile it, and play the same game as above -- you'll note, it's a lot easier than using bin/ttt_tester. (However, I'll contend that bin/ttt_tester is a better program for testing).

UNIX> make bin/ttt_player
g++ -std=c++98 -Wall -Wextra -Iinclude -c -o obj/ttt_player.o src/ttt_player.cpp
g++ -std=c++98 -Wall -Wextra -Iinclude -c -o obj/tic_tac_toe.o src/tic_tac_toe.cpp
g++ -std=c++98 -Wall -Wextra -Iinclude -o bin/ttt_player obj/ttt_player.o obj/tic_tac_toe.o
UNIX> bin/ttt_player

---
---
---



X's Move: 1 1

---
-X-
---

O's Move: 0 1

-O-
-X-
---

X's Move: 2 2

-O-
-X-
--X

O's Move: 0 0

OO-
-X-
--X

X's Move: 0 2

OOX
-X-
--X

O's Move: 1 2

OOX
-XO
--X

X's Move: 2 0

OOX
-XO
X-X

X Wins!
Stats: X:1 O:0 D:0

---
---
---

O's Move: Q
UNIX> 

Performing a scientific experiment - src/ttt_random.cpp

Our last progrom to utilize the Tic_Tac_Toe class does a little scientific experiment. Both the X and the O players are going to play the game randomly, with just one difference -- if X begins the game, he/she will choose the middle square. The program is in src/ttt_random.cpp. It takes two command-line arguments -- a number of iterations (games), and a seed for the random number generator. It then plays games randomly, except for when X starts a game, in which case X will choose the middle square.

Go ahead and read the comments for an explanation of how the code works. The important part of the code is how we read the board string, in the variable bs, and figure out the legal moves, which correspond to the dashes in the string. Each of those is pushed onto the vectors legal_moves_r and legal_moves_c, and we choose a random one by choosing a random index into these vectors:

/* This program allows us to do a scientific experiment on tic-tac-toe.  The hypothesis is that
   if you start the game in the middle square, and then play the game randomly, you will do 
   betting than simply playing randomly. 

   The program is a nice demonstration of how our Tic_Tac_Toe class can serve multiple
   goals (playing the game interactively, and doing a scientific experiment. */

#include "tic_tac_toe.hpp"
#include "MOA.hpp"
#include <iostream>
#include <sstream>
#include <vector>
#include <string>
#include <cstdio>
using namespace std;

/* On the command line, we're going to read a number of iterations and a seed
   for a random number generator (this is why we included "MOA.hpp" above). */

int main(int argc, char **argv)
{
  Tic_Tac_Toe ttt;                     // The game player
  vector <int> stats;                  // For the final Stats() call
  MOA rng;                             // Random number generator
  istringstream ss;                    // For parsing the command line arguments

  size_t iterations;                   // The number of games to play
  size_t seed;                         // Seed for the random number generator
  size_t games_played;                 // Keeping track of the number of games played
  char start;                          // Who starts the game
  char turn;                           // Whose turn it is
  char state;                          // The game state
  string bs;                           // The board string, which helps do the random choosing
  vector <size_t> legal_moves_r;       // Legal moves -- the row numbers
  vector <size_t> legal_moves_c;       // Legal moves -- the column numbers
  
  size_t i;

  /* Parse and error check the command line */

  try {
    if (argc != 3) throw((string) "usage: bin/ttt_random iterations seed");
    ss.clear(); ss.str(argv[1]); if (!(ss >> iterations)) throw((string) "Bad iterations");
    ss.clear(); ss.str(argv[2]); if (!(ss >> seed)) throw((string) "Bad seed");
  } catch (string s) {
    cout << s << endl;
    return 1;
  }

  /* Initialize everything */

  rng.Seed(seed);
  start = 'X';
  games_played = 0;
  
  /* Keep going until you complete enough games.
     You only increment games_played when a game is over. */

  while (games_played < iterations) {
 
    // printf("%s\n", ttt.Board_String().c_str());     This is useful for debugging.

    /* If the game isn't over, then determine whose turn it is, and then
       determine the legal moves that can be made.  Choose one of them randomly.
       When it's X's turn and it's the beginning of a game, set it up so that
       the only legal move is to use the center square. */

    state = ttt.Game_State();
    if (state == 'B' || state == 'X' || state == 'O') {
      turn = (state == 'B') ? start : state;
      legal_moves_r.clear();
      legal_moves_c.clear();

      /* This is how to handle when the game is starting and it's X's turn. */

      if (state == 'B' && turn == 'X') {
        legal_moves_r.push_back(1);
        legal_moves_c.push_back(1);

      /* Otherwise, use the dashes in the board string to determine the legal moves. */

      } else {
        bs = ttt.Board_String();
        for (i = 0; i < bs.size(); i++) {
          if (bs[i] == '-') {
            legal_moves_r.push_back(i/3);
            legal_moves_c.push_back(i%3);
          }
        }
      }

      /* Choose a random legal move and make it. */

      i = rng.Random_Integer()%legal_moves_r.size();
      ttt.Make_Move(turn, legal_moves_r[i], legal_moves_c[i]);

    /* Otherwise, the game is over.  Update the games played, and set the
       starting player to the other player. */

    } else {
      games_played++;
      ttt.Clear_Game();
      start = (start == 'X') ? 'O' : 'X';
    }
  }

  /* At the end, print the stats. */

  ttt.Stats(stats);
  printf("Stats: X:%d O:%d D:%d\n", stats[0], stats[1], stats[2]);
  return 0;
}

To debug this, you'll note that I have printf() statement that I have commented out. Those let me look at the Board_String strings to make sure that everything looks good. Let's call it on 1,000,000 games:

UNIX> bin/ttt_random 1000000 40
Stats: X:490563 O:388761 D:120676
UNIX> bin/ttt_random 1000000 41
Stats: X:490699 O:388937 D:120364
UNIX> bin/ttt_random 1000000 42
Stats: X:490144 O:388883 D:120973
UNIX> bin/ttt_random 1000000 43
Stats: X:491100 O:387998 D:120902
UNIX> 
You can see that X is winning over O significantly and consistently across seeds, so I think this is a pretty conclusive experiment!


Summary of the Main Points of this Lecture

  1. Rather than having all of your files in one directory, it is often helpful to break your files up into directories. Here, I'm uisng src and include for my source and include files, and obj / bin for my object files and executables. There are a few nice things about this structure. First, when you list files in the current directory, the listing is clean. Second, you know where to look for files. Third, you can do "rm -rf obj/* bin/*" and you won't get into trouble, because all of the files in those two directories are created by the compiler.

  2. It is good practice to compile your .cpp files into .o files, and then to link the .o files to make an executable. This is efficient when you have multiple programs making use of the same source files. You can automate this with make.

  3. Header files should be in an include directory, and they shouldn't contain code (unless they are header-only implementations, like MOA.hpp).

  4. You should not put "using namespace std" in a header file, because that forces your users to use the standard namespace. It's fine to put "using namespace std" in your .cpp files.

  5. A classic object-oriented style is to have methods be public, but have data be protected. That way, users of a data structure can't mess with the data directly -- they can only mess with it through the methods.

  6. If you put const at the end of a method, it means that the method does not modify the data of a class. The compiler will enforce this.

  7. When you're implementing a class, start with dummy implementations, and then write a testing program. Typically, this testing program allows you to implement each of the class' methods incrementally, and test them.

  8. Duh: You should program incrementally -- don't implement the entire class at once -- do it slowly and test as you go. It will save you time. Always.

  9. A well-designed class can be used by multiple programs.