Between the previous 163 columns I've written here in Linux Journal and the dozens of games I programmed and explored during the creation of my Wicked Cool Shell Scripts book, I've written a lot of Bash shell games. The challenge is to find one that's simple enough where a shell script will work, but isn't so simple that it ends up being only a half-dozen lines.
Magic 8-Ball is a perfect example. It turns out that the entire "predict the future" gizmo was really just a 20-sided die floating in dark purple fluid. So an array of 20 possible values and a random number selector and boom—you've got a magic 8-ball script:
#!/bin/sh
# magic 8 ball. Yup. Pick a random number, output message
# messages harvested from the Wikipedia entry
answers=("It is certain." "It is decidedly so."
"Without a doubt." "Yes - definitely."
"You may rely on it." "As I see it, yes." "Most likely."
"Outlook good." "Yes." "Signs point to yes."
"Reply hazy, try again." "Ask again later."
"Better not tell you now." "Cannot predict now."
"Concentrate and ask again." "Don't count on it."
"My reply is no." "My sources say no."
"Outlook not so good." "Very doubtful.")
echo "Oh! Magic 8 Ball, Please Tell Me True..." ; echo ""
/bin/echo -n "What is your question? "
read question
answer=$(( $RANDOM % 20 ))
echo ""
echo "I have looked into the future and I say: "
echo " ${answers[$answer]}" ; echo ""
exit 0
Let's do a quick run to see if I'm the most popular LJ writer:
$ sh magic8.sh
Oh! Magic 8 Ball, Please Tell Me True...
What is your question? Am I the most popular LJ writer?
I have looked into the future and I say:
My reply is no.
Ouch, that's harsh. I write the darn divination program, and it just drops a brick on my foot. Yeesh.
More seriously, Magic 8 Ball is too simple to make an interesting shell script. By contrast, Call of Duty is way too complex, even if I did a version with text output instead of gorgeously rendered 3D graphics.
That's why card games prove to be good as programming challenges or exercises: the core mechanism of a 52-card random deck is pretty straightforward, so it's all about the actual cardplay.
Not only that, but as I've written before about card games as shell scripts, I already have a handy set of functions to create, shuffle and display cards out of a deck. If you want to rummage in the archives, I've tackled Acey-Deucey, Baccarat and some bits and pieces of Cribbage.
In order to jump right into the new game that I'm going to describe how to build, Go Fish!, let's steal the following functions from my earlier scripts:
initializeDeck
shuffleDeck
pickCard
showCard
I've uploaded the script library to my AskDaveTaylor site, so you can grab it here if you like.
With those functions all available, let's start writing a Go Fish! game.
If you don't have kids, it might have been a while since you've played the simple game of Go Fish!. I would categorize it as a memorization game, because if you can remember what the other player has asked, you'll generally win the game.
Go Fish! is pretty simple. Each player starts with seven cards, and they take turns asking the other player if they have one or more of a particular card rank. If they do, they hand over all of their cards of that rank. If they do not, then you "go fish" and take the top card from the deck.
Once you have four of a kind, you immediately place those in front of you. If you ever have no cards in your hand, you immediately pick two from the deck and play proceeds. The game ends when there are no cards left in either person's hand and none in the deck. The winner is the one with more sets at the end of the game. It's pretty simple.
Note: the game becomes a lot more interesting with three or four players. Try it!
To get started, here's an easy way to include a set of functions from another file:
cardlib="playing-card-library.sh"
if [ ! -f $cardlib ] ; then
echo "Can't find the playing card library $cardlib"
exit 1
fi
. $cardlib
The conditional test before the "." source statement might be redundant, but why not have a nice error message in the case that the cardlib file isn't found?
The source statement (with slightly different syntax in different shells) is
interesting. It reads the specified file as if its contents were part of the current
file. This means any functions, any variables, anything that's in the file is
incorporated into the current shell, not a subshell (as would happen if you used sh
$cardlib
, for example).
Now that the functions are defined, it's time to deal seven cards to both the
computer player and the user. To track all this, I'm going to use two arrays:
myhand
and yourhand
. The shell dynamically resizes arrays as needed, which is
great for Go Fish!,
because you could end up having a lot more than seven cards during the game:
i=1
while [ $i -lt 8 ] ; do
myhand[$i]=${newdeck[$i]}
yourhand[$i]=${newdeck[$(( $i + 7 ))]}
i=$(( $i + 1 ))
done
There are a lot of punctuation symbols in this line:
yourhand[$i]=${newdeck[$(( $i + 7 ))]}
Unwrap it step by step, and you'll remember that any array reference must be
${name[x]}
, which is half the complexity. The other half is the $((
equation ))
notation
to do some basic math without a subshell. Basically, this is like the worst dealer in
the world, dealing cards 1–7 to the computer and 8–14 to the player. But if
it's
shuffled...
The showCard
function (part of cardlib
) is a bit clunky,
because it doesn't actually
output the card name, it just preloads global variable cardname
with the correct value.
So here's how to show a hand:
echo computer hand:
showCard ${myhand[1]} ; echo " $cardname"
showCard ${myhand[2]} ; echo " $cardname"
showCard ${myhand[3]} ; echo " $cardname"
showCard ${myhand[4]} ; echo " $cardname"
showCard ${myhand[5]} ; echo " $cardname"
showCard ${myhand[6]} ; echo " $cardname"
showCard ${myhand[7]} ; echo " $cardname"
Running this, it's clear that I might need to sort the cards by rank to make the game easier to play:
$ sh gofish.sh
computer hand:
5 of Diamonds
9 of Hearts
4 of Clubs
7 of Diamonds
8 of Hearts
K of Hearts
K of Clubs
your hand:
5 of Hearts
9 of Diamonds
Q of Diamonds
2 of Spades
6 of Clubs
2 of Diamonds
Q of Spades
Since the player won't ever have to specify an individual card, I think I can get away without sorting the hand, so I'll leave that as a task for you, dear reader, when you start fiddling with the code.
Won't need to specify a card? Right. You'll ask the computer if it has any, say, "twos" by entering a "2" at the prompt. In fact, to make this interesting, let's let the player potentially cheat by having the computer ask if they have a specific card, rather than automatically just taking it out of the hand.
I'm imagining a play sequence like this:
Your hand:
5 of Hearts
9 of Diamonds
Q of Diamonds
2 of Spades
6 of Clubs
2 of Diamonds
Q of Spades
You go first. You ask me if I have: 3
** You don't have any 3s so you can't ask for that!
You ask me if I have: 2
** I do not. Go fish.
(( you pick up the 9 of Hearts ))
My turn. Do you have any 7s? (yes/no):
With that in mind, let's stop here and pick up this coding project next time. See you then, and in the meantime, find a youngling and play a few games of Go Fish! to see the (admittedly fairly minimal) nuances of the game.