INFORMATICA
ZX81 Allunaggio
Le origini
Correva l’anno 1982, era appena iniziata l’era degli home computer che avrebbe portato nelle mani degli appassionati di quel tempo le prime macchine programmabili accessibili ai comuni mortali (leggi anche studenti squattrinati).
Questi microcomputer erano basati su CPU a otto bit come lo Z80 e il 6502 funzionanti a pochi MHz di clock. In confronto alle macchine attuali disponevano di pochissima memoria, da una base di 1..5 kbyte (i famosi “kappa”) fino a 16..64 kbyte tramite costose espansioni. Non avevano un vero sistema operativo, ma un interprete BASIC subito pronto all’accensione. I programmi erano salvati su audiocassetta, e caricare in memoria un programma di una decina di kbyte richiedeva alcuni minuti.
All’arrivo di una internet praticamente utilizzabile mancavano ancora tre lustri, e le riviste, a quel tempo unica fonte di informazione, si prodigavano in lezioni sul linguaggio BASIC. Alcune più specialistiche si avventuravano anche nei rudimenti del linguaggio macchina (la jungla dei numeri esadecimali).
Al momento in cui scrivo, mi accorgo con una certa inquietudine di avere l’età che avevano i miei genitori quando avevo espresso il desiderio di usare un computer. E ricordo ancora la loro espressione tra l’incredulo e il perplesso: “Un computer? E a cosa ti serve?”. Nel loro immaginario associavano la parola computer alla NASA o a grandi centri di calcolo aziendali. Dovetti mettere via molti mesi di “paghette” per potermene permettere uno di seconda mano da un compagno di classe. Ironicamente lo aveva acquistato di nascosto assieme ad un amico perché i loro genitori a loro volta non volevano vedere “quelle trappole” per casa e quindi era costretto a liberarsene… davvero altri tempi.
Ma veniamo alla programmazione. Quell’anno su una rivista è apparso il seguente programma per
Sinclair ZX81, che personalmente ha rappresentato una fonte di riflessione e ispirazione, quando ancora non avevo a disposizione alcun tipo di oggetto programmabile. Il programma è molto breve, praticamente solo un demo, in fondo era pensato per stare nella piccola memoria da un solo kappa di cui era dotato di base questo computer.
Nella parte alta dello schermo venivano riprodotti gli strumenti di bordo, mentre il lander era rappresentato da un carattere $ che si avvicinava o si allontanava dal suolo (il lato sinistro dello schermo).
Era una triste consuetudine quella di presentare listati contenenti errori, e anche questo credo non faccia eccezione. Mi sembra che con i valori specificati risulti molto poco utilizzabile, però magari sulla macchina reale aveva un comportamento adeguato. Nonostante questo, la cosa interessante era che nel suo piccolo aveva tutte le caratteristiche di interattività necessarie non solo per un gioco, ma più in generale per un qualsiasi processo di controllo che evolve nel tempo.
In pochissime righe comprendeva infatti il concetto di ciclo di elaborazione, aggiornamento periodico di valori e posizioni, variabili di stato, acquisizione nuovi dati (i tasti premuti).
- Le righe dalla 10 alla 60 inizializzano l’ambiente (variabili di lavoro e schermo).
- Le righe dalla 70 alla 160 formano il ciclo di elaborazione principale (che termina quando viene verificata la condizione alla riga 80).
- Le righe dalla 170 in poi servono per la stampa del risultato e il riavvio dell’elaborazione.
Proviamo con Python!
Ahimè, ci si rende subito conto che è semplicemente impossibile una traduzione pari pari del codice. Tanto per iniziare la console (o terminale o shell) non permette di (ri)posizionare i caratteri dove si vuole. Il terminale sotto Linux è appena un po’ più amichevole, ma la console di Windows è davvero troppo elementare, come un semplice rotolino di carta stampata… per non dire altro. E poi non c’è un modo realmente pratico e portabile per leggere direttamente la tastiera come si faceva con INKEY$ in BASIC o con keypressed in Pascal.
L’alternativa è usare un diverso tipo di interfaccia utente che permetta queste funzioni, come un’interfaccia grafica (GUI) moderna. Ma affrontare l’uso di un’interfaccia grafica richiede uno studio troppo pesante per essere affrontato in un colpo solo da chi sta muovendo i primi passi (classi, oggetti, funzioni/argomenti, eventi ecc), e in ogni caso non si potrebbe mantenere la semplicità e brevità del codice BASIC originale. Quindi il principiante, dopo i primi successi con hello world, calcoli vari, cicli e if, quando vorrebbe dare una veste di “normale interattività” e “normale aspetto” alle cose appena studiate, si trova di colpo proiettato in un mondo di eccessiva complessità.
C’è una soluzione? Ebbene no. Oggi qualunque cosa che si possa definire standard, portabile, multi piattaforma, usabile, a distanza di oltre 30 anni è oggettivamente più complessa di come erano le cose una volta, e bisogna armarsi di santa pazienza e studiare gli strumenti attuali.
Python fortunatamente rende anche questo compito più semplice e meno prolisso rispetto ad altri linguaggi.
L'interfaccia a caratteri
L’interfaccia a caratteri, considerata obsoleta da molti, e rimpianta da altri, per certe cose era effettivamente più semplice da utilizzare.
Seppure solo in modalità testuale monocromatica, con lo ZX81 era infatti possibile realizzare delle animazioni su video semplicemente (ri)scrivendo periodicamente delle stringhe di caratteri in posizioni ben precise dello schermo. Lo schermo infatti era gestito come una semplice griglia di caratteri indirizzabile con l’istruzione BASIC:
PRINT AT riga,colonna;
Vi era un unico processo in esecuzione, l’interprete BASIC, e il programma BASIC aveva accesso ad ogni parte della macchina, ad esempio era possibile leggere direttamente la tastiera per verificare se e quale tasto fosse premuto in quell’istante:
A$=INKEY$
...addirittura era anche possibile andare a leggere lo stato delle porte logiche a cui erano direttamente collegati i fili dei tasti. Oggi tutto questo non è più possibile: l’hardware è completamente diverso, i sistemi operativi sono multitasking, le azioni su tastiera e mouse vengono lette dal sistema operativo e trasmesse alle applicazioni attive sotto forma di messaggi.
La GUI (graphical user interface)
Dagli anni 90 hanno iniziato a diffondersi in massa le interfacce grafiche.
Non che non esistessero, lo
Xerox Alto
era già stato costruito vent’anni prima, ma sono gli
anni 90 a vedere l’arrivo su vasta scala delle
GUI e della multimedialità sui PC compatibili “da casa”.
In un sistema grafico a finestre è il sistema
operativo che segnala alla finestra attiva quando avviene qualcosa (azioni sulla
tastiera, sul mouse ecc), per cui il nostro
programma va completamente ripensato sotto forma di
callback, cioè funzioni da
richiamare solo in risposta ad un certo evento.
Il concetto fondamentale da comprendere è che fintanto che non
viene rilevato alcun evento, la finestra non fa niente, se non
restare in attesa tramite un ciclo interno chiamato
mainloop o
event
loop. Il codice seguente è il minimo indispensabile per creare una finestra con
la libreria grafica tk e avviare il ciclo eventi (NOTA:
se si usa Python2 scrivere Tkinter maiuscolo):
import tkinter as tk
root = tk.Tk()
root.mainloop()
Questa finestra, qui chiamata root, non fa nulla, se non avere la capacità di base di
ridimensionarsi ingrandirsi o ridursi come tutte le finestre. Una
libreria grafica fornisce già pronti da utilizzare anche numerosi
widget
(window objects), come caselle di input, label di testo, elenchi a discesa per le scelte ecc. E tra questi in particolare il canvas, su cui si possono rappresentare immagini, figure grafiche e testi arbitrari.
Inoltre, tra i possibili eventi a cui una GUI può rispondere chiamando una callback, vi sono anche i timers, grazie ai quali una funzione può essere richiamata periodicamente anche senza interventi esterni da parte dell’utente.
Alla nostra finestra di base possiamo quindi cominciare ad aggiungere
una callback per “catturare” i tasti premuti.
Si deve specificare un
bind (legame) tra l’elemento grafico che deve
reagire all’evento (in questo caso l’intera finestra), il tipo di evento
e la funzione che lo deve gestire.
import tkinter as tk
#_________________________________________________
def keygest(ev):
if ord(ev.char) == 27: # Se premuto Esc esci
root.quit()
#_________________________________________________
root = tk.Tk()
root.bind("<Key>", keygest)
root.mainloop()
Poi predisponiamo una funzione da chiamare periodicamente
circa dieci volte al secondo grazie ad un timer interno. Questa funzione
va chiamata esplicitamente almeno una volta prima di avviare il mainloop.
import tkinter as tk
#_________________________________________________
def keygest(ev):
if ord(ev.char) == 27: # Se premuto Esc esci
root.quit()
#_________________________________________________
def elabora():
root.after(100, elabora)
# Riavvia la funzione dopo
# circa 100 millisecondi
#_________________________________________________
root = tk.Tk()
root.bind("<Key>", keygest)
elabora()
root.mainloop()
Infine aggiungiamo un
canvas con sfondo nero su cui rappresentare
il testo, largo 512 pixel e alto 384 pixel. Adesso abbiamo
l’ossatura funzionale completa necessaria per il nostro programma di allunaggio rivisitato.
import tkinter as tk
#_________________________________________________
def keygest(ev):
if ord(ev.char) == 27:
root.quit()
#_________________________________________________
def elabora():
root.after(100, elabora)
#_________________________________________________
root = tk.Tk()
root.bind("<Key>", keygest)
canv = tk.Canvas(root, width=512, height=384,
bg="black", highlightthickness=0)
canv.pack()
elabora()
root.mainloop()
Allunaggio reborn
La seguente versione in Python con GUI elimina diverse imprecisioni della versione BASIC, come la possibilità di avere altezze negative, carburante sotto zero ecc, e i parametri di gioco come la gravità e la spinta sono modificati in modo da rendere usabile il programma.
Anche se il programma come numero di righe è decisamente più lungo, molte di esse servono solo alla definizione e aggiornamento degli elementi grafici, ma la logica, una volta capito come è inserita nella struttura funzionale della GUI, non è più complessa di quella del programma BASIC, anzi direi che è anche meno “intricata”.
Per mantenere elementare la struttura si è fatto uso esclusivamente di variabili globali (cioè leggibili e modificabili in ogni punto del programma), ma va da se che è una pratica sconsigliabile in un programma più grande.
È comunque importante notare come l’acquisizione dei tasti premuti sia asincrona e indipendente dal codice contenuto nella funzione elabora. E che la funzione elabora stessa non è strutturata con un ciclo while ma una volta eseguita restituisce immediatamente il controllo al mainloop della finestra. Il mainloop a sua volta riavvia la funzione dopo il tempo impostato con il metodo after. Questo permette la coesistenza sia del ciclo continuo di elaborazione, sia della responsività dell’intera finestra, che altrimenti resterebbe “congelata” fino al termine della callback.
Come ulteriore “gigionata”, il testo nel canvas è rappresentato con il font originale Sinclair su una griglia virtuale di 32×24 caratteri, che era il formato video sul televisore.
import tkinter as tk
import random
#_________________________________________________
def init():
global v, s, th, f, strumenti, lander
v = 0 # velocita'
s = 1500 # altezza
th = 0 # motori
f = 1000 # carburante
canv.delete("all")
canv.create_text(
0, 0, text="MOTORI BENZA VELOCIT. ALTEZZA",
fill="white",
font=("ZX-Spectrum", 12), anchor="nw")
strumenti = canv.create_text(
0, 16, text="", fill="white",
font=("ZX-Spectrum", 12), anchor="nw")
lander = canv.create_text(
int(s/100)*16, 80, text="$", fill="white",
font=("ZX-Spectrum", 12), anchor="nw")
#_________________________________________________
def keygest(ev):
global th
# Se premuto da 0 a 9 calcola th
if "0" <= ev.char <= "9":
th = ord(ev.char) - 48
# Se premuto Invio ricomincia
elif ev.char == "\r" and s == 0:
init()
elabora()
# Se premuto Esc esci
elif ord(ev.char) == 27:
root.quit()
#_________________________________________________
def elabora():
global v, s, th, f
if f == 0: th = 0 # No fuel no thrust
v += th - 1 # Calcola nuova velocita`
s += v # Calcola nuova posizione
if s < 0: s = 0 # No lander sotto terra
f -= 10 * th # Calcola contenuto serbatoio
if f < 0: f = 0 # No carburante sotto zero
# Aggiorna letture strumenti e posizione lander
st = "%-5d %-5d %-5d %-5d" % (th, f, v, s)
canv.itemconfigure(strumenti, text=st)
canv.coords(lander, int(s/100)*16, 80)
if s > 0: # Se ancora in volo
root.after(100, elabora) # riavvia
else:
canv.itemconfigure(
lander,
text="$ SALVO" if v >= -10 else "$ CRASH")
#_________________________________________________
root = tk.Tk()
root.title("ZX81 allunaggio")
root.resizable(False, False)
root.bind("<Key>", keygest)
canv = tk.Canvas(
root, width=512, height=384,
bg="black", highlightthickness=0)
canv.pack()
init()
elabora()
root.mainloop()