To take our example a stage further, we're going to convert this simple application into a Template Toolkit plug-in module. A plug-in is just like any other Perl module, except that it lives in a special namespace (Template::Plugin::*) and gets passed a reference to a special variable, the context, when its new( ) constructor is called. Plug-ins can be loaded and used via the USE directive. Here's what the module looks like:[63]

[63]The code assumes that Perl 5.6.0 or higher is used. If you are using an older version, use the vars pragma instead of our.

#------------------------------------------------------------------
# Template::Plugin::Games::Hangman
#
# Implementation of the classic hangman game written as a 
# plug-in module for the Template Toolkit.
#
# Written by Andy Wardley.
#------------------------------------------------------------------

package Template::Plugin::Games::Hangman;

use strict;
use Template::Plugin;
use Template::Exception;
use IO::File ( );
use CGI;

use base qw( Template::Plugin );

our $URL    = '/cgi-bin/hangman';
our $ICONS  = '/icons/hangman';
our $WORDS  = '/usr/games/hangman-words';
our $TRIES  = 6;
our @STATE  = qw( word gameno left won total guessed );

The start of the module is very similar to the CGI script. In this case we're defining everything to be in the Template::Plugin::Games::Hangman namespace and specifying that it is a subclass of the Template::Plugin module.

sub new {
    my($class, $context, $config) = @_;

    # create plugin object
    my $self = bless {
        cgi      => CGI->new( ),
        url      => $config->{ url    } || $URL,
        icons    => $config->{ icons  } || $ICONS,
        words    => $config->{ words  } || $WORDS,
        tries    => $config->{ tries  } || $TRIES,
        _context => $context,
    }, $class;

    # restore current game or start new game
    $self->restore( ) || $self->init( );

    return $self;
}

When the plug-in is loaded via a USE directive, the new( ) constructor method is called. The first (zeroth) argument is the calling class name, Template::Plugin::Games::Hangman->new($context, $config), passed as a reference to a context object through which you can access the functionality of the Template Toolkit. The second argument is a reference to a hash array of any configuration items specified with the USE directive.

This method defines an object, $self, using values defined in the $config hash or the defaults specified in the approprate package variables. It then calls the restore( ) method and, if restore( ) doesn't return a true value, the init( ) method. Here are the definitions of those methods:

sub restore {
    my $self = shift;
    my $cgi  = $self->{ cgi };
    return undef if !$cgi->param( );
    $self->{ $_ } = $cgi->param($_) foreach @STATE;
    return undef if $cgi->param('restart');
    return $self;
}

sub init {
    my $self = shift;

    # pick a word, any word
    my $list = IO::File->new($WORDS)
        || die "failed to open '$WORDS' : $!\n";
    my $word;
    rand($.) < 1 && ($word = $_) while <$list>;
    chomp $word;

    $self->{ word    }  = $word;
    $self->{ left    }  = $self->{ tries };
    $self->{ guessed }  = '';
    $self->{ gameno  } += 1;
    $self->{ won     } += 0;
    $self->{ total   } += 0;
    return $self;
}

They are just like their counterparts in the earlier CGI script, with a few minor exceptions. A CGI object is defined in $self->{ cgi } rather than using imported subroutines, and operations are performed on $self rather than on a $state hash array passed as an argument.

The guess( ) method is also very similar to the process_guess( )subroutine in the CGI script:

