Samir Parikh · Blog · Git

Originally published on 20 February 2022

The objective of the game Hangman is to guess a randomly chosen word letter-by-letter within a set number of tries (typically six to eight). As part of the game logic, you need to keep track of which letters the player guessed, whether they match any letters in the secret word, and if the player successfully guessed the word within the allotted number of tries. In “Writing Apache Modules with Perl and C” by Doug MacEachern and Lincoln Stein, which is one of the books I’ve been reading to learn about CGI, I came across an implementation of hangman with some logic that originally had me stumped but then had me impressed.

If we assume that the secret word (e.g. “elephant”) is stored in the variable $word and that the letters guessed thus far (e.g. “ae”) are stored in the variable $guesses, the book uses this piece of code to determine whether or not the player has successfully guessed all of the letters of the word:

my %guessed = map { $_ => 1 } $guesses =~ /(.)/g;
my %letters = map { $_ => 1 } $word =~ /(.)/g;
return ('won') unless grep(!$guessed{ $_ }, keys %letters);

It was that last line that originally had me flummoxed. The key for me in understanding it was to unpack the grep statement as I knew what the individual pieces were doing:

my $not_guessed = 0;

foreach (keys %letters) {
    if ( $guessed{ $_ } ) {
        print "the letter $_ was guessed\n";
    } else {
        print "the letter $_ was NOT guessed\n";

I understood that %guessed and %letters were hashes representing letters that the player has already guessed and those in the secret word, respectively. After deconstructing the grep statement into the foreach loop, I realized that the grep statement in scalar context was actually counting the number of letters in the secret word that were not matched. For example, if the secret word is “elephant” and the player has guessed the letters “a” and “e”, then the value of $not_guessed, which is equivalent to the statement grep(!$guessed{$\_}, keys %letters), would be 5 (representing the unmatched letters “l”, “p”, “h”, “n” and “t”). Since Perl interprets all non-zero return values as true, it won’t execute the return statement that is dependent on the unless conditional statement. Conversely, if the player had correctly guessed all letters of the word “elephant”, $not_guessed would be equal to zero. This is the value that the grep statement would return. Therefore, the value in the unless statement evaluates to false, causing Perl to execute the return statement indicating that the player had won. I think it was the combination of the “double-negative” of the unless statement and the not operator (!) that had me confused.

The second interesting nugget of code the authors presented concerns how the secret word is displayed. In Hangman, we represent each letter of the word by a series of dashes (“-”) or underscores (“_”) and then fill them in as the player correctly guesses a letter. From our earlier example, where the player has guessed the letters “a” and “e” for the secret word “elephant”, we would represent the state of the game as:

e _ e _ _ a _ _

To accomplish this, the authors employ a clever use of the map function and the conditional operation ?;:

print map { $guessed{ $_ } ? $_ : '_' } $word =~ /(.)/g;

The last part of that statement, $word =~ /(.)/g, uses the binding operator =~ to perform a regular expression global (g) match of all characters (or letters in our case) in the word $word and return them as a list. The match function then evaluates each item in the list to see whether or not $guessed{ $_ } returns a true statement. If the player has correctly guessed the letter from the secret word, $guessed{ $_ } equals 1 in which case this expression evaluates to true. Therefore, according to the conditional operator, Perl will print the letter. If the player had not yet guessed that letter, $guessed{ $_ } will not exist, evaluating to false. In that case, Perl will print the underscore (“_”).

It’s amazing how much cleverness you can find in just a few lines of code!