01 - Programmazione01a - RPG

Tributo RPG-5250 a John Conway – Game of Life

Last Updated on 13 Aprile 2020 by Roberto De Pedrini

Un altro effetto di questo maledetto virus: due giorni fa, 11 Aprile 2020, ci ha lasciato anche John Horton Conway, ennesima vittima del Covid-19, matematico e autore di giochi matematici, professore alla Princeton University (New Jersey), conosciuto ai più come inventore del gioco “The Game of Life” … anche se egli stesso non amasse essere riconosciuto come “quello di The Game of Life” (vedi questo video su Youtube https://www.youtube.com/watch?v=E8kUJL04ELA ).

Il Covid-19, questo piccolo bastardo, ci sta portando via una intera generazione che ha costruito, nel bene e nel male, il mondo attuale in cui stiamo vivendo: che sia un famoso matematico con i suoi giochi per Nerd e Cervelloni, o un Medico rientrato dalla pensione per mettersi ancora in corsia o la nostra cara nonna/nonno/padre/madre è sempre una perdita per noi e per tutta l’umanità e quelli che dicono che sono solo gli anziani e malati ad andarsene fanno un grande errore: se ne va una generazione di persone, di uomini, di donne, che sono stati professori, dottori, operai e casalinghe ma , soprattutto, padri, madri, mariti, mogli, amanti e amici di un tempo che fu.

Per questo ho voluto fare un tributo a John Conway e al suo famoso “The Game of Life”, facendolo in RPG (The Game of Life – RPG and 5250 Version – GAMEOFLF.SQLRPGLE), con i limiti del 5250 e con delle tecniche discutibili, sicuramente migliorabili anche in RPG,

Ho praticamente passato tutto il giorno di Pasqua, tra giardino e computer, e ho messo insieme questo piccolo programma che, simula il gioco “The Game Of Life” leggendo al configurazione iniziale (pattern iniziale) da un file di testo in IFS scritto con le regole RLE Run Length Encoded (vedi sotto).

Sorgenti RPG, DSPF, SQL e RLE sono disponibili in questo Github Gist: The Game of Life – SQLRPGLE and 5250 Tribute to John Conway

Il risultato l’ho messo in una GIF animata e in un video che trovate verso la fine del post: ha i suoi limiti, oltre ai 27 x 132 caratteri del 5250 si aggiungono i limiti di un programmatore RPG nel giorno di Pasqua dopo il classico pranzo con un ottimo Prosecco di accompagnamento. Sì, si poteva fare meglio, è vero … ma potevo anche starmene sul divano con Netflix e Spotify!


Vediamolo nel dettaglio … al di là del problema “grafica” e 5250 il progetto ha visto anche delle “sfide” interessanti: leggere un file di testo da IFS, splittare delle stringhe di testo con SQL, interpretare un linguaggio nuovo (RLE) … tutto con RPG e SQL: sono comunque benvenuti consigli e miglioramenti … vi aspetto qui sotto nei commenti!

(1) Regole del gioco “The Game Of Life”

Come leggiamo da Wikipedia il gioco “The Game Of Life” si basa su poche e ben definite regole:

  • Si tratta di un “Automa Cellulare”
  • Si comporta un po’ come le cellule nella vita… probabilmente anche il nostro incubo del momento Corona Virus Covid-19 ha un funzionamento abbastanza simile!
  • Possiamo pensarlo come una “Macchina di Turing Universale MTU”
  • E’ un gioco senza giocatori … non possiamo neanche pensare di passarci del tempo una volta che rientriamo dalla quarantena del Covid-19 nei nostri uffici.

Ma veniamo alle regole … considerando che il gioco si appoggia su una griglia-matrice di celle, detto “Mondo”, con dimensione teoricamente infinita:

  • Una cella ha 8 celle vicine, tutto intorno a se
  • Una cella si può trovare in due stati: VIVA o MORTA (on o off)
  • Lo stato della griglia evolve in intervalli di tempo discreti … tutta insieme … in un solo colpo … passando alla “generazione successiva”
  • Lo stato della cella dipende dallo stato in cui è e dallo stato delle sue 8 celle “vicine”
  • 1) Qualsiasi cella viva con meno di due celle vive adiacenti muore, come per effetto d’isolamento;
  • 2) Qualsiasi cella viva con due o tre celle vive adiacenti sopravvive alla generazione successiva;
  • 3) Qualsiasi cella viva con più di tre celle vive adiacenti muore, come per effetto di sovrappopolazione;
  • 4) Qualsiasi cella morta con esattamente tre celle vive adiacenti diventa una cella viva, come per effetto di riproduzione.

