NetBeans Project

The Game of Cribbage

Cribbage is a two-player card game with moderately complex rules. Writing a computer program to play this game between a human player and the computer is a good exercise in programming. In particular, we are going to use this opportunity to demonstrate how to use object-oriented design to build the solution to a moderately complex problem.

Before we get started building our Cribbage playing program, you should click this link and review the rules of the game.

Identifying objects

The first step in an object-oriented design process is to identify objects. For each distinct object type we can identify we will construct a class to model the characteristics and behaviors of that type.

Cribbage is like most card games: players play with cards, cards get dealt from a deck, and players hold hands, which are groups of cards. The only other class we will need to create to implement the game is some sort of game class that will be responsible for actually playing the game.

Identifying attributes

Once we have identified the classes we will be working with we work on filling the details in those classes. The first step for each class is identifying the attributes of that class. These will eventually become the member variables in the class.

We can start by making a quick list of some of the more obvious class attributes.

Identifying actions

To implement the actual mechanics of the game we will ask our classes to carry out specific actions. These actions will eventually get implemented as methods in our classes. Here are some specific actions we can identify at this stage.

Identifying algorithms

Once we have made a preliminary list of the methods that we will need to implement we should also try to identify which of these methods are going to be straightforward to implement, and which methods might be a little more challenging.

Here are some potentially challenging things we will have to be able to do.

In each case where we need to implement some algorithm we can either use a prebuilt implementation of that algorithm from the Java class library or we can craft our own solution.

Both sorting and shuffling are standard actions that are implement in the class library. For example, this is code we can use to set up and shuffle a list of cards:

ArrayList<Card> cards = new ArrayList<Card>();
for(each suit s) {
    for(each face value f) {
        cards.add(new Card(s,f));
    }
}
Collections.shuffle(cards);

The Java Collections class offers a static shuffle() method that we can use to randomize any collection.

Likewise, there is a static sort() method in the Collections class that can sort a collection of objects into order. The only requirement that this sort() method imposes is that it must be able to compare any two objects in the collection to determine whether or not they are in the correct order. When we implement the Card class I will show how to add this capability to our cards.

The one remaining problem we face here is the problem of forming all possible subsets of a hand. Unfortunately, the Java class library does not offer any code to do this for us, so we are going to have to implement our own solution.

Here is the approach we will use. When we form a subset of set of n items, we have to make n decisions. For each item in the set we have to decide whether or not we want to add that item to our subset. This makes it possible for us to set up a mapping from sequences of 0s and 1s to subsets. For example, if we want to represent the subset {1,3,5} of the set {1,2,3,4,5} we would use the sequence

10101

Another interesting thing we can do here is to map these sequences of 0s and 1s to integers: we simply reinterpret each sequence as the binary representation of an integer. For example, the sequence above corresponds to the integer 21.

These observations form the basis of an algorithm to systematically construct all possible subsets of a set of n items:

for each integer n in the range from 0 to 2n - 1
  turn n into a sequence of 0s and 1s
  use the sequence to pick out the subset

How do we determine the binary digits of an integer? Here is a well-known algorithm we can use to decompose a positive integer into its binary digits:

let L be an empty list of bits
while n > 0
  Compute n % 2 and add that to the front of L
  set n = n / 2

This algorithm works because n % 2 gives us the last bit of n, and n / 2 drops the last bit off of n.

We can also modify this algorithm to construct the subset of a set of cards that corresponds to n:

let H be a list of cards
let L be an empty list of cards
set k = 0
while n > 0
  if(n % 2 == 1) add H[k] to the front of L
  set n = n / 2
  set k = k + 1

How do we score subsets? The approach I am going to use is to construct a set of classes to handle scoring. Because there are multiple ways that a given subset can earn points, I am going to set up a different class for each distinct way to earn points. For example, one class will look for subsets whose point values sum to 15, while another class will look for subsets that form runs.

To implement these classes I am going to start by defining an interface for what a scorer does:

