Marco Marongiu
E' solo fortuna
http://www.perl.it/documenti/articoli/2006/09/e-solo-fortuna-1.html
© Perl Mongers Italia. Tutti i diritti riservati.


Vincitore Contest2005 Tutto è cominciato diversi mesi fa. Coi miei colleghi avevamo deciso di giocare al superenalotto, semplicemente scegliendo a caso delle colonne che contenessero tutti i numeri possibili. In questo articolo vedremo come Perl ci ha aiutato a scegliere le colonne, a controllare i risultati, e vedremo anche perché sarebbe stato inutile cercare di modificare la logica dei programmi cercando di massimizzare i punteggi delle estrazioni.

Premessa: come funziona il Superenalotto

Il Superenalotto è legato strettamente alle estrazioni del Lotto nazionale, che da tempo immemorabile avvengono in diverse città d'Italia. La combinazione vincente del Superenalotto si forma prendendo in considerazione i numeri primi estratti al Lotto sulle Ruote di Bari, Firenze, Milano, Napoli, Palermo, Roma (in quest'ordine). Ovviamente, nel caso in cui lo stesso numero venisse estratto per primo su due ruote, si tiene il primo numero della prima ruota e si prende il secondo estratto della seconda. Se ci fosse ancora una sovrapposizione si procederebbe allo stesso modo fino ad ottenere sei numeri diversi.

C'è un settimo numero, detto "numero jolly", che è il primo estratto della ruota di Venezia. Questo numero serve per la composizione del premio "5+1", che si ottiene quando si indovinano cinque numeri fra i primi estratti, più il numero Jolly.

Primo problema: la scelta delle colonne

Il primo problema da risolvere era la scelta casuale delle colonne. Ok, questo era facile e bastava un one-liner scritto in Perl. Eccolo:

perl -e '@n = (1..90) ; my @colonne ; while (@n) { my @colonna ; for (1..6) { push @colonna,splice(@n,rand($#n+1),1) } ; push @colonne,\@colonna } ; foreach my $c (@colonne) { print qq(@$c\n) }'