(1) Come rappresentare una matrice in 5250 e RPG

Ecco subito il primo problema … una matrice grafica in RPG, con un Display File … 24×80 o 27×132 … non è una grande matrice … ma accontentiamoci: potevo fare un unico campo a video multi-line … che occupasse più spazio possibile. Ho deciso invece di fare una matrice di 24 righe per 130 char. La scelta, come poi vediamo nel codice, serve perché voglio lavorare comunque su una matrice più grande … permettendo ai calcoli di andare “fuori schermo” e quindi anche di “ritornarci” per alcuni oggetti che si “muovono”.

(2) In RPG gestisco una matrice più grande di 130 x 24 arrivando a 300 x 100

Nel codice RPG gestisco poi questi campi video come un Array di righe orizzontali… per poter gestire una matrice teorica più grande l’array che vado a creare avrà una dimensione di 300 x 100 (ma si potrebbe fare di più se dovesse servire!): da notare la keyword POS(1) per fare l’overlay dentro la DS!

        dcl-c maxx const(130);
        dcl-c maxy const(24);
        dcl-c m_maxx const(300);
        dcl-c m_maxy const(100);
        dcl-c d_maxx const(170); // 300-130
        dcl-c d_maxy const(88);  // 100-22

        dcl-ds matrix ;
          row001 char(maxx);
          row001h char(d_maxx);
          row002 char(maxx);
          row002h char(d_maxx);
          row003 char(maxx);
          row003h char(d_maxx);
          row004 char(maxx);
          row004h char(d_maxx);
          row005 char(maxx);
          row005h char(d_maxx);
          row006 char(maxx);
          row006h char(d_maxx);
          row007 char(maxx);
          row007h char(d_maxx);
          row008 char(maxx);
          row008h char(d_maxx);
          row009 char(maxx);
          row009h char(d_maxx);
          row010 char(maxx);
          row010h char(d_maxx);
          row011 char(maxx);
          row011h char(d_maxx);
          row012 char(maxx);
          row012h char(d_maxx);
          row013 char(maxx);
          row013h char(d_maxx);
          row014 char(maxx);
          row014h char(d_maxx);
          row015 char(maxx);
          row015h char(d_maxx);
          row016 char(maxx);
          row016h char(d_maxx);
          row017 char(maxx);
          row017h char(d_maxx);
          row018 char(maxx);
          row018h char(d_maxx);
          row019 char(maxx);
          row019h char(d_maxx);
          row020 char(maxx);
          row020h char(d_maxx);
          row021 char(maxx);
          row021h char(d_maxx);
          row022 char(maxx);
          row022h char(d_maxx);
          row023 char(maxx);
          row023h char(d_maxx);
          row024 char(maxx);
          row024h char(d_maxx);
          rows char(m_maxx) dim(m_maxy) pos(1);
        END-DS;                    

(3) Viva o Morta – Dead or Alive

Lo stato vivo o morto, on o off, Dead or Alive delle celle viene, per semplicità rappresentato con un carattere … ma potremmo fare di meglio con un “full block” o qualche altro carattere esteticamente più efficace

        dcl-s on  char(1) inz('O'); // Alive
        dcl-s off char(1) inz(' '); // Dead  

(4) Evoluzione delle generazioni (iterazioni)

