My First Game - sort of

Started by
11 comments, last by eektor 18 years, 7 months ago
So I'm waiting till September 20 for my new C++ book to arrive from Amazon (Accelerated C++). So I'm stalling so I don't finish my current book so quickly. So I took the tic-tac-toe program from the book modified it into a variation of Tic-Tac-Toe. A little harder than the original version I think. Anyways here's the source:
// New Tic Tac Toe
// A variation of the basic game of tic tac toe

#include <iostream>
#include <string>
#include <vector>

using namespace std;

//global constants
const char X = 'X';
const char O = 'O';
const char EMPTY = ' ';
const char NO_ONE = 'N';

// function prototypes
void Instructions();
char AskYesNo(string question);
int AskNumber(string question, int high, int low = 1);
char HumanPiece();
char SwitchPiece(char piece);
void DisplayBoard(const vector<char>& board);
char Winner(const vector<char>& board);
bool IsLegal(const vector<char>& board, int move);
int HumanMove(const vector<char>& board, char player);
int ComputerMove(const vector<char> board, char computer);
void AnnounceWinner(char winner, char player, char computer);

// main function
int main()
{
    int turn=0;
    int current_move;
    const int NUM_PIECES = 6;
    const int NUM_SQUARES = 9;
    vector<int> move(NUM_PIECES,10);
    vector<char> board(NUM_SQUARES, EMPTY);
    
    Instructions();
    char human = HumanPiece();
    char computer = SwitchPiece(human);
    char current_player = X;
    DisplayBoard(board);
    
    while (Winner(board) == NO_ONE)
    {
          if (current_player == human)
          {
             current_move = HumanMove(board, human);
             if (move[turn%NUM_PIECES] < 10)
                board[move[turn%NUM_PIECES]] = EMPTY;
             move[turn%NUM_PIECES] = current_move;
             board[current_move] = human;
             turn++;
          }
          else
          {
              current_move = ComputerMove(board, computer);
              if (move[turn%NUM_PIECES] < 10)
                 board[move[turn%NUM_PIECES]] = EMPTY;
              move[turn%NUM_PIECES] = current_move;
              board[current_move] = computer;
              turn++;
          }
          DisplayBoard(board);
          current_player = SwitchPiece(current_player);
    }
    
    AnnounceWinner(Winner(board), computer, human);
    
    system("PAUSE");
    
    
    
    return 0;
}

// function definitions
void Instructions()
{
     cout << "Welcome to the new and improved Tic Tac Toe!\n\n";
     cout << "This game is a little bit more complicated than the original.\n";
     cout << "First of all each player can have only three pieces on the board,\n";
     cout << "for a total of just 6 pieces.\n\n";
     cout << "On the turn after you place your third piece, the first piece\n";
     cout << "that you place will be taken out and you will have to place it\n";
     cout << "somewhere else and not in the same spot.\n\n";
     
     cout << "You can make your move by entering a number 1 - 9.  The number\n";
     cout << "corresponds to the desired board position, as illustrated:\n\n";
     
     cout << "             1 | 2 | 3\n";
     cout << "            -----------\n";
     cout << "             4 | 5 | 6\n";
     cout << "            -----------\n";
     cout << "             7 | 8 | 9\n\n";
     
     cout << "Let's get started!\n";
}

char AskYesNo(string question)
{
     char response;
     do
     {
          cout << question << " (y/n): ";
          cin >> response;
     }while (response != 'y' && response != 'n');
     
     return response;
}

int AskNumber(string question, int high, int low)
{
    int number;
    do
    {
        cout << question << " (" << low << " - " << high << "): ";
        cin >> number;
    }while (number > high || number < low);
    
    return number;
}

char HumanPiece()
{
     char go_first = AskYesNo("Do you want to move first?");
     if (go_first == 'y')
     {
        cout << "\nThen take the first move.\n";
        return X;
     }
     else
     {
         cout << "\nAlright, I will go first.\n";
         return O;
     }
}

