Up to this point, we’ve focused on serious uses of shell scripts to improve your interaction with your system and make the system more flexible and powerful. But there’s another side to shell scripts that’s worth exploring: games.
Don’t worry—we’re not proposing that you write Fallout 4 as a shell script. There just happen to be some simple games that are easily and informatively written as shell scripts. And wouldn’t you rather learn how to debug shell scripts with something fun than with some utility for suspending user accounts or analyzing Apache error logs?
For some of the scripts, you’ll need files from the book’s resources, found at http://www.nostarch.com/wcss2/, so download that file now if you haven’t already.
This is a basic anagram game. If you’ve seen the Jumble game in your newspaper or played word games at all, you’ll be familiar with the concept: a word is picked at random and then scrambled. Your task is to figure out what the original word is in the minimum number of turns. The full script for this game is in Listing 12-1, but to get the word list, you’ll also need to download the long-words.txt file from the book’s resources http://www.nostarch.com/wcss2/ and save it in the directory /usr/lib/games.
#!/bin/bash # unscramble--Picks a word, scrambles it, and asks the user to guess # what the original word (or phrase) was wordlib="/usr/lib/games/long-words.txt" scrambleword() { # Pick a word randomly from the wordlib and scramble it. # Original word is $match, and scrambled word is $scrambled. match="$(➊randomquote $wordlib)" echo "Picked out a word!" len=${#match} scrambled=""; lastval=1 for (( val=1; $val < $len ; )) do ➋ if [ $(($RANDOM % 2)) -eq 1 ] ; then scrambled=$scrambled$(echo $match | cut -c$val) else scrambled=$(echo $match | cut -c$val)$scrambled fi val=$(( $val + 1 )) done } if [ ! -r $wordlib ] ; then echo "$0: Missing word library $wordlib" >&2 echo "(online: http://www.intuitive.com/wicked/examples/long-words.txt" >&2 echo "save the file as $wordlib and you're ready to play!)" >&2 exit 1 fi newgame=""; guesses=0; correct=0; total=0 ➌ until [ "$guess" = "quit" ] ; do scrambleword echo "" echo "You need to unscramble: $scrambled" guess="??" ; guesses=0 total=$(( $total + 1 )) ➍ while [ "$guess" != "$match" -a "$guess" != "quit" -a "$guess" != "next" ] do echo "" /bin/echo -n "Your guess (quit|next) : " read guess if [ "$guess" = "$match" ] ; then guesses=$(( $guesses + 1 )) echo "" echo "*** You got it with tries = ${guesses}! Well done!! ***" echo "" correct=$(( $correct + 1 )) elif [ "$guess" = "next" -o "$guess" = "quit" ] ; then echo "The unscrambled word was \"$match\". Your tries: $guesses" else echo "Nope. That's not the unscrambled word. Try again." guesses=$(( $guesses + 1 )) fi done done echo "Done. You correctly figured out $correct out of $total scrambled words." exit 0
Listing 12-1: The unscramble shell script game
To randomly pick a single line from a file, this script uses randomquote (Script #68 on page 213) ➊, even though that script was originally written to work with web pages (like many good Unix utilities, it turns out to be useful in contexts other than the one for which it was intended).
The toughest part of this script was figuring out how to scramble a word. There’s no handy Unix utility for that, but it turns out that we can scramble the word differently and unpredictably each time if we go letter by letter through the correctly spelled word and randomly add each subsequent letter to either the beginning or the end of the scrambled sequence ➋.
Notice where $scrambled is located in the two lines: in the first line the added letter is appended, while in the second it is prepended.
Otherwise the main game logic should be easily understood: the outer until loop ➌ runs until the user enters quit as a guess, while the inner while loop ➍ runs until the user either guesses the word or types next to skip to the next word.
This script has no arguments or parameters, so just enter the name and you’re ready to play!
After running, the shell script presents scrambled words of various lengths to the user, keeping track of how many words the user has successfully unscrambled, as Listing 12-2 shows.
$ unscramble Picked out a word! You need to unscramble: ninrenoccg Your guess (quit|next) : concerning *** You got it with tries = 1! Well done!! *** Picked out a word! You need to unscramble: esivrmipod Your guess (quit|next) : quit The unscrambled word was "improvised". Your tries: 0 Done. You correctly figured out 1 out of 2 scrambled words.
Listing 12-2: Running the unscramble shell script game
Clearly an inspired guess on that first one!
Some method of offering a clue would make this game more interesting, as would a flag that requests the minimum word length that is acceptable. To accomplish the former, perhaps the first n letters of the unscrambled word could be shown for a certain penalty in the scoring; each clue requested would show one additional letter. For the latter, you’d need to have an expanded word dictionary as the one included with the script has a minimum word length of 10 letters—tricky!
A word game with a macabre metaphor, hangman is nonetheless an enjoyable classic. In the game, you guess letters that might be in the hidden word, and each time you guess incorrectly, the man hanging on the gallows has an additional body part drawn in. Make too many wrong guesses, and the man is fully illustrated, so not only do you lose but, well, you presumably die too. Rather draconian consequences!
However, the game itself is fun, and writing it as a shell script proves surprisingly easy, as Listing 12-3 shows. For this script, you again need the word list we used in Script #83 on page 275: save the long-words.txt file from the book’s resources in the directory /usr/lib/games.
#!/bin/bash # hangman--A simple version of the hangman game. Instead of showing a # gradually embodied hanging man, this simply has a bad-guess countdown. # You can optionally indicate the initial distance from the gallows as # the only argument. wordlib="/usr/lib/games/long-words.txt" empty="\." # We need something for the sed [set] when $guessed="". games=0 # Start by testing for our word library datafile. if [ ! -r "$wordlib" ] ; then echo "$0: Missing word library $wordlib" >&2 echo "(online: http://www.intuitive.com/wicked/examples/long-words.txt" >&2 echo "save the file as $wordlib and you're ready to play!)" >&2 exit 1 fi # The big while loop. This is where everything happens. while [ "$guess" != "quit" ] ; do match="$(randomquote $wordlib)" # Pick a new word from the library. if [ $games -gt 0 ] ; then echo "" echo "*** New Game! ***" fi games="$(( $games + 1 ))" guessed="" ; guess="" ; bad=${1:-6} partial="$(echo $match | sed "s/[^$empty${guessed}]/-/g")" # The guess > analyze > show results > loop happens in this block. while [ "$guess" != "$match" -a "$guess" != "quit" ] ; do echo "" if [ ! -z "$guessed" ] ; then # Remember, ! –z means "is not empty". /bin/echo -n "guessed: $guessed, " fi echo "steps from gallows: $bad, word so far: $partial" /bin/echo -n "Guess a letter: " read guess echo "" if [ "$guess" = "$match" ] ; then # Got it! echo "You got it!" elif [ "$guess" = "quit" ] ; then # You're out? Okay. exit 0 # Now we need to validate the guess with various filters. ➊ elif [ $(echo $guess | wc -c | sed 's/[^[:digit:]]//g') -ne 2 ] ; then echo "Uh oh: You can only guess a single letter at a time" ➋ elif [ ! -z "$(echo $guess | sed 's/[[:lower:]]//g')" ] ; then echo "Uh oh: Please only use lowercase letters for your guesses" ➌ elif [ -z "$(echo $guess | sed "s/[$empty$guessed]//g")" ] ; then echo "Uh oh: You have already tried $guess" # Now we can actually see if the letter appears in the word. ➍ elif [ "$(echo $match | sed "s/$guess/-/g")" != "$match" ] ; then guessed="$guessed$guess" ➎ partial="$(echo $match | sed "s/[^$empty${guessed}]/-/g")" if [ "$partial" = "$match" ] ; then echo "** You've been pardoned!! Well done! The word was \"$match\"." guess="$match" else echo "* Great! The letter \"$guess\" appears in the word!" fi elif [ $bad -eq 1 ] ; then echo "** Uh oh: you've run out of steps. You're on the platform..." echo "** The word you were trying to guess was \"$match\"" guess="$match" else echo "* Nope, \"$guess\" does not appear in the word." guessed="$guessed$guess" bad=$(( $bad - 1 )) fi done done exit 0
Listing 12-3: The hangman shell script game
The tests in this script are all interesting and worth examination. Consider the test at ➊ that checks whether the player has entered more than a single letter as a guess.
Why test for the value 2 rather than 1? Because the entered value has a carriage return from when the user hit ENTER (which is a character, \n), it has two letters if it’s correct, not one. The sed in this statement strips out all non-digit values, of course, to avoid any confusion with the leading tab that wc likes to emit.
Testing for lowercase is straightforward ➋. Remove all lowercase letters from guess and see whether the result is zero (empty) or not.
Finally, to see whether the user has guessed the letter already, transform the guess such that any letters in guess that also appear in the guessed variable are removed. Is the result zero (empty) or not ➌?
Apart from all these tests, the trick behind getting hangman to work is to replace each guessed letter in the original word with a dash wherever that letter appears in the word and then compare the result to the original word in which no letters have been replaced by dashes ➍. If they’re different (that is, if one or more letters in the word are now dashes), the guessed letter is in the word. Guessing the letter a, for instance, when the word is cat, will result in the guessed variable holding your guess with a value of ‘-a-’.
One of the key ideas that makes it possible to write hangman is that the partially filled-in word shown to the player, the variable partial, is rebuilt each time a correct guess is made. Because the variable guessed accumulates each letter guessed by the player, a sed transformation that translates into a dash each letter in the original word that is not in the guessed string does the trick ➎.
The hangman game has one optional argument: if you specify a numeric value as a parameter, the code will use that as the number of incorrect guesses allowed, rather than the default of 6. Listing 12-4 shows playing the hangman script with no arguments.
$ hangman steps from gallows: 6, word so far: ------------- Guess a letter: e * Great! The letter "e" appears in the word! guessed: e, steps from gallows: 6, word so far: -e--e-------- Guess a letter: i * Great! The letter "i" appears in the word! guessed: ei, steps from gallows: 6, word so far: -e--e--i----- Guess a letter: o * Great! The letter "o" appears in the word! guessed: eio, steps from gallows: 6, word so far: -e--e--io---- Guess a letter: u * Great! The letter "u" appears in the word! guessed: eiou, steps from gallows: 6, word so far: -e--e--iou--- Guess a letter: m * Nope, "m" does not appear in the word. guessed: eioum, steps from gallows: 5, word so far: -e--e--iou--- Guess a letter: n * Great! The letter "n" appears in the word! guessed: eioumn, steps from gallows: 5, word so far: -en-en-iou--- Guess a letter: r * Nope, "r" does not appear in the word. guessed: eioumnr, steps from gallows: 4, word so far: -en-en-iou--- Guess a letter: s * Great! The letter "s" appears in the word! guessed: eioumnrs, steps from gallows: 4, word so far: sen-en-ious-- Guess a letter: t * Great! The letter "t" appears in the word! guessed: eioumnrst, steps from gallows: 4, word so far: sententious-- Guess a letter: l * Great! The letter "l" appears in the word! guessed: eioumnrstl, steps from gallows: 4, word so far: sententiousl- Guess a letter: y ** You've been pardoned!! Well done! The word was "sententiously". *** New Game! *** steps from gallows: 6, word so far: ---------- Guess a letter: quit
Listing 12-4: Playing the hangman shell script game
Obviously it’s difficult to have the guy-hanging-on-the-gallows graphic with a shell script, so we use the alternative of counting “steps to the gallows.” If you were motivated, however, you could probably have a series of predefined “text” graphics, one for each step, and output them as the game proceeds. Or you could choose a nonviolent alternative of some sort!
Note that it is possible to pick the same word twice, but with the default word list containing 2,882 different words, there’s not much chance of that. If this is a concern, however, the line where the word is chosen could also save all previous words in a variable and screen against them to ensure that there aren’t any repeats.
Finally, if you’re motivated, it’d be nice to have the guessed-letters list sorted alphabetically. There are a couple of approaches to this, but we’d use sed|sort.
Once you have a tool for choosing a line randomly from a file, there’s no limit to the types of quiz games you can write. We’ve pulled together a list of the capitals of all 50 states in the United States, available for download from http://www.nostarch.com/wcss2/. Save the file state.capitals.txt in your /usr/lib/games directory. The script in Listing 12-5 randomly chooses a line from the file, shows the state, and asks the user to enter the matching capital.
#!/bin/bash
# states--A state capital guessing game. Requires the state capitals
# data file state.capitals.txt.
db="/usr/lib/games/state.capitals.txt" # Format is State[tab]City.
if [ ! -r "$db" ] ; then
echo "$0: Can't open $db for reading." >&2
echo "(get state.capitals.txt" >&2
echo "save the file as $db and you're ready to play!)" >&2
exit 1
fi
guesses=0; correct=0; total=0
while [ "$guess" != "quit" ] ; do
thiskey="$(randomquote $db)"
# $thiskey is the selected line. Now let's grab state and city info, and
# then also have "match" as the all-lowercase version of the city name.
➊ state="$(echo $thiskey | cut -d\ -f1 | sed 's/-/ /g')"
city="$(echo $thiskey | cut -d\ -f2 | sed 's/-/ /g')"
match="$(echo $city | tr '[:upper:]' '[:lower:]')"
guess="??" ; total=$(( $total + 1 )) ;
echo ""
echo "What city is the capital of $state?"
# Main loop where all the action takes place. Script loops until
# city is correctly guessed or the user types "next" to
# skip this one or "quit" to quit the game.
while [ "$guess" != "$match" -a "$guess" != "next" -a "$guess" != "quit" ]
do
/bin/echo -n "Answer: "
read guess
if [ "$guess" = "$match" -o "$guess" = "$city" ] ; then
echo ""
echo "*** Absolutely correct! Well done! ***"
correct=$(( $correct + 1 ))
guess=$match
elif [ "$guess" = "next" -o "$guess" = "quit" ] ; then
echo ""
echo "$city is the capital of $state." # What you SHOULD have known :)
else
echo "I'm afraid that's not correct."
fi
done
done
echo "You got $correct out of $total presented."
exit 0
Listing 12-5: The states trivia game shell script
For such an entertaining game, states involves very simple scripting. The data file contains state/capital pairs, with all spaces in the state and capital names replaced with dashes and the two fields separated by a single space. As a result, extracting the city and state names from the data is easy ➊.
Each guess is compared against both the all-lowercase version of the city name (match) and the correctly capitalized city name to see whether it’s correct. If not, the guess is compared against the two command words next and quit. If either matches, the script shows the answer and either prompts for another state or quits, as appropriate. If there are no matches, the guess is considered incorrect.
This script has no arguments or command flags. Just start it up and play!
Ready to quiz yourself on state capitals? Listing 12-6 shows our state capital trivia skills in action!
$ states What city is the capital of Indiana? Answer: Bloomington I'm afraid that's not correct. Answer: Indianapolis *** Absolutely correct! Well done! *** What city is the capital of Massachusetts? Answer: Boston *** Absolutely correct! Well done! *** What city is the capital of West Virginia? Answer: Charleston *** Absolutely correct! Well done! *** What city is the capital of Alaska? Answer: Fairbanks I'm afraid that's not correct. Answer: Anchorage I'm afraid that's not correct. Answer: Nome I'm afraid that's not correct. Answer: Juneau *** Absolutely correct! Well done! *** What city is the capital of Oregon? Answer: quit Salem is the capital of Oregon. You got 4 out of 5 presented.
Listing 12-6: Running the states trivia game shell script
Fortunately, the game tracks only ultimately correct guesses, not how many incorrect guesses you made or whether you popped over to Google to get the answer!
Probably the greatest weakness in this game is that it’s picky about spelling. A useful modification would be to add code to allow fuzzy matching, so that the user entry of Juneu might match Juneau, for example. This could be done using a modified Soundex algorithm, in which vowels are removed and doubled letters are squished down to a single letter (for example, Annapolis would transform to npls). This might be too forgiving for your tastes, but the general concept is worth considering.
As with other games, a hint function would be useful, too. Perhaps it would show the first letter of the correct answer when requested and keep track of how many hints are used as the play proceeds.
Although this game is written for state capitals, it would be trivial to modify the script to work with any sort of paired data file. For example, with a different file, you could create an Italian vocabulary quiz, a country/currency match, or a politician/political party quiz. As we’ve seen repeatedly in Unix, writing something that is reasonably general purpose allows it to be reused in useful and occasionally unexpected ways.
Prime numbers are numbers that are divisible only by themselves, for example, 7. On the other hand, 6 and 8 are not prime numbers. Recognizing prime numbers is easy with single digits, but it gets more complicated when we jump up to bigger numbers.
There are different mathematical approaches to figuring out whether a number is prime, but let’s stick with the brute-force method of trying all possible divisors to see whether any have a remainder of zero, as Listing 12-7 shows.
#!/bin/bash
# isprime--Given a number, ascertain whether it's a prime. This uses what's
# known as trial division: simply check whether any number from 2 to (n/2)
# divides into the number without a remainder.
counter=2
remainder=1
if [ $# -eq 0 ] ; then
echo "Usage: isprime NUMBER" >&2
exit 1
fi
number=$1
# 3 and 2 are primes, 1 is not.
if [ $number -lt 2 ] ; then
echo "No, $number is not a prime"
exit 0
fi
# Now let's run some calculations.
➊ while [ $counter -le $(expr $number / 2) -a $remainder -ne 0 ]
do
remainder=$(expr $number % $counter) # '/' is divide, '%' is remainder
# echo " for counter $counter, remainder = $remainder"
counter=$(expr $counter + 1)
done
if [ $remainder -eq 0 ] ; then
echo "No, $number is not a prime"
else
echo "Yes, $number is a prime"
fi
exit 0
Listing 12-7: The isprime script
The heart of this script is in the while loop, so take a look at that more closely at ➊. If we were trying a number of 77, the conditional statement would be testing this:
while [ 2 -le 38 -a 1 -ne 0 ]
Obviously this is false: 77 does not divide evenly by 2. Each time the code tests a potential divisor ($counter) and finds that it doesn’t divide evenly, it calculates the remainder ($number % $counter) and increments the $counter by 1. Ploddingly, it proceeds.
Let’s pick a few numbers that seem like they could be prime and test them in Listing 12-8.
$ isprime 77 No, 77 is not a prime $ isprime 771 No, 771 is not a prime $ isprime 701 Yes, 701 is a prime
Listing 12-8: Running the isprime shell script on some numbers
If you’re curious, uncomment out the echo statement in the while loop to see the calculations and get a sense of how quickly—or slowly—the script finds a divisor that divides evenly into the number without a remainder. In fact, let’s do just that and test 77, as shown in Listing 12-9.
$ isprime 77
for counter 2, remainder = 1
for counter 3, remainder = 2
for counter 4, remainder = 1
for counter 5, remainder = 2
for counter 6, remainder = 5
for counter 7, remainder = 0
No, 77 is not a prime
Listing 12-9: Running the isprime script with debug lines uncommented
There are some inefficiencies in the implementation of the mathematical formula in this script that slow it way down. For example, consider the while loop conditional. We keep calculating $(expr $number / 2) when we can just calculate that value once and use the calculated value for each subsequent iteration, saving the need to spawn a subshell and invoking expr to find out that the value hasn’t changed one iota since the last iteration.
There are also some far smarter algorithms to test for prime numbers, and these are worth exploring, including the delightfully named sieve of Eratosthenes, along with more modern formulas such as the sieve of Sundaram and the rather more complicated sieve of Atkin. Check them out online and test whether your phone number (without dashes!) is a prime or not.
This is a handy script for anyone who enjoys tabletop games, especially role-playing games like Dungeons & Dragons.
The common perception of these games is that they’re just a lot of dice rolling, and that’s actually accurate. It’s all about probabilities, so sometimes you’re rolling a 20-sided die and other times you’re rolling six 6-sided dice. Dice are such an easy random number generator that a huge number of games use them, whether it’s one die, two (think Monopoly or Trouble), or more.
They all turn out to be easy to model, and that’s what the script in Listing 12-10 does, letting the user specify how many of what kind of dice are needed, then “rolling” them all, and offering a sum.
#!/bin/bash # rolldice--Parse requested dice to roll and simulate those rolls. # Examples: d6 = one 6-sided die # 2d12 = two 12-sided dice # d4 3d8 2d20 = one 4-side die, three 8-sided, and two 20-sided dice rolldie() { dice=$1 dicecount=1 sum=0 # First step: break down arg into MdN. ➊ if [ -z "$(echo $dice | grep 'd')" ] ; then quantity=1 sides=$dice else quantity=$(echo $dice | ➋cut -dd -f1) if [ -z "$quantity" ] ; then # User specified dN, not just N. quantity=1 fi sides=$(echo $dice | cut -dd -f2) fi echo "" ; echo "rolling $quantity $sides-sided die" # Now roll the dice... while [ $dicecount -le $quantity ] ; do ➌ roll=$(( ( $RANDOM % $sides ) + 1 )) sum=$(( $sum + $roll )) echo " roll #$dicecount = $roll" dicecount=$(( $dicecount + 1 )) done echo I rolled $dice and it added up to $sum } while [ $# -gt 0 ] ; do rolldie $1 sumtotal=$(( $sumtotal + $sum )) shift done echo "" echo "In total, all of those dice add up to $sumtotal" echo "" exit 0
Listing 12-10: The rolldice script
This script revolves around a simple line of code that invokes the bash random number generator through the expedient shortcut of referencing $RANDOM ➌. That’s the key line; everything else is just window dressing.
The other interesting segment is where the dice description is broken down ➊, because the script supports all three of these notations: 3d8, d6, and 20. This is a standard gaming notation, for convenience: number of dice + d + sides the die should have. For example, 2d6 means two 6-sided dice. See if you can figure out how each is processed.
There’s a fair bit of output for such a simple script. You’ll probably want to adjust this to your own preferences, but here you can see that the statement is just a handy way to verify that it parsed the die or dice request properly.
Oh, and the cut invocation ➋? Remember that -d indicates the field delimiter, so -dd simply says to use the letter d as that delimiter, as needed for this particular dice notation.
Let’s start easy: in Listing 12-11, we’ll use two 6-sided dice, as if we were playing Monopoly.
$ rolldice 2d6 rolling 2 6-sided die roll #1 = 6 roll #2 = 2 I rolled 2d6 and it added up to 8 In total, all of those dice add up to 8 $ rolldice 2d6 rolling 2 6-sided die roll #1 = 4 roll #2 = 2 I rolled 2d6 and it added up to 6 In total, all of those dice add up to 6
Listing 12-11: Testing the rolldice script with a pair of six-sided dice
Notice that the first time it “rolled” the two dice, they came up 6 and 2, but the second time they came up 4 and 2.
How about a quick Yahtzee roll? Easy enough. We’ll roll five six-sided dice in Listing 12-12.
$ rolldice 5d6
rolling 5 6-sided die
roll #1 = 2
roll #2 = 1
roll #3 = 3
roll #4 = 5
roll #5 = 2
I rolled 5d6 and it added up to 13
In total, all of those dice add up to 13
Listing 12-12: Testing the rolldice script with five six-sided dice
Not a very good roll: 1, 2, 2, 3, 5. If we were playing Yahtzee, we’d keep the pair of 2s and reroll everything else.
This gets more interesting when you have a more complicated set of dice to roll. In Listing 12-13, let’s try two 18-sided dice, one 37-sided die, and a 3-sided die (since we don’t have to worry about the limitations of 3D geometric shapes).
$ rolldice 2d18 1d37 1d3
rolling 2 18-sided die
roll #1 = 16
roll #2 = 14
I rolled 2d18 and it added up to 30
rolling 1 37-sided die
roll #1 = 29
I rolled 1d37 and it added up to 29
rolling 1 3-sided die
roll #1 = 2
I rolled 1d3 and it added up to 2
In total, all of those dice add up to 61
Listing 12-13: Running the rolldice script with an assortment of dice types
Cool, eh? A few additional rolls of this motley set of dice yielded 22, 49, and 47. Now you know, gamers!
There’s not much to hack in this script since the task is so easy. The only thing we would recommend is fine-tuning the amount of output that the program produces. For example, a notation like 5d6: 2 3 1 3 7 = 16 would be more space efficient.
For our last script in this chapter, we’ll create the card game Acey Deucey, which means we’ll need to figure out how to create and “shuffle” a deck of playing cards to get randomized results. This is tricky, but the functions you write for this game will give you a general purpose solution you can use to make a more complicated game like blackjack or even rummy or Go Fish.
The game is simple: deal two cards, and then bet whether the next card you’re going to flip up ranks between the two existing cards. Suit is irrelevant; it’s all about the card rank, and a tie loses. Thus, if you flip up a 6 of hearts and a 9 of clubs and the third card is a 6 of diamonds, it’s a loss. A 4 of spades is also a loss. But a 7 of clubs is a win.
So there are two tasks here: the entire card deck simulation and the logic of the game itself, including asking the user whether they want to make a bet. Oh, and one more thing: if you deal two cards that have the same rank, there’s no point betting because you can’t win.
That’ll make an interesting script. Ready? Then go to Listing 12-14.
#!/bin/bash # aceyduecey: Dealer flips over two cards, and you guess whether the # next card from the deck will rank between the two. For example, # with a 6 and an 8, a 7 is between the two, but a 9 is not. function initializeDeck { # Start by creating the deck of cards. card=1 while [ $card –le 52 ] # 52 cards in a deck. You knew that, right? do ➊ deck[$card]=$card card=$(( $card + 1 )) done } function shuffleDeck { # It's not really a shuffle. It's a random extraction of card values # from the 'deck' array, creating newdeck[] as the "shuffled" deck. count=1 while [ $count != 53 ] do pickCard ➋ newdeck[$count]=$picked count=$(( $count + 1 )) done } ➌ function pickCard { # This is the most interesting function: pick a random card from # the deck. Uses the deck[] array to find an available card slot. local errcount randomcard threshold=10 # Max guesses for a card before we fall through errcount=0 # Randomly pick a card that hasn't already been pulled from the deck # a max of $threshold times. Fall through on fail (to avoid a possible # infinite loop where it keeps guessing the same already dealt card). ➍ 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 # If we get here, we've been unable to randomly pick a card, so we'll # just step through the array until we find 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 } 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, 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" } ➐ 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=$(( ${newdeck[1]} % 13 )) # And let's get the rank values rank2=$(( ${newdeck[2]} % 13 )) # to make subsequent calculations easy. rank3=$(( ${newdeck[3]} % 13 )) # Fix to make the king: default rank = 0, make 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 let's organize them so that card1 is always lower 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 # Shhh, it's a secret for now. ➒ echo "I've dealt:" ; echo " $cardname1" ; echo " $cardname2" } function introblurb { cat << EOF Welcome to Acey Deucey. The goal of this game is for you to correctly guess whether the third card is going to be between the two cards I'll pull from the deck. For example, if I flip up a 5 of hearts and a jack of diamonds, you'd bet on whether the next card will have a higher rank than a 5 AND a lower rank than a jack (that is, a 6, 7, 8, 9, or 10 of any suit). Ready? Let's go! EOF } games=0 won=0 if [ $# -gt 0 ] ; then # Helpful info if a parameter is specified introblurb fi while [ /bin/true ] ; do initializeDeck shuffleDeck dealCards splitValue=$(( $rank2 - $rank1 )) if [ $splitValue -eq 0 ] ; then echo "No point in betting when they're the same rank!" continue fi /bin/echo -n "The spread is $splitValue. Do you think the next card will " /bin/echo -n "be between them? (y/n/q) " read answer if [ "$answer" = "q" ] ; then echo "" echo "You played $games games and won $won times." exit 0 fi echo "I picked: $cardname3" # Is it between the values? Let's test. Remember, equal rank = lose. ➓ if [ $rank3 -gt $rank1 -a $rank3 -lt $rank2 ] ; then # Winner! winner=1 else winner=0 fi if [ $winner -eq 1 -a "$answer" = "y" ] ; then echo "You bet that it would be between the two, and it is. WIN!" won=$(( $won + 1 )) elif [ $winner -eq 0 -a "$answer" = "n" ] ; then echo "You bet that it would not be between the two, and it isn't. WIN!" won=$(( $won + 1 )) else echo "Bad betting strategy. You lose." fi games=$(( $games + 1 )) # How many times do you play? done exit 0
Listing 12-14: The aceydeucey script game
Simulating a deck of shuffled playing cards is not easy. There’s the question of how to portray the cards themselves and of how to “shuffle” or randomly organize an otherwise neatly ordered deck.
To address this, we create two arrays of 52 elements: deck[] ➊ and newdeck[] ➋. The former is an array of the ordered cards where each value is replaced by a -1 as it’s “selected” and put into a random slot of newdeck[]. The newdeck[] array, then, is the “shuffled” deck. While in this game we only ever use the first three cards, the general solution is far more interesting to consider than the specific one.
That means this script is overkill. But hey, it’s interesting.
Let’s step through the functions to see how things work. First off, initializing the deck is really simple, as you can see if you flip back and examine the initializeDeck function.
Similarly, shuffleDeck is surprisingly straightforward because all the work is really done in the pickCard function. But shuffleDeck simply steps through the 52 slots in deck[], randomly picks a value that hasn’t yet been picked, and saves it in the nth array space of newdeck[].
Let’s look at pickCard ➌ because that’s where the heavy lifting of the shuffle occurs. The function is broken into two blocks: the first attempts to randomly pick an available card, giving it $threshold tries to succeed. As the function is called again and again, the first calls always succeed at this, but later in the process, once 50 cards are already moved over into the newdeck[], it’s quite possible that 10 random guesses all yield a fail. That’s the while block of code at ➍.
Once $errcount is equal to $threshold, we basically give up on this strategy in the interest of performance and move to the second block of code: stepping through the deck card by card until we find an available card. That’s the block at ➎.
If you think about the implications of this strategy, you’ll realize that the lower you set the threshold, the more likely that newdeck will be sequential, particularly later in the deck. At the extreme, threshold = 1 would yield an ordered deck where newdeck[] = deck[]. Is 10 the right value? That’s a bit beyond the scope of this book, but we’d welcome email from someone who wanted to experimentally ascertain the best balance of randomness and performance!
The showCard function is long, but most of those lines are really just about making the results pretty. The core of the entire deck simulation is captured in the two lines at ➏.
For this game, suit is irrelevant, but you can see that for a given card value, the rank is going to be 0–12 and the suit would be 0–3. The cards’ qualities just need to be mapped to user-friendly values. To make debugging easy, a 6 of clubs has a rank 6, and an ace has rank 1. A king has a default rank of 0, but we adjust it to rank 13 so the math works.
The dealCards function ➐ is where the actual Acey Deucey game comes into play: all the previous functions are dedicated to implementing the useful set of functions for any card game. The dealCards function deals out all three cards required for the game, even though the third card is hidden until after the player places their bet. This just makes life easier—it’s not so that the computer can cheat! Here you can also see that the separately stored rank values ($rank1, $rank2, and $rank3) are fixed for the king = 13 scenario. Also to make life easier, the top two cards are sorted so that the lower-rank card always comes first. That’s the if chunk at ➑.
At ➒, it’s time to show what’s dealt. The last step is to present the cards, check whether the ranks match (in which case we’ll skip the prompt that lets the user decide whether to bet), and then test whether the third card is between the first two. This test is done in the code block at ➓.
Finally, the result of the bet is tricky. If you bet that the drawn card will be between the first two cards and it is, or you bet that it won’t be and it isn’t, you’re a winner. Otherwise you lose. This result is figured out in the final block.
Specify any starting parameter and the game will give you a rudimentary explanation of how to play. Otherwise, you just jump in.
Let’s look at the intro in Listing 12-15.
$ aceydeucey intro Welcome to Acey Deucey. The goal of this game is for you to correctly guess whether the third card is going to be between the two cards I'll pull from the deck. For example, if I flip up a 5 of hearts and a jack of diamonds, you'd bet on whether the next card will have a higher rank than a 5 AND a lower rank than a jack (that is, a 6, 7, 8, 9, or 10 of any suit). Ready? Let's go! I've dealt: 3 of Hearts King of Diamonds The spread is 10. Do you think the next card will be between them? (y/n/q) y I picked: 4 of Hearts You bet that it would be between the two, and it is. WIN! I've dealt: 8 of Clubs 10 of Hearts The spread is 2. Do you think the next card will be between them? (y/n/q) n I picked: 6 of Diamonds You bet that it would not be between the two, and it isn't. WIN! I've dealt: 3 of Clubs 10 of Spades The spread is 7. Do you think the next card will be between them? (y/n/q) y I picked: 5 of Clubs You bet that it would be between the two, and it is. WIN! I've dealt: 5 of Diamonds Queen of Spades The spread is 7. Do you think the next card will be between them? (y/n/q) q You played 3 games and won 3 times.
Listing 12-15: Playing the aceydeucey script game
There’s the lingering question of whether the deck is shuffled adequately with a threshold of 10; that’s one area that can definitely be improved. It’s also not clear whether showing the spread (the difference between the ranks of the two cards) is beneficial. Certainly you wouldn’t do that in a real game; the player would need to figure it out.
Then again, you could go in the opposite direction and calculate the odds of having a card between two arbitrary card values. Let’s think about this: the odds of any given card being drawn is 1 out of 52. If there are 50 cards left in the deck because two have already been dealt, the odds of any given card coming up is 1 out of 50. Since suit is irrelevant, there are 4 out of 50 chances that any different rank comes up. Therefore, the odds of a given spread are (the number of cards in that possible spread × 4) out of 50. If a 5 and a 10 are dealt, the spread is 4, since the possible winning cards are a 6, 7, 8, or 9. So the odds of winning are 4 × 4 out of 50. See what we mean?
Finally, as with every command line–based game, the interface could do with some work. We’ll leave that up to you. We’ll also leave you the question of what other games to explore with this handy library of playing-card functions.