Una linea abbastanza lunga, è vero, ma tutto sommato comprensibile. Si parte con un array @n che contiene tutti i numeri da 1 a 90, e un array @colonne inizialmente vuoto. Poi, in un ciclo while che procede finché in @n rimangono elementi, estraiamo (letteralmente!) a caso sei numeri da @n (usando splice per pescare dall'array in posizione arbitraria) e li mettiamo dentro un array temporaneo @colonna, che una volta completato viene aggiunto a @colonne usando push. Estratti tutti i numeri bisognava stamparli, e questo è ciò che fa il semplice ciclo foreach alla fine della linea.

Secondo problema: la verifica dei risultati

Il problema della scelta delle colonne era risolto. Ora bisognava risolverne un altro: quello della verifica dei risultati con le colonne estratte. Anche in questo caso Perl viene in aiuto.

#!/usr/bin/perl

use strict ;
use warnings ;

die "Uso: $0 n1 n2 n3 n4 n5 n6 jolly\n\n" unless @ARGV == 7 ;

#######################################################################
my $colonne = 'colonne.txt' ;
#######################################################################

my @estratti = @ARGV[0..5] ;
my $jolly = $ARGV[6] ;

print "-"x72,"\n","Colonna vincente: @estratti, jolly $jolly\n","-"x72,"\n" ;

open COL,$colonne or die "Cannot read $colonne: $!" ;
while (local $_ = <COL>) {
 chomp ;
 my $col = $_ ;
 my @numbers = split ;
 my $count = 0 ;
 foreach my $number (@numbers) {
   $count++ if grep $number == $_ ,@estratti
 }

 my $match_points = $count == 1? 'punto': 'punti' ;
 $match_points .= '!'x$count if $count >= 3 ;
 if ($count == 5) {
   $count = '5 + 1' if grep $jolly == $_,@numbers ;
 }
 printf "Colonna %2u %2u %2u %2u %2u %2u: %s %s\n",@numbers,$count,$match_points ;
}
close COL ;

Anche questo è un programma estremamente semplice, privo di qualsiasi generalizzazione che lascio per esercizio al lettore.
Lo script si aspetta di trovare un file colonne.txt nella directory corrente e sette numeri estratti sulla linea di comando (i sei primi estratti e il numero jolly). In output fornisce le colonne e il punteggio risultante per ciascuna di esse. Per esempio:

bronto@marmotta:~/Desktop$ perl enalocal.pl 1 2 3 4 5 6 7
------------------------------------------------------------------------
Colonna vincente: 1 2 3 4 5 6, jolly 7
------------------------------------------------------------------------
Colonna 70 88  3 28 18 45: 1 punto
Colonna 35 54 66 71 20 21: 0 punti
Colonna 65 76  8 42 16 79: 0 punti
Colonna 85 11 64 31 68 77: 0 punti
Colonna  6 33 36 80 29 55: 1 punto
Colonna 23 44 87 43  1 30: 1 punto
Colonna  5 89 82 74 58 60: 1 punto
Colonna  7 57  9 46 52 47: 0 punti
Colonna 49 59 81 56 73 39: 0 punti
Colonna 90 12 27 25 48 84: 0 punti
Colonna 67 13 26 19  4 62: 1 punto
Colonna 51 38 17 50 32  2: 1 punto
Colonna 75 41 40 10 69 78: 0 punti
Colonna 72 15 63 61 86 37: 0 punti
Colonna 22 53 34 83 24 14: 0 punti

Il programma sa ovviamente discernere fra il 6 e il 5+1:

bronto@marmotta:~/Desktop$ perl enalocal.pl 7 57  9 46 52 45 47
------------------------------------------------------------------------
Colonna vincente: 7 57 9 46 52 45, jolly 47
------------------------------------------------------------------------
Colonna 70 88  3 28 18 45: 1 punto
Colonna 35 54 66 71 20 21: 0 punti
Colonna 65 76  8 42 16 79: 0 punti
Colonna 85 11 64 31 68 77: 0 punti
Colonna  6 33 36 80 29 55: 0 punti
Colonna 23 44 87 43  1 30: 0 punti
Colonna  5 89 82 74 58 60: 0 punti
Colonna  7 57  9 46 52 47: 5 + 1 punti!!!!!
Colonna 49 59 81 56 73 39: 0 punti
Colonna 90 12 27 25 48 84: 0 punti
Colonna 67 13 26 19  4 62: 0 punti
Colonna 51 38 17 50 32  2: 0 punti
Colonna 75 41 40 10 69 78: 0 punti
Colonna 72 15 63 61 86 37: 0 punti
Colonna 22 53 34 83 24 14: 0 punti
bronto@marmotta:~/Desktop$ perl enalocal.pl 7 57  9 46 52 47 48
------------------------------------------------------------------------
Colonna vincente: 7 57 9 46 52 47, jolly 48
------------------------------------------------------------------------
Colonna 70 88  3 28 18 45: 0 punti
Colonna 35 54 66 71 20 21: 0 punti
Colonna 65 76  8 42 16 79: 0 punti
Colonna 85 11 64 31 68 77: 0 punti
Colonna  6 33 36 80 29 55: 0 punti
Colonna 23 44 87 43  1 30: 0 punti
Colonna  5 89 82 74 58 60: 0 punti
Colonna  7 57  9 46 52 47: 6 punti!!!!!!
Colonna 49 59 81 56 73 39: 0 punti
Colonna 90 12 27 25 48 84: 0 punti
Colonna 67 13 26 19  4 62: 0 punti
Colonna 51 38 17 50 32  2: 0 punti
Colonna 75 41 40 10 69 78: 0 punti
Colonna 72 15 63 61 86 37: 0 punti
Colonna 22 53 34 83 24 14: 0 punti

Era già un buon risultato ma mancava di automazione; infatti è necessario inserire a mano i numeri estratti per poter ottenere la valutazione del punteggio conseguito. Perché non approfittare della presenza su Internet di diversi siti che pubblicano i risultati del Superenalotto? Perché non fare in modo di ricevere nella propria casella di posta elettronica i risultati dell'estrazione?

Terzo problema: la verifica automatica

Una volta scelta la pagina web da cui prelevare i risultati e individuato il modo migliore per estrarne i dati, le modifiche da fare allo script erano davvero poche:

#!/usr/bin/perl

use strict ;
use warnings ;

use LWP::UserAgent ;

#######################################################################
my $url = 'http://www.corriere.it/giochiepronostici/Corriere_superenalotto.shtml' ;
my $colonne = '/etc/local/enalotto/colonne.txt' ;
my @proxy_conf = ('http','http://proxy:3128/') ;
my $minutes_before_retrying_connection = 5 ;
my $max_attempts = 5 ;
#######################################################################
my $retry = 60 * $minutes_before_retrying_connection ;

my $ua = LWP::UserAgent->new(timeout => 5) ;
$ua->proxy(@proxy_conf) ;

my $page ;
my $attempts = 0 ;
until ($page = $ua->get($url)->as_string) {
 if ($attempts++ > $max_attempts) {
   die "Tried to get results $max_attempts times, giving up\n" ;
 }

 print STDERR "Cannot get $url, sleeping $retry seconds...\n" ;
 sleep $retry ;
}

my @estratti = ( $page =~ m|<td class="number" >(\d+)</td>|g ) ;
my $jolly = pop @estratti ;

print "-"x72,"\n","Colonna vincente: @estratti, jolly $jolly\n","-"x72,"\n" ;

open COL,$colonne or die "Cannot read $colonne: $!" ;
while (local $_ = <COL>) {
 chomp ;
 my $col = $_ ;
 my @numbers = split ;
 my $count = 0 ;
 foreach my $number (@numbers) {
   $count++ if grep $number == $_ ,@estratti
 }

 my $match_points = $count == 1? 'punto': 'punti' ;
 $match_points .= '!'x$count if $count >= 3 ;
 if ($count == 5) {
   $count = '5 + 1' if grep $jolly == $_,@numbers ;
 }
 printf "Colonna %2u %2u %2u %2u %2u %2u: %s %s\n",@numbers,$count,$match_points ;
}
close COL ;

Se notate, questo programma differisce dal precedente solo per dettagli, e precisamente:

  • stiamo utilizzando LWP::UserAgent per andare a prendere i dati
  • abbiamo aggiunto alle variabili di configurazione dello script l'indirizzo della pagina web che contiene i risultati, e le informazioni per l'utilizzo di un proxy http
  • il file colonne.txt ora si trova in una directory ben precisa
  • sono definite delle variabili che indicano al programma quante volte tentare di scaricare i dati se la pagina non è disponibile, e quanti minuti aspettare fra un tentativo e l'altro.

Lo script comincia così con un ciclo nel quale si tenta di scaricare i dati; tutta la pagina web viene inserita nella variabile $page e, con un'espressione regolare, si ricavano i numeri estratti (per meglio comprendere l'espressione regolare potete fare riferimento al sorgente della pagina web). Il resto del programma è identico al precedente.

