Cribbage Hand Counting Library






4.86/5 (13 votes)
A library to score cribbage hands
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:
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 Array
s 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.
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:
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.
So the algorithm to look for 15s looks like this:
// 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:
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.
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:
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:
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:
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:
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