ZX81 Allunaggio

>>> LINK A PAGINA AGGIORNATA <<<

  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .
  • .

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).

Listato programma BASICEra 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.

Immagine schermo

Il font “ZX-Spectrum” (zxspectrum.ttf) è scaricabile da:
fonts101.com/fonts/view/Uncategorized/47226/ZXSpectrum


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()

(1/2/2014)

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *