Luca Dante Ortolani
Dodo' Breaker - Giocare con Perl
http://www.perl.it/documenti/articoli/2005/06/dodo-breaker-gi.html
© Perl Mongers Italia. Tutti i diritti riservati.

Qualche mese fa mi feci un bel regalo comprandomi un Pocket PC. Quando però, per caso, mia moglie scoprì che al suo interno c'erano anche dei giochi, iniziai a perderne il possesso! In particolare fu attratta da "Jawbreaker" un giochino di quelli "scacciapensieri" nel quale lo scopo è quello di ripulire lo schermo cliccando su palline colorate dello stesso colore, purché vicine orizzontalmente o verticalmente.

All'inizio sembrava facile e anche un po' stupido come gioco, ma quello che lo rese interessante fu il fatto che per poter realizzare più punti (vero scopo del gioco) occorreva eliminare maggiori quantità di palline con un solo click. Ciò è possibile combinando insieme dei "serpentoni" di palline dello stesso colore, che aumentano così in modo esponenziale il punteggio realizzabile.

Ora, siccome rivolevo il mio palmare :), pensai di riscrivere il giochino. E con Perl e il Web il tutto è stato un vero "gioco". E' possibile visualizzare o se si vuole anche giocare con "dodobreaker" (così l'ho chiamato) su http://www.isogest.org/cgi-bin/dodobreaker .

dodo.png Nel caso non si conosca il gioco, si consiglia qualche partita prima di continuare a leggere.

Come funziona

Volendo realizzare il tutto in modo semplice ma soprattutto veloce, mi sono tenuto lontano da interfacce grafiche ed ho optato per una soluzione web-based. Così grazie a Perl per l'elaborazione, l'Html per la presentazione e i Cookie per la registrazione della posizione delle palline sulla maschera e del punteggio realizzato è stato possibile in poche righe assemblare il tutto.

Iniziamo a definire l'ambiente

Come si è detto dodobreaker è in Perl, quindi subito indichiamo l'amata shebang e richiamiamo il modulo CGI con i suoi "metodi standard". Tale modulo infatti permette di gestire e creare in modo semplice e veloce pagine html dinamiche, proprio quello che occorre al nostro progetto.

Ovviamente il tutto per funzionare dovrà girare in un completo sistema client/server web, vale a dire che lo script dovrà essere eseguito lato server da un web-server, presumibilmente Apache, e lato client il giocatore dovrà utilizzare un normale browser con il quale raggiungere il web-server.

Inseriti gli opportuni richiami alla shebang e al modulo CGI, definiamo alcune variabili lessicali che saranno visibili nell'intero script e che impostano la grandezza della griglia (così chiameremo lo schema grafico del gioco), il percorso per rintracciare le immagini che costituiranno le palline e le variabili per il punteggio e la posizione delle palline.

#!/usr/bin/perl
use CGI qw/:standard/;
use strict;

my $max_x = 11;
my $max_y = 12;
my $images = '/images/';
my $points;
my $schema;
my @balls;

Il Work Flow

Si è suddiviso lo script in procedure per meglio far comprendere il flusso dell'esecuzione che si alternerà tra un click dell'utente e l'altro. Difatti trattandosi di uno script cgi, dobbiamo entrare nell'ottica che ad ogni click dell'utente lo script dovrà rielaborare tutto il gioco eseguendo il flusso sotto descritto.

mk_grid();      ## Crea una nuova griglia o la ricostruisce se gia' esiste
delete_balls(); ## Cancella dalla griglia le palline selezionate dall'utente
move_balls();   ## Rielabora la griglia muovendo le palline in basso a destra
print_grid();   ## Salva la posizione delle palline e visualizza la griglia

Crea una nuova griglia o la ricostruisce se già esiste

La griglia contenente la posizione e il colore di ogni pallina è definita all'interno dell'array multidimensionale @ball il quale ha come indici di riferimento rispettivamente la posizione x e y della pallina, e 0 o 1 a seconda se si vuol ottenere il valore del colore della pallina o del punteggio ottenuto cliccando sul di essa.

Come si vede dal codice la prima operazione è la verifica che lo script non sia chiamato con un "action" uguale a "new" il che creerebbe una nuova maschera per il gioco. Dopodiché estrae lo schema e il punteggio dai cookie "dodoschema" e "dodopoint", e popola l'array @ball.

sub mk_grid
 {
  my ($x, $y, $cnt);
  if (param('action') ne "new")
   {
    $points = cookie('dodopoint');
    $schema = cookie('dodoschema');
   }
  else
   { $points = 0 }

  for $x (1..$max_x)
   {
    for $y (1..$max_y)
     {
      $balls[$x][$y][0] = $schema ? substr($schema,$cnt++,1) : int(rand(5))+1;
     }
   }
 }

