cgi-dev

Repository that serves as my CGI "scratchpad" to try things out.
git clone git://git.samirparikh.com/cgi-dev
Log | Files | Refs | README

hangman4.cgi (7178B) - raw


      1 #!/usr/bin/perl
      2 
      3 # file: hangman4.cgi
      4 # hangman game using simple cookie and message authentication
      5 # state and encyprtion to maintain state
      6 
      7 use IO::File();
      8 use CGI qw( :standard );
      9 use CGI::Cookie ();
     10 use MD5 ();
     11 use Crypt::CBC ();
     12 use warnings;
     13 use strict;
     14 
     15 use constant WORDS => '/usr/share/dict/hangman_words';
     16 use constant TRIES => 6;
     17 use constant COOKIE_NAME => 'Hangman4';
     18 use constant SECRET => 'kiddo';
     19 
     20 # retrieve the state
     21 my $CIPHER ||= Crypt::CBC->new( SECRET, 'IDEA' );
     22 my $state = get_state() unless param('clear');
     23 
     24 # reinitialize if we need to
     25 $state    = initialize($state) if !$state or param('restart');
     26 
     27 # process the current guess, if any
     28 my ($message, $status) = process_guess(param('guess') || '', $state);
     29 
     30 # generate the HTML page
     31 print   header(-charset    => 'utf-8',
     32                -cookie => save_state($state)
     33               ),
     34         start_html( -Title   => 'Hangman 1',
     35                     -bgcolor => 'white',
     36                     -onLoad  => 'if (document.gf) document.gf.guess.focus()'),
     37         h1( 'Hangman 1 Interactive Game' );
     38 
     39 # draw the statistics
     40 status( $message, $state );
     41 
     42 # prompt the user to restart or to enter the next guess
     43 if ( $status =~ /^(won|lost)$/ ) {
     44     show_restart_form($state);
     45 } else {
     46     show_guess_form($state);
     47 }
     48 print   hr,
     49         a( {-href => '/'}, "Home"),
     50         p( cite( {-style => "fontsize: 10pt"},
     51                  'graphics courtesy of Andy Wardley')),
     52         end_html();
     53 
     54 # This is called to initialize a whole new state object
     55 # or to create a new game.
     56 sub initialize {
     57     my $state = shift;
     58     $state = {} unless $state;
     59     $state->{WORD}              = pick_random_word();
     60     $state->{GUESSES_LEFT}      = TRIES;
     61     $state->{GUESSED}           = ''; # empty hash reference
     62     $state->{GAMENO}            += 1;
     63     $state->{WON}               += 0;
     64     $state->{TOTAL}             += 0;
     65     return $state;
     66 }
     67 
     68 sub save_state {
     69     my $state = shift;
     70     MAC($state, 'generate');
     71     # encrypt the cookie
     72     my $encrypted = $CIPHER->encrypt_hex(join ':', %{$state});
     73     return CGI::Cookie->new (
     74         -name   => COOKIE_NAME,
     75         -value  => $encrypted,
     76         -expires => '+1M'
     77     );
     78 }
     79 
     80 #sub get_state {
     81 #    return undef unless param();
     82 #    my $state = {};
     83 #    foreach (qw(WORD GAMENO GUESSES_LEFT WON TOTAL GUESSED)) {
     84 #        $state->{$_} = param($_);
     85 #    }
     86 #    return $state;
     87 #}
     88 
     89 sub get_state {
     90     my %cookie = cookie( COOKIE_NAME );
     91     return undef unless %cookie;
     92     # decrypt the cookie
     93     my %state = split ':', $CIPHER->decrypt_hex( %cookie );
     94     authentication_error() unless MAC(\%state, 'check');
     95     return \%state;
     96 }
     97 
     98 sub show_guess_form {
     99    my $state = shift;
    100    print start_form(-name => 'gf'),
    101          "Your guess: ",
    102          textfield(-name => 'guess', -value => '', -override => 1),
    103          submit(-value => 'Guess');
    104    #save_state($state);
    105    print end_form;
    106 }
    107 
    108 sub show_restart_form {
    109    my $state = shift;
    110    print start_form,
    111          "Do you want to play again?",
    112          submit(-name => 'restart',
    113                 -value => 'Another game'),
    114          checkbox(-name => 'clear', -label => 'Clear history');
    115    delete $state->{WORD};
    116    #save_state($state);
    117    print end_form;
    118 }
    119 
    120 sub process_guess { # returns message and status
    121     my ( $guess, $state ) = @_;
    122 
    123     # lose immediately if user has no more guesses left
    124     #return ('', 'lost') unless $state->{GUESSES_LEFT} > 0;
    125     #return ('', 'lost') unless $state->{GUESSES_LEFT};
    126 
    127     my %guessed = map { $_ => 1 } $state->{GUESSED} =~ /(.)/g;
    128     my %letters = map { $_ => 1 } $state->{WORD}    =~ /(.)/g;
    129 
    130     # return immediately  if user has already guessed the word
    131     return ('', 'won') unless grep(!$guessed{$_}, keys %letters);
    132 
    133     # do nothing more if no guess
    134     return ('', 'continue') unless $guess;
    135 
    136     # this section processes individual letter guesses
    137     $guess = lc $guess;
    138     return ("Not a valid letter or word", 'error')
    139         unless $guess =~ /^[a-z]+$/;
    140     return ("You already guessed that letter", 'error')
    141         if $guessed{$guess};
    142 
    143     # this section is called when the user guesses the whole word
    144     if (length($guess) > 1 and $guess ne $state->{WORD}) {
    145         $state->{TOTAL} += $state->{GUESSES_LEFT};
    146         return (qq(You lose. The word was "$state->{WORD}".), 'lost');
    147     }
    148 
    149     # update the list of guesses
    150     $guessed{$_}++ foreach ($guess =~ /(.)/g);
    151     $state->{GUESSED} = join '', sort keys %guessed;
    152 
    153     # correct guess -- word completely filled in
    154     unless (grep(!$guessed{$_}, keys %letters)) {
    155         $state->{WON}++;
    156         return (qq(You won! The word was "$state->{WORD}".), 'won');
    157     }
    158 
    159     # incorrect guess
    160     if (!$letters{$guess}) {
    161         $state->{TOTAL}++;
    162         $state->{GUESSES_LEFT}--;
    163         # user out of turns
    164         return (qq(Sorry, you're out of turns. The word was "$state->{WORD}".),
    165             'lost') if $state->{GUESSES_LEFT} <= 0;
    166         # user still has some turns
    167         return ("Wrong guess", 'continue');
    168     }
    169 
    170     # correct guess but word still incomplete
    171     return ("Good guess.", 'continue');
    172 }
    173 
    174 sub status {
    175     my ( $message, $state ) = @_;
    176 
    177     # print the word with underscores replacing unguessed letters
    178     print table( { -width => '50%' },
    179         TR(
    180             td(b('Word #:'), $state->{GAMENO}),
    181             td(b('Guessed:'), $state->{GUESSED})
    182           ),
    183         TR(
    184             td(b('Won:'), $state->{WON}),
    185             td(b('Current average:'),
    186                 sprintf("%2.3f", $state->{TOTAL}/$state->{GAMENO})),
    187             td(b('Overall average:'),
    188                 $state->{GAMENO} > 1 ?
    189                     sprintf("%2.3f",
    190                         ($state->{TOTAL}-(TRIES-$state->{GUESSES_LEFT}))/
    191                             ($state->{GAMENO}-1))
    192                     : '0.000')
    193            )
    194     );
    195 
    196     my %guessed = map { $_ => 1 } $state->{GUESSED} =~ /(.)/g;
    197     print h2("Word:",
    198         map {$guessed{$_} ? $_ : '_'} $state->{WORD} =~ /(.)/g);
    199     print h2(font({-color => 'red'}, $message)) if $message;
    200 }
    201 
    202 # pick a word, any word
    203 sub pick_random_word {
    204    my $list = IO::File->new(WORDS)
    205       || die "Couldn't open ${\WORDS}: $!\n";
    206    my $word;
    207    rand($.) < 1 && ($word = $_) while <$list>;
    208    chomp $word;
    209    $word;
    210 }
    211 
    212 sub MAC {
    213     my ($state, $action) = @_;
    214     return unless ref( $state );
    215     my @fields = @{$state}{qw(WORD GUESSES_LEFT GUESSED GAMENO WON TOTAL)};
    216     my $newmac = MD5->hexhash(
    217         SECRET . MD5->hexhash( join '', SECRET, @fields )
    218     );
    219     return $state->{MAC} = $newmac if $action eq 'generate';
    220     return $newmac eq $state->{MAC} if $action eq 'check';
    221     return undef;
    222 }
    223 
    224 sub authentication_error {
    225     my $cookie = CGI::Cookie->new(-name => COOKIE_NAME, -value=>'',-expires => '-1d');
    226     print header(-cookie => $cookie),
    227           start_html(-title => 'Authentication Error'),
    228           h1(font({-color => 'red'}, 'Authentication Error')),
    229           p('This application was unable to confirm the integrity of the',
    230           'cookie that holds your current score.',
    231           'Please reload the page to start a fresh session.'),
    232           p('If the problem persists, contact the webmaster.');
    233     exit 0;
    234 }