CGI and Perl

Hangman Using the GD Module

Now you're ready to use the GD module to produce a simple game. Hangman is a game that involves some dynamically drawn graphics. This example contains some solid filled shapes, outline shapes, lines, and transparency. To create this game, you combine what you've learned so far about CGI programming with this new concept of dynamic image creation.

You start by writing the function that draws the entire image. It takes an argument to determine how much to draw; for example, the value 1 indicates to simply draw the head. The value 6 indicates to draw the entire body, which, of course, means that the Hangman game is over. Here's how to get started:

sub drawMan {
    my($level)=@_;
    # create the image.
    my($image) = new GD::Image(100,100);
    my($trans) = $image->colorAllocate(128,128,128);
    my($white) = $image->colorAllocate(255,255,255);
    my($black) = $image->colorAllocate(0,0,0);
    my($red) = $image->colorAllocate(255,0,0);
    my($blue) = $image->colorAllocate(0,0,255);
    my($green) = $image->colorAllocate(0,255,0);
    $image->transparent($trans);
    $image->interlaced(1);
    $gallows=new GD::Polygon();
    $gallows->addPt(10,80);
    $gallows->addPt(10,10);
    $gallows->addPt(50,10);
    $gallows->addPt(50,20);
    $gallows->addPt(25,20);
    $gallows->addPt(25,80);
    $image->filledPolygon($gallows,$blue);
    $image->line(50,20,50,30,$green);
    $image->line(10,80,80,80,$red);
    if ($level>0) {
       $image->arc(50,35,10,10,0,360,$black);
       if ($level>1) {
          $image->line(50,40,50,60,$black);
          if ($level>2) {
             $image->line(60,35,50,50,$black);
             if ($level>3) {
                $image->line(40,35,50,50,$black);
                if ($level>4) {
                   $image->line(50,60,60,75,$black);
                   if ($level>5) {
                      $image->line(50,60,40,75,$black);
                   }
                }
             }
          }
       }
    }
    print $image->gif;
 }

You now create the Hangman game around this drawing function. To keep the game simple, use a small pool of words from which users can choose. To display an image within your HTML document, you must first store it to a file. Then you can use the <IMG> tag to embed it into your document. For this example, simply store the image to a temporary file where the name of the file is determined by the process ID. Here is how the main program looks:

#!/usr/local/bin/Perl
 use GD;
 use CGI::Form;
 @wordlist=(`navigator`,`explorer`,`hypertext`,`practical',`extraction`,
            `reporting',`language',`portable',`document`,`format`,
            `graphic',`interchange',`multimedia',`programming',
            `implementation','management');
 @letters=(`a',`b',`c',`d',`e',`f',`g',`h',`i',
           `j',`k',`l',`m','n',`o',`p',`q',`r',
           `s',`t',`u',`v',`w',`x',`y',`z');
 $guesses="";
 $numguesses=0;
 $img_file="/user/bdeng/Web/docs/temp/$$.gif";
 $q = new CGI::Form;
 print $q->header();
 print $q->start_html(-title=>`Hangman!');
 print "<H1>Hangman</H1>\n";
 if ($q->cgi->var(`REQUEST_METHOD') eq `GET') {
    &gameIntro($q);
 } else {
    $action=$q->param(`Action');
    if ($action eq "New Game") {
       &gameIntro($q);
    } else {
       &playGame($q);
    }
 }
 print $q->end_html();

As you can see from the main program, you have two main subroutines. The first, which follows, is called gameIntro(); it is used to initialize the game. Initialization involves choosing a random word, drawing the empty gallows, and setting up some variables used to keep track of the user's progress.

sub gameIntro {
    my($q)=@_;
    $numWords=@wordlist;
    srand(time|$$);
    my($index)=int(rand($numWords));
    $word=$wordlist[$index];
    $guesses="";
    $numguesses=0;
    $q->param(`word',$word);
    $q->param(`guesses',$guesses);
    $q->param(`numguesses',$numguesses);
    print "<P>Welcome to Hangman.";
    print "Select a letter and click `Guess' to start playing<BR>\n";
    &drawHangman($q,$numguesses);
    &drawWord($word,$guesses);
    &gameForm($q);
 }

You use the srand() Perl function to set the seed for generating random numbers. Using the current time together with the process ID should give you a random value. This subroutine calls three other subroutines, which are shared with the next subroutine.

The other main subroutine, which follows, is called playGame(); here you query the letter that the user guesses and fill in the found letters on each subsequent call. This routine is also responsible for determining when the user wins or loses. It also makes use of the drawHangman(), drawWord(), and gameForm() subroutines.

sub playGame {
    my($q)=@_;
    $word=$q->param(`word');
    $letter=$q->param(`Letter');
    $numguesses=$q->param(`numguesses');
    $guesses=$q->param(`guesses');
    if (index($word,$letter)>=0) {
       print "<P>Good Guess!<BR>\n";
    } else {
       $numguesses++;
       $q->param(`numguesses',$numguesses);
       if ($numguesses==6) {
          print "<P>Sorry! You lose! The word was $word<BR>";
       } else {
          print "<P>Sorry. Please try again.<BR>";
       }
    }
    $guesses .= $q->param(`Letter');
    $q->param(`guesses',$guesses);
    $q->param(`word',$word);
    if (&guessedFullWord($word,$guesses)) {
       print "<P><H3>Congratulations! You Win!</H3><BR>";
    }
    &drawHangman($q,$numguesses);
    &drawWord($word,$guesses);
    &gameForm($q);
 }

Next, look at the three common subroutines responsible for emitting the rest of the HTML. drawHangman() is just a wrapper around the drawMan() routine you saw earlier. Its role is to create a temporary file and print out the <IMG> tag for displaying the dynamic GIF. drawWord()is a bit more complicated. Its job is to mask the part of the word that the user has not already guessed. It does so using nested for loops. And gameForm() should look very familiar to you by now. This routine uses the CGI::Form methods to display all the form fields. The hidden() fields enable you to maintain the state of the game across multiple CGI requests. The subroutines are as follow:

sub drawHangman {
    my($q,$numguesses)=@_;
    open(TEMP,"> $img_file");
    select(TEMP);
    &drawMan($numguesses);
    select STDOUT;
    close(TEMP);
    chmod 0755, $img_file;
    print "<IMG SRC=/temp/$$.gif>";
 }
 sub drawWord {
    my($word,$guesses)=@_;
    my($displayWord)="";
    for ($i=0;$i<length($word);$i++) {
       my($found)=0;
       for ($j=0;$j<length($guesses);$j++) {
          if (substr($word,$i,1) eq substr($guesses,$j,1)) {
             $found=1;
          }
       }
       if ($found) {
          $displayWord .= substr($word,$i,1);
       } else {
          $displayWord .= "_";
       }
       $displayWord .= " ";
    }
    print "<BR><H2>$displayWord</H2><BR>\n";
 }
 sub gameForm {
    my($q)=@_;
    print $q->start_multipart_form();
    print $q->popup_menu(-name=>`Letter',-value=>\@letters);
    print $q->hidden(-name=>`word', -value=>"$word");
    print $q->hidden(-name=>`guesses', -value=>"$guesses");
    print $q->hidden(-name=>`numguesses', -value=>"$numguesses");
    print $q->submit(-name=>`Action',-value=>`Guess');
    print " ";
    print $q->submit(-name=>`Action',-value=>`New Game');
    print $q->endform;
 }

Finally, you need the guessedFullWord() function, which returns whether the user has completely guessed the entire word. It uses the same algorithm that drawWord uses, except that it returns FALSE as soon as it detects a letter that has not been found.

sub guessedFullWord {
    my($word,$guesses)=@_;
    my($i,$j);
    for ($i=0;$i<length($word);$i++) {
       my($found)=0;
       for ($j=0;$j<length($guesses);$j++) {
      if (substr($word,$i,1) eq substr($guesses,$j,1)) {
         $found=1;
      }
       }
       if (!$found) {
      return 0;
       }
    }
    return 1;
 }

Images of the game in action are shown in Figures 12.1 through 12.3.

Figure 12.1. The Hangman game within the Netscape browser.

Figure 12.2. The game after two wrong guesses.

Figure 12.3. The game after the user lost.

The Hangman game is a simple example of how to use the GD module to draw graphics dynamically from a CGI script. This example runs on UNIX, although you could run it on Windows using a slightly modified version of GD.pm from David Roth. This version is available at ftp://roth.net.com/pub/ntPerl/win32GD.zip. The current version of MacPerl (5.10r2) also supports the standard GD module.