Creare avventure testuali, la guida pratica

Seconda parte del corso di videogame design che spiega come scrivere e programmare opere di narrativa interattiva

Per creare avventure testuali non servono grandi conoscenze di programmazione. E qualunque linguaggio può andare bene: Java, C++, C, Basic, Pascal, Python. Esistono anche alcuni tool che permettono di semplificare la fase tecnica e di concentrarsi quindi su quella creativa. Quest, Adrift, Pandor+, Confabula, Adventure Click, AWS sono i più famosi. 

Darkiss, una avventura testuale scritta in Inform 6

Personalmente però vi consiglio Inform 6. Si tratta di un linguaggio a oggetti e procedurale, ispirato al C++. È stato sviluppato da Graham Nelson negli anni 90 per favorire la creazione di videogiochi testuali di una certa complessità. A dispetto della fama di ambiente di sviluppo complesso per non dire complicato, Inform 6 rende il lavoro di scrittura e programmazione davvero agile e completo. Dispone infatti di una enorme quantità di librerie in grado di gestire con la massima cura (e facilità, se imparerete a usarlo) ogni aspetto del gioco.

I vantaggi di usare Inform 6, anche rispetto a Inform 7

Inoltre il compilatore di Inform 6 consente di ottenere file multipiattaforma utilizzabili su Windows, Linux, Mac, BSD, iOS, Androdi, e quindi su qualunque tipo di dispositivo fisso e mobile, tramite interpreti gratuiti come Frotz, Zoom, Gargoyle, Text Fiction, Son of Hunky Punk. Con i dovuti procedimenti, potrete pure trasformare i file Z-code dei vostri giochi in pacchetti deb per Debian e Ubuntu, app per Android, file exe per Windows.

Il codice sorgente di un gioco scritto in Inform 6

Certo, c’è anche Inform 7, rilasciato da Nelson negli anni 2000, che si ispira al linguaggio naturale e per questo è considerato più accessibile. Ma Inform 7 soffre da sempre di un problema di localizzazione in italiano, mentre Inform 6 è un linguaggio solido, maturo e molto ben documentato e sviluppato anche nella nostra lingua. Se avete già qualche (minima) conoscenza di programmazione, non farete fatica a impratichirvi con la sintassi di Inform 6, che in parte ricalca quella di C e Java e descrive l’ambiente e le dinamiche di gioco tramite l’inserimento (e l’eventuale spostamento) di oggetti dentro altri oggetti, il cui comportamento può essere gestito con una serie di proprietà e attributi.

Potete leggere questa guida pratica anche se non avete letto la guida teorica su come creare avventure testuali che costituisce la prima parte del corso. Spero però che sappiate almeno che cosa sono le avventure testuali e come si gioca. Altrimenti potete rimediare consultando le pagine di approfondimento.

Salvate lo Stregatto, una avventura dimostrativa

Avventura testuale per principianti

Per iniziare a conoscere Inform 6, faremo riferimento ai file che potete . Fate particolare attenzione a stregatto.inf, il codice sorgente del mini gioco d’esempio su cui si basa questa guida. Salvate lo Stregatto! è un giallo umoristico (e paradossale) in cui ricevete l’incarico di trovare un gatto scomparso nel corso della fiera del libro che anima il centro cittadino. I vostri amici sono i responsabili dello stand preposto alla promozione dei giochi da tavolo. Lo Stregatto è la mascotte della loro associazione: senza di esso non potranno più organizzare eventi e dovranno chiudere.

La mappa del gioco, come potete vedere dall’immagine più sotto (che potete ingrandire cliccandoci sopra), è composta da tredici locazioni, che rappresentano il centro storico della città. Insieme ai luoghi della fiera del libro, come lo stand dei vostri amici, ci sono un cinema e la pellicceria in cui, ve lo dico subito, è nascosto lo Stregatto, insieme agli altri gatti rapiti dalle due titolari per dare vita a una nuova collezione di pellicce.

Un problema tira l’altro

La mappa dell'avventura testuale Salvate lo Stregatto

