Capitolo 5: Rivediamo Heidi =========================== I was an innkeeper, who loved to carouse; J was a joiner, and built up a house. Anche nella storia piu' semplice, il giocatore ha la possibilita' di tentare attivita' che voi non avete previsto. Qualche volta ci possono essere strade alternative per affrontare un problema: se non potete essere sicuri su quale approccio tentera' il giocatore, dovreste proprio permettere tutte le possibilita'. Qualche volta gli oggetti che create e le descrizioni che fornite possono suggerire al giocatore che fare cosi'-e-cosi' dovrebbe essere possibile e, percio', dovreste anche permetterlo. La realizzazione base di un gioco e' facile: quello per cui serve tempo, e che rende un gioco vasto e complesso, e' l'occuparsi di tutte le altre cose che il giocatore puo' pensare di provare. Ora vi illustreremo cosa intendiamo evidenziando alcune delle piu' notevoli deficienze nel nostro primo gioco. Ascoltare il passerotto *********************** Ecco un frammento di una partita: Nel folto del bosco Attraverso la folta vegetazione, ti pare di scorgere un edificio verso ovest. Un sentiero porta verso nord-est. Puoi vedere un uccellino qui. >ESAMINA L'UCCELLINO Troppo giovane per saper volare, il passerotto pigola inerme. >ASCOLTA L'UCCELLINO Non senti niente di inaspettato. > Non troppo furbo, vero? La nostra descrizione richiama specificatamente l'attenzione del giocatore al verso del passerotto, e dopo lui si accorge che non abbiamo niente di speciale da dire a proposito del suo inerme pigolio. La libreria ha una serie di azioni e risposte per ognuno dei verbi definiti nel gioco, cosi' puo' rispondere alla maggior parte dei comandi del giocatore con un comportamento standard, invece di rimanere impertinente in silenzio o dire che non capisce cosa intenda il giocatore. "Non senti niente di inaspettato" e' la risposta standard della libreria al comando ASCOLTA, sufficientemente buona per rispondere ad un ASCOLTA IL NIDO o ASCOLTA L'ALBERO, ma decisamente inappropriata in questo caso; abbiamo veramente bisogno di sostituirla con una risposta piu' rilevante per un ASCOLTA L'UCCELLO. Ecco come facciamo: Object uccello "uccellino" foresta with description "Troppo giovane per saper volare, il passerotto pigola inerme.", name 'uccello' 'uccellino' 'passero' 'passerotto' 'volatile', before [; Listen: print "Sembra spaventato e bisognoso d'aiuto.^"; return true; ], has ; Vediamo quello che abbiamo fatto un passo alla volta: 1. Abbiamo aggiunto una nuova proprieta' before al nostro oggetto uccello. L'interprete controllera' la proprieta' before prima di effettuare qualsiasi azione diretta specificatamente a questo oggetto: before [; ... ], 2. Il valore della proprieta' e' una routine incapsulata, contenente un'etichetta e due istruzioni: Listen: print "Sembra spaventato e bisognoso d'aiuto.^"; return true; 3. L'etichetta e' il nome di un'azione, Listen (Ascolta) in questo caso. Stiamo dicendo all'interprete che, se l'azione che state per eseguire sull'uccello e' Listen (Ascolta), allora deve prima di tutto eseguire queste due istruzioni; se invece e' una qualsiasi altra azione, deve comportarsi normalmente. Cosi', se il giocatore scrive ESAMINA L'UCCELLO, RACCOGLI L'UCCELLO, METTI L'UCCELLO NEL NIDO, COLPISCI L'UCCELLO o ACCAREZZA L'UCCELLO, otterra' il comportamento standard. Se il giocatore scrive ASCOLTA L'UCCELLO, allora quelle due istruzioni vengono eseguite prima che accada qualsiasi altra cosa. Quando facciamo cio', si dice che stiamo "intercettando" o "catturando" l'azione di ascoltare l'uccello. 4. Le due istruzioni che eseguiamo sono, prima: print "Sembra spaventato e bisognoso d'aiuto.^"; che fa si' che l'interprete stampi la stringa racchiusa tra le virgolette; ricordate che il carattere ^ in una stringa rappresenta il ritorno a capo. Dopo eseguiamo: return true; che comunica all'interprete che non deve fare niente altro, perche' abbiamo gestito l'azione Listen da soli. E il nostro gioco ora si comporta - perfettamente - in questo modo: >ASCOLTA L'UCCELLINO Sembra spaventato e bisognoso d'aiuto. > L'uso dell'istruzione "return true" probabilmente ha bisogno di qualche spiegazione in piu'. La proprieta' before di un oggetto intercetta subito all'inizio un'azione diretta verso quell'oggetto, prima che l'interprete inizi a fare qualsiasi cosa. Questo e' il punto in cui vengono eseguite le istruzioni nella routine incapsulata. Se l'ultima di queste istruzioni e' "return true", allora l'interprete assume che l'azione sia stata gestita dalle istruzioni precedenti, e non c'e' rimasto altro da fare: nessuna azione, nessun messaggio, niente. D'altra parte, se l'ultima istruzione e' "return false" allora l'interprete continua ad eseguire le azioni standard come se niente fosse stato intercettato. Ogni tanto potrebbe essere quello che volete veramente che succeda, ma non in questo caso: se aveste scritto questo: Object uccello "uccellino" foresta with description "Troppo giovane per saper volare, il passerotto pigola inerme.", name 'uccello' 'uccellino' 'passero' 'passerotto' 'volatile', before [; Listen: print "Sembra spaventato e bisognoso d'aiuto.^"; return false; ], has ; allora l'inteprete avrebbe prima visualizzato la nostra stringa, e poi avrebbe continuato con il comportamento normale, ovvero avrebbe visualizzato la risposta standard: >ASCOLTA L'UCCELLINO Sembra spaventato e bisognoso d'aiuto. Non senti niente di inaspettato. > La tecnica che abbiamo usato qui - intercettare un'azione diretta ad un particolare oggetto in modo da fare qualcosa di appropriato per tale oggetto - e' una che useremo ancora molte e molte volte. Entrare nella baita ******************* All'inizio del gioco il personaggio principale si trova "di fronte a una baita", il che puo' portare il giocatore a pensare di poter entrare all'interno: Di fronte a una baita Ti trovi davanti a una baita. Verso est si stende la foresta. >ENTRA Non puoi andare da qualle parte. > Ancora una volta questa non e' forse la risposta piu' appropriata, ma e' facile da cambiare: Object davanti_baita "Di fronte a una baita" with description "Ti trovi davanti a una baita. Verso est si stende la foresta.", e_to foresta, in_to "E' una stupenda giornata, meglio stare all'aperto.", cant_go "L'unico sentiero conduce ad est.", has light; La proprieta' in_to normalmente dovrebbe condurre a un'altra stanza, nello stesso modo in cui la proprieta' e_to contiene l'identificatore interno dell'oggetto foresta. Comunque, se invece fate in modo che il suo valore sia una stringa, l'interprete visualizzera' quella stringa quando il giocatore prova a entrare. Le altre direzioni non specificate, come NORD e SU, dovrebbero sempre fornire la risposta standard "Non puoi andare da quella parte", ma noi possiamo cambiare anche quello, specificando una proprieta' cant_go il cui valore sia una stringa appropriata. Otteniamo cosi' questo comportamento: Di fronte a una baita Ti trovi davanti a una baita. Verso est si stende la foresta. >ENTRA E' una stupenda giornata, meglio stare all'aperto. >NORD L'unico sentiero conduce ad est. >EST Nel folto del bosco ... Abbiamo un altro problema adesso; siccome non abbiamo implementato un oggetto per rappresentare la baita, un comando perfettamente ragionevole come ESAMINA LA BAITA riceve ovviamente una risposta priva di senso come "Non vedi nulla del genere". Questo e' facile da sistemare; possiamo aggiungere un nuovo oggetto baita, rendendolo parte della scenografia, proprio come l'albero. Object baita "piccola baita" davanti_baita with description "E' piccola e semplice, ma qui tu sei felice.", name 'piccola' 'baita' 'case' 'capanna' 'rifugio', has scenery female; Questo risolve il problema, ma immediatamente ci fornisce un'altra risposta irragionevole: Di fronte a una baita Ti trovi davanti a una baita. Verso est si stende la foresta. >ENTRA NELLA BAITA Non e' qualcosa in cui puoi entrare > La situazione e' simile a quella del problema "ASCOLTA L'UCCELLINO" e la soluzione che adottiamo e' estremamente simile: Object baita "piccola baita" davanti_baita with description "E' piccola e semplice, ma qui tu sei felice.", name 'piccola' 'baita' 'case' 'capanna' 'rifugio', before [; Enter: print_ret "E' una stupenda giornata, meglio stare all'aperto."; ], has scenery female; Usiamo una proprieta' before per intercettare l'azione Enter (Entra) applicata all'oggetto baita, cosi' possiamo visualizzare una risposta piu' appropriata. Questa volta, pero', usiamo una sola istruzione invece di due. Succede che la sequenza "stampa una stringa che finisce con un ritorno a capo e subito dopo fai un return true" e' usata cosi' di frequente che c'e' un'istruzione apposta per fare tutto cio'. Questa e' l'istruzione print_ret (e notate che la stringa non ha bisogno del carattere ^ in fondo): print_ret "E' una stupenda giornata, meglio stare all'aperto."; funziona esattamente come print "E' una stupenda giornata, meglio stare all'aperto.^"; return true; Avremmo potuto usare le forma piu' breve anche per ASCOLTA L'UCCELLINO, e la useremo da ora in poi. Scalare l'albero **************** Nella radura, quando ha in mano il nido e sta guardando l'albero, il giocatore dovrebbe scrivere SU. Pero' e' altrettanto probabile che egli scriva SCALA L'ALBERO (che correntemente fornisce la risposta completamente fuorviante "Non penso si possa raggiungere qualcosa da qui"). Questa e' un'altra opportunita' per usare una proprieta' before - sono veramente utili - ma ora con una differenza: Object albero "albero di sicomoro" radura with description "Fieramente alto nel mezzo della radura, l'albero sembra molto facile da scalare.", name 'albero' 'sicomoro' 'tronco', before [; Climb: PlayerTo(sull_albero); return true; ], has scenery; Questa volta, quando intercettiamo l'azione Climb (Scala) applicata all'oggetto albero, non lo facciamo per mostrare un messaggio migliore; lo facciamo perche' vogliamo muovere il personaggio del giocatore in un'altra stanza, esattamente come se il giocatore avesse scritto SU. Muovere il personaggio del giocatore e' un operazione abbastanza complicata, ma fortunatamente tutta questa complessita' e' nascosta: c'e' una routine della libreria standard per svolgere questo compito, non una routine che abbiamo scritto noi, ma una che viene fornita come parte del sistema Inform. Ricorderete che, quando abbiamo menzionato per la prima volta le routine (vedete "Routine autonome" nel capitolo 4), abbiamo usato l'esempio di Initialise() e abbiamo detto che "tutto quello di cui abbiamo bisogno per chiamare la routine e' il nome della routine seguito da una parentesi aperta e una chiusa". Questo e' vero per Initialise(), ma non e' tutto. Per spostare il personaggio, dobbiamo specificare dove vogliamo che vada, e dobbiamo far cio' fornendo l'identificatore interno della stanza di destinazione tra le due parentesi. Cioe', invece di chiamare soltanto PlayerTo(), chiamiamo PlayerTo(sull_albero), e definiamo sull_albero come l'argomento della routine. Anche se abbiamo mosso il personaggio in un'altra stanza, siamo sempre nel mezzo dell'azione Climb intercettata. Come in precedenza, dobbiamo comunicare all'interprete che abbiamo gestito noi l'azione, e che non vogliamo il messaggio standard. L'istruzione return true fa questo, come al solito. Far cadere gli oggetti dall'albero ********************************** In una stanza normale, come la foresta o la radura, il giocatore puo' POSARE qualcosa che sta portando, e l'oggetto lasciato cadra' in effetti ai suoi piedi. Semplice, conveniente, prevedibile - eccetto quando il giocatore e' in cima all'albero. Quando egli POSA qualcosa, e' un po' improbabile che poi se lo trovi accanto; piu' facilmente l'oggetto cadrebbe nella radura sottostante. Sembra che dovremmo intercettare l'azione Drop (Posare), ma non nel modo in cui l'abbiamo fatto fin'ora. Anzitutto non vogliamo complicare le definizioni dell'uccellino, del nido e di ogni altro oggetto che potremmo intrudurre: meglio trovare una soluzione piu' generale che funzioni per ogni oggetto. E in secondo luogo, dobbiamo riconoscere che non tutti gli oggetti sono posabili; il giocatore non puo', ad esempio, POSARE IL RAMO. Il miglior approccio al secondo problema e' quello di intercettare l'azione Drop dopo che e' accaduta, piuttosto che prima. In questo modo facciamo si' che la libreria si occupi degli oggetti che non sono trasportati dal giocatore o non possono essere posati, e interveniamo solo quando l'azione Drop e' stata svolta con successo. E il miglior approccio al primo problema e' quello di intercettare il comando agendo non oggetto-per-oggetto, come abbiamo fatto fin'ora, ma invece per ogni azione Drop che si svolge nella nostra problematica stanza sull_albero. Questo e' quello che doppiamo scrivere: Object sull_albero "In cima all'albero" with description "Ti tieni precariamente appesa al tronco.", d_to radura, after [; Drop: move noun to radura; return false; ], has light; Rivediamola passo dopo passo: 1. Abbiamo aggiunto una nuova proprieta' after al nostro oggetto sull_albero. L'interprete controlla questa proprieta' dopo aver fatto qualsiasi azione in questa stanza: after [; ... ], 2. Il valore della proprieta' e' una routine incapsulata, contenente un'etichetta e due istruzioni: Drop: move noun to radura; return false; 3. L'etichetta e' il nome di un'azione, in questo caso Drop (Posa). Stiamo dicendo all'interprete che, se l'azione che ha appena eseguito e' Drop, allora deve eseguire queste istruzioni prima di dire al giocatore cosa ha fatto; se e' una qualsiasi altra azione deve procedere normalmente. 4. Le due istruzioni che eseguiamo sono prima: move noun to radura; che prende l'oggetto che e' appena stato mosso dall'oggetto player (giocatore) all'oggetto sull_albero (da una azione Drop eseguita con successo) e lo sposta ancora in modo che suo padre sia l'oggetto raduda. Quel noun e' una variabile di libreria che contiene sempre l'identificatore interno dell'oggetto verso cui si rivolge l'azione correte; se il giocatore scrive POSA UCCELLINO, allora noun conterra' l'identificatore interno dell'oggetto uccello. Poi, eseguiamo: return false; che dice all'interprete che ora puo' far sapere al giocatore cosa e' successo. Ecco il risultato di tutto questo: In cima all'albero Ti tieni precariamente appesa al tronco. >POSA IL NIDO Posato >GUARDA In cima all'albero Ti tieni precariamente appesa al tronco. >DOWN Una radura nella foresta Un alto sicomoro si erge al centro di questa radura. Il sentiero si inoltra tra gli alberi, serso sud-ovest. Puoi vedere un nido di uccelli (nel quale c'e' un uccellino) qui. > Ovviamente potreste pensare che il messaggio standard "Posato" non e' molto d'aiuto in queste circostanze particolare. Se preferite suggerire al giocatore cosa e' realmente successo, potreste usare questa soluzione alternativa: Object sull_albero "In cima all'albero" with description "Ti tieni precariamente appesa al tronco.", d_to radura, after [; Drop: move noun to radura; print_ret "Posato... nella radura sottostante."; ], has light; L'istruzione print_ret fa due cose per noi: ci mostra un messaggio piu' esauriente, e esegue un return_true per dire all'interprete che non c'e' bisogno di far sapere al giocatore cosa e' successo - ci abbiamo gia' pensato noi. L'uccellino e' nel nido? ************************ Il gioco finisce quando il giocatore mette il nido sul ramo. Abbiamo assunto che l'uccellino sia all'interno del nido, ma potrebbe anche non essere cosi'; il giocatore potrebbe aver prima portato su l'uccello, e poi essere tornato indietro per il nido, o viceversa. Sarebbe meglio non interrompere il gioco fino a che non abbiamo controllato che l'uccellino sia effettivamente nel nido; fortunatamente e' cosa facile a farsi: Object ramo "ramo largo e robusto" sull_albero with description "E' abbastanza largo da sostenere un'oggetto.", name 'ramo' 'largo' 'robusto', each_turn [; if (uccello in nido && nido in ramo) deadflag = 2; ], has static supporter; L'istruzione che abbiamo esteso: if (uccello in nido && nido in ramo) deadflag = 2; ora deve essere letta cosi': "Controlla se l'uccello attualmente e' nel (o sul) nido, e se il nido e' attualmente sul (o nel) ramo; se entrambe le cose sono vere, imposta a 2 il valore di deadflag; altrimenti non. fare niente". Riassumendo il tutto ******************** Ora dovreste aver chiaro il bisogno di gestire non soltanto le azioni ovvie che vi si presentano alla mente quando progettate il gioco, ma anche quanti piu' possibili modi alternativi in cui il giocatore potra' interagire con gli oggetti che gli presentiamo. Alcuni tra questi modi saranno altamente intelligenti, altri totalmente idioti; in ogni caso dovreste provare ad assicurarvi che la risposta del gioco sia almeno consistente, anche se state dicendo al giocatore "mi dispiace, ma questo non puoi farlo". I nuovi argomenti che abbiamo incontrato qui sono: *** Proprieta' degli oggetti Gli oggetti possono avere una proprieta' before - se questa esiste, l'interprete la controlla prima di compiere un'azione che coinvolge quell'oggetto. Similmente potete fornire una proprieta' after, che l'interprete controllera' dopo avere eseguito un'azione, ma prima di comunicare il risultato al giocatore. Sia la proprieta' before che la proprieta' after possono essere usate non solo con oggetti tangibili come l'uccellino, la baita e l'albero (nel qual caso intercettano azioni dirette verso quel particolare oggetto) ma anche con le stanze (nel qual csao intercettano azioni dirette a qualsiasi oggetto nella stanza). Il valore di ogni proprieta' after o before e' una routine incapsulata. Se questa routine finisce con return false, l'interprete continua a elaborare l'azione che era stata intercettata; se invece finisce con return true, l'interprete non fa altro per quell'azione. Combinando queste possibilita', si puo' integrare il lavoro svolto da un'azione standard con proprie istruzioni, o si puo' sostituire completamente l'azione standard. In precedenza abbiamo visto le proprieta' di connessione, usate con l'identificatore interno della stanza a cui conducevano. In questo capitolo abbiamo mostrato come il loro valore possa anche essere una stringa (che spieghi come mai il movimento in quella direzione non e' possibile). Ecco un esempio di entrambe, e anche della proprieta' cant_go, che fornisce una spiegazione per ogni connessione non esplicitamente elencata: e_to foresta, in_to "E' una stupenda giornata, meglio stare all'aperto.", cant_go "L'unico sentiero conduce ad est.", *** Routine e argomenti La libreria ha al suo interno un buon numero di routine utili, disponibili per eseguire alcuni compiti comuni se voi ne avete bisogno; c'e' una lista in "Routine di libreria" nell'appendice F. Abbiamo usato la routine PlayerTo, che sposta il personaggio del giocatore dalla sua stanza corrente ad un'altra - non necessariamente adiacente alla prima stanza. Quando chiamiamo PlayerTo, dobbiamo dire alla libreria quale e' la stanza di destinazione. Facciamo cio' fornendo l'identificatore interno tra parentesi, quindi: PlayerTo(radura); Un valore tra parentesi e' chiamato argomento della routine. In effetti una routine puo' avere piu' di un argomento; se serve, gli argomenti sono separati da virgole. Ad esempio, per spostare il personaggio del giocatore in una stanza senza visualizzare la descrizione di quest'ultima, possiamo fornire un secondo argomento: PlayerTo(radura,1); In questo esempio, l'effetto di quell'1 e' quello di impedire che la descrizione della radura venga visualizzata. *** Istruzioni Abbiamo incontrato diverse nuove istruzioni: return true; return false; Abbiamo usato queste due istruzioni alla fine delle routine incapsulate per controllare cosa avrebbe fatto l'interprete dopo. print "stringa"; print_ret "stringa"; L'istruzione print semplicemente visualizza la stringa di caratteri rappresentata qui da stringa. L'istruzione print_ret fa la stessa cosa, e poi fa un ritorno a capo e finalmente esegue un return true; move obj_id to parent_obj_id L'istruzione move risistema l'albero degli oggetti, rendendo obj_id figlio di parent_obj_id. if (condition && condition) ... Abbiamo esteso la semplice istruzione if incontrata prima. I caratteri && (da leggersi "and" o "e") sono un singolo operatore usato comunemente per testare piu' condizioni allo stesso tempo. Significa "se questa condizione e' vere, e anche quest'altra condizione e' vera..." C'e' anche un operatore || , da leggersi "or" o "o". NOTA: in aggiunta ci sono anche gli operatorii & e |, ma essi svolgono un compito diverso e sono molto meno comuni. Fate attenzione a non confondervi. *** Azioni Abbiamo parlato parecchio dell'intercettare azioni come Listen (ascolta), Enter (entra), Climb (scala) e Drop (posa). Un'azione e' una rappresentazione generalizzata di qualcosa che deve essere fatto, determinato dal verbo immesso dal giocatore. Per esempio, i verbi ASCOLTA e SENTI sono due modi di dire piu' o meno la stessa cosa, e cosi' entrambi risultano nella stessa azione, Listen. Similmente, comandi come ENTRA NELLA ..., VAI AL ..., SIEDITI SULLA ..., portano tutti all'azione Enter, VAI SU ..., SCALA, SALI SUL ..., portano all'azione Climb, e POSA, LASCIA, METTI GIU ..., portano tutti all'azione Drop. Questo rende la vita molto piu' semplice per lo sviluppatore; anche se Inform definisce parecchie azioni, esse sono molte meno dei modi in cui queste stesse azioni possono essere espresse in Italiano. Ogni azione e' rappresentata internalmente da un numero, e il valore dell'azione corrente e' mantenuto in una variabile di libreria chiamata action. Altre due variabili sono utili: noun contiene l'identificatore interno dell'oggetto verso cui l'azione si rivolge, e second contiene l'identificare interno dell'oggetto secondario (se ce n'e' uno). Ecco qualche esempio: | Comando del giocatore | action | noun | second | +--------------------------+---------+---------+---------+ | ASCOLTA | Listen | nothing | nothing | | ASCOLTA L'UCCELLINO | Listen | uccello | nothing | | PRENDI L'UCCELLINO | Take | uccello | nothing | | METTI L'UCCELLO NEL NIDO | Insert | uccello | nido | | POSA IL NIDO | Drop | nido | nothing | | METTI IL NIDO SUL RAMO | PutOn | nido | ramo | +--------------------------+---------+---------+---------+ Il valore nothing e' una costante (come true e false) che significa che non c'e' un oggetto a cui far riferimento. C'e' una lista delle azioni della libreria standard in "Azioni di gruppo 1", "Azioni di gruppo 2" e "Azioni di gruppo 3", nell'appendice F. Abbiamo finalmente raggiunto la fine del nostro primo gioco. In questi tre capitoli vi abbiamo mostrato i principi base su cui quasi tutti i giochi sono fondati, e vi abbiamo introdotto molte delle componenti di cui avrete bisogno per creare dei giochi piu' interessanti. Wi suggeriamo di dare un'ultima occhiata al codice sorgente (vedi La Storia di "Heidi", appendice B), e poi di procedere alla prossima fase.