Work the Shell

Cribbage: Pairs and Three of a Kinds

Dave Taylor

Issue #230, June 2013

Debugging his last article's script and calculating straights in a Cribbage hand keep Dave busy coding this month, with punctuation graffiti included!

The Cribbage game programming continues with further expansion of the subhand evaluation code. You'll recall that in a two-player game of Cribbage, you're dealt six cards but have to put two into the “crib”, a third hand that alternates between players.

The challenge is this: which four cards of the six leave you with the most points possible?

There's a secondary consideration, because you also want to avoid putting points in the crib when it's not yours, if you can help it, but for now, I'm going to stick with the six-choose-four challenge.

And a challenge it is, because cards are worth points based on whether they have the same rank (for example, 9S and 9C = 2 points for a pair); whether they add up to 15, with all face cards = 10 (for example, 7S and 8C = 15 = 2 points); whether all four cards have the same suit (for example, 3D, 7D, 9D, QD = 4 points); and whether three or four of the cards are in sequential rank order (for example, 9D, 10C and JS = 3 points), even if they aren't the same suit.

I wrapped up my last article with code that could figure out the six-choose-four combinations (it's a straight combinatorics problem—I knew that stuff I learned in college eventually would come in handy), then evaluate each four-card set for possible fifteens and pairs. With some debugging code added, the current output looks like this:

$ sh cribbage.sh
Hand: 2C, 4S, 6S, 8C, 8H, 9C.
Subhand 0:  2C  4S  6S  8C
calc15() given ranks: 2 4 6 8
  total 15-point value of that hand: 0

Subhand 1:  2C  4S  6S  8H
calc15() given ranks: 2 4 6 8
  total 15-point value of that hand: 0

Subhand 2:  2C  4S  6S  9C
calc15() given ranks: 2 4 6 9
  total 15-point value of that hand: 4
...

As you can see, the third subhand is worth more than the first two. In fact, 2C + 4S + 9C and 6S + 9C are both fifteens, so it's worth four points. Not too bad.

Further down in the debugging output, subhands start to appear with the pair of eights:

Subhand 6:  2C  6S  8C  8H
calc15() given ranks: 2 6 8 8
we've got a pair: 8 and 8
  total 15-point value of that hand: 0

So at this point the code recognizes pairs, but the point accumulator isn't actually scoring them. That's not good.

Let's start by fixing that. The scoring code is getting pretty long, so I'll just share the two-card code, which is a bit simpler too:

for subhand in {0..5}
do
  sum=0

  for thecard in ${fourtwo[$subhand]}
  do
    sum=$(( $sum + ${c15[$thecard]} ))
  done
  if [ $sum -eq 15 ] ; then
    points=$(( $points + 2 ))
  fi

  # now let's look at pairs
  #  remember:  ${string:position:length}

  twocards=${fourtwo[$subhand]}
  card1=${twocards:0:1}
  card2=${twocards:2}

  if [ ${cr15[$card1]} = ${cr15[$card2]} ] ; then
    echo "we've got a pair: ${cr15[$card1]} and
         ${cr15[$card2]}"
    points=$(( $points + 2 ))
  fi

done

Here's the line that fixed the scoring problem for pairs:

points=$(( $points + 2 ))

It's easy shell math, and something I hope you're using with some frequency. In fact, $( ) for subshells and $(( )) for math equations that alternatively could be handled by eval are good.

That single line fixes the problem, as demonstrated in the very first test run:

Hand: 3H, 3D, 4C, 8H, 9H, JH.
Subhand 0:  3H  3D  4C  8H
calc15() given ranks: 3 3 4 8
we've got a pair: 3 and 3
  total point value of that hand: 6

How did I get six points? 3H + 3D is a pair (2 points), then 3H + 4C + 8H = 15 (2 points) and 3D + 4C + 8H = 15 (2 points). That's a pretty decent little four-card Cribbage hand, actually.

What about when there are three cards that are the same? It turns out that Cribbage has a very logical scoring system, and three of a kind are scored as 3 * 2-card pairs, which makes sense. Here's an example to illustrate:

Hand: 4C, 4D, 4H, 7D, 10H, JS.
Subhand 0:  4C  4D  4H  7D
calc15() given ranks: 4 4 4 7
we've got a pair: 4 and 4
we've got a pair: 4 and 4
we've got a pair: 4 and 4
  total point value of that hand: 12

So 4C + 4D, 4C + 4H and 4D + 4H are the three pair and are worth six points. This subhand is really superb, however, because there also are a number of card combinations that add up to fifteen, totaling 12 points. Very good for four cards!

The piece that's missing with the scoring is straights. This is going to get a bit complicated, so stick with me.

Calculating Straight Runs

There's already code in place that generates all three-card combinations that catches when three cards sum up to fifteen points, so that's easily tapped within a “for” loop to extract the three-card index values:

combo=${fourthree[$subhand]}

That's going to be set to “0 1 2”, “0 1 3” and so on.

The card's normalized rank (for example, J=10, Q=10) is set in the point calculation function as the local array $cardrank[], and the original rank (J=11, Q=12 and so on) is in $cardrankfull[]. These originally were c15[] and cr15[], but I renamed them to make their purpose a bit clearer in the script.

With “combo” set to the card indices, the full rank of a specific card in the four-card subhand can be referenced like this:

${cardrankfull[${combo:0:1}]}

As Douglas Adams would say, don't panic. Let's unwrap it instead.

The reference ${combo:0:1} is a string slice and extracts a one-character-long substring starting at index 0. The second value in the combo array is :2:1 and the third is :4:1. That's used directly, so it's akin to ${cardrankfull[1]}.

Put the three together and output the three ranks:

echo "testing card ranks ${cardrankfull[${combo:0:1}]}
and ${cardrankfull[${combo:2:1}]} and
${cardrankfull[${combo:4:1}]}"

Testing the values is easy because the hand's already sorted by lowest-to-highest rank. There's more notational complexity because I'm going to use the $(( )) mathematical shortcut again, but here's the conditional test to see if the three-card subset is in sequential rank order:

if [ $(( ${cardrankfull[${combo:0:1}]} + 1 )) -eq
     ${cardrankfull[${combo:2:1}]} -a
     $(( ${cardrankfull[${combo:2:1}]} + 1 )) -eq
     ${cardrankfull[${combo:4:1}]} ] ; then

I warned you, it was notationally complex and once the mathematics are added, the -eq for algebraic equals and -a for the logical “AND” between two statements, well, it's pretty thick with punctuation, to say the least.

The result (a subset to show it's working):

Subhand 13:  4H  5H  6D  6H
Calc4cardValue() given original ranks: 4 5 6 6
combo set to 0 1 2
testing card ranks 4 and 5 and 6
yup, those three cards are a run for three!
combo set to 0 1 3
testing card ranks 4 and 5 and 6
yup, those three cards are a run for three!
combo set to 0 2 3
testing card ranks 4 and 6 and 6
combo set to 1 2 3
testing card ranks 5 and 6 and 6
  total point value of that hand: 6

This calculation is correct, that both cards 1,2,3 and cards 1,2,4 are runs, so it's worth twice 3-points. But, there's another bug looming: the situation where all four cards are a four-card run. That's worth four points, not six. But we'll have to figure out that bug fix next month—I've already gone way long on this column.

Dave Taylor has been hacking shell scripts for more than 30 years. Really. He's the author of the popular Wicked Cool Shell Scripts and can be found on Twitter as @DaveTaylor and more generally at www.DaveTaylorOnline.com.