LJ Archive

Work the Shell

Let's Play Cards with Acey-Deucey

Dave Taylor

Issue #250, February 2015

Time to get your gambling on as Dave implements the simple betting card game Acey-Deucey.

I've been looking at the fundamentals of Bash shell script writing and programming in my past few articles, so it's time to get back to some game coding. In this article, I'm writing about a simple card game called Acey-Deucey. You might have played this with your kids—it's a really simple betting game popular with people who don't get poker (mostly just kidding there).

The idea is easy. The dealer flips up two cards, one on each side of the (face down) deck, then players bet on whether the next card to be flipped up is going to be numerically between the two exposed cards.

For example, if I flipped up a 6 of diamonds and a jack of hearts, you would then bet on whether the next card would be in the range of 7–10. Suit doesn't matter; it's all about rank.

This means, yes, you can do the math easily enough. If you have the entire deck available each time, the chance of any given card coming up is 1/52. Suit doesn't matter, so each card rank has a 1/13 chance of being chosen. In the above example, 7–10 is four possible winning ranks, so the chances are 4/13 or 30%.

But this isn't a math game, so I apologize for the slight sidetrack! This is a programming problem, and it starts where all card games start, with simulating a deck of cards.

To make things interesting, I'm going to approach the problem by having two decks, one sorted one unsorted. The “shuffle” then randomly selects from the sorted deck and adds it to the unsorted (that is, shuffled) deck. Then cards can be “pulled” from the unsorted/shuffled deck sequentially, a simulation that adds a bit of arguably unnecessary computation, but lets us debug by previewing the top cards on the deck and so on. It also opens up the possibility of cheating, but that's another story entirely.

To start, let's create the sorted deck, modeling it as an array:

card=1
while [ $card -le 52 ]       # 52 cards in a deck
do
  deck[$card]=$card
  card=$(( $card + 1 ))
done

To create the shuffled deck—herein called “newdeck”—we'll need to have a new function pickCard that randomly tries to grab a card out of the ordered deck. Note that this has some nuances, but let's start with the base code:

while [ $errcount -lt $threshold ]
do
  randomcard=$(( ( $RANDOM % 52 ) + 1 ))
  errcount=$(( $errcount + 1 ))

  if [ ${deck[$randomcard]} -ne 0 ] ; then
    picked=${deck[$randomcard]}
    deck[$picked]=0         # picked, remove it
    return $picked
  fi
done

The threshold variable is because although the first 80% of card picks are easy, the last few end up problematic, because we could, for example, end up with an array of 51 empty slots and one available card that we're trying to find with random selections. So threshold limits the number of random picks made. If we don't get a valid card within that count, the script will drop down to the less random sequential search for an available card:

randomcard=1
while [ ${newdeck[$randomcard]} -eq 0 ]
 do
    randomcard=$(( $randomcard + 1 ))
 done
 picked=$randomcard
 deck[$picked]=0             # picked, remove it
 return $picked

It's not so glamorous, but it's functional, and really, by the time we hit this code, we're down to the last half-dozen cards in the deck anyway, and a lack of randomness is unlikely to be noticed, certainly not in Acey-Deucey where we use the entire deck each time we play.

Now that we have a card picking function, we can create the shuffle deck function, which instantiates the array newdeck with a randomly chosen ordering of the sorted cards in deck:

while [ $count -le 52 ]
do
  pickCard
  newdeck[$count]=$picked
  count=$(( $count + 1 ))
done

Still with me? These functions are mnemonically named initializeDeck, shuffleDeck and pickCard, meaning that all we need to do to be ready to start writing some game playing code is this sequence:

initializeDeck
shuffleDeck

Nice. Now the first step of the actual game is to pick those first two cards, right? Right!

The core code is straightforward, but we have to deal with the fact that numerically we're representing a king as a rank of zero (so that all the other rank cards are their own value). So we need to keep fixing that each time a card is picked:

function dealCards
{
    # Acey Deucey has two cards flipped up...

    card1=${newdeck[1]}      # since deck is shuffled, we take
    card2=${newdeck[2]}      # the top two cards from the deck
    card3=${newdeck[3]}      # and pick card #3 secretly

    rank1=$(( $card1 % 13 ))     # get the rank values
    rank2=$(( $card2 % 13 ))     # to make subsequent
    rank3=$(( $card3 % 13 ))     # calculations easy

    # fix to make the king, default rank = 0, have rank = 13

    if [ $rank1 -eq 0 ] ; then
      rank1=13;
    fi
    if [ $rank2 -eq 0 ] ; then
      rank2=13;
    fi
    if [ $rank3 -eq 0 ] ; then
      rank3=13;
    fi

    # now organize them so that card1 is always lower value 
    # than card2

    if [ $rank1 -gt $rank2 ] ; then
      temp=$card1; card1=$card2; card2=$temp
      temp=$rank1; rank1=$rank2; rank2=$temp
    fi

    showCard $card1 ; cardname1=$cardname
    showCard $card2 ; cardname2=$cardname

    showCard $card3 ; cardname3=$cardname    
    echo "I've dealt:"
    echo "   $cardname1" ; echo "   $cardname2"
}

There's a lot going on in that particular function, but don't be too bewildered, the main work is here in these two lines:

card1=${newdeck[1]}     
rank1=$(( $card1 % 13 ))    

On first glance, the omission of a $RANDOM might cause anxiety, but remember we've already created the shuffled deck as newdeck[] in the script. Now we can just grab the top card and save it as $card1, then calculate its rank by doing the old mod 13 trick. This, of course, is why a king with a value of 13 ends up being a zero, so we need to fix it a bit later in the function.

The eagle-eyed among you will notice that we've already picked the third card in this function, so we could actually just say “winner!” or “loser!” immediately, dispensing with the entire betting sequence, but what's the point in that?

There's one more function that's needed for the playing card library in the shell, and that's showCard. You can see it referenced above, so let's have a look. Fundamentally, it's easy: pull the rank and suit from the card value, then translate each as needed to make the output pretty.

Here's my shot at that, demonstrating my zeal for case statements:

function showCard
{
   # This uses a div and a mod to figure out suit and rank, though
   # in this game only rank matters. Still, presentation is
   # important too, so this helps make things pretty.

   card=$1

   if [ $card -lt 1 -o $card -gt 52 ] ; then
     echo "Bad card value: $card"
     exit 1
   fi

   # div and mod. See, all that math in school wasn't wasted!

   suit="$(( ( ( $card - 1) / 13 ) + 1))"
   rank="$(( $card % 13))"

   case $suit in
     1 ) suit="Hearts"    ;;
     2 ) suit="Clubs"    ;;
     3 ) suit="Spades"   ;;
     4 ) suit="Diamonds" ;;
     * ) echo "Bad suit value: $suit"; exit 1
   esac

   case $rank in
     0 ) rank="King"    ;;
     1 ) rank="Ace"     ;;
     11) rank="Jack"    ;;
     12) rank="Queen"   ;;
   esac

   cardname="$rank of $suit"
}

You also could do the same thing, probably quicker, by setting up an array of rank names and an array of suit names, but that can be your version of the code. Me? I like case statements.

Now we have everything we need for the basic game, but I'm going to hold off on the overall game logic and some demonstrations of the game in play until my next article.

Dave Taylor has been hacking shell scripts for more than 30 years. Really. He's the author of the popular Wicked Cool Shell Scripts (and just completed a 10th anniversary revision to the book, coming very soon from O'Reilly and NoStarch Press). He can be found on Twitter as @DaveTaylor and more generally at his tech site www.AskDaveTaylor.com.

LJ Archive