L’azione inizia al Barcobaleno (che nel codice sarà rappresentato dall’oggetto Bar appartenente alla classe Room), dove Mario, uno dei membri dell’associazione, vi mette al corrente della missione. Nel bar c’è anche la vostra bottiglietta di birra, che potete prendere, provare a bere, rompere, dare (o mostrare) a qualcuno, o versare su qualcosa (come poi in effetti dovrete fare). Tutto questo tramite le proprietà dell’oggetto birra che vedremo tra poco. Secondo la mappa, dal bar potete solo spostarvi a est, verso la strada, che poi vi permetterà di raggiungere, ancora a est, lo stand dell’associazione, oppure di proseguire a nord lungo la via. Se proverete a andare a sud, dove effettivamente si potrebbe andare, il programma vi risponderà che “Laggiù non c’è niente di interessante” perché quella zona non fa parte del gioco.

Anche se sono visibili sulla mappa, all’inizio della storia ci sono ambienti che non è possibile raggiungere: il retro della pellicceria e lo stanzino in cui è celato lo Stregatto. Questo perché il giocatore ha bisogno di compiere una serie di azioni per arrivare alla fine della storia. Quindi se proverà a andare nel retro del negozio senza prima aver tolto di mezzo la proprietaria, si sentirà dire che “Il retro del negozio è chiuso al pubblico”.

Addirittura, lo stanzino in cui sono tenuti prigionieri i gatti nel retro del negozio, è inizialmente nascosto perché la porta è mimetizzata nella tappezzeria. Per vederla il giocatore deve indossare gli occhialini 3D che si trovano all’ingresso del cinema e gli permettono di “aumentare la percezione della realtà”. Quando però tenta di prenderli, il giocatore viene preceduto da un bambino che, dopo averglieli fregati, dice che per averli dovrà dargli qualcosa di altrettanto interessante.

Ludo Ergo Sum

A questo punto entra in gioco lo stand di Ludo Ergo Sum, dove è in esposizione il mazzo di carte di Darkiss, nuovo gioco horror che va di moda tra i bambini. Per prenderlo, sarà necessario far allontanare dallo stand Luigi, il tizio che vigila affinché nessuno si freghi la merce in esposizione. Parlando con lui noterete che ha una certa fame, che potrà essere placata col tortino che troverete all’Oasi del Gusto, lo stand della fiera deputato agli assaggi dei prodotti tipici del territorio.

Una volta prese le carte per il bambino, avrete il problema di non poter lasciare lo stand incustodito. Sarete bloccati finché non riuscirete ad attirare l’attenzione di un altro membro del club (tramite un’esibizione musicale di dubbia qualità). Recuperati gli occhiali dal bambino, avrete la necessità di far allontanare Mara dalla pellicceria per intrufolarvi nel retro. In giro per gli stand della fiera troverete un pacchetto di sigarette, da dare alla standista dell’Oasi del Gusto, che dopo essersene accesa una lascerà distrattamente il suo accendino sul banco. Quello, insieme alla birra, vi permetterà di dare fuoco al totem di cartone che riporta la mappa degli stand della fiera del libro.

L’incendio si svilupperà poco lontano dalla pellicceria, facendo uscire Mara e dandovi il via libera per la parte finale del gioco, nella quale vi servirà anche il libro che potrete vincere sottoponendovi al quiz dello scrittore Ido Mariani a una delle bancarelle della fiera. Come vedete, pur trattandosi di un gioco di dimensioni ridotte, le cose da fare non sono poche, così come gli elementi da gestire.

Iniziamo a esaminare il codice del gioco

La sintassi di Inform 6 ci permette di fare tutto con poche e chiare righe di codice, grazie alla potenza della classica routine di controllo IF (condizione) azione [ELSE altra azione] che è il fondamento di ogni linguaggio di programmazione. Per capire come opera il linguaggio e conoscere le potenzialità delle proprietà e degli attributi degli oggetti, iniziamo a esaminare il codice del gioco. Il file stregatto.inf dovrebbe aprirsi così:

Constant Story "SALVATE LO STREGATTO!^"; 
Constant Headline "Un intrigo interattivo";
Release 1;
Serial "141103";
Constant MAX_SCORE = 20;
Constant TASKS_PROVIDED;
Constant NUMBER_TASKS = 15;
Array task_scores -> 1 1 1 1 1 1 2 1 1 1 2 2 2 2 1;
Include "parser";
Include "verblib";
Include "replace";