char SwitchPiece(char piece)
{
     if (piece == X)
        return O;
     else
         return X;
}

void DisplayBoard(const vector<char>& board)
{
     cout << "\n\t" << board[0] << " | " << board[1] << " | " << board[2];
     cout << "\n\t" << board[3] << " | " << board[4] << " | " << board[5];
     cout << "\n\t" << board[6] << " | " << board[7] << " | " << board[8];
     cout << "\n\n";
}

char Winner(const vector<char>& board)
{
     // all possible winning rows
     const int WINNING_ROWS[8][3] = { {0, 1, 2}, {3, 4, 5},
                                    {6, 7, 8}, {0, 3, 6},
                                    {1, 4, 7}, {2, 5, 8},
                                    {0, 4, 8}, {2, 4, 6} };
     
     const int TOTAL_ROWS = 8;
     
     // if any winning row has three values that are the same (and not EMPTY),
     // then we have a winner
     for(int row = 0; row < TOTAL_ROWS; ++row)
     {
             if ( (board[WINNING_ROWS[row][0]] != EMPTY) &&
                  (board[WINNING_ROWS[row][0]] == board[WINNING_ROWS[row][1]]) &&
                  (board[WINNING_ROWS[row][1]] == board[WINNING_ROWS[row][2]]) )
             {
                  return board[WINNING_ROWS[row][0]];
             }
     }
     
     
     // since nobody has won, the game is not over
     return NO_ONE;
}

bool IsLegal(int move, const vector<char>& board)
{
     return (board[move] == EMPTY);
}

int HumanMove(const vector<char>& board, char player)
{
    int move = AskNumber("Where will you move?", (board.size()));
    while(!IsLegal(move-1,board))
    {
         cout << "\nSorry, you cannot move there.\n";
         move = AskNumber("Where will you move?", (board.size()));
    }
    cout << "Ok.\n";
    return move-1;
}

int ComputerMove(vector<char> board, char computer)
{
    cout << "I shall take square number ";
    
    // if coumputer can win on next move, make that move
    for(int move = 0; move < board.size(); ++move)
    {
            if (IsLegal(move, board))
            {
                  board[move] = computer;
                  if (Winner(board) == computer)
                  {
                        cout << move + 1 << endl;
                        return move;
                  }
                  // done checking this move, undo it
                  board[move] = EMPTY;
            }
    }
    
    // if human can win on next move, block that move
    char human = SwitchPiece(computer);
    for(int move = 0; move < board.size(); ++move)
    {
            if(IsLegal(move,board))
            {
                  board[move] = human;
                  if (Winner(board) == human)
                  {
                       cout << move + 1 << endl;
                       return move;
                  }
                  // done checking this move, undo it
                  board[move] = EMPTY;
            }
    }
    
    // the best moves to make, in order
    const int BEST_MOVES[] = {4, 0, 2, 6, 8, 1, 3, 5, 7};
    // since no one can win on next move, pick the best open square
    for(int i = 0; i < board.size(); ++i)
    {
            int move = BEST_MOVES;
            if (IsLegal(move,board))
            {
                   cout << move + 1 << endl;
                   return move;
            }
    }
}

void AnnounceWinner(char winner, char computer, char human)
{
     if (winner == computer)
     {
                cout << winner << "'s won!\n";
                cout << "Good game.  Practice some more and then play me again\n";
     }
     
     else
     {
          cout << winner << "'s won!\n";
          cout << "Wow!  You got lucky.  Let's have a rematch soon.\n";
     }
}

Please feel free to tell me if I'm doing anything wrong or bad habits or better ways of doing things. I know the AI could use some work but I don't know anything about AI yet. Also I thought this was short enough to put in the source but how could i upload a .zip file if I zipped the program with source file? Thanks.
Advertisement
Looking pretty good.