Cancella dalla griglia le palline selezionate dall'utente

Ogni click del giocatore su una pallina richiama lo stesso script ma con i parametri "x" e "y" valorizzati dalla posizione della pallina che si vuol eliminare. Con questi parametri lo script, grazie alla sub get_snake, intercetta la posizione di tutte le palline confinanti con la pallina selezionata e ne cambia i valori all'interno dell'array @ball, mettendo a 0 sia il colore che il punteggio.

Da notare la formula $points += int((2**$cnt_ball)/2); che somma al punteggio ulteriori punti calcolati in modo esponenziale in funzione del numero delle palline eliminate.

sub delete_balls
 {
  my ($x, $y);
  my $cnt_ball;
  return if !param('x') || !param('y');
  for ( get_snake(param('x'),param('y')) )
   {
    ($x,$y) = split /\,/, $_;
    $balls[$x][$y][0] = $balls[$x][$y][1] = '0';
    $cnt_ball++
   }
  $points += int((2**$cnt_ball)/2);
 }

Rielabora la griglia muovendo le palline in basso a destra

Ad ogni eliminazione delle palline selezionate dall'utente, per effetto della gravità dobbiamo translare tutte le palline sovrastanti verso il basso. Non solo, il gioco originale oltre a spostarle verso il basso, le spinge verso destra in modo da non lasciare spazi vuoti tra esse.

La procedura seguente è suddivisa in due cicli, nella prima spostiamo tutte le palle verso il basso, e nella seconda le spostiamo verso destra, ma in realtà la logica applicata è la stessa per entrambe le parti.

Il tutto si svolge molto semplicemente esaminado la griglia dal basso verso l'altro nella prima parte, e da destra verso sinistra nella seconda, se ci sono spazi vuoti seguiti da spazi pieni. Nel caso si verifichi questa situazione lo script sposta le palline semplicemente manipolando il contenuto di @balls.

sub move_balls
 {
  my ($x, $y, $k);

  ## move down
  for $y (1..$max_y)
   {
    for ($x=$max_x; $x > 0; $x--)
     {
      next if $balls[$x][$y][0] > 0;
      for ($k=$x; $k > 0; $k--)
       {
        next if $balls[$k][$y][0] == 0;
        $balls[$x][$y][0] = $balls[$k][$y][0];
        $balls[$k][$y][0] = 0;
        last;
       }
     }
   }

  ## move right
  for ($y=$max_y; $y > 0; $y--)
   {
    next if $balls[$max_x][$y][0] > 0;
    for ($k=$y; $k > 0; $k--)
     {
      next if $balls[$max_x][$k][0] == 0;
      for (1..$max_x)
       {
        $balls[$_][$y][0] = $balls[$_][$k][0];
        $balls[$_][$y][1] = $balls[$_][$k][1];
        $balls[$_][$k][0] = $balls[$_][$k][1] = 0;
       } 
      last;
     }
   }
 }

Salva la posizione delle palline e visualizza la griglia

Siamo giunti alla fine dello script, abbiamo la nuova griglia da mostrare all'utente, dobbiamo solo farne il rendering in html e salvarla per poterla riutilizzare nella sessione successiva al click dell'utente. Notoriamente il sistema più pratico per mantenere informazioni tra sessioni http sono i cookie. Con questi lo sviluppatore può salvare informazioni sul client del visitatore e richiamarle ad ogni successiva sessione. Lo script quindi salverà in un cookie l'intero schema della griglia aggiornandolo ad ogni click dell'utente.

Abbiamo detto che le informazioni inerenti la griglia sono contenute nell'array @ball, sarà infatti il valore di $balls[x][y][1] a definire il colore delle palline semplicemente richiamando il nome del file delle immagini delle palline da visualizzare. Abbiamo infatti chiamato le immagini delle palline con nomi tipo a1.gif, a2.gif, a3.gif etc in modo che il valore di $balls[x][y][1] sostitusca quel numero che segue la lettera "a".

Si noti nello script la definizione dell'attributo "alt" del tag "img" che permetterà all'utente spostando il mouse, di visualizzare il punteggio che otterrà cliccando su una determinata pallina piuttosto che su un'altra.

