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 }