Capitolo 8: Gugliemo Tell: il cuore della storia ================================================ L'azione del nostro gioco si avvicina al suo apice nella piazza principale della città. In questo dapitolo definiremo le stanze che costituiscono la piazza e ce la vedremo con Wilhelm che si avvicina al cappello che si trova sopra il palo - lo saluterà, oppure rimarra fieramente ribelle? La parte sud della piazza ************************* La piazza principale, nozionalmene un unico enorme spazio aperto, è rappresentata da tre stanze. Questa è la parte sud: Room piazza_sud "Lato sud della piazza" with description "La piccola strada che conduce verso sud si apre nella piazza principale, per poi riprendere dalla parte opposta di questo affollato luogo di ritrovo. Per continuare lungo la strada, verso la tua destinazione - la conceria di Johansson - devi attraversare, andando verso nord, la piazza, al centro della quale vedi il cappello di Gessler messo su di un palo. Se vuoi andare avanti, non puoi evitare di passarci vicino. Soldati imperiali si fanno largo rudemente tra la folla, spingendo, calciando e imprecando ad alta voce.", n_to centro_piazza, s_to vicino_piazza; Prop "palo" with name 'palo' 'legno', description "Sei troppo lontano per vedere qualsiasi dettaglio.", found_in piazza_sud piazza_nord; Prop "cappello" with name 'cappello', description "Sei troppo lontano per vedere qualsiasi dettaglio.", found_in piazza_sud piazza_nord; Prop "soldati di Gessler" with name 'soldato' 'soldati', description "Uomini sgarbati e violenti, non di queste parti.", before [; FireAt: print_ret "Sono abbondantemente in sovrannumero."; Talk: print_ret "Una simile feccia non merita la tua considerazione."; ], found_in piazza_sud piazza_nord centro_piazza mercato, has animate pluralname; Tutta roba abbastanza standard: solo una stanza (Room) e tre oggetti di scenografia (Prop). Il "vero" palo si trova nella stanza che rappresenta il centro della piazza, il ciò vuol dire che il giocatore non può ESAMINARLO da questa stanza (tecnicamente, non è "in scope"). Comunque, siccome noi reputiamo che Wilhelm possa vedere l'intera piazza da dove sta, abbiamo bisogno di creare un falso palo e un falso cappello, che possono essere trovati (proprietà found_in) sia in questa stanza che nella parte nord della piazza, anche se sono "troppo lontani" per una descrizione dettagliata. Gli odiosi soldati sono anch'essi implementati abozzandoli appena; devono star lì, ma non devono fare molto altro. La loro caratteristica più interessante è probabilmente il fatto che intercettano due azioni, FireAT e Talk, che non sono parte della libreria, ma piuttosto sono due nuove azioni che abbiamo definito apposta per questo gioco. Parleremo di queste azioni in "Verbi, Verbi, Verbi", all'interno del capitolo 10, e a quel punto il ruolo di questa proprietà before avrà molto più senso. Il centro della piazza ********************** Le attività che si svolgono qui sono fondamentali per la trama del gioco. Wilhelm è arrivato dalla parte sud della piazza, e ora incontra il palo su cui è stato posto il cappello. Egli può fare tre cose: 1. Ritornare a sud. Questo è permesso, ma non è altro che una piccola perdita di tempo - non c'è niente di utile a sud. 2. Prestare omaggio al palo, e poi procedere verso nord. Questo è permesso, ma stravolge tutta la storia. 3. Provare ad andare a nord senza salutare il palo. Per due volte un soldato lo fermerà e gli darà un avviso verbale. Al terzo tentativo, la sua pazienza sarà esaurita e Wilhelm verrà portato a fare il suo numero. Così ci sono due azioni che dobbiamo aspettarci: Salute (intercettata dal palo) e Go (che può essere intercettata dalla stessa stanza). Go è un'azione standard della libreria. Salute è un'azione che noi abbiamo sviluppato; vediamocela con questa, anzitutto. Ecco una prima versione della stanza e del palo: Room centro_piazza "In mezzo alla piazza" with description "C'è meno folla al centro della piazza; la maggior parte della gente preferisce tenersi il più lontano possibile dal palo che troneggia in questo luogo, reggendo quell'assurdo cappello cerimoniale. Un gruppo di soldati rimane nei pressi, osservando chiunque passi di qui.", n_to piazza_nord, s_to piazza_sud; Furniture palo "palo di legno" centro_piazza with name 'palo' 'legno' 'tronco' 'pino' 'cappello' 'nero' 'rosso' 'cuoio' 'tesa' 'piume', description "Il palo, il tronco di un piccolo pino, solo pochi centimetri di diametro, sarà alto tre o quattro metri. Sulla cima è stato appoggiato con cura il ridicolo cappello di cuoio nero e rosso di Gessler, dalla larga tesa e adornato con un ciuffo di piume d'oca tinte.", salutato false, before [; Salute: self.salutato = true; print_ret "Saluti il cappello sull'alto palo. ^^ ~Grazie davvero, messere~, sghignazzano i soldati."; ], has scenery; La stanza avrà presto bisogno di nuovo lavoro, ma il palo è completo (notate che abbiamo semplificato leggermente la faccenda facendo sì che un solo oggetto rappresentasse sia il palo, sia il cappello che si trova sul palo). Esso menziona una proprietà che non abbiamo ancora incontrato: salutato. Che coincidenza: la libreria fornisce una proprietà con un nome perfettamente adatto al nostro gioco, e per di più in italiano; sicuramente no? No, naturalmente no. La proprietà salutato non è una proprietà standard della libreria; è una proprieta che noi abbiamo appena inventato. Notate come è stato facile - abbiamo semplicemente incluso la riga: salutato false, nella definizione dell'oggetto, e voilà, abbiamo aggiunto la nostra proprietà fatta in casa, inizializzandola al valore false. Ora, quando dobbiamo cambiare lo stato della proprietà, possiamo semplicemente scrivere: palo.salutato = true; ... palo.salutato = false; o soltanto (all'interno dell'oggetto palo): self.salutato = true; ... self.salutato = false; Possiamo anche controllare, se necessario, quale è lo stato attuale della proprietà: if (palo.salutato == true) ... Notate che usiamo == come test per "è uguale a"; sono due segni d'ugualianza, e non uno solo come accade per assegnare un valore. Definire una nuova variabile proprietà che, invece che applicarsi a tutti gli oggetti nel gioco (come fanno le proprietà standard della libreria), è specifica solo di una classe di oggetti o anche - come in questo caso - solo di un singolo oggetto, è una tecnica potente e comune. In questo gioco abbiamo bisogno di una variabile vero/falso per indicare se Wilhelm ha salutato il palo o no: il modo migliore è quello di crearne una che sia parte del palo. Così, quando il palo intercetta l'azione Salute, noi facciamo due cose: usiamo l'istruzione self.salutato = true per registrare il facco, e usiamo un'istruzione print_ret per dire al giocatore che il saluto è stato "graditamente" ricevuto. NOTA: creare una nuova proprietà variabile come questa - all'interno del palo, come in questo caso - è l'approccio raccomandato, ma non è l'unica possibilità. Menzioneremo brevemente alcuni approcci alternativi in "Leggere il codice di altre persone", nel capitolo 14. Torniamo al centro della piazza. Come abbiamo già detto, vogliamo individuare i tentativi di Wilhelm di lasciare questa stanza, cosa che possiamo fare intercettando l'azione Go nella proprietà before. Abbozziamo il codice di cui abbiamo bisogno: before [; Go: if (noun == s_obj) { Wilhelm prova ad andare verso sud } if (noun == n_obj) { Wilhelm prova ad andare verso nord } ]; Possiamo facilmente intercettare l'azione Go, ma in che direzione si sta muovendo? Bene, scopriamo che l'interprete trasforma un comando VAI VERSO SUD (o solo SUD, o S) in un'azione Go applicata ad un oggetto s_obj. Questo oggetto è definito dalla libreria; ma perchè non è chiamato semplimente "south"? Bene, perchè abbiamo già un altro tipo di "sud", la proprietà s_to usata per dire cosa si trova in direzione sud quando definiamo una stanza. Per evitare di confonderle, s_to significa "verso sud" e s_obj significa "sud quando il giocatore lo scrive come oggetto di un verbo". L'identità dell'oggetto verso cui è diretta l'azione corrente è conservata nella variabile noun, così possiamo scrivere l'istruzione if (noun == s_obj) per verificare se il contenuto della variabile noun è uguale all'identificatore dell'oggetto s_obj - e quindi se Wilhelm sta provando ad andare verso sud. Un'altra simile istruzione controlla se egli sta cercando di andare verso nord, e questo è tutto quello che ci interessa; possiamo lasciare che gli altri movimenti se la sbrighino da soli. Le parole non fanno parte del nostro gioco; sono solo un'annotazione temporanea che ci ricorda che, se delle istruzioni devono essere eseguite in questa situazione, questo è il punto in cui metterle. Attualmente, questo è il caso più semplice; è quando Wilhelm prova ad andare a nord che il divertimento comincia. Dobbiamo scegliere una fra due diverse linee di comportamento, a seconda che egli abbia o meno salutato il palo. Ma noi sappiamo se ciò è successo; la proprietà salutato del palo può dircelo. Così espandiamo il nostro abbozzo in questo modo: before [; Go: if (noun == s_obj) { Wilhelm prova ad andare verso sud [1]} if (noun == n_obj) { Wilhelm prova ad andare verso nord... if (palo.salutato == true) { ...ed ha salutato il palo [2] } else { ...ma non ha salutato il palo [3] } } ]; Ehi! guarda qua: un'istruzione if annidata dentro un'altra istruzione if. E c'è di più: l'istruzione if interna ha una clausola else, il che significa che possiamo eseguire un blocco di istruzioni se la condizione (palo.salutato == true) è vera, e un altro blocco di istruzioni se invece non è vera. Rileggetela con attenzione, guardando come le graffe si accoppiano; è abbastanza complessa, e avete bisogno di comprendere cosa sta succedendo. Una cosa importante da ricordare è che, a meno che voi non inseriate graffe per cambiare ciò, la clausola else si riferirà sempre con l'istruzione if più recente. Paragonate questi due esempi: if (condizione1) { if (condizione2) { sia condizione1 che condizione2 sono vere } else { condizione1 è vera e condizione2 è falsa } } if (condizione1) { if (condizione2) { sia condizione1 che condizione2 sono vere } } else { condizione1 è falsa } } Nel primo esempio, la clausola else si riferisce all'if più recente, mentre nel secondo caso la diversa posizione delle graffe porta la clausola else a riferirsi con l'if precedente. Ripensateci fino a che non comprendete il perchè. NOTA: noi abbiamo usato l'indentazione come guida visiva per indicare come sono in relazione gli if e gli else. Siate cauti, però; il compilatore assegna ogni else al suo if soltanto sulla base del raggruppamento logico (n.d.t.: dato dalle graffe), senza prestar attenzione a come è disposto il codice. Torniamo alla proprietà before. Dovreste essere in grado di vedere che i casi marcati con [1], [2] e [3] corrispondono ai tre possibili sviluppi dell'azione che abbiamo elencato all'inizio di questa sezione. Scriviamo il codice per questi, uno alla volta. Caso 1: Ritornare a sud Nel primo caso, Wilhelm cerca di tornare a sud; non c'è molto da fare: avvertimenti 0, ! per contare gli avvertimenti dei soldari before [; Go: if (noun == s_obj) { self.avvertimenti = 0; palo.salutato = false; } if (noun == n_obj) { if (palo.salutato == true) { andando verso nord, dopo aver salutato il palo } else { andando verso nord, senza aver salutato il palo } } ]; Wilhelm può arrivare fino al centro della piazza, dare un'occhiata al palo e ritornare prontamente a sud. Oppure può fare uno o due (ma non tre) tentativi per andare a nord, prima, e poi andare a sud. Oppure potrebbe essere veramente perverso e salutare il palo per poi tornare verso sud. In tutti questi casi, noi lo riporteremo a zero, come se non avesse ricevuto avvertimenti (indipendentemente da quanti ne ha effettivamente ricevuti) e come se il palo non fosse stato salutato (indipendentemente dal fatto che lo abbia salutato oppure no). In effetti facciamo conto che il soldato abbia davvero la memoria corta, e che si dimentici completamente di Wilhelm se il nostro eroe si allontana dal palo. Per fare tutto ciò abbiamo aggiunto una nuova proprietà e due istruzioni. La proprietà è avvertimenti, e il suo valore contine il numero di volte che Wilhelm ha provato ad andare verso nord senza aver salutato il palo: 0 inizialmente, 1 dopo il pirmo avvertimeno, 2 dopo il secondo, 3 - oh, no! salutato non è una proprità della libreria standard, è una che noi abbiamo creato per andare incontro ad un bisogno specifico. La nostra prima istruzione è self.avvertimenti = 0, che azzera il valore della proprietà avvertimenti dell'oggetto corrente - la stanza centro_piazza. La seconda istruzione è palo.salutato = false, che significa che il palo non è stato salutato. Ecco qua: la memoria del soldato è cancellata, e le azioni di Wilhelm sono dimenticate. Caso 2: Andare a nord dopo aver prestato omaggio al palo Wilhelm va verso nord... ed ha salutato il palo; un altro caso facile: avvertimenti 0, ! per contare gli avvertimenti dei soldati before [; Go: if (noun == s_obj) { self.avvertimenti = 0; palo.salutato = false; } if (noun == n_obj) { if (palo.salutato == true) { print "^~Arrivederci, e buona giornata.~^"; return false; } else { andando verso nord, senza aver salutato il palo } } ]; Tutto quello che dobbiamo fare è stampare un saluto sarcastico da parte dei soldati, e poi restituire false. Vi ricorderete che così facendo diciamo all'interprete di continuare a gestire l'azione, che in questo caso è un tentativo di andare a nord. Siccome questo è un movimento permesso, Wilhelm si ritroverà nella parte nord della piazza del villaggio, che definiremo tra poco. Caso 3: Andare a nord senza aver reso omaggio al palo Questo ci lascia soltanto con l'ultimo caso: andando verso nord, senza aver salutato il palo. Questo caso è più complesso degli altri, visto che dobbiamo codificare il meccanismo "tre strike e sei fuori". Abbozziamolo un po' meglio: avvertimenti 0, ! per contare gli avvertimenti dei soldati before [; Go: if (noun == s_obj) { self.avvertimenti = 0; palo.salutato = false; } if (noun == n_obj) { if (palo.salutato == true) { print "^~Arrivederci, e buona giornata.~^"; return false; } ! fine (palo salutato) else { self.avvertimenti = self.avvertimenti + 1; switch (self.avvertimenti) { 1: primo tentativo di andare a nord 2: secondo tentativo di andare a nord default: terzo e ultimo tentativo di andare a nord } } } ]; Anzitutto dobbiamo contare quante volte Wilhelm ha provato ad andare a nord. avvertimenti è la variabile che contiene questo numero, quindi noi aggiungiamo uno a qualunque valore essa contenga: self.avvertimenti = self.avvertimenti + 1. Poi, a seconda del valore di questa variabile, dobbiamo decidere che azione intraprendere: primo tentativo, secondo tentativo o confronto finale. Potremmo usare tre diverse istruzioni if: if (self.avvertimenti == 1) { primo tentativo di andare a nord } if (self.avvertimenti == 2) { secondo tentativo di andare a nord } if (self.avvertimenti == 3) { ultimo tentativo di andare a nord } o un paio if if annidati: if (self.avvertimenti == 1) { primo tentativo di andare a nord } else { if (self.avvertimenti == 2) { secondo tentativo di andare a nord } else { ultimo tentativo di andare a nord } } ma per una serie di controlli sulla stessa variabile, un'istruzione swiche è generalmente il modo più pulito per ottenere lo stesso effetto. La sintassi generica per l'istruzione switch è: switch(espressione) { valore1: cosa succede quando l'espressione vale valore1 valore2: cosa succede quando l'espressione vale valore2 .... valoreN: cosa succede quando l'espressione vale valoreN } Questo significa che, a seconda del valore corrente dell'espressione, noi possiamo ottenere diversi esiti. Ricordate che l'espressione può essere una variabile Globale o locale, una proprietà di un oggetto, una delle variabili definite nella libreria o qualsiasi altra espressione che possa avere più di un valore. Potete scrivere switch (x), se x è una variabile definita, ma anche, ad esempio, switch (x+y), se sia x che y sono variabili definite. Quei "cosa succede quando..." sono collezioni di istruzioni che implementano l'effetto desiderato per un certo valore dell'espressione valutata. Anche se l'istruzione switch (espressione { ... } ha bisogno di un paio di graffe, non ce n'è invece bisogno attorno ad ogni singolo "caso", non importa da quante istruzioni è composto. Come immaginiamo, i casi 1 e 2 contengono soltanto una singola istruzione print_ret ciascuno, così ci muoveremo velocemente oltre, al ben più interessante terzo caso - quando self.avvertimenti vale 3. Avremmo potuto scrivere questo: switch (self.avvertimenti) { 1: primo tentativo di andare a nord 2: secondo tentativo di andare a nord 3: ultimo tentativo di andare a nord } ma usare la parola default - che significa "ogni valore non ancora interessato" - è una migliore pratica di sviluppo; è meno facile produrre risultati fuorvianti se per qualche ragione non prevista il valore di self.avvertimenti non è 1, 2 o 3 come previsto. Ecco il resto del codice (con un po' di testo omesso): self.avvertimenti = self.avvertimenti + 1; switch (self.avvertimenti) { 1: print_ret "..."; 2: print_ret "..."; default: print "^~Va bene, "; style underline; print "Herr"; style roman; print " Tell, ora siete nei guai. Ve l'ho chiesto ... tiglio che cresce nella piazza del mercato.^"; move mela to figlio; PlayerTo(mercato); return true; } La prima parte in realtà visualizza solo un mucchio di testo, ed è un po' più complicata del solito perchè abbiamo voluto aggiunre enfasi alla parola "Herr" usando la sottolineatura (che in realtà appare come corsivo sulla maggior parte degli interpreti). Poi ci assicuriamo che Walter abbia la mela (nel caso non gliela avessimo data prima, durante il gioco), ci spostiamo nella stanza finale usando PlayerTo(mercato) e finalmente restituiamo true per comunicare all'interprete che abbiamo gestito da soli questa parte dell'azione Go. E così, alla fine, ecco il codice completo per il centro della piazza, l'oggetto più complicato di tutto il gioco: Room centro_piazza "In mezzo alla piazza" with description "C'è meno folla al centro della piazza; la maggior parte della gente preferisce tenersi il più lontano possibile dal palo che troneggia in questo luogo, reggendo quell'assurdo cappello cerimoniale. Un gruppo di soldati rimane nei pressi, osservando chiunque passi di qui.", n_to piazza_nord, s_to piazza_sud, avvertimenti 0, ! per contare gli avvertimenti dei soldati before [; Go: if (noun == s_obj) { self.avvertimenti = 0; palo.salutato = false; } if (noun == n_obj) { if (palo.salutato == true) { print "^~Arrivederci, e buona giornata.~^"; return false; } ! fine (palo salutato) else { self.avvertimenti = self.avvertimenti + 1; switch (self.avvertimenti) { 1: print_ret "Un soldato ti sbarra la strada. ^^ ~Hey, tu, spilungone; hai dimenticato le buone maniere? Forse sarebbe il caso di fare un bel saluto al cappello del balivo, non trovi?~"; 2: print_ret "^~Ti conosco, Tell, sei uno che porta solo guai, vero? Non vogliamo teste calde qui, quindi fai il bravo ragazzo e porgi il tuo saluto al dannato cappello. Fallo ora, non voglio chiedertelo di nuovo...~"; default: print "^~Va bene, "; style underline; print "Herr"; style roman; print " Tell, ora siete nei guai. Ve l'ho chiesto gentilmente, ma voi siete troppo orgoglioso e troppo stupido. Penso che il balivo voglia scambiare qualche parola con voi.~ ^^ Dicendo ciò, i soldati prendono te e Walter e, mentre il sergente corre via a cercare Gessler, il resto degli uomini vi trascina rudemente verso il vecchio tiglio che cresce nella piazza del mercato.^"; move mela to figlio; PlayerTo(mercato); return true; } ! fine switch } ! fine (palo non salutato) } ! fine (noun == n_obj) ]; La parte nord della piazza ************************** L'unico modo per arrivare qua è quello di salutare il palo e poi andare a nord; non molto probabilme, ma il buon design di un gioco consiste nel predire l'imprevedibile. Room piazza_nord "Lato nord della piazza" with description "Una piccola strada conduce verso nord, lasciando la piazza affollata. Al centro della piazza, di poco più a sud, vedi ancora il palo e il cappello.", n_to [; deadflag = 3; print_ret "Con Walter al tuo fianco, lasci la piazza percorrendo la strada verso nord, andando verso la conceria di Johansson."; ], s_to "Non vuoi assolutamente passarci di nuovo."; C'è solo una nuova caratteristica in questa stanza: il valore della proprietà n_to è una routine, che l'interprete esegue quando Wilhelm prova a lasciare la piazza dal lato nord. Tutto quello che fa la routine è assegnare il valore 3 alla variabile di libreria deadflag, stampare un messaggio di conferma e restituire true, concludendo così l'azione. A questo punto l'interprete noterà che deadflag non è più uguale a zero e terminerà il gioco. In effetti l'interprete controlla deadflag alla fine di ogni turno; questi sono i valori che si aspetta di trovare: * 0 : questo è lo stato normale; il gioco continua * 1 : il gioco è finito. L'interprete visualizza "Sei morto". * 2 : il gioco è finito. L'interprete visualizza "Hai vinto". * ogni altro valore : il gioco è finito, ma non ci sono nella libreria messaggi ap propriati. Piuttosto l'interprete cerca una routine chiamata DeathMessage - che noi dobbiamo fornire - dove noi possiamo definire i nostri "messaggi di conclusione" personalizzati. In questo gioco non impostiamo mai deadflag a 1, ma usiamo i valori 2 e 3. Così dovremmo definire una routine DeathMessage per dire al giocatore ciò che ha fatto: [ DeathMessage; print "Hai rovinato una bellissima leggenda."; ]; Il nostro gioco ha solo un finale personalizzato, così la semplice routine DeathMessage che abbiamo scritto è sufficiente per i nostri scopi. Se volete preparare finali multipli per un gioco, potete specificare i messaggi adatti controllando il valore corrente della variabile deadflag: [ DeathMessage; if (deadflag == 3) print "Lasci Rossella O'Hara al suo destino"; if (deadflag == 4) print "Stringi Rossella in un abbraccio appassionato"; if (deadflag == 5) print "Sei riuscito a divorziare da Rossella"; ... ]; Ovviamente, dovete assegnare il valore appripriato a deadflag quando il gioco raggiunge uno di questi finali. Abbiamo quasi finito. Nel capitolo conclusivo di questo gioco parleremo del fatale tiro dell'arco di Wilhelm.