Story e Headline sono le costanti che contengono titolo e sottotitolo del gioco, indispensabili per indicizzare il programma, così come Release e Serial, il cui senso è facilmente intuibile. Max_score, Tasks_provided, Number_Tasks gestiscono il sistema di punteggio insieme all’array Task_scores che attribuisce i singoli punteggi alle azioni degne di nota, definite nella funzione PrintTaskName (cercatela verso il fondo del file).

Le librerie di Inform 6

Parser, Verblib, Replace sono le librerie che, insieme alle traduzioni dei comandi e alle regole della grammatica italiana contenute in italiang, inclusa più avanti, gestiscono il parsing, ovvero l’analisi dei comandi inseriti dal giocatore al prompt e l’eventuale corrispondenza con le azioni previste dal programma.

Tra le librerie incluse nel sorgente dello Stregatto c’è anche Scenic_it, che permette di aggiungere descrizioni di elementi del gioco (in particolare dello scenario) per i quali non è necessario un oggetto specifico. In fondo al file si trova pure la procedura Initialise che dà il via al gioco, indicando in quale location il player inizia l’avventura e scrivendo, se necessario, un breve testo di presentazione, che per motivi di spazio qui taglieremo:

[ Initialise; 
location=Bar;
"^^Anche quest'anno hai deciso di fare un giro alla fiera del libro che si tiene nel centro storico della citt@`a...";
];

Ora analizziamo alcuni oggetti, con le loro proprietà e gli eventuali attributi. Le prime, assegnate con with, permettono di descrivere l’oggetto e definirne il rapporto con la presenza e le azioni del giocatore, rappresentate dalle routine dei verbi, etichettate in inglese: Take, Drop, Open, Close, SwitchOn, SwitchOff, Attack, Wear, Disrobe, Eat, Drink, Rub, Pull, Push, PushDir, Empty, EmptyT, Tie, eccetera (spero non ci sia bisogno di tradurle). I secondi, assegnati con has, servono a conferire all’oggetto caratteristiche particolari che permettono di gestirlo in maniera migliore.

Gli oggetti stanza e la classe Room

Innanzitutto definiamo la classe Room con l’attributo light per fare in modo che ogni locazione sia illuminata senza bisogno di fonti di luce come torce e candele:

Class Room has light;

La prima stanza, cioè l’oggetto che avrà la classe Room, sarà il Barcobaleno:

Room Bar "Barcobaleno" with name 'barcobaleno' 'bar' 'locale', 
scenic 'tavolino' 'tavolo' 'sedie' 'bancone' 'banco' 'sedia' 0 "L'arredamento del bar non ha alcuna importanza per lo svolgimento della tua missione.",
description "Sei al Barcobaleno, popolare ritrovo di via della Repubblica in cui ogni scusa @e buona per abbuffarsi di salatini e brindare alla salute di amici e parenti, ubriancandosi oltre misura. L'uscita è a est.", e_to Via_della_Repubblica_Sud, cant_go "L'uscita del bar @e a est.";

Come vedete, la libreria scenic_it, utilizzata come una proprietà, entra in azione permettendoci di liquidare tutto ciò che può esserci nel bar, e che a noi non serve, con una semplice battuta. La sintassi prevede la presentazione dell’oggetto con l’istruzione object oppure col nome della classe alla quale appartiene (e che gli trasmette proprietà e attributi già definiti), in questo caso Room, e poi il nome con cui sarà richiamato nel codice, Bar, e quello che invece sarà utilizzato nel gioco, “Barcobaleno”, e che potrà essere usato dal giocatore, tramite le parole indicate tra gli apici dalla proprietà name.

Le proprietà sono separate tra loro dalla virgola “,” e dopo name e l’eventuale scenic di solito viene description che è l’effettiva descrizione della stanza, stampata quando si entra. N_to, s_to, e_to, w_to, u_to, d_to, ne_to, nw_to, se_to, sw_to, in_to, out_to servono a indicare i collegamenti della stanza con le altre (quando ci sono). Cant_go può essere utilizzata per scrivere qualcosa di diverso da “Non puoi andare da quella parte.” quando il giocatore prova a muoversi in una direzione non prevista. La definizione di un oggetto si chiude con il punto e virgola “;”.

Altri oggetti

Vediamo ora tre oggetti più complessi, con cui il giocatore può interagire. Il primo è la birra:

