Program 2 (Final) -- Solitaire in C++
CS 3411 - Programming Language Concepts - Fall '98
Due:  midnight Tuesday 8 December (late date midnight Friday 12 December)

Got Questions? Try office hours, email or the newsgroup git.cc.class.3411.

"If you want it done right, do it yourself..."

This document describes a rough design for your second programming assignment, an implementation of the Windows Solitaire game in C++ with a textual user-interface. A previous handout "Program 2 (Prelude)" described the rules of the game, the textual user-interface and a simple command language.

Main

Execution of a C++ program begins in a global main() function, just like C. It is customary when implementing a game to have a class that encapsulates an instance of the game and supports a simple .play() function. All that main() needs to do is instantiate the game object and invoke the play() function. A common design decision arises: does play() mean play the game once or repeatedly? In this case, I've incorporated a "newgame" command in the command language, so the play() function should itself contain a loop that allows the game to be played repeatedly. Therefore main() looks like this:

// FILE: solitaire.cc -- driver for Solitaire game
#include "Solitaire.h"
int main() {
    Solitaire game(parameters); // create a game instance

    game.play(); // play the game!
    return 0;
}
You may wish to use argc and argv to supply command line parameters to your program to make it more flexible. You can pass arguments along to the Solitaire class constructor to get different behaviors. (Maybe different "Draw" or "Scoring" options.)

If the play() method only played the game once, we might write:


int main() {
    Solitaire game(parameters);
    do {
        game.play();
        cout << "Play again? " << flush;
        char c;
        cin >> c;
    } while ( c == 'y' || c == 'Y' );
}
(Notice that this is a classic use of the do-while loop. You always want to play the game one time.)

You should put your main() in a file called solitaire.cc as indicated above.

The Classes

Solitaire

We will use the convention of beginning each class name with an uppercase letter. Constants will be all caps and variables and functions will begin with lowercase and capitalize the first letter of subsequent words. (Don't use underscores.) Each class will be placed in a separate file with the same name as the class. Classes in C++ should always have a .h and .cc file. The .h file will contain the class declaration. The .cc file will contain the member function definitions. The .h file should be included by each client of the class and you should include it in the .cc file as well (this lets the C++ compiler ensure that the declarations and definitions are consistent.) Each class should have a brief header comment as should each member function definition. All header files should have preprocessor "guards" to avoid multiple inclusions:

// FILE: Solitaire.h
#ifndef INCLUDED_SOLITAIRE_H
#define INCLUDED_SOLITAIRE_H
class Solitaire {
    public:
        //public member function prototypes
        constructor
        play()
        display()
    private:
        //private fields
        DeckPile deckPile;
        DiscardPile discardPile;
        TablePile tableau[7];
        SuitPile suitPiles[4];

        // private member function prototypes
        getCmd()
        doCmd()
        newGame()
        newCard()
        moveKing()
        moveAce()
        move()
}
#endif

The Solitaire class will have private fields to represent the "state" of the game. We need a DeckPile, a DiscardPile, an array of 4 SuitPiles and an array of 7 TablePiles. These various card piles are initialized by the Solitaire constructor. Solitaire needs a public play() method that implements a "command dispatch" loop. Something like this:
void Solitaire::play() {
    String line;
    Cmd cmd;
    while ( (cmd = getCmd(line)) != QUIT ) {
        doCmd(cmd, line);
        display();
     }
}


You can use an enumeration to define symbolic constants for the valid commands, maybe something like this:


typedef enum {
    NEWGAME, NEWCARD,
    MOVE, MOVE_ACE, MOVE_KING,
    QUIT
} Cmd;
The function Cmd getCmd(String& line) will prompt the user for a command, validate the command typed in, complain if there is an error, and otherwise return the appropriate Cmd code and the actual line read (so we can get the to and from arguments for a move). Notice that we implement an OUT parameter in C++ by using the REFERENCE (&) capability.

The doCmd(cmd, line) method is just a simple switch statement that dispatches the appropriate method: newCard(), newGame(), moveAce(Card c), moveKing( Card c), move( Card c, Card dst ). These methods (move in particular) encode and implement the rules of the game by changing the game state (decks). Note that Solitaire methods have access to all the private fields in the Solitaire game object.

There are more sophisticated ways to implement this functionality using a Cmd class with subclasses Move, NewGame, NewCard, etc. and a polymorphic execute() method but the above approach is fine. (In general, you can replace switch statements with a polymorphic method invocation.)

Card

Create a simple card class that contains the suit and rank of each card. It is convenient to encode these as small integers. Your card class should have a method to return a string representation of the card including a leading color designation ("r-3H"). Card have a boolean attribute (field faceup). Provide a constructor and accessor methods for suit, rank, color and a boolean method to ask if the card if faceUp(). Provide a method to turn the card over (flip()). You might want to use C++'s operator overloading capability to provide < and > operators for comparing rank.

Stack

Implement a simple bounded stack class to represent card piles. (You can assume stacks never contain more than 52 cards.) You need void push(Card c), Card pop(), Card peek() and bool isEmpty(). It will be convenient to violate the Stack abstraction to implement the move of "builds" of cards. There are lots of ways to do this. We will discuss this more in class.

CardPile

CardPile is the "generic" parent of all the specific types of card piles. CardPile provides that standard stack operations and special addCard() and canTake() methods. These have reasonable default implementations but can be overriden by other "derived" piles as necessary.

SuitPile

SuitPile overrides canTake(Card c) to only accept:

1) any ace if the stack is empty, or
2) a card if it is of the same suit as the top of the pile and its rank it one larger.
DeckPile

