INTRODUZIONE AD AREXX – 5^ parte		Settembre 2020

di Gianluca Girelli

GAME CODING CON AREXX - PARTE 2 di 2

Concludiamo in questo numero la carrellata sul linguaggio ARexx pubblicando la seconda ed ultima
parte del tutorial su come implementare una avventura testuale. Ricordo che per meglio capire gli argomenti che verranno trattati 
consiglio, per chi non l'avesse fatto, di riprendere le parti precedenti pubblicate su RetroMagazine nei numeri 20, 22, 23 e 24, nonché
l'articolo "Cenni di Game Coding" pubblicato sul numero 17.

Alla fine di questo tutorial avremo appreso tutte le basi per costruire un motore di gioco per avventure testuali: passare da
una all'altra sarà "solo" una questione di cambiare l'ambientazione della storia (ad esempio western, piuttosto che cyberpunk) 
e le interazione sugli oggetti che ne guidano il progresso.


1. GESTIRE GLI EVENTI

Nei numeri passati abbiamo imparato come costruire fisicamente il mondo di gioco (per semplicità abbiamo deciso di rappresentarlo
come una matrice bidimensionale), come descriverne le locazioni (anch'esse raccolte all'interno di una seconda matrice, 
complementare alla prima), come navigare in tale mondo (gestione delle variabili di movimento)e come prepararci ad interagire con esso (implementazione di
un semplice parser sintattico). Quello di cui ora abbiamo bisogno è capire come effettivamente gestire gli eventi (ovvero farli
accadere) per poter proseguire nella storia. Supponiamo, ad esempio, di iniziare a giocare: siamo all'interno di un banco dei pegni
dove in vetrina è esposto hardware pregiato, indispensabile per "vivere" nel mondo cyberpunk che stiamo costruendo. 
La meccanica classica che sta dietro ad un'avventura testuale è: leggere (o rileggere tramite adeguato comando) la descrizione 
della locazione; esaminare il luogo nel dettaglio per capire se ci sono elementi da prelevare o con cui interagire; prendere (usare) detti
elementi; se presi, esaminarli per scoprire eventuali informazioni nascoste. Tradotto in codice:

/*-------------------------*/
/*     GUARDA              */
/*-------------------------*/
Guarda:
  call Scenario(Pos)
  say
return

N.B. L'implementazione della routine Scenario() (che rimanda a video la descrizione della locazione attuale) è stata pubblicata
sul numero precedente.

Come visto in precedenza, la penultima riga della descrizione di ogni locazione (contenuta nella precitata matrice) 
può contenere informazioni importanti, in questo caso:

Situazione.6.34='In una delle vetrine puoi vedere il tuo DECK "Ono-Sendai", modello UXB 7000.'

Prendiamo ora quindi l'oggetto:

/*-------------------------*/
/*     PRENDI              */
/*-------------------------*/
Prendi:
.....
.....
if Pos=34 & nome='DECK' then do
                                og_num.1.1=1
                                say 'Visto che avevi il ticket con te, Shin ti rida'' il deck, ma non'
                                say 'prima di averti "prelevato" 50 crediti!!'
                                og_num.5.1=0
                                og_num.6.1=0
                                Situazione.6.34='' 
                               end
if Pos~=34 & nome='DECK' & og_num.1.1=0 then say 'Un biglietto che hai in tasca ti dice che il DECK e'' da Shin.'
.....
.....
return

Per chiarezza riporto che:
og_nome.1=deck
og_nome.5=crediti
og_nome.6=ticket

Inoltre, grazie alla potenza e alla flessibilità di ARexx nel gestire le variabili, abbiamo implementato un costrutto che,
se og_nome.1=deck allora: og_num.1.1=1 significa "deck preso", og_num.1.2=1 significa "deck esaminato" mentre 
og_num.1.3=1 significa "deck usato".

Senza entrare troppo nel dettaglio delle variabili, la storia inizia con il protagonista che possiede solo due oggetti: una
ricevuta di un Banco dei Pegni ed un pò di spiccioli. Lo script prevede che se viene usato il comando "prendi deck" all'interno
della locazione 34 (Negozio dei Pegni di Shin, confronta la mappa in figura) al protagonista venga restituito il suo "cyberspace
 deck" in cambio della ricevuta ed ovviamente dei soldi. Qualsiasi altro comando che usi la parola "deck" al di fuori della locazione
34 informerà il giocatore che l'oggetto sta da un'altra parte, sempre che il protagonista abbia ancora la ricevuta in tasca, 
altrimenti l'oggetto è evidentemente già stato preso. Infine, è importante notare come la routine Prendi() si occupi anche di
azzerare la riga 6 all'interno della descrizione della locazione attuale (Situazione.6.34='') in tal modo rientrando da Shin 
(o usando il comando "Guarda") non verrà più menzionato il deck all'interno dello scaffale. 

