La sfida di Natale, edizione 2023

Logiker ha appena presentato i risultati dell’edizione 2023 della Vintage Computing Christmas Challenge (VC³ 2023), un’interessante concorso non competitivo di programmazione, orientato principalmente ai sistemi vintage, che si svolge nel periodo natalizio.

L’obiettivo è la realizzazione di un programma in grado di rappresentare fedelmente la seguente immagine, utilizzando un codice più compatto possibile:

Screenshot dell'immagine da realizzare per la Vintage Computing Christmas Challenge 2023
L’immagine da realizzare per la Vintage Computing Christmas Challenge 2023

I partecipanti sono stati più di 200 (Logiker ha condiviso tutte le entry su Google Drive, demozoo.org e scene.org; consulta la homepage per ulteriori link e dettagli) e, oltre ai consueti BASIC e Assembly, sono stati utilizzati i linguaggi più disparati, tra cui APL, SQL e Canvas (ambiente di cui non ero a conoscenza).
Alcuni programmi sono davvero impressionanti e meritevoli di studio!

Il mio programma

Avendo già partecipato all’edizione dell’anno precedente, a inizio Dicembre 2023 sono stato invitato da Logiker a prendere parte alla nuova sfida; ho così realizzato un programma in linguaggio BASIC per l’home computer Sinclair ZX Spectrum.

Il programma, chiamato The Snowing Christmas Sweater, è costituito da un’unica riga di codice che occupa solamente 63 byte e simula un effetto “nevicata”, stampando gli asterischi “*” in modo pseudo-casuale, fino a produrre l’immagine richiesta:

3 LET x=RND*VAL "18": PRINT AT x,INT (RND+RND)*VAL "18"-ABS (x+VAL "6"*INT (RND*INT PI)-VAL "15");"*": GO TO PI
Animazione che mostra il mio programma in esecuzione su ZX Spectrum
Il mio programma in esecuzione su ZX Spectrum

Per semplificare la realizzazione, ho deciso di procedere per piccoli passi, ponendomi inizialmente l’obiettivo di stampare l’immagine richiesta nella sua interezza, per poi modificare il programma per aggiungere l’effetto “nevicata” ed apportare le opportune ottimizzazioni.

Il primo passo è stato stampare una linea obliqua di asterischi. Sperimentando con il codice, mi sono reso conto che nella stampa con PRINT AT, è possibile specificare valori negativi come numero di riga e/o di colonna, senza causare errori nell’interprete, che utilizza effettivamente i valori assoluti. Con questo accorgimento, è stato possibile disegnare due segmenti con un singolo ciclo FOR:

100 FOR x=0 TO 18
200 PRINT AT x,x-3;"*"
300 NEXT x
Animazione che mostra 2 segmenti di asterischi, disegnati con un ciclo FOR
2 segmenti di asterischi, disegnati con un ciclo FOR

Iterando questo procedimento per 3 volte, sono riuscito ad ottenere metà della forma desiderata:

90 FOR a=-15 TO -3 STEP 6
100 FOR x=0 TO 18
200 PRINT AT x,a+x;"*"
300 NEXT x
310 NEXT a
Animazione che mostra il programma che disegna metà dell'immagine
Metà dell’immagine

L’immagine completa può quindi essere prodotta aggiungendo l’immagine stessa, ribaltata specularmente rispetto all’asse verticale, vale a dire stampando, in ciascuna iterazione, un ulteriore “*” alla colonna 18 – ABS(colonna corrente). La dimensione del programma ottenuto è: 116 byte:

90 FOR a=-15 TO -3 STEP 6
100 FOR x=0 TO 18
200 PRINT AT x,a+x;"*"
210 PRINT AT x,18-ABS (a+x);"*"
300 NEXT x
310 NEXT a
Animazione che mostra il programma che disegna l'immagine completa
Immagine completa

L’effetto ottenuto è carino, ma volevo che i fiocchi di neve*” apparissero casualmente sullo schermo. Ho quindi trasformato il programma in un ciclo infinito costituito da una sola riga di codice, che determina in ciascuna iterazione in modo pseudocasuale la posizione degli asterischi appartenenti a ciascuna delle due metà dell’immagine:

3 LET a=6*INT (RND*INT PI)-15: LET x=INT (RND*19): PRINT AT x,a+x;"*": PRINT AT x,18-ABS (a+x);"*": GO TO PI

Questa versione del programma occupa 88 byte. L’istruzione GO TO finale utilizza un espediente per ridurre la dimensione, infatti il π PI richiede solamente un token, mentre l’utilizzo della costante numerica “3” avrebbe richiesto 6 byte in più (si veda il capitolo relativo alla memoria sul manuale dello ZX Spectrum).

Nel passaggio successivo, ho ridotto il programma per utilizzare un’unico statement di PRINT, per stampare in ciascuna iterazione un asterisco, appartenente a una delle due metà dell’immagine scelta casualmente (dimensione: 83 byte):

3 LET a=6*INT (RND*INT PI)-15: LET x=INT (RND*19): PRINT AT x,INT (RND+RND)*18-ABS (a+x);"*": GO TO PI

Nel passaggio successivo, con un refactoring per rimuovere la variabile a, la dimensione del programma è scesa a 78 byte:

3 LET x=INT (RND*19): PRINT AT x,INT (RND+RND)*18-ABS (x+6*INT (RND*INT PI)-15);"*": GO TO PI

Nel passaggio successivo, sono riuscito a risparmiare ancora alcuni byte, sostituendo le costanti numeriche con l’utilizzo della funzione VAL (dimensione: 66 byte):

3 LET x=INT (RND*VAL "19"): PRINT AT x,INT (RND+RND)*VAL "18"-ABS (x+VAL "6"*INT (RND*INT PI)-VAL "15");"*": GO TO PI

Con un’ultima piccola modifica al codice che determina il valore x della riga in cui stampare il carattere “*“, ho risparmiato ancora 3 byte. La dimensione finale del programma è quindi 63 byte:

3 LET x=RND*VAL "18": PRINT AT x,INT (RND+RND)*VAL "18"-ABS (x+VAL "6"*INT (RND*INT PI)-VAL "15");"*": GO TO PI

Nota: la dimensione del programma è calcolata con: PRINT PEEK 23627+256*PEEK 23628-23755 (si veda il capitolo sulle variabili di sistema nel manuale dello ZX Spectrum).

Sicuramente, con un po’ più di tempo a disposizione (la scadenza era il 25 Dicembre), sarei riuscito a limare ancora qualche byte; mi sono riproposto di provarci comunque, come sfida personale.

Penso che con l’opportuna combinazione di colori, l’output del programma ricordi la trama di un maglione natalizio; ecco spiegato il nome The Snowing Christmas Sweater.

Screenshot dell'immagine a colori (bordo verde, carta rossa, inchiostro bianco brillante)
L’immagine a colori

Non sei d’accordo? Dai un’occhiata alla seguente immagine e dimostrami che ho torto! 😄

Ho anche realizzato un breve video per presentare il mio programma, disponibile sul mio canale YouTube:

Video di presentazione di The Snowing Christmas Sweater

Accoglienza e ulteriori ottimizzazioni

Al termine della scadenza per l’invio delle varie realizzazioni, ho pubblicato alcuni dettagli del mio programma sul gruppo Facebook BASIC on the ZX Spectrum, ricevendo con mia somma gioia l’apprezzamento da parte di Johan “Dr Beep” Koelman e Uwe Geiken, due autorità per quanto riguarda la programmazione dei computer Sinclair ZX.

Inoltre, Dr Beep ha ulteriormente ottimizzato il mio codice, riducendone la dimensione a soli 54 byte!

Partendo dal mio listato BASIC, una prima ottimizzazione apportata al codice che sceglie casualmente il valore 0 o 1 è stata la sostituzione di: INT (RND+RND) con la versione più concisa: (RND>RND).

Un’altra ottimizzazione è costituita dallo spostamento delle funzioni RND, ABS and INT all’interno della stringa processata dalla funziona VAL. Nel fare ciò, sono utilizzati i codici dei token corrispondenti alle funzioni e non i loro nomi, per cui ciascuna chiamata a una di queste funzioni occuperà solamente 1 byte. Il codice così ottenuto occupa solamente 55 byte:

3 LET x=VAL "RND*18": PRINT AT x,VAL "(RND>RND)*18-ABS (x+6*INT (RND*3)-15)";"*": GO TO PI

Un refactoring del calcolo della colonna del carattere “*” ha permesso di risparmiare ancora un byte, per cui la versione finale del codice è:

3 LET x=VAL "RND*18": PRINT AT x,VAL "(RND>RND)*18-ABS (x-3-6*INT (RND*3))";"*": GO TO PI

Riporto anche il codice formattato per BasinC (Sinclair BASIC IDE), in cui è evidente l’utilizzo dei codici dei token corrispondenti alle funzioni RND (165), INT (186) e ABS (189) all’interno delle stringa passate come parametri alla funzione VAL (vedi il set di caratteri dello ZX Spectrum):

3 LET x=VAL "\#165*18": PRINT AT x,VAL "(\#165>\#165)*18-\#189(x-3-6*\#186(\#165*3))";"*": GO TO PI

e lo stesso codice, formattato per il tool bas2tap (in cui i codici dei token sono espressi in notazione esadecimale):

3 LET x=VAL "{A5}*18": PRINT AT x,VAL "({A5}>{A5})*18-{BD}(x-3-6*{BA}({A5}*3))";"*": GO TO PI

Infine, Johan ha pubblicato sul gruppo ZX81 Owners Club il port per Sinclair ZX81 del programma. Dato che l’interprete BASIC dello ZX81 consente solo un’istruzione per linea, il programma è costituito da 3 linee:

3 LET x=VAL "RND*18"
4 PRINT AT x,VAL "(RND>RND)*18-ABS (x-3-6*INT (RND*3))";"*"
5 GOTO PI
Immagine che mostra il port per ZX81 di The Snowing Christmas Sweater
The Snowing Christmas Sweater, portato su ZX81 da Dr Beep

Concludendo, sono stato felice di partecipare, sia per i risultati raggiunti ma soprattutto perché questa sfida mi ha dato l’opportunità di divertirmi e di imparare qualcosa di nuovo sullo ZX Spectrum!


Read in English

Lascia un commento

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

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.