I notice that you pass the "board" to the AI evaluation function by value; that seems to be deliberate so that you make a copy (and don't mess up the actual board with "trial" moves). Not bad. :) I was going to suggest just using a plain char[] for the board since the size is supposed to be a constant (std::vector is designed specifically *not* to enforce that, and as a rule of thumb, you should choose data types that help you enforce your *design constraints*), but copying those into a function is a bit of a pain :)

In any case, you should at least typedef your "board" type. If you wanted to make something simpler for the board, you could wrap an array into a struct:

struct board {  char data[9];  char& operator[](int x) { return data[x]; }  const char& operator[](int x) const { return data[x]; }  // You don't need to write a copy constructor, etc. for this;  // an array member will get bitwise-copied over. However you  // *would* need it if you used a pointer as a data member  // (and you'd also have to make some allocation for it to point at).}


Now you have a real type for the board: that gives you a meaningful name for it, and in this case saves a bit of typing. This sort of thing also insulates the code against changes in the representation: Say for example you just did a typedef and kept using the vector. Then you could replace the typedef with the struct definition, and the rest of the code wouldn't need to be changed - either way, everything is a "board", with the same interface. :)

Overall though, you're way ahead of the game compared to most of the TTT programs I see around here - I take it you've been paying attention to what some of us old-timers have been saying ;)

EDIT: You are, however, missing some fairly critical error-checking code here:

int AskNumber(string question, int high, int low){    int number;    do    {        cout << question << " (" << low << " - " << high << "): ";        cin >> number;    }while (number > high || number < low);        return number;}


Look what happens when the input isn't numeric: the read fails, so the stream is in a failed state, and 'number' is still an uninitialized (garbage) value. If you're really lucky, it will exit right away because the garbage value is in range. Otherwise, you get an infinite loop: the subsequent cin's all fail, and don't wait for another line of input - the cin is in a "failed state", and will not read again until you reset() it. And even then, the garbage text is still "on the stream", so you would need to ignore() that, too.

Another approach, one that I am currently recommending because it's a bit more intuitive, is to grab a line at a time explicitly (that will also insulate you e.g. if the user types two numbers before hitting return), and re-parse the line - by using a temporary stringstream. If the reading fails, no problem: the main stream is still good to go, so you just toss away the old stringstream (implicitly, by looping), and create a new one with the next input line. It looks like this:

int AskNumber(string question, int high, int low){    int number;    do    {        cout << question << " (" << low << " - " << high << "): ";        // grab the whole line of input        string input;        getline(cin, input);        // and wrap it up in a new stringstream for parsing        stringstream ss;        ss.str(input); // assign the input line to the stringstream's buffer    } while (!(ss >> number) || number > high || number < low);    // if we couldn't read a number at all, loop, implicitly trashing the    // old stringstream object. Otherwise, if the number was invalid, we    // do the same. If both those checks are ok, we break the loop and return    // our 'good' value :)    return number;}
Thanks Zahlman, let me ask you some questions to make sure I get it.

You said its best to make the board an array of char values instead of have it in a vector like I did? Or I could make a struct for the board? I don't think I've read about structs yet,(I guess it wasn't covered in the beginning book I have) but it looks similar to a class. Do you think I should make the board a class?

Oh thanks for the modification for the AskNumber function. I haven't read about stringstream. How did you assign the input to the number? I saw you put the input into the stringstream, in the while where (ss >> number), is that where you assigned the input to number? That would have helped me a lot when debugging. Somehow I was assigning a number to a char and the SwitchPiece() was constantly returning X so the person to start first kept going without giving the other a chance to play.

EDIT: I added the function with your modification with the stringstream and it wouldn't compile. Is there some header file i need to include in order to use the stringstream?

EDIT: I just found the header file from another post in forum, I think <sstream> is it. Either way i still have problems compiling said that ss is not declared, any ideas why?

[Edited by - eektor on September 7, 2005 8:45:10 PM]
Fun =) I like that concept; congratulations on getting a game completed and presentable! Kudos!
my siteGenius is 1% inspiration and 99% perspiration
well done :)

Quote:Also I thought this was short enough to put in the source but how could i upload a .zip file if I zipped the program with source file?


you can use rapidshare to host your file and post the download link it gives you.
Thanks for the encouragement silver and gnomer. Thanks for the link to that's just what I was looking for.
Quote:Original post by eektor
Thanks Zahlman, let me ask you some questions to make sure I get it.

You said its best to make the board an array of char values instead of have it in a vector like I did? Or I could make a struct for the board? I don't think I've read about structs yet,(I guess it wasn't covered in the beginning book I have) but it looks similar to a class. Do you think I should make the board a class?


In C++, structs and classes are interchangeable: when you write 'struct' you will default to public inheritance and members (as opposed to defaulting to private with a class), and you have to match struct for struct and class for class when you declare/define things, but otherwise they are the exact same sort of thing.

In idiomatic C++ usage, the keyword 'struct' is preferred to mark a class/struct that is intended primarily as a data container - something that just aggregates related bits of data and doesn't really have its own "behaviour". Keyword 'class' by contrast suggests one that tries to actually enforce some design invariant (beyond "size of data").

Actually, I was going to move you gradually towards turning it into a class ;) I wasn't sure if you'd heard of them, and OOP isn't always something you want to show right off the bat, but it is still valuable pretty early. Getting at it from a "refactoring approach" is IMHO the nicest way to teach it, and you've provided a nice opportunity :)

See this part?

void DisplayBoard(const vector<char>& board);char Winner(const vector<char>& board);bool IsLegal(const vector<char>& board, int move);int HumanMove(const vector<char>& board, char player);int ComputerMove(const vector<char> board, char computer);


All of this is stuff that a "board" can "do", once we have that logical concept. You've been consistent about putting the board as a first parameter, which is good: this is conventional when you imitate OOP in C (which doesn't support it). Your const reference to the object basically serves the role of a this-pointer, in class-speak.