Il passaggio alle nuove generazioni avviene su una matrice di appoggio (MatrixNextGen) dove ogni singola cella viene valutata secondo le 4 regole del gioco e poi … nel programma…. una procedure “check_matrix()” passa tutta la matrice, legge lo stato di ogni cella “row.pixel()”, calcola il numero di celle vive intorno “neighbours_count()” e applica le regole a seconda che la stessa cella sia viva “alivePixel_rules()” o morta “deadPixel_rules()“. Alla fine viene fatto il passaggio dalla MatrixNextGen alla Matrix … per fare lo “scatto” della nuova generazione tutta in un istante.

        //-----------------------------------
        // check_matrix;
        //-----------------------------------
        dcl-proc check_matrix;
          dcl-pi check_matrix;
          END-PI;
          dcl-ds row qualified;
                 pixel char(1) dim(m_maxx);
          END-DS;
          dcl-s  neighbours int(5);
          for y=1 to maxy;
              row=rows(y);
              for x=1 to m_maxx;
                  // if pixel is alive
                  if row.pixel(x)=on;
                     neighbours=neighbours_count(x:y:bounce);
                     row.pixel(x)=alivePixel_rules(neighbours);
                  else;
                  // if is death
                     neighbours=neighbours_count(x:y:'N');
                     row.pixel(x)=deadPixel_rules(neighbours);
                  ENDIF;
              ENDFOR;
              // save row in the matrix
              matrixNextGen.rows(y)=row;
          ENDFOR;

          // Show Next Generation
          matrix=MatrixNextGen;

        END-PROC;


        //-----------------------------------
        // neighbours_count(x:y)
        //-----------------------------------
        dcl-proc neighbours_count ;
        dcl-pi   neighbours_count int(5);
            x int(5) const;
            y int(5) const;
            ibounce char(1) const;
        END-PI;
            dcl-s count int(5);
            dcl-ds row qualified;
                   pixel char(1) dim(m_maxx);
            END-DS;
            // Same row
            row=getrow(y:ibounce);
            count+=ifalive(row:x-1);
            count+=ifalive(row:x+1);
            // upper row
            row=getrow(y-1:ibounce);
            count+=ifalive(row:x-1);
            count+=ifalive(row:x);
            count+=ifalive(row:x+1);
            // lower row
            row=getrow(y+1:ibounce);
            count+=ifalive(row:x-1);
            count+=ifalive(row:x);
            count+=ifalive(row:x+1);

            return count;
        end-proc ;


        //-----------------------------------
        // getrow(y)
        //-----------------------------------
        dcl-proc getrow ;
        dcl-pi   getrow char(m_maxx);
            y int(5) const;
            ibounce char(1) const;
        END-PI;
            if y>=1 and y<=maxy;
               return rows(y);
            else;
               if ibounce='Y';
                  return rowon;
               else;
                  return rowoff;
               ENDIF;
            ENDIF;

        end-proc ;

        //-----------------------------------
        // ifalive(x:y)
        //-----------------------------------
        dcl-proc ifalive ;
        dcl-pi   ifalive int(5);
            irow char(m_maxx) const;
            x int(5) const;
        END-PI;
            dcl-ds row qualified;
                   pixel char(1) dim(m_maxx);
            END-DS;
            row=irow;
            if x>=1 and x<=m_maxx;
               if row.pixel(x)=on;
                  return 1;  // alive
               ENDIF;
            ENDIF;
            return 0; // death
        end-proc ;
        //-----------------------------------
        // alivePixel_rules
        //-----------------------------------
        dcl-proc alivePixel_rules ;
        dcl-pi   alivePixel_rules char(1);
            neighbours int(5) const;
        END-PI;
            select;
               when neighbours<=1; // die
                    return off;
               when neighbours<=3; // rest alive
                    return on;
               when neighbours>3; // die
                    return off;
            ENDSL;
        end-proc ;

        //-----------------------------------
        // deathPixel_rules
        //-----------------------------------
        dcl-proc deadPixel_rules ;
        dcl-pi   deadPixel_rules char(1);
            neighbours int(5) const;
        END-PI;
            select;
               when neighbours=3; // becomes alive
                    return on;
               other; // rest dead
                    return off;
            ENDSL;
        end-proc ;


        //-----------------------------------
        // set_NPixel
        //-----------------------------------
        dcl-proc set_nPixel ;
        dcl-pi   set_nPixel;
            currentx int(5);
            currenty int(5);
            repetition int(5);
            on_off ind const;
        END-PI;
            dcl-s i int(5);
            if repetition=0;
               repetition=1;
            ENDIF;
            for i=1 to repetition;
              if on_off;
                 setPixel_alive(currentx:currenty);
              else;
                 setPixel_dead(currentx:currenty);
              ENDIF;
              currentx+=1;
            ENDFOR;
            // Reset repetition;
            repetition=0;
        end-proc ;
        //-----------------------------------
        // setPixel_alive
        //-----------------------------------
        dcl-proc setPixel_alive ;
        dcl-pi   setPixel_alive;
            x int(5) const;
            y int(5) const;
        END-PI;
            dcl-ds row qualified;
               pixel char(1) dim(m_maxx);
            END-DS;
            if x>=1 and x<=m_maxx and
               y>=1 and y<=m_maxy;
             row=rows(y);
             row.pixel(x)=on;
             rows(y)=row;
            endif;
        end-proc ;
        //-----------------------------------
        // setPixel_dead
        //-----------------------------------
        dcl-proc setPixel_dead ;
        dcl-pi   setPixel_dead;
            x int(5) const;
            y int(5) const;
        END-PI;

            dcl-ds row qualified;
                   pixel char(1) dim(m_maxx);
            END-DS;
            if x>=1 and x<=m_maxx and
               y>=1 and y<=m_maxy;
             row=rows(y);
             row.pixel(x)=off;
             rows(y)=row;
            endif;
        end-proc ;                         

