Card engine in Java

How to create reusable classes for card games

This all started when we noticed that there were very few card game applications or applets written in Java. First we thought about writing a couple of games, and started by figuring out the core code and classes needed for creating card games. The process continues, but now there is a fairly stable framework to use for creating various card game solutions. Here we describe how this framework was designed, how it operates, and the tools and tricks that were used to make it useful and stable.

Design phase

With object-oriented design, it is extremely important to know the problem inside and out. Otherwise, it's possible to spend a lot of time designing classes and solutions that aren't needed or will not work according to specific needs. In the case of card games, one approach is to visualize what is going on when one, two, or more persons play cards.

A card deck usually contains 52 cards in four different suits (diamonds, hearts, clubs, spades), with values ranging from deuce to the king, plus the ace. Immediately a problem arises: depending on the rules of the game, the aces can be either the lowest card value, the highest, or both.

Furthermore, there are players who take cards from the deck into a hand and manage the hand based on rules. You can either show the cards to everyone by placing them on the table or look at them privately. Depending on the particular stage of the game, you might have N number of cards in your hand.

Analyzing the stages this way reveals various patterns. We now use a case-driven approach, as described above, that is documented in Ivar Jacobson's Object Oriented Software Engineering. In this book, one of the basic ideas is to model classes based on real-life situations. That makes it much easier to understand how relations operate, what depends on what, and how the abstractions operate.

We have classes such as CardDeck, Hand, Card, and RuleSet. A CardDeck will contain 52 Card objects at the start, and CardDeck will have fewer Card objects as these are drawn into a Hand object. Hand objects talk with a RuleSet object that has all the rules concerning the game. Think of a RuleSet as the game handbook.

Vector classes

In this case, we needed a flexible data structure that handles dynamic entry changes, which eliminated the Array data structure. We also wanted an easy way to add an insert element and avoid a lot of coding if possible. There are different solutions available, such as various forms of binary trees. However, the java.util package has a Vector class that implements an array of objects that grows and shrinks in size as necessary, which was exactly what we needed. (The Vector member functions are not fully explained in the current documentation; this article will further explain how the Vector class can be used for similar dynamic object list instances.) The drawback with Vector classes is additional memory use, due to a lot of memory copying done behind the scenes. (For this reason, Arrays are always better; they are static in size, so the compiler could figure out ways to optimize the code). Also, with larger sets of objects, we might have penalties concerning lookup times, but the biggest Vector we could think of was 52 entries. That's still reasonable for this case, and long lookup times were not a concern.

A brief explanation of how each class was designed and implemented follows.

Card class

The Card class is a very simple one: it contains values signalling the color and the value. It may also have pointers to GIF images and similar entities that describe the card, including possible simple behavior such as animation (flip a card) and so on.

class Card implements CardConstants {
  public int color;
  public int value;
  public String ImageName;
}

These Card objects are then stored in various Vector classes. Note that the values for the cards, including color, are defined in an interface, which means that each class in the framework could implement and this way include the constants:

interface CardConstants {
  // interface fields are always public static final!
  int HEARTS 1;
  int DIAMOND 2;
  int SPADE 3;
  int CLUBS 4;
  int JACK 11;
  int QUEEN 12;
  int KING 13;
  int ACE_LOW 1;
  int ACE_HIGH 14;
}

CardDeck class

The CardDeck class will have an internal Vector object, which will be pre-initialized with 52 card objects. This is done using a method called shuffle. The implication is that every time you shuffle, you indeed start a game by defining 52 cards. It is necessary to remove all possible old objects and start from the default state again (52 card objects).

  public void shuffle () {
    // Always zero the deck vector and initialize it
from scratch.
    deck.removeAllElements ();
  20
    // Then insert the 52 cards. One color at a time
    for (int i ACE_LOW; i < ACE_HIGH; i++) {
    Card aCard new Card ();
      aCard.color HEARTS;
      aCard.value i;
      deck.addElement (aCard);
      }
    // Do the same for CLUBS, DIAMONDS and SPADES.
  }

When we draw a Card object from the CardDeck, we are using a random number generator that knows the set from which it will pick a random position inside the vector. In other words, even if the Card objects are ordered, the random function will pick an arbitrary position within the scope of the elements inside the Vector.

As part of this process, we are also removing the actual object from the CardDeck vector as we pass this object to the Hand class. The Vector class maps the real-life situation of a card deck and a hand by passing a card:

  public Card draw () {
    Card aCard null;
    int position (int) (Math.random () * (deck.size =
()));
    try {
      aCard (Card) deck.elementAt (position);
    }
    catch (ArrayIndexOutOfBoundsException e) {
      e.printStackTrace ();
    }
    deck.removeElementAt (position);
    return aCard;
  }

Note that it is good to catch any possible exceptions related to taking an object from the Vector from a position that is not present.

There is a utility method that iterates through all the elements in the vector and calls another method that will dump an ASCII value/color pair string. This feature is useful when debugging both the Deck and the Hand classes. The enumeration features of vectors are used a lot in the Hand class:

  public void dump () {
   Enumeration enum deck.elements ();
    while (enum.hasMoreElements ()) {
    Card card (Card) enum.nextElement ();
      RuleSet.printValue (card);
      }
  }

Hand class

The Hand class is a real workhorse in this framework. Most of the behavior required was something that was very natural to place in this class. Imagine people holding cards in their hands and doing various operations while looking at the Card objects.

First, you also need a vector, since it's unknown in many cases how many cards will be picked up. Although you could implement an array, it's good to have some flexibility here, too. The most natural method we need is to take a card:

  public void take (Card theCard){
    cardHand.addElement (theCard);
  }

CardHand is a vector, so we are just adding the Card object into this vector. However, in the case of the "output" operations from the hand, we have two cases: one in which we show the card, and one in which we both show and draw the card from the hand. We need to implement both, but using inheritance we write less code because drawing and showing a card is a special case from just showing a card:

  public Card show (int position) {
    Card aCard null;
    try {
      aCard (Card) cardHand.elementAt (position);
    }
    catch (ArrayIndexOutOfBoundsException e){
      e.printStackTrace ();
    }
    return aCard;
  }
20
  public Card draw (int position) {
    Card aCard show (position);
    cardHand.removeElementAt (position);
    return aCard;
  }

In other words, the draw case is a show case, with the additional behavior of removing the object from the Hand vector.

In writing test code for the various classes, we found an increasing number of cases in which it was necessary to find out about various special values in the hand. For example, sometimes we needed to know how many cards of a specific type were in the hand. Or the default ace low value of one had to be changed into 14 (highest value) and back again. In every case the behavior support was delegated back into the Hand class, as it was a very natural place for such behavior. Again, it was almost as though a human brain was behind the hand doing these calculations.

The enumeration feature of vectors may be used to find out how many cards of a specific value were present in the Hand class:

  public int NCards (int value) {
    int n 0;
    Enumeration enum cardHand.elements ();
    while (enum.hasMoreElements ()) {
        tempCard (Card) enum.nextElement (); // =
tempCard defined
        if (tempCard.value= value)
            n++;
      }
    return n;
  }

Similarly, you could iterate through the card objects and calculate the total sum of cards (as in the 21 test), or change the value of a card. Note that, by default, all objects are references in Java. If you retrieve what you think is a temporary object and modify it, the actual value is also changed inside the object stored by the vector. This is an important issue to keep in mind.

RuleSet class

The RuleSet class is like a rule book that you check now and then when you play a game; it contains all the behavior concerning the rules. Note that the possible strategies a game player may use are based either on user interface feedback or on simple or more complex artificial intelligence (AI) code. All the RuleSet worries about is that the rules are followed.

Other behaviors related to cards were also placed into this class. For example, we created a static function that prints the card value information. Later, this could also be placed into the Card class as a static function. In the current form, the RuleSet class has just one basic rule. It takes two cards and sends back information about which card was the highest one:

  public int higher (Card one, Card two) {
    int whichone 0;
    if (one.value= ACE_LOW)
        one.value ACE_HIGH;
    if (two.value= ACE_LOW)
        two.value ACE_HIGH;
    // In this rule set the highest value wins, we don't
take into
    // account the color.
    if (one.value > two.value)
        whichone 1;
    if (one.value < two.value)
        whichone 2;
    if (one.value= two.value)
        whichone 0;
// Normalize the ACE values, so what was passed in has
the same values.
    if (one.value= ACE_HIGH)
        one.value ACE_LOW;
    if (two.value= ACE_HIGH)
        two.value ACE_LOW;
      return whichone;
  }