public interface Scorer {
    public int score(List<Card> cards);
    public String what();
}

I will then set up separate scoring classes to look for subsets that do the following:

The classes

We now know enough about the classes that we need to create that we can go ahead and construct them. At the top of these lecture notes you will find a button: clicking that button will download a NetBeans project that contains the full code for a first version of the Cribbage program.

The rest of these notes will walk you through each of the classes in the program. For each class I will point out some of the special features of the class and how it is implemented.

Card class

Here is the code for the Card class, which represents playing cards.

package cribbage;

public class Card implements Comparable {
    public enum Suit {CLUB,DIAMOND,HEART,SPADE};
    public enum Face {ACE,TWO,THREE,FOUR,FIVE,SIX,SEVEN,EIGHT,NINE,TEN,JACK,QUEEN,KING};
    
    private Suit suit;
    private Face face;
    
    public Card(Suit s,Face f) {
        suit = s;
        face = f;
    }
    
    public Suit getSuit() { return suit; }
    public Face getFace() { return face; }
    public int getPoints() { 
        if(face.ordinal() <= Face.TEN.ordinal())
            return face.ordinal() + 1; 
        else 
            return 10;
    }
    
    public String toString() {
        String[] names = {"Ace","2","3","4","5","6","7","8","9","10","Jack","Queen","King"};
        String[] suits = {"Clubs","Diamonds","Hearts","Spades"};
        return names[face.ordinal()] + " of " + suits[suit.ordinal()];
    }

    @Override
    public int compareTo(Object o) {
        Card other = (Card) o;
        int first = this.face.ordinal();
        int second = other.getFace().ordinal();
        if(first < second)
            return -1;
        else if(first > second)
            return 1;
        return 0;
    }
    
    
    public static void main(String[] args) {
        Card c = new Card(Suit.HEART,Face.QUEEN);
        System.out.println(c.toString() + " is worth " + c.getPoints() + " points.");
    }
}

A card consists of a Suit and a Face value. To represent possible values for these types, we use a Java enum construct. A Java enum declaration introduces a new type and gives the possible values that variables of that type can take. For example, the enum declaration

public enum Suit {CLUB,DIAMOND,HEART,SPADE};

improves on the older technique programmers would use in this situation, which was to introduce a large number of numeric constants:

public static final int CLUB = 0;
public static final int DIAMOND = 1;
public static final int HEART = 2;
public static final int SPADE = 3;

One advantage of enum types is that introduce distinct types, which makes working with these quantities less error prone. For example, in the code above the suit and face member variables and the constructor are set up as

private Suit suit;
private Face face;
    
public Card(Suit s,Face f) {
    suit = s;
    face = f;

}

In the older way of doing things this would be

private int suit;
private int face;

public Card(int s,int f) {
    suit = s;
    face = f;
}

In this way of doing things both the suit and the face parameters in the constructor are ints, which may make it possible to pass these parameters in the wrong order.

When we use an enum type we may occasionally want to convert an enum constant like HEART or KING to an integer. We can easily do this by using the ordinal() method of the enumerated type.

In addition to a Suit and a Face, each card also has a point value. To compute point values I have also provided a getPoints() method in the Card class.

I have also provided a couple of convenience methods here: a toString() method and a compareTo() method. The toString() method makes it easy to convert a Card to a String on demand. In the main() method that I have provided here for testing purposes, you can see some test code that uses that method. To make it possible to sort Cards, I have made the Card class implement the Comparable interface and I have provided an override of the compareTo() method. This method uses face values to compare a card against another card. The expectation is that compareTo() will return a negative value if this card is less than the other card, 0 if the two cards have the same value, and a positive value if this card is greater than the other card.

The Deck class

Next, we need a class to represent a deck of cards:

package cribbage;

import java.util.ArrayList;
import java.util.Collections;

public class Deck {
    private ArrayList<Card> cards;
    private int dealt;
    