Object birra "bottiglietta di birra" Bar 
with name 'birra',
initial "Sul tavolino che hai davanti puoi vedere la tua bottiglietta di birra.",
description [;
if (self hasnt general) "Una bottiglietta di birra ancora mezza piena."; "Una bottiglietta di birra vuota.";
],
before [;
Take: achieved(0);
Drink: "Non hai sete.";
EmptyT: if (second~=totem) "Dubito che servir@`a a qualcosa.";
if (self has general) "Ancora?!";
give self general; achieved(10);
"In segno di spregio nei confronti di questa orrida installazione, rovesci la birra sul totem della fiera del libro, lasciando che il cartone colorato si impregni ben bene di alcool.";
],
has female;

Questo oggetto introduce l’importante proprietà before che permette di gestire le azioni del giocatore con la birra prima che abbiano effetto. In base alla situazione corrente, possiamo decidere se eseguirle o meno, e come. Nel primo caso: Take: achieved(0); al giocatore basta prendere la birra per conquistare (solo la prima volta) il primo dei punti messi in palio dal gioco. E siccome nel codice non ci sono contrordini l’azione viene eseguita e la birra finisce nell’inventario del giocatore. Drink: “Non hai sete”; si occupa di un’azione plausibile, bere la birra, ma che in questo caso non è utile al completamente del gioco e anzi lo precluderebbe, dal momento che la birra serve per un’altra cosa.

La routine stampa un messaggio negativo e chiude l’azione grazie al fatto che le stringhe di messaggio a se stanti hanno il potere, come l’istruzione rtrue che vedremo più avanti, di terminare la serie di controlli sul comando del giocatore, a meno che non esistano condizioni più importanti da verificare, di solito contenute nella proprietà each_turn o in un daemon. EmptyT, che sta per Empty To, cioè Versa Su / Sul / Sullo / Sulla / Sugli / Sulle, propone un altro aspetto fondamentale della programmazione in Inform 6, ovvero la possibilità di utilizzare in un comando più di un oggetto.

Un totem da bagnare e bruciare

La routine permette di eseguire l’azione >versa la birra sul totem, che serve per rendere il totem “bruciabile” con l’accendino. C’è anche il modo per eseguirla scrivendo soltanto >versa la birra, con la routine Empty ma questo lo vedrete nel sorgente del gioco. Nel caso di EmptyT la variabile di sistema second contiene il secondo oggetto citato nella frase, che dev’essere uguale a totem perché il comando dia un risultato.

Quindi se è diverso, if (second~=totem), il gioco stamperà l’eloquente messaggio “Dubito che servirà a qualcosa.”; mettendo fine all’azione. La riga successiva verifica che la bottiglietta di birra sia ancora piena, tramite l’attributo general che, non avendo una definizione precisa (se è general è general…), può essere utilizzato come flag per le condizioni più disparate, come appunto quella della bottiglietta, che può essere (mezza) piena o vuota.

All’interno della definizione dell’oggetto possiamo riferirci a esso anche come self. Quindi la condizione if (self has general), che va messa sempre tra parantesi, sarà vera se non avremo ancora versato la bottiglietta di birra. Altrimenti stamperà: “Ancora?!”.

L’attributo general gestisce lo stato della bottiglietta

Dopo aver superato le due verifiche, la routine eseguirà l’azione richiesta che conferirà l’attributo general alla bottiglietta svuotata (cosa che ci permetterà anche di sapere più avanti che il totem è inzuppato di birra e può essere bruciato, senza “sprecare” un altro attributo general per lo stesso totem) e darà punti al giocatore, chiudendosi con un messaggio di “missione compiuta”.

Infine, l’attributo female indica il genere femminile dell’oggetto, che permette a Inform 6 di trattarlo con una grammatica adeguata. Per gli oggetti plurali come gli occhiali si usa pluralname che può essere combinato con female qualora ce ne sia bisogno (aiuole, panchine, ciliegie, sigarette…).

Il secondo oggetto di cui ci occupiamo è quello di un personaggio, lo scrittore Ido Mariani, il quale vi sottoporrà a un quiz di storia per permettervi di vincere uno dei suoi voluminosi… volumi!

Lo scrittore Ido Mariani

