Continuing our development of a Cribbage game, this month Dave tackles the tricky task of sorting a hand by rank value.
We're been working on writing code for the game Cribbage, and last month, I ended this column by creating 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 month 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 next month's installment, for sure.