sub print_grid
 {
  my ($html, $x, $y, $cookie1, $cookie2);
  my $gameover = "Impossibile continuare! inizia una nuova partita."; 
  $schema = '';

  for $x (1..$max_x)
   {
    for $y (1..$max_y)
     {
      $balls[$x][$y][1] = (get_snake($x,$y))**2;
      if ( $balls[$x][$y][1] == 1 )
       {
        $html .= "<td width=30 height=30>
                   <img src=\"${images}a$balls[$x][$y][0].gif\" border=0>
                  </td>";
       }
      elsif ($balls[$x][$y][1]>1)
       {
        $html.="<td width=30 height=30>
                 <a href=\"dodobreaker?x=$x&y=$y\">
                 <img src=\"${images}a$balls[$x][$y][0].gif\"
                      alt=\"$balls[$x][$y][1]\" border=0>
                 </a>
                </td>";
        $gameover = '';
       }
      else
       {
        $html.='<td width=30 height=30>&nbsp;</td>';
       }
      $schema .= $balls[$x][$y][0] || '0';
     }
    $html.='</tr>';
   }

  $cookie1 = cookie(-name=>'dodoschema', -value=>$schema);
  $cookie2 = cookie(-name=>'dodopoint', -value=>$points);

  print header(-cookie=>[$cookie1,$cookie2],-expires=>'-1d'),
                       start_html('DoDo/' Breaker'),
                       h1("DoDo' Breaker"),
                       h3($gameover ? $gameover : "Punteggio: $points");

  print "<table border=1>
           <tr><td><table border=0 cellspacing=1 cellpadding=0>
           $html
           </table></td></tr></table>
           <a href=\"dodobreaker?action=new\">[Nuova Partita]</a>
           <a href=\"/docs/dodobreaker\">[Perl Code]</a>",
           end_html;
 }

Procedure interne

Le procedure sopra descritte hanno bisogno di due piccole procedure "get_nearest" e "get_snake".

Cerca le coordinate delle palline vicino la posizione selezionata

Molto semplicemente questa procedura restituisce un array contenente la posizione x/y delle palline vicine ad una determinata pallina. Vale a dire la posizione delle pallina superiore e inferiore e quella destra e sinistra di una determinata coordinata, escludendo ovviamente quelle posizioni non facenti parte della griglia esistente.

sub get_nearest
 {
  my ($x,$y) = @_;
  my @pos = ();
  push @pos, "$x,".($y-1) if $y>1;
  push @pos, "$x,".($y+1) if $y < $max_y;
  push @pos, ($x-1).",$y" if $x>1;
  push @pos, ($x+1).",$y" if $x < $max_x;
  return @pos;
 }

Restituisce la posizione del serpentone

Il nome di questa procedura ne fa comprendere lo scopo. Per il calcolo del punteggio o anche l'eliminazione delle palline dello stesso colore e vicine alla pallina selezionata, abbiamo bisogno delle relative coordinate. Questa procedura restituisce un array contenente le coordinate x e y, di tutte le palline facenti parte del serpentone dello stesso colore, che si eliminerà cliccando su una determinata pallina.

sub get_snake
 {
  my ($x,$y) = @_;
  return() if !$balls[$x][$y][0];
  my @pos = ("$x,$y");
  my $dejavu = "$x,$y-";
  for (@pos)
   {
    my ($k,$j) = split /\,/,$_; 
    for (get_nearest($k,$j))
     {
      my ($w,$z) = split /\,/ , $_;
      push @pos, $_ 
        if $balls[$k][$j][0] == $balls[$w][$z][0] && $dejavu !~ /\Q$_/;
      $dejavu.="$_-";
     }
   }
  return @pos;
 }

Ottimizzare lo script

Nonostante la sua semplicità, lo script presentato offre la possibilità di provare il funzionamento di molteplici aspetti che riguardano la programmazione in Perl e in special modo la programmazione in un ambiente web-based. Difatti in poche righe di codice abbiamo potuto vedere sia come Perl interagisce con l'ambiente CGI ma anche come conservare informazioni tra sessioni web.

Gli aspetti che mancano e che lasciamo alla fantasia del lettore potrebbero riguardare:

  • Una migliore suddivisione tra logica e presentazione, magari attraverso l'uso di un semplice sistema di templating, che esegua il rendering della griglia e la inserisca in una pagina html predefinita;
  • La gestione di un "High scores" con i risultati migliori raggiunti dagli utenti;
  • Un sistema che intercetti la pressione del tasto "Back" da parte dell'utente, e che ne renda vane le conseguenze;
  • In ultimo sarebbe interessante rielaborare il gioco in modo che possa essere eseguito con mod_perl sia per ottimizzarne le prestazioni ma anche per gestire lo schema di gioco in maniera persistente.

Buon divertimento!