Object Ido "Ido Mariani" Via_Milano 
with name 'ido',
description "Il famoso scrittore di romanzi storici Ido Mariani…",
life [;
Tell, AskFor, Ask:
print "Lo scrittore Ido Mariani non ha idea di dove sia lo Stregatto, per@o ti segnala che in occasione della fiera del libro @e in atto una favolosa promozione sui suoi romanzi storici. Ti baster@a rispondere a una domanda sulla storia d'Italia per avere in omaggio, con tanto di dedica, il suo ultimo bestseller.^^Accetti? (s/n) "; if (yesorno()) { give self general; "^Ido Mariani @e contento che tu abbia accettato di partecipare alla sua promozione e ti pone subito la domanda a cui devi rispondere. ~In quale citt@a @e nato Leonardo da Vinci?~";
}
"^Come non detto!";
Answer:
if (self hasnt general) "Ido Mariani non ti ha fatto nessuna domanda.";
give self ~general;
if (noun~='vinci//') "~Sbagliato!~ esclama Ido Mariani…";
move libro to player;
remove ido;
achieved(3);
"~Esatto!~ esclama Ido Mariani, molto colpito dalla tua preparazione in materia…";
],
react_before [;
if (self has general && action~=##answer) "Invece di tergiversare, rispondi alla domanda di Ido Mariani.";
],
has proper animate;

La proprietà life degli oggetti animate e talkable

L’oggetto di Ido fa entrare in gioco la proprietà life che gestisce i verbi dedicati all’interazione con i personaggi, che per essere tali devono avere l’attributo animate, in questo caso affiancato a proper per segnalare che l’oggetto ha un nome proprio che non vuole l’articolo.

Tell e Ask sono utilizzati per il dialogo e raccolgono nella variabile second gli argomenti di conversazione, quando ci sono: >chiedi a ragazza di film, >racconta a ragazza di vacanza.

AskFor serve a chiedere degli oggetti: >chiedi giornale a edicolante. Answer entra in gioco quando occorre rispondere a una domanda: >rispondi stracciatella al gelataio, >rispondi no alla maestra.

Give e Show servono a dare e mostrare oggetti, che in questo caso sono raccolti dalla variabile noun: >dai libro a professore, >mostra fumetto a tizio.

Da non dimenticare Attack, Kiss, WaveHands che però possono essere utilizzati anche con oggetti non viventi e quindi vanno inseriti nella proprietà before.

Nel nostro caso, essendo il gioco molto semplice, Tell, Ask e AskFor hanno una routine unica che porta Ido, dopo essere stato approcciato dal giocatore, a proporre il suo quiz. La funzione preimpostata (yesorno()) ci permette di decidere se accettare o meno la sfida, digitando “s” o “n”. Se accettiamo, il gioco innescherà la proprietà react_before di Ido (tramite l’assegnazione di general) che ci impedirà di fare qualunque altra azione se prima non avremo risposto al quiz. Un espediente utile per evitare che il giocatore si sottragga alle sue “responsabilità”, utilizzabile anche in situazioni più critiche.

Il terzo e ultimo oggetto che analizziamo in realtà… sono due!

Il retro della pellicceria

Room Retro "Retro della pellicceria" 
with name 'retro',
description "Sei nel retro della pellicceria…",
before [;
Go: if (noun==n_obj && porta notin location) "Non puoi andare da quella parte.";
if (noun==n_obj && porta hasnt open) "La porta @e chiusa!"; Listen: "Ti sembra di sentire dei miagolii provenire da qualche parte intorno a te."; ], each_turn [; if (occhiali has worn && porta notin retro) { achieved(12); move porta to retro; "^Grazie agli occhiali 3D che indossi in questo momento, puoi notare che sulla parete nord della stanza c'@e una porta mimetizzata nella tappezzeria.";
}
switch(random(4))
{
1: "^Ti sembra di sentire dei miagolii provenire da qualche parte intorno a te.";
2, 3, 4: rtrue;
}
],
n_to porta,
s_to Negozio;

La porta nascosta nella tappezzeria

Object porta "porta" 
with name 'porta',
describe "^Mimetizzata nella tappezzeria della parete nord della stanza, puoi vedere una porta.",
description [;
if (self has open) "La porta @e aperta."; else "La porta @e chiusa.";
],
door_to Nascondiglio,
door_dir n_to,
with_key chiave,
before [;
Attack: if (self has open) "Non c'@e pi@u bisogno di buttare gi@u la porta, ora che @e aperta.";
"Una mezzasega come te non riuscirebbe mai a buttare gi@u una porta con una spallata."; Open: if (Lara hasnt general) "~Stai lontano da quella porta, maledetto ficcanaso!~ strilla Lara impedendoti di raggiungerla."; if (chiave notin player) "@E chiusa a chiave.";
if (self has locked && chiave in player) <>;
],
after [;
Unlock: if (self hasnt locked && self hasnt open) give self open;
],
has door static female lockable locked openable;

I fantastici occhialini 3D

Innanzitutto, nel sorgente del retro del negozio notiamo la proprietà each_turn che, a ogni mossa del giocatore in quella stanza o in presenza di quell’oggetto o personaggio, verifica delle condizioni per eseguire certe azioni. In questo caso, fa apparire la porta nascosta nella tappezzeria non appena il giocatore indossa gli occhiali 3D che “aumentano la percezione della realtà”. Inoltre, tramite switch(random(4)), che funziona come qualunque altra istruzione simile, stampa un messaggio di allerta sulla presenza di gatti nei dintorni se il numero estratto a caso da random tra 1 e 4 è 1.

Da vedere anche la routine Go: di before, che regola i movimenti del giocatore verso la direzione nord, rappresentata dall’oggetto di sistema n_obj. Se la porta non è stata ancora trovata, stampa un ordinario “Non puoi andare…”. Se invece la porta c’è ma non ha l’attributo open la routine ci dice che è chiusa. La stessa proprietà n_to della stanza è collegata all’oggetto “porta” anziché a quello del nascondiglio. Questo perché la porta, grazie allo specifico attributo door, possiede proprietà speciali che le permettono di essere utilizzata come collegamento tra un oggetto e l’altro (che quando richiesto può essere un ponte, una scala, una finestra, eccetera).

Le proprietà dell’attributo door

Door_to stabilisce in quale stanza condurrà la porta, una volta aperta e attraversata. Door_dir indica la direzione associata alla porta, mentre with_key implica che la porta, se chiusa a chiave con l’attributo Locked, possa essere sbloccata solo con quel determinato oggetto (nel nostro caso la chiave che troveremo dopo aver steso Lara).

Quando il giocatore raggiungerà il nascondiglio in cui lo Stregatto è tenuto prigioniero, potremo mettere fine al gioco grazie alla variabile Deadflag che serve a stampare il messaggio finale della storia e terminare il programma, e il cui contenuto positivo o negativo dipende dalla situazione in cui si trova il giocatore (1 = Sei morto; 2 = Hai vinto; 3, 4, 5… = messaggi a piacere personalizzabili nella funzione DeathMessage).

Il brivido della compilazione

Una volta che il gioco sarà pronto, potremo provare a compilarlo nel formato .z5 grazie al compilatore gratuito inform, scaricabile da vari repository (compreso quello di Ubuntu) o da http://ifarchive.org/if-archive/programming/inform6/executables/.

Dopo esservi assicurati che il file sia eseguibile anche al di fuori della sua directory /usr/bin o quella che sarà, scrivete da riga di comando:

inform stregatto.inf +language_name=Italian +include_path=lib -C0

+language_name serve a specificare che il gioco è stato scritto in italiano e richiede la relativa libreria, +include_path segnala dove cercare i file non compresi nella directory corrente (in questo caso quelli delle librerie richiamate dal gioco), -C0 indica che il gioco utilizza caratteri speciali come le lettere accentate.

In realtà è molto più comodo scrivere e compilare codice utilizzando un editor dedicato, come l’ottimo Wide di Alessandro Schillaci, che permette anche di progettare giochi nel formato multimediale Glulx, con grafica e sonoro.

Su Inform 6, la sua sintassi e le enormi potenzialità nella programmazione di videogiochi testuali ci sarebbero ancora molte cose da dire, ma lo spazio qui volge al termine. Per approfondire il discorso ci sono molti manuali, in inglese e in italiano, scaricabili da Internet insieme ai listati dei giochi di esempio. Per cominciare, date un’occhiata ai file scaricati tramite questa guida, non vi arrendete alle prime difficoltà e ricordate che anche nella vita, a volte, ci vuole un po’ di spirito di avventura!

Marco Vallarino