Ricevuto l'oggetto è quindi ora il caso di esaminarlo da vicino:

/*-------------------------*/
/*     ESAMINA             */
/*-------------------------*/
Esamina:
select
....
....
   when nome='DECK' & og_num.1.1 then 
      do 
        say 'E'' il tuo buon vecchio cyberspace deck Ono-Sendai UXB 7000. Con questo puoi collegarti ad un JACK.'
        og_num.1.2=1
        if regeneration=0 then say 'Ma non sei ancora pronto.'
      end
....
....
otherwise say "Non hai questo oggetto con te"; say; return
end
return

Di fatto ora abbiamo il deck (og_num.1.1=1, inizializzato nella routine "prendi") e lo abbiamo esaminato (og_num.1.2=1,
 inizializzato nella routine "esamina"). Come in questo caso, a seconda dello stato di altre variabili, ci sono cose che
possiamo già fare oppure no. Nel prosieguo della storia, una volta recuperato l'harware, il giocatore avrà bisogno di recuperare
anche adeguato software che però, ancorchè combinato con il deck, non permetterà al protagonista di entrare nel cyberpazio finchè
egli non sarà guarito da un precedente danno neurale (cioè sino a quando la variabile "regeneration" verrà inizializzata ad 1).

Supponiamo ora di aver reperito, dopo aver visitato alcuni ambienti, un dischetto contenente il software che ci serve. Abbiamo
quindi bisogno di una routine che ci permetta di usarlo:
 
/*-------------------------*/
/*       USA               */
/*-------------------------*/
Usa:
....
....
if nome='DISCHETTO' & og_num.8.1 & og_num.1.1 then do
                                          say
                                          say 'Hai inserito il dischetto nel deck e caricato il software.'
                                          og_num.8.3=1
                                        end
else do
      if (nome='DISCHETTO' & og_num.8.1=0) then say 'Non hai il DISCHETTO con te.'
      if (nome='DISCHETTO' & og_num.8.1 & og_num.1.1=0) then say 'Non hai un DECK in cui caricarlo.'
     end
	 
if nome='DECK' & og_num.1.1 then say 'Il DECK non puo'' essere usato direttamente. Devi trovare un JACK.'
if nome='DECK' & og_num.1.1=0 then say 'Non hai questo oggetto con te.'

if nome='JACK' then call Combat(Pos)
....
....
return

Come si intuisce, nel caso il dischetto sia in nostro possesso (og_num.8.1=1) esso verrà caricato nel deck (og_num.8.3=1).
Nel caso mancasse un elemento (il deck o il dischetto stesso) la procedura gestirà la relativa eccezione. Usare l'oggetto
giusto nel posto giusto è la chiave delle avventure testuali pertanto la routine "usa" si occupa anche di invocare (qualora
le condizioni al contorno lo consentano) la routine di "combattimento". Essa può non essere sempre necessaria e può assumere
varie forme. Nel nostro caso si occupa di simulare un hacking al sistema semplicemente stampandone a video le fasi. 
L'uso delle variabili "hack_1_disp" e "first_attack" ci aiuta a tenere traccia della situazione per monitorare la progressione degli eventi.
Altre variabili dalle analoghe sintassi e funzioni verranno inizializzate a mano a mano che il giocatore scopre nuovi oggetti
che gli permetteranno di potenziare il suo equipaggiamento e di proseguire nella storia sino alla sua conclusione.

/*-------------------------*/
/*   COMBAT (hacking)      */
/*-------------------------*/
Combat:
select
   when Pos=44 & hack_1_disp then do          /* Amiga R&D */
                say cancella
                say 'Inizio Hacking di Primo Livello'
                call Sleep()
                say
                say 'Accesso al CyberSpace. Violazione in corso ....'
                say 'Trovati dati su: BLIPVERT PROJECT'
                say 'Gli esperimenti effettuati da questa divisione Ricerca e Sviluppo confermano la'
                say 'risposta positiva dell''individuo ai segnali subliminali. Nascondendo la foto di un'
                say 'prodotto all''interno di un filmato, il soggetto provera'' un forte impulso di procurarselo.'
                say 'Abbiamo la tecnologia per comprimere il messaggio lungo tutto il segnale portante della TV'
                say 'per mandare un messaggio subliminale continuo.'
                say '================================================'
                say 'Hacking terminato. Uscita dal CyberSpace effettuata.'
                first_attack=1
          end
....
....
 otherwise say 'Non sei ancora pronto.'
end
return


2. TENERE TRACCIA DELLA PROGRESSIONE DELLA STORIA

