12
SHELL SCRIPT FUN AND GAMES

image

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.

#83 Unscramble: A Word Game

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.

The Code

   #!/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

How It Works

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.

Running the Script

This script has no arguments or parameters, so just enter the name and you’re ready to play!

The Results

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!

Hacking the Script

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!

#84 Hangman: Guess the Word Before It’s Too Late

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.

The Code

   #!/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

How It Works

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 .

Running the Script

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.

The Results

$ 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

Hacking the Script

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.

#85 A State Capitals Quiz

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.

The Code

   #!/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

How It Works

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.

Running the Script

This script has no arguments or command flags. Just start it up and play!

The Results

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!

Hacking the Script

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.

#86 Is That Number a Prime?

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.

The Code

   #!/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

How It Works

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.

Running the Script

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.

The Results

$ 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

Hacking the Script

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.

#87 Let’s Roll Some Dice

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.

The Code

   #!/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

How It Works

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.

Running the Script

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!

Hacking the Script

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.

#88 Acey Deucey

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.

The Code

   #!/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

How It Works

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. image

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.

Running the Script

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.

The Results

$ 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

Hacking the Script

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.