Work the Shell: Bash Shell Games: Continuing Development of the Go Fish! Game

This article picks up where I left off developing the Go Fish! game and considers ways to cheat.

In my last article, I began describing how to write a simple Go Fish! game as a shell script. It turns out that there's not much complicated in a game where you and another player take turns asking each other for cards by rank order until a player gets a full set of four. The play continues until all cards have been matched up, and the player with the most sets wins—easy enough. Heck, you've probably played it dozens of times with younger gamers in your family!

Building Out the Functions

I hadn't gotten too far on the gofish script last time. The code basically creates a random array that represents a deck of cards and then allocates seven to each player. A quick run reveals:


$ sh gofish.sh
computer hand:
  2 of Spades
  9 of Spades
  8 of Spades
  5 of Spades
  6 of Spades
  9 of Clubs
  8 of Diamonds
your hand:
  10 of Spades
  Q of Clubs
  K of Spades
  A of Clubs
  3 of Diamonds
  4 of Hearts
  7 of Hearts

Since suit is irrelevant in Go Fish!, these hands almost could be summarized as just 2,5,6,8,8,9,9 and 3,4,7,10,Q,K,A. That's actually helpful, because it reveals how to proceed with the basics of the interactive code portion. Specifically, you can't ask for a card unless you already have one in your hand.

Getting this to work actually involves rather a lot of modifications to the script. First, each player's hand now has 52 slots. The worst-case scenario is a player can have 15 or more cards (imagine having lots of three of a kinds, waiting for the fourth to show up in the deck).

But more than that, you don't want to know the computer's hand—that's called cheating—so that needs to be tweaked too.

To start, here's how the computer and player hands are "dealt":


function dealCards
{
    # start with seven cards deal to each player

    i=1

    while [ $i -lt 8 ] ; do     # first 8 slots = cards
      myhand[$i]=${newdeck[$i]}
      yourhand[$i]=${newdeck[$(( $i + 7 ))]}
      i=$(( $i + 1 ))
    done

    while [ $i -le 52 ] ; do    # all other slots empty
      myhand[$i]=-1
      yourhand[$i]=-1
      i=$(( $i + 1 ))
    done
}

The first block deals card 1 to the computer and card 1+7 to the player, then 2 and 2+7, and so on, until seven cards are in each hand. From this point, all available slots in both the myhand and yourhand arrays are set to -1 to indicate they're empty.

Your hand, at any time, can be shown with the function showHands. Add an argument, and it'll show both hands (yes, handy for cheating), but without it, the following few lines of code are all that's invoked:


echo "Your Hand:"
for i in {1..52} ; do
  if [ ${yourhand[$i]} -gt 0 ] ; then
    showCard ${yourhand[$i]} ; echo "  $cardname"
  fi
done

You can see that each time the hand is analyzed, all 52 slots are examined. Is that efficient? No. Does it matter on modern hardware? No.

The next and perhaps most important function is to convert the common one-letter abbreviations for face cards and other special cards into their corresponding numeric value—a perfect use for a Bash case statement:


case $1 in
   j|J) fixedrequest=11 ;;
   q|Q) fixedrequest=12 ;;
   k|K) fixedrequest=13 ;;
   a|A) fixedrequest=1  ;;
     *) fixedrequest=$1 ;;
esac

Don't ask me why the end of a case statement is the only place in all of the Bash shell where a word is used backward (well, other than fi to end an if statement, I suppose). It's just odd.

Asking for Cards

Now you can (finally) have a loop that lets users specify what card they want to ask for and checks to verify that they already have at least one of that card in their hand (recall that the rules of Go Fish! require you to have a card from a given rank before you can request more):


function doYouHave
{
    # check if you have the card rank you're asking for

    for i in {1..52} ; do
      if [ $(( ${yourhand[$i]} % 13 )) -eq $1 ] ; then
        return 1
      fi
    done
    return 0
}

Key to remember with the above code snippet is that the card value % 13 = card rank, so all the complexity above is simply comparing cards against the requested card ($1). Once there's a match, you're done, and return true (value = 1), and if you fall out of the loop after testing all 52 possible card slots for yourhand, it returns false.

Using the return code and testing function call results is a really common way to accept return values from functions in shell scripts. Why? Because there aren't more sophisticated function parameter mechanisms like more sophisticated programming languages have. I miss them, but you've got to work with what Bash gives you.

How does this look in a query loop asking players what they want to ask the computer? It looks like this:


echo -n "You ask me if I have a: "
read request
fixFacecards $request

doYouHave $fixedrequest

if [ $? -eq 1 ] ; then
   echo "you have $request you can ask"
else
  echo "you don't have $request, you can't ask for it"
fi

The echo command is, of course, the standard way to push out information to users, but add the -n flag, and it skips the usual CR/LF at the end of the output. The result is that that the cursor sits on the same line as what was output. That's perfect for input:


You ask me if I have a: [cursor]

Problem: if the user types in something like king of hearts, it's going to fail, and there's no error code to prevent an ugly error situation. Robust code is good, but this is a prototype, so it can defer a more sophisticated parsing system until the last phase of development. When the time comes, however, let's also not forget to allow the user to type quit to end the game.

Go back to the code block above. The fixFacecards function is what ensures that users can type a "J" or "A" or similar, however. Then doYouHave is invoked with the numerical rank value to test against your own hand, not that of the computer player. The result is tucked neatly into the function return value, and that's accessible with the $? special variable notation.

This means that if [ $? -eq 1 ] ; then is the same as saying "if the function returns true", which means that yes, you do have at least one card of the rank you're requesting.

Let's do a quick test:


$ sh gofish.sh 
Your Hand:
  5 of Clubs
  6 of Diamonds
  3 of Spades
  9 of Spades
  9 of Clubs
  2 of Hearts
  3 of Hearts
You ask me if I have a: J
you don't have J, you can't ask for it
You ask me if I have a: 3
you have 3 you can ask
You ask me if I have a:

Looks good! There's still a lot of work to do, however, but let's stop here and pick up the development in another article. Meanwhile, here's a homework assignment for you: find someone and go through a few games of Go Fish! to see how it plays out in real life.

About the Author

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.

Dave Taylor