Il nostro lavoro è quasi concluso: l'unica cosa che ancora manca è un modo per tenere traccia dei progressi ottenuti
dal giocatore in modo che possa avanzare nella storia a mano a mano che i compiti assegnati sono stati completati.
Dal momento che questo software è stato sviluppato sostanzialmente per motivi didattici, la procedura di cui abbiamo
bisogno sarà molto semplice. Il suo livello di complessità però può essere liberamente aumentato secondo le più 
diverse necessità. Vediamo un esempio:

/*-------------------------*/
/*    GAME PROGRESS        */
/*-------------------------*/
GameProgress:
 if regeneration & og_num.1.1 & og_num.8.3 then do
                                            hack_1_disp=1 
                                            say '==========================='
                                            say 'Ora sei pronto per entrare nel cyberspazio. L''hacking di primo livello e'' ora disponibile.'
                                            say '==========================='
                                        end
 if first_attack & og_num.3.3 then do
                                    hack_2_disp=1
                                    say '==========================='
                                    say 'Ora sei pronto per entrare nel cyberspazio 3D. L''hacking di secondo livello e'' ora disponibile.'
                                    say '==========================='
                                  end
....
....
return

GameProgress() lavora congiuntamente con altre routine del gioco e aggrega i risultati da loro prodotti per abilitare
nuove opzioni permettendo così di avanzare nella storia. Abbiamo visto nel paragrafo precedente che il giocatore, dopo aver
recuperato adeguato hardware (deck), software (dischetto) ed essere guarito dal danno neurale (regeneration), aveva raggiunto
i requisiti minimi per progredire nel gioco. Ciò è stato ottenuto proprio grazie al primo IF-THEN di questa routine che, dopo
aver verificato l'esistenza di tali requisiti, ha inizializzato la variabile "hack_1_disp". Se andate ora a rileggere il codice della routine Combat()
vi apparirà chiaro il perchè alla fine del primo attacco sia stata inserita la variabile "first_attack": essa è uno dei prerequisiti
del secondo livello. L'altro è il reperire ed usare un nuovo oggetto (nel nostro caso una nuova interfaccia neurale, og_num.3.3),
cosa che ci abiliterà all'hacking successivo. 

GameProgress() potrebbe fare molte altre cose, ad esempio aggiornare il sistema di punteggi (nel caso ci sia, come nelle vecchie avventure
della Infocom) o assegnare "achievements" e "trofei" (come per XBox e PlayStation). All'aumentare della complessità e della lunghezza della
nostra avventura potrebbe poi aver bisogno di aggiornare l'ambiente di gioco durante il run time. Il motivo è dovuto al fatto che il contenuto originale 
di tutte le variabili (ed in questo caso il contenuto integrale delle matrici che descrivono il mondo di gioco) deve sempre essere preservato.
In caso diverso, successivamente al primo salvataggio tale informazioni andrebbero irrimediabilmente perse (poichè sovrascritte) e non ci sarebbe modo di
giocare una seconda partita. Una soluzione al problema è espandere la matrice delle descrizioni in modo trasparente all'utente, aggiungendo
le descrizioni aggiuntive in locazioni non raggiungibili dal giocatore. In questo modo il software potrà caricare il mondo originale ad
ogni partita preservandolo nel tempo, mentre GameProgress() conterrà istruzioni dedicate che modificheranno tale mondo solo durante il 
runtime. Nel nostro caso, ad esempio, la routine potrebbe contenere un'istruzione del tipo: 

if og_nome.1=1 then Situazione.6.34='Lo scaffale ora è vuoto.". 

Poichè GameProgress() è invocata ad ogni ciclo siamo sicuri che essa
rispecchierà fedelmente i nostri progressi senza però alterare il costrutto originale.


3. INIZIALIZZAZIONE E MAIN LOOP

Dopo aver scomposto l'engine in tutte le sue parti ed averle realizzate una ad una, riporto di seguito l'intera sequenza di inizializzazione
e il main loop del gioco presa integralmente dal sorgente dell'avventura:

/*===================== GAME INIT =======================*/
do until (risposta='NO' | risposta='N')

  call Livelli()          /* imposta descrizione livelli */
  call Variabili()        /* inizializza variabili di gioco */
  call Titoli()           /* routine di presentazione */
  call Istruzioni()       /* visualizza le istruzioni di gioco */
  call Scenario(Pos)      /* stampa situazione e posizione */
  options prompt ">"

/* inizia main routine */

