Cribbage: Sorting Your Hand
We've been working on writing code for the game Cribbage, and last time, I created the code needed to pick a random subset of six cards out of a "deck" and display them in an attractive format—like this:
$ sh cribbage.sh
Card 0: 7C
Card 1: 5H
Card 2: 9H
Card 3: 10S
Card 4: 5D
Card 5: AS
The primary task on the agenda this article is to sort the cards after
they've been dealt. This means we're going to have to sort
the cards by rank while ignoring the suit, then slot them back into
the "hand" array. Is there an easy way to do that? Actually,
we'll use the
sort
function.
We can prototype this by using the command line to see what result we get:
$ sh cribbage.sh | sort -n
Card 0: 4S
Card 1: 7C
Card 2: 9S
Card 3: JC
Card 4: 7H
Card 5: 8C
What the heck? Oh! You can see the problem, right? By telling
sort
to order things numerically, it properly ignores "Card" but then
sees the ordinal value of the card and sorts based on that, rather than
on the actual card value itself.
Even if we fix this, however, we still have the problem that face cards will sort before numeric value cards, which isn't what we want. In fact, we want aces to sort as lower than 2s, while jacks, queens and kings sort as higher than 10s.
If you wanted to have aces "high", the easiest way to do that would be to change the display routine, of course: 1 = a deuce, 2 = a three, 12 = king and 13 = ace. Poof. Everything sorts ace-high. That's just not how Cribbage scores them.
To accomplish Cribbage-rank sorting, we'll need to change the output to push out two values: the rank and the total card value. It's going to look ugly, but it's just an interim result.
Here's how I tweak the code to display these values:
showcard()
{
# given a card value of 0..51 show the suit and rank
suit=$(( $1 / 13 ))
rank=$(( ( $1 % 13 ) + 1 ))
case $rank in
1) orank="A" ;;
11) orank="J" ;;
12) orank="Q" ;;
13) orank="K" ;;
*) orank=$rank ;;
esac
showcardvalue=$orank${suits[$suit]}
}
If you compare it to the version we built last month, the main difference
is that instead of calculating the rank of the card and then overwriting
it with "A", "J", "Q" or
"K" as appropriate, we're
using a new variable, orank
, to store the corrected value. Why? Because
now in the main section of the script we also can access the $rank of
the card as desired:
showcard ${hand[$card]}
echo "$rank ${hand[$card]}"
For each card chosen, the script has an interim output of rank followed by the numeric value of the card, with no fancy display (even though we're still tapping the showcard function for simplicity). The result:
$ sh cribbage.sh
13 38
6 31
8 33
10 35
5 30
12 24
Ugly? Definitely. But now we can sort it and get useful results, even if they might not look like it quite yet:
$ sh cribbage.sh | sort -n
1 26
2 14
2 40
3 2
7 45
10 22
It still looks confusing, but you can see that it's in rank order.
So, how do we get that back into the "hand" array now that we know how to sort it? That's actually rather tricky because of variable scoping issues, as you'll see.
Before we go there, however, I've written a new "showhand" function that displays all the cards in the hand on a single line, with the help of /bin/echo for echoes without a trailing line break:
showhand()
{
# show our hand neatly
/bin/echo -n "Hand: "
for card in {0..4}
do
showcard ${hand[$card]}
/bin/echo -n "$showcardvalue, "
done
showcard ${hand[5]}
echo "$showcardvalue."
}
With that available, our main code starts to look nice and clean:
dealhand;
showhand; # for testing sorthand only
sorthand;
showhand;
For debugging purposes, I'm going to display the hand before and after we've sorted by rank. Eventually, the first "showhand" would just be axed, of course.
Now, let's get back to the code needed to sort the cards in our hand (a feature that a lot of iOS Cribbage games seem to omit, as far as I can tell).
My first stab at writing "sorthand" took advantage of a very slick feature in Bourne shell that lets you tie the output of one loop to the input of another with a pipe. For example:
for card in {0..5}
do
showcard ${hand[$card]}
echo "$rank ${hand[$card]}"
done | sort -n | while read rank value
do
hand[$index]=$value
index=$(( $index + 1 ))
done
The problem is that the shell's pipe implementation pushes the second loop
into a subshell without any easy way to get the changed values back up
to the parent shell. The result: by the line immediately after the last
done
statement, all the new values have been lost.
That's too bad, because it definitely was more elegant. But then again, it's not about elegant, it's about functional, right?
Here's how I actually solved it, by using a temporary file to store the intermediate results instead. It's considerably less elegant, for sure:
sorthand()
{
# hand is dealt, now sort it by card rank...
index=0
tempfile="/tmp/.deleteme"
for card in {0..5}
do
showcard ${hand[$card]}
echo "$rank ${hand[$card]}"
done | sort -n > $tempfile
while read rank value
do
hand[$index]=$value
index=$(( $index + 1 ))
done < $tempfile
rm -f $tempfile
}
Note that to get the input of the temporary file as the input for the
while
loop, I simply redirect stdin for the loop at
the very end
of the loop: done < $tempfile
.
Let's test it by dealing a few hands and then showing them immediately post-deal and then after they've been rearranged with the sorthand function:
$ sh cribbage.sh
Hand: 9H, 6D, KC, AH, 9S, JH.
Hand: AH, 6D, 9S, 9H, JH, KC.
$ sh cribbage.sh
Hand: 4D, QS, AC, 9H, 10C, JS.
Hand: AC, 4D, 9H, 10C, JS, QS.
$ sh cribbage.sh
Hand: 9H, 10C, 7C, 7H, 5H, AS.
Hand: AS, 5H, 7C, 7H, 9H, 10C.
It looks like it's working exactly as we'd hope. Yeee-ha!
Yes, there are undoubtedly more efficient ways to write this code and you can quite reasonably ask if a shell script is the optimal development environment for this sort of project, but, seriously, lighten up. Let's enjoy this project, not flagellate ourselves over punctuation!
And on that note, let's wrap up this month's column and start thinking about a considerably harder challenge we'll face starting next month: how to evaluate the value of the hand so that we can recommend which four of the six cards dealt should be kept to optimize the Cribbage hand.
You are learning Cribbage as we go, right? You'll want it for the next installment, for sure.