    public Deck() {
        cards = new ArrayList<Card>();
        for(Card.Suit s : Card.Suit.values()) {
            for(Card.Face f : Card.Face.values()) {
                cards.add(new Card(s,f));
            }
        }
        Collections.shuffle(cards);
        dealt = 0;
    }
    
    public void shuffle() {
        Collections.shuffle(cards);
        dealt = 0;
    }
    
    public Card deal() {
        if(dealt < cards.size()) {
            Card c = cards.get(dealt);
            dealt++;
            return c;
        }
        return null;
    }
    
    public static void main(String[] args) {
        Deck d = new Deck();
        System.out.println("First Hand");
        for(int n = 0;n < 6;n++)
            System.out.println(d.deal());
        System.out.println("Second Hand");
        for(int n = 0;n < 6;n++)
            System.out.println(d.deal());
        
        d.shuffle();
        System.out.println("Shuffle");
        System.out.println("First Hand");
        for(int n = 0;n < 6;n++)
            System.out.println(d.deal());
        System.out.println("Second Hand");
        for(int n = 0;n < 6;n++)
            System.out.println(d.deal());
    }
}

A Deck consists of a list of Cards, along with a count of how many of the cards we have already dealt out. The constructor for this class sets up a full list of cards for the deck and then shuffles the list.

I have also provided a main method for this class so we can test that its methods are working correctly.

The Hand class

The Hand class represents a set of cards that a player is holding. We will use this class to represent each player's cards and also the crib. Since we are going to be moving cards into and out of hands at various points in the game I have provided several methods for adding and remove cards from the hand.

package cribbage;

import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;

public class Hand {
    private List<Card> cards;
    private Card temp;
    
    public Hand(Card[] cs) {
        Arrays.sort(cs);
        cards = new ArrayList<Card>();
        for(Card c : cs)
            cards.add(c);
    }
    
    public Hand(List<Card> cs) {
        Collections.sort(cs);
        cards = cs;
    }
    
    public void print() {
        for(int n = 0; n < cards.size();n++)
            System.out.println(String.valueOf(n) + ") " + cards.get(n));
    }
    
    public List<Card> allCards() { return cards; }
    
    public void addTemp(Card c) {
        temp = c;
        cards.add(c);
        Collections.sort(cards);
    }
    
    public void removeTemp() {
        cards.remove(temp);
        temp = null;
    }
    
    public Card remove(int n) {
       Card discard = cards.get(n);
       cards.set(n, null);
       return discard;
    }
    
    /** Calling remove() sets some of the cards in the hand to null.
     *  To avoid problems caused by null cards, call consolidate()
     *  after calling remove. consolidate() removes the null cards.
     */
    public void consolidate() {
        ArrayList<Card> newCards = new ArrayList<Card>();
        
        for(Card c : cards) {
            if(c != null)
                newCards.add(c);    
        }
        cards = newCards;
    }
    
    public List<Card> subset(int n) {
        LinkedList<Card> s = new LinkedList<Card>();
        int loc = this.cards.size() - 1;
        while(n > 0 && loc >= 0) {
            if(n % 2 == 1)
                s.addFirst(cards.get(loc));
            n = n/2;
            loc--;
        }
        return s;
    }
    
    /** Given a subset of cards to keep, remove and return unwanted cards. **/
    public List<Card> retain(List<Card> keep) {
        ArrayList<Card> discards = new ArrayList<Card>();
        for(Card c : cards)
            if(!keep.contains(c))
                discards.add(c);
        this.cards = keep;
        return discards;
    }
    
    public static void main(String[] args) {
        Deck d = new Deck();
        Card[] h = new Card[6];
        for(int n = 0;n < 6;n++) {
            h[n] = d.deal();
        }
        Hand hand = new Hand(h);
        System.out.println("Hand");
        hand.print();
        
        for(int n = 1;n < 64;n++) {
            List<Card> s = hand.subset(n);
            int pts = 0;
            for(Card c : s) {
                pts += c.getPoints();
            }
            if(pts == 15) {
                System.out.println();
                System.out.println("Subset adds to 15:");
                for(Card c : s)
                    System.out.println(c);
            }
        }
    }
}