Now that we know what we *can* do, we should think about what we *should* do for a proper class. First let's consider the existing "interface":

- We adjust the board contents directly in the main loop. We'll probably want to avoid that, and instead offer a function to place a piece at a particular location.
- DisplayBoard: This can be included. It makes sense for objects to be able to present themselves. However, we'll want to rename it to just "display()"; otherwise we're just being redundant. Often functions like this will be generalized by having them accept an ostream parameter (the stream to which to write the data), but that isn't useful here - maybe if we wanted to save the game to a file or something, but in that case we don't really have it in the most convenient format anyway.
- IsLegal(): Definitely stays. We might not want to expose it publically, though.
- HumanMove, and ComputerMove: Don't really belong. A game board shouldn't prompt you for input or do AI. We'll probably want a separate AI function where we make temporary copies of the board to test out trial moves.

That in turn implies that we need:
- Copy construction - but for a char[] or a vector<char> for storage, the default one generated by C++ is fine. We would have a bunch of extra work to do with a char*, though.

And thus we can begin to write:

class Board {  // the data storage goes here; now it's private.  public:  // We won't provide an operator[]. Instead, we'll have a placement method  // that is checked:  void place(int where, char what) {    if (isLegal(where)) { storage[where] = what; }  }  bool isLegal(int where) const; // check if it's possible to move there.  // The other option is to hide isLegal(), and have place() return the  // value it got from its isLegal() call (so that it then becomes a   // success code).  void display() const; // output the board.}// Note the const-correctness in there. All our parameters are primitives being // passed by value, so constness is not really valuable there. In order to// inform C++ about which functions may change the board, though, we use the// const keywords in the function signatures. Note their placement: 'const'// there means "the data pointed to by the this-pointer is const".// Now for example, we can rewrite the HumanMove():// We'll actually make the move at this point, since that's easy to do.// Also that way we'll actually use the 'player' parameter. Some compilers// will give you warnings about unused parameters; you can normally shut them// up by not giving the parameter a name. (Sometimes that's useful when working// with someone else's API.)// I'll also use a do-while loop to remove the repetition of prompting...void makeHumanMove(const Board& board, char player) {    do {      // We'll subtract right away upon getting the value; that's cleaner      int move = AskNumber("Where will you move?", (board.size()));    } while(!board.IsLegal(move) &&             cout << "\nSorry, you cannot move there.\n");    // That part requires a bit of explanation for the uninitiated.    // If the move is legal, C++'s short-circuit evaluation will skip the cout,     // and the loop just breaks (because the while condition is false).     // If illegal, C++ does the cout and evaluates the result. Recall that     // "cout << foo" is an *expression*; it has a type and value (a reference     // to the cout object itself; that's how chaining "cout << foo << bar"     // works), and can be interpreted as a boolean (for ostreams, the object     // evaluates true iff the stream is "ok" for further writing). Assuming no     // catastrophic failures, the cout expression should thus always evaluate     // true, so the whole thing is true, and the loop continues.    // Complicated, yes, but considered fairly idiomatic too. A lot of the    // language design actually seems to revolve around making it possible to     // do things like this and have them be useful. :\    // Finally, make the move.    board.place(move, player);}


Quote:
Oh thanks for the modification for the AskNumber function. I haven't read about stringstream. How did you assign the input to the number? I saw you put the input into the stringstream, in the while where (ss >> number), is that where you assigned the input to number? That would have helped me a lot when debugging. Somehow I was assigning a number to a char and the SwitchPiece() was constantly returning X so the person to start first kept going without giving the other a chance to play.


That's reading the stringstream into the number, the same as you would from cin. Mnemonic: note the direction of the angle brackets; they point to the data recipient. :) The result of that expression is of course the stringstream object, and evaluating it in a boolean context tells us whether or not the assignment succeeded. (See comments in the previous source block; it's the same trick.)