You need to change the ace values that have the natural value of one to 14 while doing the test. It's important to change the values back to one afterward to avoid any possible problems as we assume in this framework that aces are always one.

In the case of 21, we subclassed RuleSet to create a TwentyOneRuleSet class that knows how to figure out if the hand is below 21, exactly 21, or above 21. It also takes into account the ace values that could be either one or 14, and tries to figure out the best possible value. (For more examples, consult the source code.) However, it's up to the player to define the strategies; in this case, we wrote a simple-minded AI system where if your hand is below 21 after two cards, you take one more card and stop.

How to use the classes

It is fairly straightforward to use this framework:

    myCardDeck new CardDeck ();
    myRules new RuleSet ();
    handA new Hand ();
    handB new Hand ();
    DebugClass.DebugStr ("Draw five cards each to hand A
and hand B");
    for (int i 0; i < NCARDS; i++) {
        handA.take (myCardDeck.draw ());
        handB.take (myCardDeck.draw ());
      }
// Test programs, disable by either commenting out or
using DEBUG flags.
    testHandValues ();
    testCardDeckOperations();
    testCardValues();
    testHighestCardValues();
    test21();

The various test programs are isolated into separate static or non-static member functions. Create as many hands as you want, take cards, and let the garbage collection get rid of unused hands and cards.

You call the RuleSet by providing the hand or card object, and, based on the returned value, you know the outcome:

    DebugClass.DebugStr ("Compare the second card in
hand A and Hand B");
    int winner myRules.higher (handA.show (1), =
handB.show (1));
    if (winner= 1)
        o.println ("Hand A had the highest card.");
    else if (winner= 2)
        o.println ("Hand B had the highest card.");
    else
        o.println ("It was a draw.");

Or, in the case of 21:

    int result myTwentyOneGame.isTwentyOne (handC);
    if (result= 21)
        o.println ("We got Twenty-One!");
    else if (result > 21)
        o.println ("We lost " + result);
    else {
        o.println ("We take another card");
      // ...
      }

Testing and debugging

It is very important to write test code and examples while implementing the actual framework. This way, you know at all times how well the implementation code works; you realize facts about features and details about implementation. Given more time, we would have implemented poker -- such a test case would have provided even more insight into the problem and would have shown how to redefine the framework.

Another positive aspect of writing test code is that such code will document how to use the classes. In many cases, writing the test code is much better than writing plain text descriptions of the classes.

Finally, by repeatedly retriggering the test code, you can see if something suddenly stops working, and based on the earlier code activity, you have a good chance of stopping a possible bug. In this case, plain single stepping with a debugger also helps a lot. Single stepping lets you know completely how your code behaves; if it suddenly does something strange, such a debug session will help you to quickly isolate and fix the problems.

We used a scheme of defining debug levels in the test code and classes, by defining constants in another interface:

interface DebugConstants {
  boolean DEBUG true;     //  turns on and off =
information message
  boolean DEBUG1 DEBUG;
  boolean DEBUG2 true;    //  enables/disables minor =
dump information
  boolean DEBUG3 false;   //  enables/disables a lot =
of dump information
}

This way we could enable and disable various output information while running the test code. We also isolated assertions and debugged print statements into a separate class using static member functions:

class DebugClass implements DebugConstants {
  static final PrintStream o System.out;
  static void Assert (boolean b){
    if (!b) {
        new Throwable ().printStackTrace ();
      }
  }
  static void DebugStr (String string) {
    if (DEBUG)
      System.out.println (string);
  }
}

Example of use:

    DebugClass.DebugStr ("Starting Card Test.");
    DebugClass.DebugStr ("Creating new Card Deck.");
    myCardDeck new CardDeck ();

By defining DEBUG to false such output is no longer present.

I can't count how many times such preventive coding has helped me catch a bug before it shows up much later. It's good to prepare for the worst and aim at the best; writing preventive code will substantially improve the quality of the final product.

Conclusion

You could use the Vector class for other possible dynamic classes from which you send and retrieve objects. This exercise provides you with a lot of sample code for other projects you may have in mind. And this framework is a good starting point if you want to provide the core functionality for a card game. The

full source code for the framework and test code

is available.

Kent Sandvik works as a developer support engineer for Silicon Graphics, Inc. He really likes developer support and has worked in this field for over ten years. Kent has held various positions at SGI and Apple, including technical lead for the Newton Developer support group and QuickTime developer support lead.
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more