Work the Shell - Counting Cards

by Dave Taylor

In my last few columns, we've had a good stab at starting to build a Blackjack game within the confines and capabilities of the shell. The last column wrapped up the discussion of how to shuffle an array of 52 integer values and how to unwrap a given card to identify suit and rank so it can be displayed attractively.

This column goes further into the mathematics of Blackjack, with a routine that can be given an array of cards and return the numeric value of the hand. If you're a Blackjack player though, you'll instantly catch something we're skipping for now. In Blackjack, an Ace can be scored as having one point or 11 points, which is how the hand of Ace + King can be a blackjack (that is, worth 21 points).

We'll just count the Ace as being worth 11 points for this first pass through the game, and perhaps later we'll come back and add the nuance of having the Ace be worth one or 11. Note, by the way, that this adds significant complexity, because there are then four ways to score the hand of Ace + Ace (as 2, 12, 12 or 22), so theoretically, the routine that returns the numeric value of a given hand actually should return an array of values.

But, let's start with the straightforward case. Last month, I showed how to extract the rank of a given card with the equation:


rank=$(( $card % 13 ))

In a typically UNIX way, rank actually ranges from 0-12, rather than the expected 1-13, so because we'd like to leave cards #2-10 in each suit to be the corresponding value, that means we have the rather odd situation where rank 0 = King, rank 1 = Ace, rank 11 = Jack and rank 12 = Queen. No matter, really, because we're going to have to map card rank into numeric values anyway for one or more of the cards—however we slice it.

With that in mind, here's a function that can turn a set of card values into a point value, remembering that all face cards are worth ten points and that, for now, the Ace is worth only 11 points:


function handValue
{
   # feed this as many cards as are in the hand
   handvalue=0  # initialize
   for cardvalue
   do
     rankvalue=$(( $cardvalue % 13 ))
     case $rankvalue in
       0|11|12 ) rankvalue=10   ;;
       1       ) rankvalue=11   ;;
     esac

     handvalue=$(( $handvalue + $rankvalue ))
    done
}

Let's examine some nuances to this before we go much further. First, notice that the conditional case statements can be pretty sophisticated, so we catch the three situations of rankvalue = 0 (King), rankvalue = 11 (Jack) and rankvalue = 12 (Queen) with a succinct 0|11|12 notation.

What I like even better with this function is that by using the for loop without specifying a looping constraint, the shell automatically steps through all values given to the function and then terminates, meaning we have a nice, flexible function that will work just as well with four or five cards as it would with only two. (It turns out that you can't have more than five cards in a Blackjack hand, because if you get five cards and haven't gone over a point value of 21, you have a “five card monty”, and it's rather a good hand!)

Invoking this is typically awkward, as are all functions in the shell, because you can't actually return a value and assign it to a variable or include it in an echo statement or something similar. Here's how we can easily calculate the initial point values of the player's hand and the dealer's:


handValue ${player[1]} ${player[2]}
echo "Player's hand is worth $handvalue points"

handValue ${dealer[1]} ${dealer[2]}
echo "Dealer's hand is worth $handvalue points"

Blackjack is a game that's very much in the dealer's favor, because the player has to take cards and play through the hand before the dealer has to take a single card. There's a significant house advantage for this reason, but in this case, we now can have a loop where we ask players if they want to receive another card (a “hit”) or stick with the hand they have (a “stand”) by simply keeping track of their cards and invoking handValue after each hit to ensure they haven't exceeded 21 points (a “bust”).

To get this working though, we have to restructure some of the code (not an uncommon occurrence as a program evolves). Instead of simply referencing the deck itself, we now have a pair of arrays, one for the player and one for the dealer. To initialize them, we drop the value -1 into each slot (in the initialization function). Then, we deal the hands with:


player[1]=${newdeck[1]}
player[2]=${newdeck[3]}
nextplayercard=3           # player starts with two cards

dealer[1]=${newdeck[2]}
dealer[2]=${newdeck[4]}
nextdealercard=3           # dealer also has two cards

nextcard=5                 # we've dealt the first four cards already

You can see the tracking variables we need to use to remember how far down the deck we've moved. We don't want to give two players the same card!

With that loop in mind, here's the main player loop:


while [ $handvalue -lt 22 ]
do

  echo -n "H)it or S)tand? "

  read answer
  if [ $answer = "stand" -o $answer = "s" ] ; then
     break
  fi

  player[$nextplayercard]=${newdeck[$nextcard]}

  showCard ${player[$nextplayercard]}

  echo "** You've been dealt: $cardname"

  handValue ${player[1]} ${player[2]} ${player[3]} ${player[4]} ${player[5]}

  nextcard=$(( $nextcard + 1 ))
  nextplayercard=$(( $nextplayercard + 1 ))
done

That's the simplified version of this loop. The more sophisticated version can be found on the Linux Journal FTP site (ftp.linuxjournal.com/pub/lj/listings/issue145/8860.tgz). Notice that it's pretty straightforward. As long as the hand value is less than 22 points, the player can add cards or opt to stand. In the latter case, the break statement pulls you out of the while loop, ready to proceed with the program.

Because nextcard is the pointer into the deck that keeps track of how many cards have been dealt, it needs to be incremented each time a card is dealt, but as we're using nextplayercard to keep track of the individual player array, we also need to increment that each time through the loop too.

Let's look at one simple tweak before we wrap up, however. Instead of merely asking whether the player wants to hit or stand, we can recommend a move by calculating whether the hand value is less than 16:


if [ $handvalue -lt 16 ] ; then
   echo -n "H)it or S)tand? (recommended: hit) "
else
  	  echo -n "H)it or S)tand? (recommended: stand) "
fi

Generally, we'll have a quick demo, but notice that we do have some bugs in this script that need to be dealt with first, though:


$ blackjack.sh
** You've been dealt: 3 of Clubs, Queen of Clubs
H)it or S)tand? (recommended: hit) h
** You've been dealt: 8 of Hearts
H)it or S)tand? (recommended: stand) s
You stand with a hand value of 21

Perfect. And here's another run:


$ blackjack.sh
** You've been dealt: 4 of Clubs, Jack of Hearts
H)it or S)tand? (recommended: hit) h
** You've been dealt: 10 of Diamonds

*** Busted!  Your hand is worth 24 **

Ah, tough luck on that last one!

Rather than point out specific problems, let me note here that being dealt either of the following two sequences is quite a problem: A A or 2 2 2 2 3 4. Can you see why?

Next month, we'll look at solving these problems!

Dave Taylor is a 26-year veteran of UNIX, creator of The Elm Mail System, and most recently author of both the best-selling Wicked Cool Shell Scripts and Teach Yourself Unix in 24 Hours, among his 16 technical books. His main Web site is at www.intuitive.com.

Load Disqus comments