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

guess_number3.cgi (6670B) - raw


      1 #!/usr/bin/perl
      2 
      3 use warnings;
      4 use strict;
      5 use CGI;
      6 use CGI::Cookie ();
      7 use MD5 ();
      8 
      9 use constant TRIES       => 6;
     10 use constant COOKIE_NAME => 'guess_number_3';
     11 use constant SECRET      => 'mySuperSecretSecret';
     12 my $game                 =  CGI->new;
     13 
     14 # state maintenance subroutines
     15 sub get_state {
     16     my %state = $game->cookie( COOKIE_NAME );
     17     return undef unless %state;
     18     authentication_error() unless cookie_check( \%state, 'check');
     19     return \%state;
     20 }
     21 
     22 sub initialize {
     23     my $state = shift;
     24     $state = {} unless $state;
     25     $state->{NUMBER}            = int( rand( 99 ) ) + 1;
     26     $state->{GAMENO}            += 1;
     27     $state->{GUESSES_LEFT}      = TRIES;
     28     $state->{WON}               += 0;
     29     $state->{GUESSED}           = '';
     30     return $state;
     31 }
     32 
     33 sub save_state {
     34     my $state = shift;
     35     cookie_check( $state, 'generate' );
     36     return CGI::Cookie->new (
     37         -name           => COOKIE_NAME,
     38         -value          => $state,
     39         -expires        => '+1M',
     40     );
     41 }
     42 
     43 # game play logic
     44 sub process_guess {
     45     my ( $guess, $state ) = @_;
     46 
     47     # process list of previous guesses
     48     my %guessed = map { $_ => 1 } split /,/, $state->{GUESSED};
     49 
     50     # do nothing if user did not enter a guess
     51     return ('', 'continue') unless $guess;
     52 
     53     # ensure guess is a valid number between 1 and 100
     54     return ("Not a valid guess.", 'error')
     55         unless $guess =~ m/^[1-9][0-9]?$|^100$/;
     56 
     57     # check if user already entered this guess previously
     58     return ("You already guessed that number.", 'error')
     59         if $guessed{ $guess };
     60 
     61     # we have a valid guess.  update list of guesses
     62     $guessed{ $guess } = 1;
     63     $state->{GUESSED} = join ',', sort keys %guessed;
     64 
     65     $state->{GUESSES_LEFT}--;
     66 
     67     # the order of the checks is important here.  At this point, it's
     68     # possible that the player has used their last guess.  Therefore,
     69     # we first check whether the guess is correct
     70 
     71     # guess is correct!
     72     if ( $guess == $state->{NUMBER} ) {
     73         $state->{WON}++;
     74         return ("Congratulations! You guessed that the correct number
     75             was $state->{NUMBER}!", 'won');
     76     }
     77 
     78     # if the guess is not correct AND we are out of guesses...
     79     return ("Sorry, you ran out of turns. The number was $state->{NUMBER}.",
     80         'lost') unless ($state->{GUESSES_LEFT} && $guess != $state->{NUMBER});
     81 
     82     # otherwise...
     83     # guess too low
     84     return ("Your guess is too low.", 'continue')
     85         if ( $guess < $state->{NUMBER} );
     86 
     87     # ...or
     88     # guess is too high
     89     return ("Your guess is too high.", 'continue')
     90         if ( $guess > $state->{NUMBER} );
     91 
     92 }
     93 
     94 # html generation subroutines
     95 sub print_header {
     96     my $state = shift;
     97     print $game->header(
     98         -type       => "text/html",
     99         -charset    => 'utf-8',
    100         -cookie     => save_state( $state ),
    101     );
    102     print $game->start_html(
    103         -title      => "Guess a Secret Number!",
    104         -onLoad     => 'if (document.gf) document.gf.guess.focus()'
    105     );
    106     print $game->h1( 'Guess a Secret Number Interactive Game' );
    107 }
    108 
    109 sub print_status {
    110     my ( $message, $state ) = @_;
    111 
    112     # draw table
    113     print
    114         $game->table( { -border=>"0", -width=>"50%" },
    115             $game->Tr(
    116                 $game->td( $game->b("Game #: "), $state->{GAMENO} ),
    117                 $game->td( $game->b("Wins: "),   $state->{WON} )
    118             ),
    119             $game->Tr(
    120                 $game->td( $game->b("Guesses Left: "),    $state->{GUESSES_LEFT} ),
    121                 $game->td( $game->b("Numbers Guessed: "), $state->{GUESSED} )
    122             )
    123         );
    124     print $game->h2( $game->font( {-color => 'red'}, $message ))
    125         if $message;
    126 }
    127 
    128 sub show_restart_form {
    129     my $state = shift;
    130     print $game->start_form();
    131     print $game->p(
    132         #-style => 'padding-right: 25px',
    133         'Do you want to play again?',
    134     );
    135     print $game->submit (
    136         -name  => 'restart',
    137         -value => 'Play again',
    138         #-style => 'padding-left: 25px; padding-right: 25px',
    139     );
    140     print $game->checkbox (
    141         -name  => 'clear',
    142         -label => 'Clear history',
    143     );
    144     print $game->end_form();
    145 }
    146 
    147 sub show_guess_form {
    148     my $state = shift;
    149     print $game->start_form( -name => 'gf' );
    150     print "Your guess: ";
    151     print $game->textfield (
    152         -inputmode     => 'numeric', # force number pad on mobile
    153         -pattern  => '[0-9]*',       # force number pad on mobile
    154         -name     => 'guess',
    155         -value    => '',
    156         -override => 1
    157     );
    158     print $game->submit (
    159         -name     => '_guess',
    160         -value    => 'Guess'
    161     );
    162     print $game->end_form();
    163 }
    164 
    165 sub print_footer {
    166     print $game->hr;
    167     print $game->p(
    168         'inspired by Hangman game from ',
    169         $game->a(
    170             { -href => "https://www.oreilly.com/library/view/writing-apache-modules/156592567X/"},
    171             $game->i( 'Writing Apache Modules with Perl and C')
    172         ),
    173         ' by Lincoln Stein and Doug MacEachern.'
    174     );
    175     print $game->end_html();
    176 }
    177 
    178 # message authentication checks
    179 sub cookie_check {
    180     my ($state, $action) = @_;
    181     return unless ref( $state );
    182     my @fields = @{$state}{qw(NUMBER GUESSES_LEFT GUESSED GAMENO WON)};
    183     my $newmac = MD5->hexhash(
    184         SECRET . MD5->hexhash( join '', SECRET, @fields )
    185     );
    186     return $state->{MAC} = $newmac if $action eq 'generate';
    187     return $newmac eq $state->{MAC} if $action eq 'check';
    188     return undef;
    189 }
    190 
    191 sub authentication_error {
    192     my $cookie = CGI::Cookie->new(-name => COOKIE_NAME, -value=>'',-expires => '-1d');
    193     print $game->header(-cookie => $cookie),
    194           $game->start_html(-title => 'Authentication Error'),
    195           $game->h1($game->font({-color => 'red'}, 'Authentication Error')),
    196           $game->p('This application was unable to confirm the integrity of the',
    197           'cookie that holds your current score.',
    198           'Please reload the page to start a fresh session.'),
    199           $game->p('If the problem persists, contact the webmaster.');
    200     exit 0;
    201 }
    202 
    203 # begin main program
    204 # retrieve current state
    205 my $state = get_state() unless $game->param( 'clear' );
    206 
    207 # initialize state if we are starting from the beginning (!$state)
    208 # or if we are playing another game ($game->param( 'restart' )).
    209 if ( !$state || $game->param( 'restart' ) ) {
    210     $state = initialize( $state );
    211 }
    212 
    213 # process current guess, if any
    214 my ( $message, $status ) =
    215     process_guess( $game->param( 'guess' ) || '', $state );
    216 
    217 # generate the HTML page
    218 print_header( $state );
    219 print_status( $message, $state );
    220 
    221 # prompt user
    222 if ( $status =~ /^(won|lost)$/ ) {
    223     show_restart_form( $state );
    224 } else {
    225     show_guess_form( $state );
    226 }
    227 
    228 print_footer();