(5) Pattern iniziale – Configurazione – Rappresentazione RLE Run Lenght Encoding

Per testare il gioco “The Game of Life” sono disponibili su internet migliaia di pattern iniziali, oscillatori, still-life, astronavi e infinite altre forme che date in pasto al gioco si comportano in modo differente e sfidante. Il problema è che tutti questi pattern, configurazioni di celle vive sulla matrice, sono disponibili in una rappresentazione RLE – Run Lenght Encoding, praticamente un sistema di “compressione” di una immagine che sfrutta la ripetizioni di un “colore” nei “pixels” vicini (un po’ un antenato delle compressioni JPG e PNG di oggi).

La rappresentazione RLE (con regole B3/S23 per “The Game of Life”) è molto semplice: partendo dalle coordinate iniziali una stringa definirà la sequenza di celle vive (‘o’) o morte (‘b’) con un eventuale numero di ripetizioni e indicando il salto alla riga successiva con un simbolo del dollaro ‘$’: nell’esempio che segue (example07.rle) c’è un pattern da me inventato

#C Faq400 RLE Test         
x = 10, y = 10, rule = B3/S23         
o3bo3b$2o3b2o$o3bo3b!                                           
  • Coordinate iniziali 10 , 10
  • 1 Cella viva (‘0’) – 3 celle morte (‘b’) -1 viva – 3 morte – salto riga (‘$’)
  • 2 vive – 3 morte – 2 vive – salto riga
  • 1 viva – 3 morte – 1 viva – 3 morte – fine

Ok … indubbiamente una sfida per la giornata di Pasqua … se riesco a fare un programma che legge un file di testo … fa uno split delle stringe utilizzando il simbolo dollaro ($) , esegue il parsing e inizializza la mia matrice posso concedermi una bella fetta di “Tiramisù” che ha fatto mia moglie.

(6) Leggere un file di testo in IFS da RPG / SQL

Per leggere un file di testo con RPG ci sono diverse strade … utilizzando delle procedure di sistema operativo oppure fopen ecc visto più volte nei tutorial di Scott Klement e altri.