Putting the input into the stringstream is acheived via the .str() call. You should also be able to do it directly with the constructor, but this way is a bit more explicit.

As for assigning a number to a char, you are of course aware that 'char' is a numeric type? :)

Quote:EDIT: I added the function with your modification with the stringstream and it wouldn't compile. Is there some header file i need to include in order to use the stringstream?

EDIT: I just found the header file from another post in forum, I think <sstream> is it. Either way i still have problems compiling said that ss is not declared, any ideas why?


You got it. 'stringstream' is in the std namespace (you're already 'using namespace std', so no problem), and in header <sstream>. Assuming everything is still in one file, there shouldn't be any problems :s Exact messages and code would be useful to help you fix it.

Thanks Zahlman. I'm pretty new with OOP so I wasn't sure how I could get a class in my game, but the way you explained it was really helpful. I'm going to work on putting the class Board into my code.

As for the errors when I put the code for the modified AskNumber function, I get a compile error on the line with the "}while (!(ss >> number) || number > high || number < low);"
The error says In function int AskNumber(std::string, int, int), ss undeclared (first use this function) (Each undeclared identifier is reported only once for each function it appears in.)

Here's the source for the function only:

int AskNumber(string question, int high, int low){    int number;    do    {        cout << question << " (" << low << " - " << high << "): ";        string input;        getline(cin,input);        stringstream ss;        ss.str(input);    }while (!(ss >> number) || number > high || number < low);        return number;}
You might have to declare the stringstream variable outside of the do loop - I think it's already out of scope once you get to the "while" bit of the loop. But if you declare it outside of the loop you might have to write some more code inside the loop to clear out any existing data in it (from the last iteration of the loop).

Sorry I can't be any more helpful, I haven't used stringstream before. Anyone?

D
Deniz ÖzsenSoftware Engineerhttp://www.symbian.com/
That was the problem Deniz. Thank you very much.

Adding the board class into the program, I finally got everything to compile, but now there are some bugs that appear in the program. I need to debug it a bit then I'll post up the source code to see if I implemented the class right and whether my code is readable.

This topic is closed to new replies.

Advertisement