Hacía tiempo que tenía ganas de escribir otra entrada sobre el entrañable Spectrum, y The Spectrum, una máquina retro que se agotó al poco de salir, me ha dado la excusa perfecta.
La máquina viene con una ROM creada para la ocasión soportando 128k, con el editor del 48k. En realidad, puede emular casi cualquier modelo real de Spectrum, si le proporcionas las ROM's del Speccy original adecuadas. Basta formatear un pendrive en FAT32 o exFAT, y crear una carpeta en su interior llamada "THESPECTRUM", de la que cuelgue "ROMS". y allí dentro estarán las roms en formato <memoria>-x.rom
, donde memoria es la memoria de la máquina y x
el número de ROM. Por ejemplo, la del Speccy original será simplemente el archivo 48.rom
, la del 128k inglés o español, o incluso la del +2 gris, 128-0.rom
y 128-1.rom
. Finalmente, el Plus3 llevaba cuatro ROMs que serán numeradas del 0 al 3. Si tenemos las ROMS en el subdirectorio speccy_roms
de Descargas/
, lo haríamos así:
$ mkdir -p THESPECTRUM/ROMS
$ cd THESPECTRUM/ROMS
$ pwd
/run/media/baltasarq/SPECCY/THESPECTRUM/ROMS
$ cp ~/Downloads/speccy_roms/*.rom .
$ ls
128-0.rom 128-1.rom 48.rom plus3-0.rom plus3-1.rom plus3-2.rom plus3-3.rom
Extraemos el pendrive, conectamos el TheSpectrum, le enchufamos el pendrive, esperamos a que se inicie y... ¡ta-chán! En lugar de esa ROM con el editor del 48k (oh Dios mío), tendremos la ROM escogida según las preferencias de nuestra máquina retro emuladora. En mi caso, los archivos que escogí fueron los del Spectrum 128k de Investrónica, que después se vendió en inglaterra como Spectrum 128k con algunas modificaciones. Suele ser reconocible por la famosa parrilla a su derecha, y que la versión inglesa no llevaba teclado independiente.
Así, en mi caso pude volver a experimentar el arranque de mi Speccy de cuando tenía once años, en aquellas navidades de 1985, con su mensaje de copyright modificado ("ESPAÑOL") y un pequeño pitido indicando el estar preparado. Sería con el manual del Speccy que aprendería a manejarme con BASIC, y acabaría haciéndome informático.
Normalmente TheSpectrum arranca en el modo carrousel, donde podemos jugar a cualquier juego, pero podemos configurarlo para que arranque ya en el "modo clásico"... ¡y entonces podemos programar directamente en BASIC, como si fuera el Speccy real!
Decidí hacer una pequeña prueba. ¿Cómo dibujar un árbol fractal en BASIC? Los fractales son dibujos que podrñiamos decir que provienen del mundo matemático, y se popularizaron en los 70 del siglo XX. Son dibujos que se realizan repitiendo un patrón determinado con una escala diferente. En el caso de un árbol fractal, utilizaremos el dibujo de tres rectas en Y para crear el tronco y las dos ramas principales. Si seguimos dibujando estas Y desde las dos ramas, pero utilizando un tamaño más pequeño, tendremos ramas y ramas (creando un "frondoso follaje") hecho a partir de Y cada vez más pequeñas.
En el caso de lo que vamos a programar, cada vez la longitud de los segmentos formando la Y será la mitad de la anterior.
El lienzo
El Spectrum está siempre en modo gráfico, y de hecho no existe forma de cambiar a un modo texto "puro" (eso sería un "invento" del IBM PC, todavía por venir). Las coordenadas 0, 0 se sitúan en la esquina inferior izquierda de la pantalla, de forma que si escribimos PLOT 0, 0: DRAW 100, 100
veremos que aparece una línea desde el borde inferior izquierdo hasta casi la mitad de la pantalla. Es decir, las coordenadas son positivas hacia la derecha y hacia arriba, como si estuviéramos viendo el cuadrante positivo de un modelo 2D matemático.
La instrucción PLOT x, y
acepta dos coordenadas absolutas, donde x es la posición horizontal e y la vertical. El máximo de resolución se alcanza con x = 256
e y = 192
.
La máquina
Programar en The Spectrum no es una experiencia deliciosa, precisamente. En lugar de una carcasa de un Spectrum Plus, o incluso un PLUS 2 o PLUS 3, optaron por la carcasa (y el teclado) del modelo original. Aunque seguro que es muy icónico, es bastante poco confortable. Al margen del tacto del teclado (dead flesh o carne muerta, que era como se conocía dicho tacto en la época), ciertas funcionalidades se obtienen pulsando combinaciones de teclas con Caps Shift (CS) y Symbol shift (SS). Por ejemplo, el borrado de caracteres se obtiene pulsando CS + 0, los cursores para moverse CS + 5 hasta CS + 8, las comillas se obtienen con SS + P, los dos puntos con SS + Z...
Pero había que hacerlo y se hizo. Como era de esperar, al cabo de veinte minutos ya cometía muy pocos errores.
Guardando y cargando
¿Tenemos que hacerlo todo de una sentada? ¿Y después perder el código?
Afortunadamente, no, Retro Games nos tiene cubiertos aquí. Si guardamos el programa (SAVE guardaría el programa en cinta, con LINE 10 le decimos que se autoejecute en la línea 10):
SAVE "tree.bas" LINE 10
Veremos aparecer los colores del borde según los cuales sabemos que el Speccy original estaría guardando el programa codificado en sonido en una cinta de cassette.
Lo impresionante es que, independientemente de la ROM que estemos usando, en nuestro pendrive se habrá creado un archivo *WRT_tree.bas.tap". Los archivos TAP son archivos que simulan contener archivos del Speccy, como si fueran un contenedor. Podremos cargarlos en un emulador... y en BASINC. Este program simula ser un Spectrum, permitiéndonos programar en BASIC con modernidades como los atajos de teclado habituales... ¡y también cargar archivos TAP con programas BASIC dentro!
BasinC solo está disponible para Windows, pero funciona perfectamente con Wine.
Si además queremos cargar el trabajo que hayamos realizado previamente en BASIC en el propio TheSpectrum, solo tendremos que ir al modo carrousel y seleccionar el pendrive, marcar el archivo TAP en su interior, y pulsar ENTER para cargarlo.
Lo cierto es que esta parte es algo problemática. Podemos cambiar la configuración de ese TAP (pulsando SPACE), para que se abra en el modelo 128k, pero lo cierto es que no lo he conseguido. Por algún motivo, vuelve a cargar en lo que sería un modelo 48k.
En cualquier caso, ya tenemos dispuesto un entorno de trabajo.
Manos a la obra
En primer lugar, tenemos que "portar" la idea de la función recursiva a las limitaciones de Sinclair BASIC, es decir, si tenemos una función foo()
...
function foo(int n)
{
if ( n == 0 ) {
return;
} else {
console.log( "*" );
foo( n - 1 );
}
}
Esta función JavaScript, invocada por ejemplo con n = 3
, es decir foo( 3 )
, imprime tres asteriscos en la consola. El caso base se produce cuando n = 0
, donde se suceden todos los retornos de las llamadas pendientes. El caso regular es imprimir un asterisco y llamarse recursivamente con n - 1
.
...entonces lo más parecido en Sinclair BASIC sería...
10 CLS
20 LET n = 3
30 GOSUB 100
40 STOP
100 REM foo
110 IF n = 0 THEN RETURN
120 PRINT "*";
130 LET n = n - 1
140 GOSUB 100
Lo que lo hace más lioso, sin duda, son dos cuestiones: a) los números de línea (ya que hay que organizarse, intentando dejar suficientes "huecos" entre ellos), y b) solo hay variables globales, por lo que n
no puede ser una variable local de foo
. Simplemente, Sinclair BASIC no soporta esa posibilidad.
El código fuente
Como no tenemos variables locales, necesitaremos una pila donde guardaremos la longitud anterior de los segmentos cada vez que realicemos una llamada recursiva. Es decir, al principio los segmentos se dibujarán con tamaño 50, después 25, después 12, 6, 3... eso quiere decir que iremos guardando en la pila 50, 25, 12, 6, y 3. ¿Por qué? Porque cuando dibujemos cada una de las dos ramas de la Y, llamaremos recursivamente a nuestra rutina para que vuelva a pintar, con un tamaño más pequeño, otras dos Y.
Un truco importante es que, para volver a la unión de las dos ramas lo único que podemos hacer es movernos de nuevo tanto como dibujamos en un principio. Es decir, que si dibujamos con DRAW 0, 50
para el tronco principal, y DRAW -50, 50
para la rama izquierda, tendremos que redibujar DRAW 50, -50
para volver a la unión entre las ramas y dibujar con DRAW 50, 50
la rama derecha. Después con DRAW -50, -50
volvemos a la unión de las ramas, y con DRAW 0, -50
volvemos a la posición inicial de dibujo del tronco.
dibuja_Y():
if long < 2
return
end
draw 0, long
draw -long, long // Left branch
push(long)
long /= 2
dibuja_Y()
pop(long)
draw long, -long // Back to the branches union
draw long, long // Right branch
push(long)
long /= 2
dibuja_Y()
pop(long)
draw -long, long // Back to the branches union
draw 0, -long // Back to the beginning
end
¿Podemos evitar los guardados (push) de long y sus recuperaciones (pop) haciendo el cálculo inverso? Es decir, si hacemos la llamada recursiva con long / 2
, podemos recuperar el valor original con long * 2
, ¿no? El problema es que no todos los valores de long van a ser pares, con lo cual las inexactitudes al convertir el valor a entero para poder dibujar desbaratarán nuestros planes. Además, haciéndolo así podemos experimentar y cambiar la reducción de long mediante la división entre dos por la multiplicación por 0.75, por ejemplo.
El comienzo es situarnos aproximadamente en la mitad inferior de la pantalla y crear las variables necesarias. No pongo los números de línea que no son destino de GO SUB
para no sobrecargar el listado.
10 REM Variables
LET depth=30: LET long=40
LET lpos=0
DIM l(depth)
50 REM Start point
PLOT 125,0
REM Empezar a pintar
GO SUB 1000
Una vez que hemos creado las variables, podemos empezar a pintar. Nótese que depth lleva la profundidad máxima del dibujo fractal (o lo que es lo mismo, hasta cuántas veces es capaz de llamarse a sí mismo para pintar), ya que es el tamaño de la pila. La variable long lleva la longitud para los segmentos de la Y, que al principio es de 40 (debemos contemplar hasta cuán lejos puede llegar a dibujar el algoritmo, ya que es posible que nos salgamos de la pantalla.) Las variables l y lpos están muy relacionadas: son nuestra pila. En l se guarda el contenido de la variable long antes de reducirlo y llamarse recursivamente para seguir dibujando. La variable lpos guarda la última posición guardada en el vector l.
3000 REM Push
LET lpos=lpos+1: LET l(lpos)=long
RETURN
3100 REM Pop
LET long=l(lpos): LET lpos=lpos-1
RETURN
3200 REM Reduce long
LET long=INT(long/2)
Las líneas comentadas como Push guardan la nueva longitud de long en el vector l. De forma complementaria, las líneas comentadas como Pop recogen el último valor guardado en l.
El listado de diubjo completo aparece justo debajo.
1000 REM Draw
IF long<2 THEN RETURN
DRAW 0,long :REM Trunk
DRAW -long-2,long :REM Left branch
GO SUB 3000 :REM Push
GO SUB 3200 :REM Reduce long
GO SUB 1000 :REM rec call Draw
GO SUB 3100 :REM Pop
DRAW long+2,-long :REM Undo left branch
DRAW long+2,long :REM Right branch
GO SUB 3000 :REM Push
GO SUB 3200 :REM Reduce long
GO SUB 1000 :REM rec call Draw
GO SUB 3100 :REM Pop
DRAW -long-2,-long :REM Undo right branch
DRAW 0,-long :REM Undo trunk
RETURN
Finalmente, aquí está el listado completo.
10 BORDER 7: PAPER 7: INK 0: CLS
20 LET depth=30: LET long=30
30 LET lpos=0
40 DIM l(depth)
50 REM Start point
60 PLOT 125,0
100 GO TO 9000
110 STOP
1000 REM Draw
1005 IF long<2 THEN RETURN
1007 DRAW 0,long
1010 DRAW -long-2,long
1020 GO SUB 3000
1030 GO SUB 3200
1040 GO SUB 1000
1050 GO SUB 3100
1060 DRAW long+2,-long
1070 DRAW long+2,long
1080 GO SUB 3000
1090 GO SUB 3200
1100 GO SUB 1000
1110 GO SUB 3100
1120 DRAW -long-2,-long
1130 DRAW 0,-long
2000 RETURN
3000 REM Push
3010 LET lpos=lpos+1: LET l(lpos)=long
3020 RETURN
3100 REM Pop
3110 LET long=l(lpos): LET lpos=lpos-1
3120 RETURN
3200 REM Reduce long
3210 LET long=INT (long*0.6)
3220 RETURN
9000 REM Rec entry point
9020 GO SUB 1000
Solo resta aclarar que al comienzo del programa movemos la ejecución a la línea 9000 para que termine con una ejecución correcta u Ok, 9020:1
. De otra forma, si sustituimos la línea 100 por 100 go sub 1000
, el programa terminará con el error "Sentencia STOP, 110:1"
Speccy rulez!!
Top comments (0)