Visto che oltre a leggere il file di testo dovevo poi farne un parsing … splittando la stringa un righe differenti … ho preferito usare direttamente SQL … ho prima giocato un po’ con le variabili CLOB_FILE SQL per caricare un tabella in QTEMP ma ho avuto un problema con il LINE-FEED delle righe … nel leggendo dal CLOB-FILE mi veniva creato un unico record con tutto il contenuto del file di testo …

          dcl-s InFile sqltype(clob_file) ;
          dcl-s Path varchar(300);     
....
          path='/home/faq400/gameOfLife/'+pattern;
          Infile_Name = %trimr(Path) ;
          Infile_NL = %len(%trimr(Infile_Name)) ;
          Infile_FO = SQFRD ;       
 // Load RLE text file pattern in QTEMP.PATTERN table
          exec sql DROP TABLE   QTEMP.PATTERN ;
          exec sql CREATE TABLE QTEMP.PATTERN (PROW CHAR(300)) ;
          exec sql INSERT INTO  QTEMP.PATTERN VALUES(:InFile) ;  

Visto che con il CLOB ho fallito sono andato a riciclare una vecchia e ottima utility di Micheal Sansoterra, READIFSFR disponibile su MCPress Online, per leggere riga x riga un text file in SQL.

-- For a CR+LF line divided text
select * from table(faq400.ReadIFSFR('/home/faq400/GameOfLife/gosper.rle', X'0D25'));
-- For a LF line divided text
select * from table(faq400.ReadIFSFR('/home/faq400/GameOfLife/gosper.rle', X'25'));

(7) Fare lo SPLIT di una stringa dividendola per il simbolo dollaro ($)

Una volta letto il TXT devo splittare il risultato utilizzando il separatore ‘$’ per il cambio di riga. Non avendo sottomano una 7.3 con la sua funzione di split SQL ( come ci aveva spiegato Massimo Duca in questo post sul nostro blog “SQL: tagli perfetti con SPLIT”) , ricorro ancora al grande Micheal Sansoterra con la sua UDTF Split.

Con un JOIN il gioco è fatto:

 exec sql
             declare RLER cursor for
              select trim(value) as RLErow
               from table(faq400.ReadIFSFR(trim(:pathfile), X'25'))
                   ,table(faq400.split(linetext, '$')) s
                   where lower(linetext) not like 'x =%'
                     and lower(linetext) not like 'x=%'
                     and lower(linetext) not like '#n%'
                     and lower(linetext) not like '#c%'
                   order by line_no;

Risultato

Alla fine sono riuscito … posso prendere un pattern dall’enorme raccolta di Copy.sh, caricarla su IFS e testarla nel mio fantastico GAMEOFLF.SQLRPLGE program … sono soddisfazioni!

Ecco un video con qualche esempio funzionante … che ne dite?

Conclusione

Ennesima prova che con il nostro amato IBM i e RPG possiamo fare tutto!

Certo, se guardiamo il tempo per farlo e il risultato grafico, forse conveniva fare una piccola ricerca su Google e cercare qualche cosa di già pronto in Python o Node.js … installare qualche pacchetto sul nostro IBM i e fare girare tutto dentro un browser… sarebbe stato sicuramente più bello.

Ma voleva essere un tributo a John Conway, fatto con RPG, SQL e 5250 che mi sembra di non aver visto da nessuna parte cercando su Google!

Addio John Conway, grazie per quello che ci hai lasciato!

E alla fine sono riuscito a leggere un file di testo con SQL, farne lo split delle stringhe, interpretare uno script di configurazione dei pattern RLE… posso finalmente buttarmi sul “Tiramisù” di mia moglie … me lo merito!

--- Roberto De Pedrini Faq400.com
About author

Founder di Faq400 Srl, IBM Champion, ideatore del sito Faq400.com e del Blog blog.faq400.com. Sviluppatore RPG da quando avevo i pantaloni corti, forte sostenitore della piattaforma IBM i (ex AS400), ho sempre cercato di convididere le mie conoscenze con gli altri tramite forum, eventi e corsi. Oggi, tramite Faq400 Srl, cerchiamo di aiutare le aziende a sfruttare al meglio questa fantastica piattaforma IBM i.

Rispondi

%d blogger hanno fatto clic su Mi Piace per questo: