Cribbage Scoring

It’s something that’s always nagged me. When it gets complicated I usually do it incorrectly—score cribbage hands. When you score car license plates according to cribbage rules trust me it gets complicated. A conversation with my friend, and former computer science nerd (possibly still one), Andrea proved that the algorithm for scoring cribbage hands might not be that hard. This post will attempt to bore my non-computer science friends as I explain the algorithms I used and how I wrote it in Perl.

I don’t want to get into the details of how to play cribbage, I’ll limit it to just tallying your own cribbage hand. Points are gathered in three ways:

  • Runs of three or more cards.
  • Pairs of two.
  • Sets of cards summing to 15.

The points gathered in each of these ways is slightly different:

  • the length of the run is the number of points gathered.
  • each pair is worth 2 points.
  • each group of cards summing to 15 points is worth 2 points.

Examples always makes things clearer:

  • 2 3 4 = 3 points
  • 2 2 = 2 points
  • 7 8 = 2 points
  • 2 3 3 4 = 8 points (two runs of three plus one pair)
  • 6 7 8 9 = 8 points (one runs of four plus two groups of cards summing to 15)
  • 5 5 5 5 = 20 points (four sets summing to 15 for eight points and six pairs for twelve)

I approached this problem by breaking the algorithm down into it’s component parts—computing pairs, runs and groups of 15.

Scoring Pairs

Scoring pairs turns out to be the easiest of all the scoring methods. Scoring runs and scoring sets of 15 is too complicated and probably would encourage check your email and eventually lose interest!

I presented a hand of cribbage cards as an array of cards. The suit of the card doesn’t really matter, so I’ll only store the value of the card in the array. To make the algorithm easier the array is sorted in ascending order. This simplifies this scoring algorithm and others.

What we are looking for is a way to compute all pairs of two cards. To do this I use two loops, one loop over all but the last element in the array and a nested loop from the next element to the end of the array. Basically the first loop isolates one element and the second loop searches for matches among the remaining elements. Every match is a pair worth two points:

sub ScorePairs {
    my $handRef = shift;
    my @hand = @$handRef;
    my $ret = 0;

    for(my $pos = 0; $pos < int(@hand) - 1; $pos++) {
        my $val = $hand&#91;$pos&#93;;
        for(my $ppos = $pos + 1; ($ppos < int(@hand)) && ($hand&#91;$ppos&#93; == $val); $ppos++) {
            $ret += $PAIR_PTS;
        }
    }
    return $ret;
}&#91;/sourcecode&#93;
I won’t get into the details of what’s being passed in—what’s important is the hand of cards that’s passed in is copied into the array @hand.Another way of writing this is scan the array and compute the total number of each card type—it’s not important what type it is. From that use the <a href="http://www.mathwords.com/c/combination_formula.htm">combination formula</a> formula, nCr, where n is the number of a particular card type and r is two. Repeat this for every card type and multiply by two to generate the score for your pairs. Heck..that might have been easier, it wouldn’t have been O(n**2), rather it be O(n).<P>

<b>Scoring Runs</b><P>

Computing runs are made easier by sorting the array earlier of cards. But nonetheless not simple because one duplicate card in the run doubles the value of the run. The hand “1 2 3” is worth three points. The hand “1 2 2 3” is worth 8 points ( two runs of three plus the pair). So how to handle this? The solution I came up with is probably not terribly efficient but, it works.
I walk the array increasing a length variable for every adjacent cards with a difference of one. When there is a matching pair of cards, I increase a multiplier by one (initially set to 1), and move to the next card not increasing the length at all. When two adjacent cards are encountered that are neither equal nor have a difference of one, the run has ended and the total score of the run is computed. The score is the length * the multiplier. In the case of “1 2 2 3” the when the end of the array is reached the the score is, 3 * 2, length of 3, multiplier of 2 because two “2s” where found.Wait..what about the hand “1 1 2 2 3”? Is the score 3 * 3 for 9 points (let’s ignore pairs for now). No, sadly that’s wrong. There are four runs of 3 [BTW when should I write 'three' instead of the digit '3'? Sorry for the inconsistencies in this post!]. Herumph! The cardinality of each group of equal cards in a run, is a multiplier of the run. What the heck does that mean? In the example above, “1 1 2 2 3”, there are two groups of equal cards in the run, (1, 1) and (2, 2). Each one generates a multiplier of 2. So the total multiplier is 4. Let’s look at the Perl code:
<b></b>

sub ScoreRuns {
    my $hand = shift;

    my $len = 1;
    my @mult = ( 1 );
    my $ret = 0;
    my @cards = @$hand;

    for(my $pos = 0; $pos = $MIN_RUN_LENGTH ) {
                $ret += $len * ComputeMult(\@mult);
                $len = 1; # reset and move on
                @mult = ( 1 );
            }
        }
    }
    # could exit loop at end of valid run.
    if ( $len >= $MIN_RUN_LENGTH ) {
        $mult = ComputeMult(\@mult);
        $ret += ($len * $mult);
    }
    return $ret;
}

Key to understanding how this works is the multiplier vector. Anytime a non-duplicate is encountered in the hand, a “1” is pushed onto the multiplier vector. If the next card is a duplicate, the current multiplier entry is increased by one. When a run of cards ends the length of the run is multiplied by all the elements in the multiplier vector. Since a new run is starting, the length is reset to 0 and the multiplier vector reset to just the value 1. Let’s look at an example:array: 1 2 2 3 5 6 6 7Scoring only runs, the total number if points in this hand is 12. Why? There are 4 runs of three. There are two places the runs end—between the 3 and 5, and between the 7 and the end of the array. The multiplier vector is (1 2 1) when the 5 is reached. So the multiplier vector times the length, 3, is 1 * 2 * 1 * 3 which is 6. At the end of the array the current run is scored and it is the same as the previous computation 1 * 2 * 1 * 3 = 6. The total points is therefore 12. This portion of the algorithm is O(n). Fun!

Scoring Combos

I don’t know if combos is the right term. Really we’re scoring groups of cards that sum to a particular value. Saying combos, however, sounds cool.Anyway, scoring combos is really a depth first search for groups where the sum is a particular value. Again the algorithm is simplified by the cards being sorted—we can cut off the search when adding a card exceeds the sum amount (again 15 for cribbage). I wrote it originally in C because I found it easier to write using pointer arithmetic:

int work(int array[], int sum) {
  int runningscore = 0;
  if ( sum == 15 ) {
    return 2;
  }
  else if ( *array == -1 ) {
    return 0;
  }
  else if ( sum > 15 ) {
    return 0;
  }
  else {
    for(; *array != -1; array++) {
      runningscore += work(array+1, sum + *array);
    }
  }
  return runningscore;
}

There are three terminating base cases:

  • the sum of the set is 15.
  • you reach the end of the array (in this example I pad the end of the array with a -1 so I don’t have to track the size of the array).
  • the sum of the group is greater than 15.

The inductive step is looping through the entire array recursively calling the score function on each of the elements.

I was having problems translating it to Perl, mostly I think because of getting the array slices correct. Then Andrea suggested (I don’t know if jokingly) writing it in Lisp. Hmm…why not?

(defun score (list sum)
  (cond
    ((= sum 15) 2)
    ((> sum 15) 0)
    ((null list) 0)
    (t
      (+ (score (cdr list) (+ sum (car list))) (score (cdr list) sum))))

I think this is right—I haven’t tested it but what’s important here is the lack of iteration! Using a loop in Lisp is a sin so I called score recursively on the remainder of the list:

(+ (score (cdr list) (+ sum (car list))) (score (cdr list) sum))))

Note that I’m not modifying the sum, I’m just skipping the head of the list by passing the remainder of the list to the score function. So, I should be able to rewrite that C function to not use iteration:

int work(int array[], int sum) {
  int runningscore = 0;
  if ( sum == 15 ) {
    return 2;
  }
  else if ( *array == -1 ) {
    return 0;
  }
  else if ( sum > 15 ) {
    return 0;
  }
  else {
    runningscore += work(array+1, sum + *array) + work(array+1, sum);
  }
  return runningscore;
}

OK, so how does this translate to Perl?

sub ScoreComboWork {
  my $sum = shift;
  my $combo_sum = shift;
  my @hand = @_;</span>

  my $runningScore = 0;

  return 2 if ( $sum == $combo_sum );
  return 0 if ( $sum > $combo_sum );
  return 0 if ( @hand == 0 );

  my @subarray = @hand[1..$#hand];
  $runningScore += ScoreComboWork($sum + $hand[0], $combo_sum, @subarray)
                           + ScoreComboWork($sum, $combo_sum, @subarray);
  return $runningScore;
}

The equivalent of the lisp cdr function is the array hash from 1 to the end of the array which is assigned to @subarray.What is the big-O notation for this algorithm. Hmm..I think it’s O(n*2). Why? In the worst case this will through every element and then within that from the next element to the end of the array. Classic nested loops.

7 comments so far

  1. Liam on

    Wow, I think you’ve achieved the ultimate in geekdom!

    Really I’m just jealous because I understand neither cribbage nor algorithms.

  2. Chris on

    Nice work. It’s always interesting to see how others are trying to reach the same goal.

    I found an interesting way to count pairs by creating a 13 element array (0 to 12 – A to K) and storing the quantity of each card in the array. By iterating through the array it is easy to determine the points:

    for (int i = 0; i <= CardRanks.GetUpperBound(0); i++)
    {
    if (CardRanks[i] == 2) CardScore += 2;
    if (CardRanks[i] == 3) CardScore += 4;
    if (CardRanks[i] == 4) CardScore += 6;
    }

    This method can also be used for runs. Points are scored any time sequential cards that have a quantity greater than zero.

  3. mywhackyvisions on

    Chris,

    I like the space vs. time solution you chose. My implementation while more generic–it can handle arbitrary number of pairs, is more costly in terms of recursion. Yours is neat in that it recognizes the inherit physical limitations of the problem and reduces down to a simple solution. Clearly I was addicted to a generic solution that involved recursion–my mathematical mind *loves* recursion.

    I think you are right–using this approach for runs would work nicely. A run is as you three or more adjacent cells in the array with a value > 1. If a value is > 2 then that becomes a multiplier for the length of the array. This is far easier, I think, to understand than my algorithm and that means easier to maintain for whoever inherits the code.

  4. debbie on

    help figure score for A hearts, 2 hearts, 2 diamonds,3 hearts, 3 diamonds,4 clubs, 4 diamonds. Then add a 4 spade to above. What are the 2 scores

  5. Rosalinda Wilkes on

    mywhackyvisions.wordpress.com’s done it once more! Great article.

  6. sam on

    Why not just check ALL subsets of the hand, and see if it’s worth points? It sounds inefficient, but anything you do will probably be inefficient, and here the code is simple. first, implement isFlush, is15, isRun, isPair. Then just write:

    foreach (S \subset Hand) {
    if |S|=0
    next
    if S adds to 15
    add 2.
    if |S|=1 and it is nibs
    add 1
    else if |S|=2 and it is a pair
    add 2
    else if |S|=3 and it is a run
    add 3
    else if |S|=4
    if it is a run subtract 6 and add 4
    if it is a flush add 4
    else if |S|=5
    if it is a run subtract 8 and add 5
    if it is a flush subtract 20 and add 5
    }

    To implement subsets, just count from 0 to 31 in binary, and take the cards corresponding to a 1 digit.

    • mywackyvisions on

      While that will work it is, as you point out, terribly inefficient. Sorting the cards and then searching them reduces the search space. Sorting helps you find duplicates, runs and combinations of 15. Just because one can find code to generate all combinations of a set doesn’t mean it’s more efficient. It might be faster to complete the code and sometimes an employer wants that, for me this is a study in algorithmic efficiency.


Leave a comment