Click here to Skip to main content
15,884,637 members
Articles / Programming Languages / C#

Cribbage Hand Counting Library

Rate me:
Please Sign up or sign in to vote.
4.86/5 (15 votes)
4 Sep 2006CPOL12 min read 83K   960   22   15
A library to score cribbage hands

Sample Image - criblib01.png

Background

Since MSN games removed multi-player cribbage from their list of online games, I've been interested in writing my own. There are many interesting problems associated with that goal, but one of the core requirements would be a way to score a hand. After Googling for existing scoring algorithms and not finding any, I set about writing my own. I'll show you how to write a library to score a cribbage hand, and share a couple of the challenges along the way.

The Game

An excellent overview of the game can be found here, so I won't go too far into the details.

Very briefly, it is a card game played between two or three people (for simplicity, I'll only consider two-player games here). Each of the players is dealt 6 cards, and must contribute two cards to a secret hand (sometimes called the "kitty" or the "crib"), later scored by the dealer. Once everyone has added cards to the kitty, a community card is cut from the deck for later use by all the players in scoring their hands. The players then play their cards in the first phase of scoring for "pegging points". Once all the cards have been played, the non-dealer player scores their hand and adds their points to their game total. The dealer scores his hand, then scores his kitty. The first player to get 121 wins.

It is the scoring of the hand at the end of the round that interests us here.

Scoring

A "hand" is the four cards the player is holding, plus the "cut" card. For most scoring events, the distinction between the cards of the hand and the cut card is irrelevant, so the hand can be scored mainly as a single unit of five cards. I'll point out the situation when it makes a difference below. There is also a slight difference in scoring if the hand is a kitty, and I'll highlight that difference shortly too.

There are five ways to win points scoring a cribbage hand:

Pair For every pair in the hand, the player scores two points.
15 For every combination of cards that adds to 15, the player scores two points.
Runs For each card in a run of 3, 4, or 5 cards, the player scores one point.
Flush If the four cards of a hand are the same suit, the player scores four points. If they also match the cut card, the player scores five. If a player is scoring their kitty though, all five cards must match and score five points.
Nobs This was always called "the right Jack" when I was taught how to play, but if you have a Jack in your hand of four cards, and it is the same suit as the cut card, you score one point.

My Card Class

For the grillionth time in the history of computers, we need a Card class. This has been done so many times, I'm faintly surprised there isn't a standard Card class in the base classes of the .NET Framework. Anyway, my Card class has a couple of constructors:

C#
public Card(Int32 index) 
public Card(char name, char suit) 
public Card(string card) // card[0] = name, card[1] = suit

Once constructed, the Card exposes a couple of properties that aid in scoring.

Name Type Description
DeckIndex Int32 The index of the card in the deck. From 0 to 51. Ace -> King, Spades, Hearts, Clubs, Diamonds.
Name char One of {A, 2, 3, 4, 5, 6, 7, 8, 9, T, J, Q, K}.
Suit char One of {S, H, C, D}.
Ordinal Int32 Represents the index of the card within the suit (i.e., 0 - 12).
Value Int32 Returns the counting value of a card (Ace = 1, face cards = 10).

The class implements System.IComparable, so you can sort arrays of them easily.

Scoring Algorithms

The basic requirement to score the hand involves breaking the groups of 4 and 5 cards down into smaller sets for analysis. To count the number of pairs in a hand, for instance, you need to compare each card to each other card in the hand, once. You want an exhaustive list of the subsets of length two from your set of five (the hand).

When I started thinking about this problem a couple of months ago, I began looking for functions in the .NET libraries to break Arrays or List<> into smaller subsets. Not finding any, I started Googling generic algorithms for doing this.

Then, I realized that the number of sub-sets of 2 in a group of 5 is actually a finite, and indeed small, number. Since there are only 10 subsets of 2 in a group of 5, I created an array of those subsets. I'm not trying to score an arbitrarily large cribbage hand (although that would be an interesting problem too), I'm scoring a hand where size always equals 5. Rather than calculating the subsets at run-time, I hard coded them into an array.

Sets of 2 - criblib02.png

Image 1. Sets of 2 in a hand of 5.

So if I have an array of five Card objects, the "Sets of 2" array contains a list of pairs of indices to compare. The pseudo-code for scoring pairs would look like this:

C#
for (Int32 i = 0; i < setsOf2.Length; i++)
   if (card[setsOf2[i][0]].Name == card[setsOf2[i][1]].Name)
      AddPairToScore(card[setsOf2[i][0]], card[setsOf2[i][1]]);

As I explained above, the "Name" of a card is one of {A, 2, 3, 4, 5, 6, 7, 8, 9, T, J, Q, K}. setsOf2[i][0] points to the first array index in the ith element of the setsOf2 array. setsOf2[i][1] points to the second. If the Card objects in the hand pointed to by those two indices is the same "type" (i.e., they're both Aces or Kings), then you've found a pair. When the i loop is finished, you've checked all of the cards against each other, and all of the possible pairs have been found.

Pairs are pretty easy to score, since they can only (by definition) involve sets of 2. The algorithm to score 15s is slightly more complicated, since 15s can be made from 2, 3, 4, or all 5 cards. I added arrays called setsOf3 and setsOf4 that encompass the indices of subsets for their respective lengths.

Sets of 3 - criblib03.png

Image 2. Sets of 3 in a hand of 5.

Sets of 4 - criblib04.png

Image 3. Sets of 4 in a hand of 5.

So the algorithm to look for 15s looks like this:

C#
// Look for 15s made up of 2 cards
for (Int32 i = 0; i < setsOf2.Length; i++)
   if (card[setsOf2[i][0]].Value + card[setsOf2[i][1]].Value == 15)
      Add15ToScore(card[setsOf2[i][0]], card[setsOf2[i][1]]);
   
// Look for 15s made up of 3 cards
for (Int32 i = 0; i < setsOf3.Length; i++)
   if (card[setsOf3[i][0]].Value + card[setsOf3[i][1]].Value +
          card[setsOf3[i][2]].Value == 15)
      Add15ToScore(card[setsOf3[i][0]], card[setsOf3[i][1]], card[setsOf3[i][2]]);
   
// Look for 15s made up of 4 cards
for (Int32 i = 0; i < setsOf4.Length; i++)
   if (card[setsOf4[i][0]].Value + card[setsOf4[i][1]].Value +
         card[setsOf4[i][2]].Value + card[setsOf4[i][3]].Value == 15)
      Add15ToScore(card[setsOf4[i][0]], card[setsOf4[i][1]], 
            card[setsOf4[i][2]], card[setsOf4[i][3]]);
   
// Look for 15s made up of 5 cards.  Trivial case.
for (Int32 i = 0; i < setsOf5.Length; i++) // Length == 1
   if (card[setsOf5[i][0]].Value + card[setsOf5[i][1]].Value +
         card[setsOf5[i][2]].Value + card[setsOf5[i][3]].Value +
         card[setsOf5[i][4]].Value == 15)
      Add15ToScore(card[setsOf5[i][0]], card[setsOf5[i][1]], 
            card[setsOf5[i][2]], card[setsOf5[i][3]], card[setsOf5[i][2]]);

Once I had coded that atrocity, I realized that the separate arrays should be collapsed into a single array, and the first index of the super-array should be the number of cards being compared. Once I did that, the 15-counting pseudo-code could be shortened to this:

C#
for (Int32 i = 2; i <= 5; i++) {
   for (Int32 j = 0; j < sets[i].Length; j++) {
      for (Int32 k = 0; k < sets[i][j].Length; k++)
         sum += card[sets[i][j][k]].Value
   
      if (sum == 15)
         Add15ToScore(cardsPointedToByIndicies sets[i][j]);
   }
}

Flushes are trivial to count. Iterate over the cards of the hand, and as soon as the suit of a card is not equal to the suit of the first card in the hand, exit, and score the player 0. If you pass the end of the loop, it means all of the suits are the same, and the player is entitled to the count for the flush. Remember that all five cards must match to score flush points in the kitty, but only the four hand cards are required to score four points. If all five cards between the hand and the cut card match, the player is entitled to five points.

Counting nobs is similarly easy.

C#
for (Int32 i = 0; i < hand.Length; i++)
   if ((hand[i].Value == 'J') && (hand[i].Suit == cutCard.Suit))
      AddNobsToScore(hand[i], cutCard);

I found the hardest sets of cards to score were the runs. A run is a set of cards in order, not necessarily of the same suit, and can be 3, 4, or 5 cards long. There can be more than one run in a given hand. Consider the following hand:

Quadruple Run - criblib05.png

Image 5. Quadruple run of 3.

There are four runs:

  • Ace of Spades, 2 of Hearts, 3 of Clubs
  • Ace of Spades, 2 of Clubs, 3 of Clubs
  • Ace of Hearts, 2 of Hearts, 3 of Clubs
  • Ace of Hearts, 2 of Clubs, 3 of Clubs

Also, consider the following hand of two runs:

Double Run - criblib06.png

Image 6. Double run of 4.

I want to count only the two runs of 4 (Ace of Spades, 2 of Clubs, 3 of Clubs, 4 of Diamonds, and Ace of Hearts, 2 of Clubs, 3 of Clubs, 4 of Diamonds), and not any of the runs of 3.

The solution, obvious to me now, of course, is to start looking for the longest runs and as soon as a run is found, don't look for smaller runs. Start looking for a run of 5, and if one exists (there can only be one), don't look for runs of 4 or 3. If a run of 4 is found, continue looking for other runs of 4, but don't look for runs of 3.

Runs are actually found by picking the appropriate subset comparison array, and iterating over the Ordinal values of the cards. If two cards are more than 2 away from each other (or are equal), there's no run and the run-seeking loop can be exited without scoring. If the loop completes, it means all the cards compared are 1 away from each other and a run exists. Score the run, and go back to look for more. This can only work if the hand has been sorted first, which was the original impetus to have Card implement IComparable.

The Code

Now that we have Card objects and all the algorithms we need, we can look at scoring groups of them. The Hand class has a single static method, Count(). It accepts an array of Card objects, a single Card object representing the cut card, a pointer to a List<> of ScoreSet objects (more on this later), and a bool that specifies whether the hand to be evaluated is a kitty or a regular hand.

The ScoreSet class contains a string Name property of the scoring event (15, pair, run, etc.), the Count of the event, and an array of Card objects that constitute the scoring event. These objects are accumulated in the List<> so which cards contributed to which part of their score can be reported back to the player.

I included a small Deck object to manage picking random cards. It exposes two methods, Shuffle(), and NextCard(), which returns an Int32 representing the card index (0 - 51) of the next un-dealt card.

The Demo App

The demo application is a console app that puts all of these concepts together. If you don't pass it any command-line parameters, it generates a random hand, picks a random "cut" card, and scores the hand. This is done with this code:

C#
List<ScoreSet> scoringPlays = new List<ScoreSet>();
Card[] hand = new Card[4];
Deck deck = new Deck();
for (Int32 i = 0; i < 4; i++) 
   card[i] = new Card(deck.NextCard());
Card cutCard = new Card(deck.NextCard());
Int32 score = Hand.Count(hand, cutCard, scoringPlays, false); // Not a kitty

If you do pass command-line parameters, you can specify a hand for it to score. Use it like this:

C#
criblibhandcounter.exe Card1 Card2 Card3 Card4 CutCard [true]

The cards are specified as NS, where N is the Name character and S is the Suit character. If you specify the sixth optional parameter, and it is "true", the hand you supply will be scored as a crib.

The rest of the code in the demo app just prettifies the output a bit.

Deep Thought

I enjoyed playing crib on MSN a great deal when it was available, and ended up playing over 2000 games. Not obsessively, but two or three games a night during the week, and a couple more each night of the weekend adds up over time. All this happened before my son arrived, of course.

One of the things that annoyed me about playing MSN cribbage was the fact that your ranking could be negatively affected by losing games that you couldn't hope to win. There were some close games that I'd lose due to being too aggressive with pegging near the end, but there were other games that I'd lose because I'd get four "19" hands in the game, while my opponent would get 12s and 16s. I'd get skunked, my rating would take a nose-dive, and it didn't really represent my skill at the game.

If anyone out there is contemplating writing their own crib game, I'd like to add two cents here. I'd like to introduce the concept of "standard games", where the hands for the game are not random but fixed. It would allow the ranking engine to compare how well players play given sets of cards. Some standard games would have a really good set of cards up against a set of poor cards, but if the player with the pre-destined poor hands managed to eke out two points more than other players who've played the same standard hand, it would reflect positively in their ranking, even if they lost that game.

Players could even see their ranking change without playing games. If the player was the first one to play a standard game, and ten subsequent players do worse with it, the first player's ranking should rise. A player's Win/Loss record would be static, but their "Standard Game" ranking could fluctuate without their action.

Initially, I thought the database would have to be populated with a very large number of standard games (so a player wouldn't be able to recognize a standard hand they've already played), but it would be far more efficient to generate standard games from random ones played initially. It would be trivial to track which players had played which standard games, and if either of the players had played all of the standard games, the engine would generate a new random game for them (which would subsequently be added to the catalog of standard games for other players).

Anyway, if there are any statistics, math, or actuarial PhD candidates looking for a thesis, I'd be happy to work with you on this.

Conclusion

I have been playing cribbage since my dad taught me when I was 9 or 10; about 30 years ago, and I really enjoyed deconstructing the scoring process for this library. There are probably more crib-related articles on the way, so consider yourself warned.

Pick up a deck and try it today!

Revision History

  • 5th September, 2006 - Initial revision

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
Canada Canada
I'm a graduate of the University of Toronto with a degree in zoology. I'm currently a software development manager with a large Canadian financial institution, and a passionate squash player.

I am a proud daddy to Alex and Sarah.

Comments and Discussions

 
QuestionWP7 app Pin
Don Kackman10-Mar-12 10:30
Don Kackman10-Mar-12 10:30 
AnswerRe: WP7 app Pin
Sean Michael Murphy19-Mar-12 17:22
Sean Michael Murphy19-Mar-12 17:22 
SuggestionQuickly Count Cribbage Points using Lambdas Pin
P Dubbia16-Feb-12 9:02
P Dubbia16-Feb-12 9:02 
GeneralRe: Quickly Count Cribbage Points using Lambdas Pin
Sean Michael Murphy17-Feb-12 5:51
Sean Michael Murphy17-Feb-12 5:51 
GeneralRe: Quickly Count Cribbage Points using Lambdas Pin
P Dubbia17-Feb-12 6:43
P Dubbia17-Feb-12 6:43 
GeneralRe: Quickly Count Cribbage Points using Lambdas Pin
Sean Michael Murphy19-Mar-12 17:22
Sean Michael Murphy19-Mar-12 17:22 
GeneralRe: Quickly Count Cribbage Points using Lambdas Pin
Don Kackman2-Mar-12 13:49
Don Kackman2-Mar-12 13:49 
GeneralRe: Quickly Count Cribbage Points using Lambdas Pin
P Dubbia20-Mar-12 6:09
P Dubbia20-Mar-12 6:09 
Generalcode source Pin
Det0nator6-Oct-07 6:08
Det0nator6-Oct-07 6:08 
GeneralRe: code source Pin
Sean Michael Murphy16-Oct-07 2:43
Sean Michael Murphy16-Oct-07 2:43 
GeneralRe: code source Pin
Det0nator16-Oct-07 3:35
Det0nator16-Oct-07 3:35 
GeneralRe: code source Pin
Det0nator16-Oct-07 3:49
Det0nator16-Oct-07 3:49 
GeneralExcellent article Pin
Greg Russell5-Sep-06 21:42
professionalGreg Russell5-Sep-06 21:42 
GeneralRe: Excellent article Pin
J4amieC6-Sep-06 5:23
J4amieC6-Sep-06 5:23 
GeneralRe: Excellent article Pin
Sean Michael Murphy7-Sep-06 20:13
Sean Michael Murphy7-Sep-06 20:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.