Questo programma può essere eseguito automaticamente la mattina dopo l'estrazione dei numeri, e i risultati spediti alle persone interessate. In ufficio lo abbiamo messo in crontab su una macchina Linux in questo modo:

30 8 * * 3,5,7  root    /etc/local/enalotto/enalotto.pl | mailx -s Estrazione enalotto

Lo script viene eseguito il mercoledì, il venerdì e la domenica alle 8:30 del mattino. L'opzione -s di mailx indica l'oggetto (subject) del messaggio (in questo caso: Estrazione), mentre "enalotto" è l'indirizzo a cui spedire il messaggio. Ovviamente "enalotto" è un alias che corrisponde a tutti gli indirizzi delle persone che vogliono ricevere le notifiche via e-mail.

E ora, i fuochi artificiali

Mancava ancora una cosa per i palati più difficili: una pagina web per quelli che non volevano ricevere le notifiche per posta. Nessun problema: ancora una volta ho usato Perl e un tool che mi piace molto: AxKit.

Cos'è AxKit? In due parole, è un server XML per Apache v1.3, basato su mod_perl. AxKit inserisce in Apache un motore di generazione e trasformazione di XML. Tramite AxKit, ad esempio, è possibile estrarre dati da un database, comporli in un documento XML e trasformarli mediante fogli di stile (scritti nello standard XSLT o con XPathScript, un linguaggio di trasformazione basato su Perl) in qualunque altro formato: HTML, PDF, DocBook, testo semplice... Maggiori informazioni su AxKit e sulle sue potenzialità si trovano sul sito http://www.axkit.org/ e sul bellissimo libro "XML Publishing with AxKit" edito da O'Reilly & Associates (cfr.: http://www.oreilly.com/catalog/xmlaxkit/index.html) e scritto da Kip Hampton, uno degli sviluppatori del progetto.

Avevo già un sito interno dell'azienda basato su AxKit, quindi avevo già pronti i fogli di stile per trasformare un certo formato XML in una pagina web. Quello che rimaneva da fare era produrre dinamicamente un documento XML che poi AxKit avrebbe trasformato in HTML e "trasmesso" ai browser.

Per la generazione dinamica di XML AxKit mette a disposizione le XSP (eXtensible Server Pages), una specifica originariamente introdotta nell'ambito del progetto Cocoon (http://cocoon.apache.org/). La differenza fra Cocoon e AxKit è che il linguaggio "embedded" nei file XSP è Java nel primo caso e Perl nell'altro.

Per ottenere una pagina con i risultati delle estrazioni dovevo solo adattare e "immergere" il codice già esistente in una pagina XSP. Anche questo non è stato troppo difficile e il codice risultante è il seguente:





  
<xsp:page xmlns:xsp="http://apache.org/xsp/core/v1">
  <xsp:structure>
    <xsp:import>LWP::UserAgent</xsp:import>
  </xsp:structure>
  
  <xsp:logic><![CDATA[
    my $url = 'http://www.corriere.it/giochiepronostici/Corriere_superenalotto.shtml' ;
    my $colonne = '/etc/local/enalotto/colonne.txt' ;
    my $ua = LWP::UserAgent->new(timeout => 5) ;
    $ua->proxy('http','http://proxy:3128') ;
    my %bgcolor = ('0'     => '#ffffff',
                   '1'     => '#ccffcc',
                   '2'     => '#ccff66',
                   '3'     => '#ffff66',
                   '4'     => '#ffcc33',
                   '5'     => '#ff9900',
                   '6'     => '#ff3333',
                   '5 + 1' => '#ff3333',) ;
  ]]></xsp:logic>
  
<bs>
  <meta>
    <title>Finché c'è Superenalotto c'è speranza</title>
    <author>Marco Marongiu</author>
    <published>28 Febbraio 2005</published>
    <lastmod>2 Marzo 2005</lastmod>
  </meta>
  
  <section>
    <title>Ultima estrazione</title>
    <table class="panel" cellspacing="0" cellpadding="3">
      <xsp:logic><![CDATA[
        my $page = $ua->get($url)->as_string ;
        my @estratti = ( $page =~ m|<td class="number" >(\d+)</td>|g ) ;
        my $jolly = pop @estratti ;
      ]]></xsp:logic>
  
      <tr bgcolor="#e5e9f0">
        <th colspan="7">Colonna vincente:
          <xsp:expr>join(" ",@estratti)</xsp:expr>,
          jolly: <xsp:expr>$jolly</xsp:expr></th>
      </tr>
  
      <xsp:logic><![CDATA[
        open COL,$colonne or die "Cannot read $colonne: $!" ;
  
        while (local $_ = <COL>) {
        ]]>
        <xsp:element name="tr">
          <xsp:logic><![CDATA[
          chomp ;
          my $col = $_ ;
          my @numbers = split ;
          my $count = 0 ;
          foreach my $number (@numbers) {
          ]]></xsp:logic>
          <xsp:element name="td">
            <xsp:attribute name="align">center</xsp:attribute>
            <xsp:logic>
            if ($number  $jolly) {
              border-color: #ff0000 ; border-width: 3px ;
            } elsif (grep $number  $_ ,@estratti) {
              $count++ ;
              <xsp:attribute name="bgcolor">#ffff00</xsp:attribute>
            }
            </xsp:logic>
            <xsp:expr>$number</xsp:expr>
          </xsp:element>
          <xsp:logic><![CDATA[
          }
  
          my $match_points = $count  1? 'punto': 'punti' ;
          $match_points .= '!'x$count if $count >= 3 ;
          if ($count  5) {
            $count = '5 + 1' if grep $jolly == $_,@numbers ;
          }
          ]]></xsp:logic>
          <xsp:element name="td">
            <xsp:attribute name="align">right</xsp:attribute>
            <xsp:attribute name="bgcolor"><xsp:expr>$bgcolor{"$count"}</xsp:expr></xsp:attribute>
            <xsp:expr>$count." ".$match_points</xsp:expr>
          </xsp:element>
        </xsp:element>
        }
        close COL ;
      </xsp:logic>
    </table>
  </section>
  
  <section>
      <title>La scala della felicità</title>
    <table class="panel" cellspacing="0" cellpadding="3">
      <tr>
        <xsp:logic>
          my $perc = int(100/keys(%bgcolor)) ;
          foreach my $k (sort keys %bgcolor) {
            <xsp:element name="td">
              <xsp:attribute name="align">center</xsp:attribute>
              <xsp:attribute name="width"><xsp:expr>$perc</xsp:expr>%</xsp:attribute>
              <xsp:attribute name="bgcolor"><xsp:expr>$bgcolor{"$k"}</xsp:expr></xsp:attribute>
              <xsp:expr>$k</xsp:expr>
            </xsp:element>
          }
        </xsp:logic>
      </tr>
    </table>
  
  </section>
</bs>
</xsp:page>

Tralascio una descrizione approfondita di questo codice perché richiederebbe una lunga digressione su XML in generale e XSP in particolare. Mi limiterò a dire che questa pagina produce codice XML al cui interno si trova una tabella HTML; successivamente questo codice viene processato con un foglio di stile in XPathScript che lo trasforma in HTML, al quale viene applicato un comune foglio di stile CSS. Il risultato finale è quello illustrato in figura (opportunamente offuscata per non dare troppe indicazioni né ai giocatori scaramantici né al cracker di turno).

Si noti inoltre che i numeri estratti vengono evidenziati in giallo, il numero jolly viene cerchiato in rosso e il punteggio totale della colonna viene evidenziato in un colore diverso a seconda del valore.

Repetita non juvant

Come sempre accade in questi casi, qualcuno si è posto il dubbio se, per caso, le colonne potessero essere scelte in base a qualche legge statistica per aumentare le probabilità di successo. La risposta ovviamente è stata: "NO, tutte le colonne sono equiprobabili". Se non siete convinti, tentiamo una spiegazione semplice.

Giocate a tombola con gli amici, e avete un sacco con 90 numeri ben mescolati. Chiedetevi: che probabilità c'è che alla prima estrazione venga fuori il numero 1? Ovviamente: 1/90. Finita la prima partita ne seguirà un'altra, con il sacco di nuovo pieno e un con i numeri opportunamente mescolati. Fatevi nuovamente la stessa domanda, e la risposta sarà chiaramente la stessa: 1/90, sia che il numero 1 sia effettivamente stato estratto per primo la volta precedente, sia che sia stato estratto un numero diverso. Questo accade perché ogni partita, ogni estrazione, ha una storia a sé che non è influenzata dalle estrazioni precedenti e l'evento che state osservando è completamente casuale.

Potreste essere ancora scettici: in questo caso vi raccomando la lettura dell'interessante saggio-articolo di Adam Atkinson (http://www.ghira.mistral.co.uk/ritardi.ps) dal titolo "FAQ sull'inutilità dei ritardi nel lotto (e altrove)". Atkinson spiega con un linguaggio informale e con rigore matematico insieme perché tutte quelle sedicenti teorie sui ritardi siano in realtà delle baggianate senza senso. Buona lettura e in bocca al lupo!