A classic word game with a macabre metaphor, hangman is nonetheless popular and enjoyable. 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. Not very pleasant!
However, the game itself is fun, and writing it as a shell script proves surprisingly easy.
#!/bin/sh # hangman - A rudimentary 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 # arg. wordlib="/usr/lib/games/long-words.txt" randomquote="$HOME/bin/randomquote.sh" # Script #76 empty="\." # we need something for the sed [set] when $guessed="" games=0 if [ ! -r $wordlib ] ; then echo "#!/bin/sh # hangman - A rudimentary 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 # arg. wordlib="/usr/lib/games/long-words.txt" randomquote="$HOME/bin/randomquote.sh" # Script #76 empty="\." # we need something for the sed [set] when $guessed="" games=0 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 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")" while [ "$guess" != "$match" -a "$guess" != "quit" ] ; do echo "" if [ ! -z "$guessed" ] ; then echo -n "guessed: $guessed, " fi echo "steps from gallows: $bad, word so far: $partial" echo -n "Guess a letter: " read guess echo "" if [ "$guess" = "$match" ] ; then echo "You got it!" elif [ "$guess" = "quit" ] ; then sleep 0 # a 'no op' to avoid an error message on 'quit' 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" 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... <SNAP!>" 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: 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 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")" while [ "$guess" != "$match" -a "$guess" != "quit" ] ; do echo "" if [ ! -z "$guessed" ] ; then echo -n "guessed: $guessed, " fi echo "steps from gallows: $bad, word so far: $partial" echo -n "Guess a letter: " read guess echo "" if [ "$guess" = "$match" ] ; then echo "You got it!" elif [ "$guess" = "quit" ] ; then sleep 0 # a 'no op' to avoid an error message on 'quit' 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" 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... <SNAP!>" 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
The tests in this script are all interesting and worth examination. Consider this test to see if the player has entered more than a single letter as his or her guess:
elif [ $(echo $guess wc -c sed 's/[^[:digit:]]//g') -ne 2 ] ; then
Why test for the value 2 rather than 1? Because the entered value has a carriage return appended by the read statement, and so it has two letters if it's correct, not one. The sed in this statement strips out all nondigit 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 if the result is zero (empty) or not:
elif [ ! -z "$(echo $guess sed 's/[[:lower:]]//g')" ] ; then
And, finally, to see if 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, and see if the result is zero (empty) or not:
elif [ -z "$(echo $guess sed "s/[$empty$guessed]//g")" ] ; then
Apart from all these tests, however, the trick behind getting hangman to work is to translate into dashes all occurrences in the original word of each guessed letter and then to compare the result to the original word. If they're different, the guessed letter is in that word:
elif [ "$(echo $match sed "s/$guess/-/g")" != "$match" ] ; then
One of the key ideas that made it possible to write hangman was 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:
partial="$(echo $match sed "s/[^$empty${guessed}]/-/g")"
The hangman game has one optional argument: If you specify a numeric value as a parameter, it will use that as the number of incorrect guesses allowed, rather than the default of 6.
$ 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
Obviously it's quite difficult to have the fancy guy-hanging-on-the-gallows graphic if we're working with a shell script, so we use the alternative of counting "steps to the gallows" instead. 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 , of course!
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 occurring. 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 were motivated, it'd be nice to have the guessed letters list be sorted alphabetically . There are a couple of approaches to this, but I think I'd try to use sedsort .