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.
If you aren't familiar with Cribbage, you've got time to learn more about the game, because I won't actually get to any game-specific elements until next month. Need a good place to learn? Try this: http://www.bicyclecards.com/card-games/rule/cribbage.
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:
until [ $card -ne -1 ]
do
card=${deck[$RANDOM % 52]}
done
echo "Picked card $card from the deck"
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:
for card in {0..5} ; do
until [ ${hand[$card]} -ne -1 ]
do
pick=$(( $RANDOM % 52 ))
hand[$card]=${deck[$pick]}
done
echo "Card ${card} = ${hand[$card]}"
deck[$pick]=-1 # no longer available
done
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:
showcard()
{
suit=$(( $1 / 13 ))
rank=$(( ( $1 % 13 ) + 1 ))
showcardvalue=$rank${suits[$suit]}
}
Actually, that's not quite right, because we don't want results like 11H or 1D; we want to convert 1 into an Ace, 11 into a Jack and so on. It's the perfect use for a case statement:
case $rank in
1) rank="A" ;;
11) rank="J" ;;
12) rank="Q" ;;
13) rank="K" ;;
esac
Now we're ready to deal a hand and see what we get:
for card in {0..5} ; do
until [ ${hand[$card]} -ne -1 ]
do
pick=$(( $RANDOM % 52 ))
hand[$card]=${deck[$pick]}
done
showcard ${hand[$card]} # sets 'showcardvalue'
echo "Card ${card}: $showcardvalue"
deck[$pick]=-1 # no longer available
done
And the result of running this? Here are a few iterations:
$ sh cribbage.sh
Card 0: 5D
Card 1: 5C
Card 2: JS
Card 3: QD
Card 4: 4D
Card 5: JD
$ sh cribbage.sh
Card 0: 10C
Card 1: 5D
Card 2: KC
Card 3: 7S
Card 4: 4S
Card 5: 8C
Cool. Now that we have the basics of how to model a deck and deal a hand of unique cards, we can start with the interesting elements—next month. In the meantime, your homework is to learn Cribbage.
Cribbage photo via Shutterstock.com.