Search

Counting Cards: Cribbage

I've spent the past few months reviewing shell scripting basics, so I
think it's time to get back into an interesting project. It's
always a good challenge to capture game logic in a shell script,
particularly because we're often pushing the envelope with the
capabilities of the Bash shell.

For this new project, let's model how a deck of cards works in
a script, developing specific functions as we proceed. The game that
we'll start with is a two- or three-player card game called
Cribbage.
The basic functions we'll create also will be easily extended to simple
Poker variants and other multi-card evaluation problems.

The first and most obvious challenge with any card game is modeling the
deck of cards. It's not just the deck, however, it's the challenge
of shuffling too. Do you need to go through the deck multiple times to
randomize the results? Fortunately, that isn't necessary, because you can
create a deck—as an array of integer values—in sequential order and
randomly pick cards from the deck instead of worrying about shuffling the
deck and picking them in sequential order.

This is really all about arrays, and in a shell script, arrays are easy to
work with: simply specify the needed index in the array, and it'll be
allocated so that it's a valid slot. For example, I simply could use
deck[52]=1, and
the deck array will have slots 0..52 created (though all the other elements
will have undefined values).

Creating the ordered deck of cards, therefore, is really easy:

for i in {0..51}
do
deck[$i]=$i
done

Since we're going to use the value -1 to indicate that the card has
been pulled out of the deck, this would work just as well if everything
were set to any value other than -1, but I like the symmetry of
deck[$i]=$i.

Notice also the advanced for loop we're employing. Early
versions of Bash can't work with the {x..y}
notation, so if that fails, we'll need to increment the variable by hand.
It's not a big hassle, but
hopefully this'll work fine.

To pick a card, let's tap into the magic $RANDOM variable, a variable
that has a different value each time you reference it—darn handy, really.

So, picking a card randomly from the deck is as easy as:

card=${deck[$RANDOM % 52]}

Note that to avoid incorrect syntactic analysis, it's a good habit
always to reference arrays as ${deck[$x]} rather than the more succinct
$deck[$x].

How do you know whether you've already picked a particular card out of the
deck? I don't care what game you're playing, a hand like 3H,
4D, 5D, 9H, 9H and 9H is going to get you in trouble!
To solve this, the algorithm we'll use looks like this:

pick a card
if it's already been picked before
pick again
until we get a valid card

Programmatically, remembering that a value of -1 denotes a card that's
already been picked out of the deck, it looks like this:

The first card picked isn't a problem, but if you want to deal out 45 of
the 52 cards, by the time you get to the last few, the program might well
bounce around, repeatedly selecting already dealt cards, for a half-dozen
times or more. In a scenario where you're going to deal out the entire
deck or a significant subset, a smarter algorithm would be to count how
many random attempts you make, and when you've hit a threshold, then
sequentially go through the deck from a random point until you find one
that's available—just in case that random number generator isn't
as random as we'd like.

The piece missing in the fragment above is the additional snippet of code
that marks a given card as having been picked so that the algorithm
identifies twice-picked cards. I'll add that, add an array of six
cards I'm going to deal, and also add a variable to keep track of the
array index value of the specific card chosen:

You can see that I've added the use of a "pick" variable, and
because the equation appears in a different context, I had to add the
$(( )) notation around the actual random selection.

There's a bug in this code, however. Can you spot it?
It's a classic mistake that programmers make, actually.

The problem? The until loop is assuming that the value of
$hand[n] is -1 and remains so until a valid card randomly picked
out of the deck is assigned to it. But the value of an array element is
undefined when first allocated—not good.

Instead, a quick initialization is required just above this snippet:

# start with an undealt hand:
for card in {0..5} ; do
hand[$card]=-1
done

We're almost actually ready to deal out a hand and see what we get.
Before we do, however, there's one more task: a routine that can
translate numeric values like 21 into readable card values like "Nine
of Diamonds" or, more succinctly, "9D".

There are four suits and 13 possible card values in each, which means that
the div and mod functions are needed: rank = card %
13 and suit = card /
13.

We need a way to map suit into its mnemonic: hearts, clubs, diamonds and
spades. That's easy with another array:

suits[0]="H"; suits[1]="C"; suits[2]="D"; suits[3]="S";

With that initialized, showing a meaningful value for a given card is
surprisingly straightforward:

Dave Taylor has been hacking shell scripts on UNIX and Linux systems for a
really long time. He's the author of Learning Unix for Mac OS
X and Wicked Cool Shell Scripts. You can find him on Twitter
as @DaveTaylor, and you can reach him through his tech Q&A site: Ask Dave Taylor.