sub guess {
    my $self  = shift;
    my $cgi   = $self->{ cgi };
    my $guess = $cgi->param('guess') || return;

    # lose immediately if user out of guesses
    return $self->state('lost') 
        unless $self->{ left } > 0;

    my %guessed = map { $_ => 1 } $self->{ guessed } =~ /(.)/g;
    my %letters = map { $_ => 1 } $self->{ word    } =~ /(.)/g;

    # return immediately if user has already guessed the word
    return $self->state('won')
        unless grep(! $guessed{ $_ }, keys %letters);

    # do nothing more if no guess
    return $self->state('continue') unless $guess;

    # process individual letter guesses
    $guess = lc $guess;
    return $self->state(continue => 'Not a valid letter or word!') 
        unless $guess =~ /^[a-z]+$/;
    return $self->state(continue => 'You already guessed that letter!')
        if $guessed{$guess};

    # handle the user guessing the whole word
    if (length($guess) > 1 and $guess ne $self->{word}) {
        $self->{ total } += $self->{ left };
        return $self->state(lost => "You lose.  The word was $self->{word}.");
    }

    # update the list of guesses and word map
    foreach ($guess =~ /(.)/g) { $guessed{$_}++; }
    $self->{ guessed } = join '', sort keys %guessed;

    # correct guess -- word completely filled in
    unless (grep(!$guessed{$_}, keys %letters)) {
        $self->{ won }++;
        return $self->state(won => qq{You got it!  The word was "$self->{word}".});
    }

    # incorrect guess
    if (!$letters{$guess}) {
        $self->{total}++;
        $self->{left}--;
        return $self->state(lost => 
            qq{No dice, dude! The word was "$self->{word}".})
                if $self->{left} <= 0;
        return $self->state(continue => 'Wrong guess!');
    }

    # correct guess but word still incomplete
    return $self->state(continue => 'Good guess!');
}

As a matter of convenience, we also provide the state( ) method, to retrieve the current state (when called without arguments) or set both state and message (when called with one or more arguments):

sub state {
    my $self = shift;
    if (@_) {
        $self->{ state   } = shift;
        $self->{ message } = join('', @_);
    }
    else {
        return $self->{ state };
    }
}

We also define averages( ) and wordmap( ) as object methods:

sub averages {
    my $self = shift;
    return {
        current => $self->{ total } / $self->{ gameno },
        overall => $self->{ gameno } > 1 
             ? ($self->{ total } + $self->{ left } - $self->{ tries }) 
             / ($self->{ gameno } - 1)
             : 0
    };
}

sub wordmap {
    my $self = shift;
    my %guessed = map { $_ => 1 } $self->{ guessed } =~ /(.)/g;
    join ' ', map { $guessed{$_} ? "$_ " : '_ ' } 
        $self->{ word } =~ /(.)/g;
}

We can also encode the high-level game logic in a method:

sub play {
    my $self = shift;

    # process any current guess
    $self->guess( );

    # determine which form to use based on state
    my $form = (exists $self->{ state } &&
                $self->{ state } =~ /^won|lost$/)
        ? 'restart' : 'guess';

    # process the three templates: header, form and footer
    $self->{ _context }->include([ 'header', $form, 'footer' ]);
}

The play( ) method calls guess( ) to process a guess and then calls on the context object that we previously saved in _context to process three templates: the header template, the form relevant to the current game state, and the footer template.

The script that uses this plug-in can now be made even simpler, as shown in Example D-8.

Example D-8. hangman3.pl

#!/usr/bin/perl
#
# hangman3.pl
#
# CGI script using Template Toolkit Hangman plug-in.
#

use strict;
use Template;

# may need to tell Perl where to find plug-in module
use lib qw( /usr/local/tt2/hangman/hangman3/perl5lib );

use constant TEMPLATES => '/home/stas/templates/hangman3';
use constant SHARED    => '/usr/local/tt2/templates';
use constant URL       => '/cgi-bin/hangman3.pl';
use constant ICONS     => '/icons/hangman';
use constant WORDS     => '/usr/games/hangman-words';

# create a Template object
my $tt = Template->new({
    INCLUDE_PATH => [ TEMPLATES, SHARED ],
});

# define Template variables
my $vars = {
    url   => URL,
    icons => ICONS,
    words => WORDS,
    title => 'Template Toolkit Hangman #3',
};

# process the main template
$tt->process(*DATA, $vars)
    || die $tt->error( );

Other than creating a Template object and defining variables, we don't need to do any special processing relevant to the hangman application. That is now handled entirely by the plug-in.

The template defined in the __DATA__section can be made to look very similar to the earlier example. In this case, we're loading the plug-in (Games.Hangman, corresponding to Template::Plugin::Games::Hangman) and aliasing the object returned from new( ) to the hangman variable. We manually call the guess( ) method and PROCESS external templates according to the game state:

__DATA__
Content-type: text/html