do until (frase='QUIT')
  say
  richiesta=random(1,4,time("E")*100)  /* tra 1 e 4 compresi */
  say prompt.richiesta
  pull frase
  call Parser(frase)
  select
        when verbo='VAI' then call Vai(nome, Pos)
        when verbo='GUARDA' then call Guarda()
        when verbo='PRENDI' then call Prendi(nome)
        when verbo='ESAMINA' then call Esamina(nome)
        when verbo='USA' then call Usa(nome)
        when verbo='LISTA' then call Lista()
        when verbo='VOC' then call Vocabolario()
        when verbo='DEBUG' then call Debug()
        when verbo='VAR' then call Var(nome)
        when verbo='QUIT' then nop
        otherwise say 'non so che significa ' || verbo
  end
  call GameProgress()
  if ~tesoro_trovato then numero_mosse=numero_mosse+1
end

  say 'un''altra partita?'
  pull risposta
end
/* fine main routine */

say cancella
say 'grazie per aver giocato'
say
say 'hai fatto ' numero_mosse 'mosse'; say
do j=1 to 7
  totale=totale+punteggio.j
end
say 'il tuo punteggio e'':' totale; say
if tesoro_trovato~=1 then say '....ma non hai risolto il rebus del BlipVert Project!'
call Sleep()
call Crediti()
exit
/*========================= GAME FINISHED =======================*/

Come si può notare, l'aver scomposto il problema nelle sue parti elementari permette di scrivere un main loop estremamente
semplice da leggere e da manutere: dopo la fase di inizializzazione si entra nel loop di gioco, all'interno del quale il parser
dirigerà le azioni chiamando le routine adeguate a seconda degli input dell'utente. Ad ogni ciclo GameProgress() gestisce i
progressi del giocatore. All'uscita dal gioco verrà comunicato all'utente il suo punteggio e verranno visualizzati i crediti. 


4. ULTERIORI ELEMENTI PER IL CODICE DI GIOCO

Come detto in precedenza questo motore di gioco per avventure testuali fu sviluppato anni fa per motivi essenzialmente
didattici. Per questa ragione manca di alcuni elementi che sarebbero invece indispensabili nel caso si voglia confezionare
un prodotto quantomeno semi-professionale. La cosa più importante è decisamente la realizzazione delle routine di caricamento
e salvataggio del gioco: è infatti impensabile pretendere che l'utente possa arrivare alla fine dell'avventura rapidamente e
senza bisogno di pause intermedie. Un'altra cosa importante è poi la localizzazione del software: il sistema di matrici contenenti
le descrizioni ed il parser andrebbero quindi sviluppati congiuntamente ad un insieme di variabili di supporto che permettano di
evitare l'uso delle stringhe "hard-coded" e quindi, a titolo di esempio, la routine "esamina" andrebbe scritta così:

/*-------------------------*/
/*     ESAMINA             */
/*-------------------------*/
Esamina:
select
....
....
   when nome=var_deck & og_num.1.1 then 
      do 
        say stringa1
        og_num.1.2=1
        if regeneration=0 then say stringa2
      end
....
....
otherwise say stringa3; say; return
end
return

Dove "var_deck" contiene il nome dell'oggetto, mentre stringa1, stringa2 e stringa3 contengono le descrizioni delle azioni. Queste
stringhe sono contenute nel file di localizzazione specifico per la lingua scelta dal giocatore dal menù principale pre gioco.

Infine, potremmo voler espandere il gioco con side quests, un sistema di punteggi o con un vocabolario più variegato che, unito
ad un parser più raffinato, possa fornire all'utente un'esperienza più immersiva.

 
5. CONCLUSIONI

Il viaggio è stato lungo ma siamo finalmente arrivati alla nostra meta. Spero che vi siate divertiti a leggere quanto io mi 
sono divertito a progettare il gioco ed a realizzare questa serie di articoli. Il codice è troppo lungo per essere inserito nell'
articolo ma l'intero sorgente è disponibile a richiesta presso la redazione di RetroMagazine World e sarà pubblicato sul sito web
in un prossimo futuro. Per giocarlo sarà sufficiente importarlo nel vostro emulatore preferito o, ancora meglio, sull'hardware
reale se ne siete un fortunato possessore. Qualsiasi versione di AmigaOS, a partire dalla versione 2, basterà allo scopo. 
 
 
========================== BOX ===========================================
Utilizzo degli esempi:

Ricordiamo che per utilizzare gli esempi bisogna salvare lo script in modalita' testo
nel formato "nome_script.rexx". Per lanciare lo script basta digitare da shell ">rx nome_script.rexx"
o, piu' semplicemente, ">rx nome_script".
==========================================================================


BIBLIOGRAFIA

- Mike Cowlishaw "The REXX Language: A Practical Approach to Programming" (1985) Prentice Hall. ISBN 0-13-780651-5.
- Chris Zamara, Nick Sullivan "Using Arexx on the Amiga" (1991) Abacus Software Inc. ISBN 1-55755-114-6.
- AmigaOS 2.0 Manuale di sistema