The deck pile constructor initializes all 52 cards and then shuffles them using a random number generator. The Solitaire constructor can "deal" the cards to initialize the TablePiles once the DeckPile has been constructed. The DeckPile constructor can accept a reference to the DiscardPile so that the DeckPile can "recycle" the cards in the DiscardPile when the user asks for a newcard and the DeckPile is empty. This requires a bit of special C++ syntax we will go over in class.

DiscardPile

The DiscardPile just nees to add cards faceup. (Variations of Solitaire take 3 cards at a time from the DeckPile and add them to the DiscardPile. This makes the game harder to play.)

TablePile

The TablePile constructor just creates an empty stack. The Solitaire constructor will "deal" cards to initialize the table piles before the game begins. TablePile needs to override canTake(). We need to be able to turnover the top card if it is facedown. Return a string representation of the pile when asked. It should include the "3----" line at the top indicating the number of facedown cards. Subsequent faceup cards should appear in the string representation separated by newlines: "3-----\nb-10C\r-9H \b-8S ". We can use this format to cleverly print the entire tableau using the method described in the Details section below.

The only real complication of TablePile arises when we try to support the move of "builds". We need a way of asking if a card appears faceup anywhere in the pile. Add a method int contains(Card c) that returns the index in the stack array where the card appears or -1 if it does not appear in the pile. Then we need a way to move an entire build from one pile to another. Several approaches are possible. One simple technique is to pop all the card into a temporary CardPile stack and then push them onto the new TablePile. Other approaches would carefully violate the stack abstraction to allow a more efficient copying of elements from
one TablePile to another and then appropriately resizing the two piles. You can use the C++ friend feature to do tricks like this.

Details

The display() routines are a bit tricky with the textual interface. It is easy to display the entire game state with a GUI since every component maintains its position. It actually doesn't matter what order things are drawn. With a textual representation we are constrained to printing left to right and top to bottom. This is a problem for printing the "multiline" textual representation of a TablePile. One clever solution would be to develop a multiline string concatentation method. Given the following two strings:

    s1 = "one a\one b\n";
    s2 = "two a\ntwo b\n"
    s3 = multilineConcat( s1, s2 );

would produce the string:

    s3 = "one a two a\none b two b\n"

which prints as:

    one a two a
    one b two b

This way you can "accumulate" the textual representation for the tableau (all the TablePiles) by asking each TablePile for its string representation and doing the funny multilineConcat().

What Do I Do First?

Understand the game first. Read the detailed Java design (Budd Chapter 9) handed out in class. Then learn some C++. We will provide a Web page describing how to compile and execute C++ program. Pay special attention to simple io and inheritance. Work incrementally. Start by developing the command dispatch code. Then develop the Card class, then the Stack class, then CardPile, then the specific CardPile subclasses. Finally put it all together by implementing the game commands. Start with a few simple cases for the move() command and gradually add more functionality. Test your code. Have fun playing the game!

Extra Credit

If you get the basic assignment going, feel free to add options. Just make sure they are well described in the source code or in an accompanying README file.