The addTemp()/removeTemp() methods will get used when we score player hands and the crib, since the scoring process will require us to temporarily add the starter card to a hand, score then hand, and then remove the starter card again.

The subset() method implements the subset creation scheme that I described above.

The Scoring classes

To implement the process of scoring a hand of cards I have created a number of Scoring classes. These classes all implement the Scoring interface I showed above.

Here is one the Scoring classes, a class that determines whether or not a set of cards has points that sum to 15:

import cribbage.Card;
import java.util.List;

public class FifteenScorer implements Scorer {

    @Override
    public int score(List<Card> cards) {
        int pts = 0;
        for(Card c : cards) {
            pts += c.getPoints();
        }
        if(pts == 15) {
            return 2;
        }
        return 0;
    }

    @Override
    public String what() {
        return "Sums to 15";
    }
    
}

The Cribbage class

The most complex class in the program is the Cribbage class, which will ultimately implement the full game.

I have provided enough code in this class to implement only part of the game logic. The methods here will make it possible to deal cards to the players, manage discarding cards into the crib, and then scoring the players's hands and the crib.

package cribbage;

import cribbage.scoring.FifteenScorer;
import cribbage.scoring.FlushScorer;
import cribbage.scoring.PairScorer;
import cribbage.scoring.RunScorer;
import cribbage.scoring.Scorer;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Cribbage {
    private Deck deck;
    List<Scorer> scorers;
    private Hand player;
    private Hand computer;
    private Hand crib;
    private Card starter;
    private int playerCount;
    private int computerCount;
    private boolean playerTurn;
    private boolean gameOver = false;
    
    public Cribbage() {
        deck = new Deck();
        scorers = new ArrayList<Scorer>();
        scorers.add(new FifteenScorer());
        scorers.add(new PairScorer());
        scorers.add(new RunScorer());
        playerTurn = false;
        gameOver = false;
        playerCount = 0;
        computerCount = 0;
    }
    
    public boolean isGameOver() { return gameOver; }
    
    public void deal() {
        if(playerTurn)
            System.out.println("Your crib");
        else
            System.out.println("My crib");
        
        System.out.println("Your hand:");
        Card[] h = new Card[6];
        Card[] c = new Card[4];
        
        for(int n = 0;n < 6;n++) {
            h[n] = deck.deal();
        }
        player = new Hand(h);
        player.print();
        
        System.out.print("Pick two to discard: ");
        int one,two;
        Scanner input = new Scanner(System.in);
        one = input.nextInt();
        c[0] = player.remove(one);
        two = input.nextInt();
        c[1] = player.remove(two);
        player.consolidate();
        
        for(int n = 0;n < 6;n++) {
            h[n] = deck.deal();
        }
        computer = new Hand(h);
        
        int bestScore = 0;
        List<Card> bestSubset = null;
        for(int n = 0;n < 64;n++) {
            List<Card> temp = computer.subset(n);
            if(temp.size() == 4) {
                Hand tempHand = new Hand(temp);
                int points = 0;
                for(int k = 0;k < 16;k++) {
                    List<Card> subset = tempHand.subset(k);
                    for(Scorer sc : scorers) {
                        points += sc.score(subset);
                    }
                }
                if(points > bestScore) {
                    bestScore = points;
                    bestSubset = temp;
                }
            }
        }
        if(bestSubset == null) {
            c[2] = computer.remove(0);
            c[3] = computer.remove(1);
            computer.consolidate();
        } else {
            List<Card> discards = computer.retain(bestSubset);
            c[2] = discards.get(0);
            c[3] = discards.get(1);
        }
        crib = new Hand(c);
        
        starter = deck.deal();
        System.out.println("Starter is " + starter);
        
    }
    
    public int scoreHand(Hand h) {
        int points = 0;
        for(int n = 1;n < 32;n++) {
            List<Card> s = h.subset(n);
            for(Scorer sc : scorers) {
                int scored = sc.score(s);
                points += scored;
                if(scored != 0) {
                    System.out.println();
                    System.out.println(sc.what() + ": " + scored);
                    for(Card c : s)
                        System.out.println(c);
                }
            }
        }
        FlushScorer f = new FlushScorer();
        int fs = f.score(h.allCards());
        if(fs > 0) {
            points += fs;
            System.out.println("Hand is flush: " + fs);
        }
        
        System.out.println();
        System.out.println("Total points: " + points);
        return points;
    }
    
    public void doRound() {
        if(playerTurn) {
            System.out.println("My hand:");
            computer.addTemp(starter);
            computer.print();
            computerCount += scoreHand(computer);
            computer.removeTemp();
            if(computerCount > 60) {
                System.out.println("My score is now " + computerCount);
                System.out.println("I win!");
                gameOver = true;
                return;
            }
            System.out.println("Your hand:");
            player.addTemp(starter);
            player.print();
            playerCount += scoreHand(player);
            player.removeTemp();
            System.out.println("Your crib:");
            crib.addTemp(starter);
            crib.print();
            playerCount += scoreHand(crib);
            crib.removeTemp();
            if(playerCount > 60) {
                System.out.println("Your score is now " + playerCount);
                System.out.println("You win!");
                gameOver = true;
            }
        } else {
            System.out.println("Your hand:");
            player.addTemp(starter);
            player.print();
            playerCount += scoreHand(player);
            player.removeTemp();
            if(playerCount > 60) {
                System.out.println("Your score is now " + playerCount);
                System.out.println("You win!");
                gameOver = true;
                return;
            }
            System.out.println("My hand:");
            computer.addTemp(starter);
            computer.print();
            computerCount += scoreHand(computer);
            computer.removeTemp();
            System.out.println("My crib:");
            crib.addTemp(starter);
            crib.print();
            computerCount += scoreHand(crib);
            crib.removeTemp();
            if(computerCount > 60) {
                System.out.println("I win!");
                gameOver = true;
            }
        }
        System.out.println("My score is now " + computerCount);
        System.out.println("Your score is now " + playerCount);
                     
        playerTurn = !playerTurn;
        deck.shuffle();
    }
    
    public static void main(String[] args) {
        Cribbage game = new Cribbage();
        while(!game.isGameOver()) {
            game.deal();
            game.doRound();
        }
    }
    
}