[%  WRAPPER html/page
        html.head.title  = title
        html.body.onload = 'if (document.gf) document.gf.guess.focus( )';

        TRY;
            # load the hangman plug-in
            USE hangman = Games.Hangman(
                words = words
                icons = icons
                url   = url
            );

            # process a guess
            CALL hangman.guess;

            # print header showing game averages
            PROCESS header;

            # process the right form according to game state
            IF hangman.state =  = 'won'
            OR hangman.state =  = 'lost';
                PROCESS restart;
            ELSE;
                PROCESS guess;
            END;

            # now print the footer
            PROCESS footer;
        CATCH;
            # and if any of that goes wrong...
            CLEAR;
            PROCESS error;
        END;
    END
%]

One other enhancement we've made is to enclose the body in a TRY block. If the plug-in init( ) method fails to open the words file, it reports the error via die( ). The TRY directive allows this error to be caught and handled in the corresponding CATCH block. This clears any output generated in the TRY block before the error occured and processes an error template instead to report the error in a nice manner.

The template in this example controls the overall flow of the game logic. If you prefer, you can simply call the play( ) method and have the plug-in take control. It handles all the flow control for you, processing the guess and then making calls back into the Template Toolkit to process the header, relevant form, and footer templates.

__DATA__
Content-type: text/html

[%  #Template Toolkit Hangman #4
        WRAPPER html/page
        html.head.title  = title
        html.body.onload = 'if (document.gf) document.gf.guess.focus( )';

        TRY;
            USE hangman = Games.Hangman(
                words = words
                icons = icons
                url   = url
            );
            hangman.play;

        CATCH;
            CLEAR;
            PROCESS error;
        END;
    END
%]

The complete set of templates that go with this final example are presented in Examples D-9 through D-15.

Example D-9. hangman3/templates/header

<h1>[% title %]</h1>

[% # how many guesses left to go?
   tries_left = hangman.tries - hangman.left
%]

[%# display the appropriate image -%]  
<img src="[% hangman.icons %]/h[% tries_left %].gif"
     align="left" alt="[[% tries_left %] tries left]" />

[% PROCESS status %]

Example D-10. hangman3/templates/status

[% # define a format for displaying averages
   USE format('%2.3f');
   average = hangman.averages;
%]

<table width="100%">
<tr>
  <td><b>Word #: [% hangman.gameno %]</b></td>
  <td><b>Guessed: [% hangman.guessed %]</b></td>
</tr>
<tr>
  <td><b>Won: [% hangman.won %]</b></td>
  <td><b>Current average: [% format(average.current) %]</b></td>
  <td><b>Overall average: [% format(average.overall) %]</b></td>
</tr>
</table>

<h2>Word: [% hangman.wordmap %]</h2>

[% IF hangman.message -%]
<h2><font color="red">[% hangman.message %]</font></h2>
[% END %]

Example D-11. hangman3/templates/guess

<form method="post" action="[% hangman.url %]" 
      enctype="application/x-www-form-urlencoded" name="gf">
  Your guess: <input type="text" name="guess" />
  <input type="submit" name=".submit" value="Guess" />
  [% PROCESS state %]
</form>

Example D-12. hangman3/templates/restart

<form method="post" action="[% hangman.url %]" 
      enctype="application/x-www-form-urlencoded">
  Do you want to play again?
  <input type="submit" name="restart" value="Another game" />
  [% PROCESS state %]
</form>

Example D-13. hangman3/templates/state

[% FOREACH var = [ 'word' 'gameno' 'left' 'won' 'total' 'guessed' ] -%]
<input type="hidden" name="[% var %]" value="[% hangman.$var %]" />
[% END %]

Example D-14. hangman3/templates/footer

<br clear="all">
<hr />
<a href="[% hangman.url %]">Home</a>
<p>
  <cite style="fontsize: 10pt">graphics courtesy Andy Wardley</cite>
</p>

Example D-15. hangman3/templates/error

<h3>Hangman Offline</h3>
<p>
Hangman is unfortunately offline at present, reporting sick with 
the following lame excuse:
<ul>
<li><b>[[% error.type %]]</b> [% error.info %]</li>
</ul>
</p>