To manage the complexity of playing the game, I have introduced a number of helper methods to manage different aspects of game play.

The deal() method takes care of these steps:

To determine which cards the computer player will discard the method will form all possible subsets of size four in the computer player's hand and score them. The method will then retain the subset that produces the highest score and discard the two unused cards into the crib.

The doRound() method manages the rest of the mechanics of playing a round:

Finally, I have provided a main() method that implements the game mechanics for this simplified version of the game.

Assignment

The game as I have implemented is incomplete. If you look at the section titled 'The Play and the Showing' in the description of the game rules you will see that all that I have implemented is the 'Showing' part of the game rules. Your job in this assignment is to implement the 'Play' portion of each game round.

To implement this you will need to break the doRound() method in the code above into two methods, a doPlay() method and doShowing() method. To implement the logic of the doPlay() method you will need to set up some lists where you can store the cards the players discard and a list where you can keep track of the sequence of cards played to help you score the play in this phase of the round. To implement the logic in the doPlay() method you may need to add some additional methods to the Hand class as well.

When you have finished implementing the full game, compress your project folder and upload it on Canvas.

This assignment is a partner project in which you will be working with a partner. When you submit your work on Canvas you will need to submit just one copy of the project. Canvas should ask you who are the members of the group submitting the project.

Homework Solution

Solution

Click the button to download my solution to the homework assignment.