Designed by vectorpouch / Freepik

De Sinclair ZX Basic a Boriel ZX Basic

En este artículo explicaremos una serie de conceptos de Boriel ZX Basic, orientados a los programadores que vienen del clásico Sinclair ZX Basic, y que quieren empezar a programar en Boriel ZX Basic. Y ya que estamos, aunque los conceptos sirven para el ZX Spectrum clásico, nos enfocaremos más hacia el Sinclair ZX Spectrum Next.
No se puede resumir ni explicar todo en un simple artículo, y tampoco es un camino que se recorra en 2 minutos, así que en este texto explicaremos conceptos genéricos sin entrar en excesivos detalles ni cuestiones avanzadas.

Para seguir los pasos de este artículo, es mejor tener preparado el ambiente de programación NextBuild, que explicamos en el post “Preparando el ambiente para programar para Next con Boriel ZX Basic y NextBuild”.

Para probar los ejemplos, crearemos una carpeta “Test” dentro de “C:\ZXNext\NextBuildv7\Sources\”. Una vez creada esa carpeta, podemos crear un archivo “Test1.bas”, por ejemplo, e ir haciendo las pruebas en él.

 

¿Pero que me aporta Boriel ZX Basic?

Esta es la primera pregunta que se suelen hacer los programadores al oír hablar de Boriel ZX Basic (Boriel de aquí en adelante). Y la respuesta no es sencilla, no por ser complicado explicar que aporta, sino porqué es una respuesta larga:

  • Compila el código: Boriel es un compilador, lo que significa que Boriel “convierte” nuestro código BASIC a código máquina. Sinclair ZX BASIC es un lenguaje interpretado, lo que significa que cada vez que se va a ejecutar un comando, la ROM lo tiene que leer, decodificar y ejecutar. En cambio, el código compilado se ejecuta mucho más rápido.
  • Optimiza el código: Boriel es capaz de, una vez compilado nuestro código, optimizarlo para que aún sea más rápido y ocupe menos memoria. Por ejemplo, la mayoría de las operaciones matemáticas son más rápidas, CIRCLE corre más, PRINT vuela, y así un largo etcétera. Otro aspecto importante, es que el código que no se ejecuta nunca, es eliminado, reduciendo la memoria utilizada.
  • Incorpora mejoras del lenguaje: Boriel añade características de los lenguajes modernos, como el C o el Visual Basic, incluso permite el uso de assembler y permite una interacción sencilla entre este y Boriel, por ejemplo, facilitando el acceso a las variables de BASIC desde assembler.
  • Permite estructurar el código: La estructuración de código hace que este sea más sencillo de leer, y al mismo tiempo de reutilizable en otros proyectos.
  • Rompe la barrera del assembler: Muchos de nosotros, o al menos yo, nos encontramos con la barrera de la velocidad. Los juegos en BASIC eran lentos y el assembler era muy complicado. Boriel permite ir integrando el assembler en nuestros programas de forma gradual y muy natural.
  • Soporte: El canal de Telegram de “Boriel ZX Basic” o el foro de Boriel son muy activos, y el creador del compilador “Boriel”, se pasa por esos canales bastante a menudo.

 

Diferencias entre Sinclair ZX Basic y Boriel ZX Basic

Boriel es compatible al 95% con Sinclair ZX Basic. En el siguiente listado podemos ver las principales diferencias:

 

Números de línea

En Boriel son opcionales, es decir, no hace falta poner números de línea, lo que supone un problema menos al no tener que preocuparlos por la numeración o el límite de 9999 líneas.

10 BORDER 0: PAPER 0: INK 6: CLS
20 FOR N=0 TO 20
30 PRINT N
40 NEXT N

Esto se traduce como:

BORDER 0: PAPER 0: INK 6: CLS
FOR N=0 TO 20
     PRINT N
NEXT N

Fijaos que el PRINT lo separamos con un tabulador delante, lo que hace que el código sea más fácil de leer.

 

Etiquetas

Este mecanismo de Boriel, es el que nos permite eliminar los números de línea. En vez de usar “GO TO 1000” o “GO SUB 1000” podemos usar “GO TO MiEtiqueta” o “GO SUB MiEtiqueta”, y en vez de la línea 1000 ponemos “MiEtiqueta:”

10 LET A=0
20 LET A=A+1
30 PRINT AT 0,0;A
40 IF A<100 THEN GOTO 20
50 PRINT "FIN"

Se puede escribir como:
LET A=0
Bucle:
     LET A=A+1
     PRINT AT 0,0;A
     IF A<100 THEN GOTO Bucle
PRINT "FIN"

 

Saltos calculados

Boriel no permite saltos del tipo “GO TO 1000+variable” o “GO TO variable”. En este caso hay que cambiar un poco el concepto o usar tablas de salto con el comando “ON GOTO” u “ON GOSUB”, por ejemplo: “ON variable GOTO 1000,1100,1200” saltará a la línea 1000 si variable vale 0, a 1100 si vale 1, a 1200 si vale 2, y no saltará si tiene cualquier otro valor.

10 GOTO 1000+(variable*100)

Ese traduce como:

ON variable GOTO 1000,1100,1200

 

Base de las matrices (arrays)

Aunque el parámetro “–sinclair» de Boriel, cambia este comportamiento, por defecto, los índices de arrays “DIM a(100)” empiezan en 0 en Boriel, mientras que en Sinclair ZX Basic empiezan en 1. Esto implica que este “DIM a(100)” en Boriel tendrá 101 elementos (del 0 a 100), mientras que en Sinclair ZX Basic solo tendrá 100 (del 1 al 100).

Esta base 0 también se aplica a operador “TO” en operaciones con cadenas:

LET A$="12345"
PRINT A$(1 TO 4)

En Sinclair ZX Basic imprimirá “1234”, mientras que en Boriel se mostrará “2345”.

Todo este comportamiento se puede modificar con los parámetros “–sinclair», “–array-base» y “–string-base”, pero lo más normal es acostumbrarse a usar el 0 como elemento base.

 

Variables de cadena sin $

El uso de $ para referirnos a una variable de texto ya no es necesario, es decir, podemos usar a=”Hola” en vez de a$=”Hola”.

Esto también significa que A es la misma variable que A$

 

Comandos por defecto

A menos que indiquemos lo contrario, no todos los comandos de Sinclair ZX Basic están disponibles si no usamos el modificador “–sinclair», aunque esto no es un problema, ni mucho menos, como veremos más adelante. Algunos de estos comandos son INPUT, ATTR y SCREEN$.

 

Mejoras de Boriel ZX Basic

Boriel mejora Sinclair ZX BASIC, ampliando y flexibilizando, lo que nos permite usar técnicas modernas que hasta el momento solo estaban disponibles en otros lenguajes como el C.

 

Ampliación del tipo de variables

Las variables en Sinclair ZX Basic solo pueden ser de dos tipos, numéricas o de texto.

Las variables numéricas son siempre del tipo FLOAT, y ocupan 5 bytes siempre. Todas las operaciones matemáticas, por simples que sean, pasan por una calculadora de punto flotante, lo que ralentiza la ejecución muchísimo.

En Boriel se incorporan 6 tipos de variables para números enteros, dos tipos para números con decimales y una para cadenas de texto. Si no indicamos el tipo de variable, Boriel intentará asignar la más adecuada, pero esto es un tanto peligroso, así que es conveniente definir siempre el tipo de variable.

En la siguiente tabla se muestran los tipos de variables numéricas:

Tipo Tamaño (bytes) Rango Descripción
Byte 1 -128..127 Entero de 8 bits con signo
UByte 1 0..255 Entero de 8 bits sin signo
Integer 2 -32768..32767 Entero de 16 bits con signo
UInteger 2 0..65535 Entero de 16 bits sin signo
Long 4 −2,147,483,648 .. +2,147,483,647 Entero de 32 bits con signo
ULong 4 0 .. 4,294,967,295 Entero de 32 bits sin signo

 

En cuanto a los decimales, tenemos los siguientes tipos:

Tipo Tamaño (bytes) Rango Descripción
Fixed 4 -32767.9999847 .. 32767.9999847 16 bits para la parte entera y 16 para la parte decimal
Float 5 Pierde precisión con muchos decimales o con números muy grandes Es el tipo que usa Sinclair ZX Basic. 8 bits para el exponente y 24 bits para la mantisa (datos)

 

Las cadenas de texto no tienen un tamaño asignado, y ocupan 2 bytes más el contenido del texto. Eso dos bytes extra contienen el tamaño actual del texto. Las variables de texto se definen con la clave “string”.

Boriel permite definir el tipo de variables que queremos usar, lo que permite acelerar las operaciones realizadas con esta variable. Podemos definir variables de la siguiente forma:

DIM numeroPeque AS BYTE
DIM numeroPequeSinSigno AS UBYTE
DIM numeroEntero1, numeroEntero2 AS UINTEGER
DIM cadena AS STRING
DIM arrayEntero(100) AS UINTEGER
DIM arrayBidimensional(10,5) AS UBYTE

Hay que tener en cuenta que el valor indicado al declarar un array, indica el número de elementos +1, ya que los índices empiezan por el 0. En pocas palabras, 100 define que tiene 101 elementos, del 0 al 100.

La recomendación es utilizar el tipo de variable más pequeño posible, pero hay que ir con mucho cuidado a no rebasar los valores máximos y mínimos.

 

SUBrutinas

En Sinclair ZX Basic las subrutinas se llaman con el comando “GOSUB línea” y volvemos de ella con ”RETURN”:

10 LET Y=10
20 LET X=5
30 LET A$="HOLA"
30 GOSUB 1000

1000 PRINT AT Y,X;A$
1010 RETURN

Las subrutinas tienen una serie de pegas, por ejemplo, la rutina del ejemplo imprime el texto contenido por A$ en la fila Y y la columna X, pero no tenemos ninguna referencia de las variables que se requieren.

En Boriel podemos implementarlas de esta forma:

Imprime(10,5,"Hola")

SUB Imprime(Y AS UBYTE, X AS UBYTE, TEXTO AS STRING)
     PRINT AT Y,X;TEXTO
END SUB

Como se puede ver en el ejemplo de Boriel, se entiende de forma sencilla las variables que requiere la subrutina. Por cierto, que estas variables se llaman “parámetros”, es decir, son los parámetros de la subrutina.

En cierta forma, los SUB nos permiten crear nuestros propios comandos.

Veamos algunas variaciones:

DIM Fila, Columna AS UBYTE
Fila=10
Columna=5
Imprime(Fila,Columna,"Hola")

SUB Imprime(Y AS UBYTE, X AS UBYTE, TEXTO AS STRING)
     PRINT AT Y,X;TEXTO
END SUB

Aquí podemos ver que se pueden usar variables como parámetros.

Ahora vamos a mejorar la rutina de impresión:

SUB Imprime(Y AS UBYTE, X AS UBYTE, TEXTO AS STRING)
     DIM N, COLOR AS UBYTE
     COLOR=0
     PRINT AT Y,X;"";
     FOR N=0 TO LEN(TEXTO)-1
          PRINT INK COLOR;TEXTO(N);
          COLOR=COLOR+1
     NEXT N
END SUB

En este ejemplo hemos mejorado la subrutina Imprime, que ahora imprime el texto en colores, pero lo más interesante es que hemos definido dos variables (“N” y “COLOR”). Pero estas variables solo “viven” dentro del SUB, es decir, al llamar a la subrutina, se crean dos variables nuevas que al salir de esta, se eliminarán.
La nota curios es que podemos tener una variable con el mismo nombre fuera del SUB, por ejemplo:
DIM N AS UBYTE
FOR N=0 TO 10
     Imprime(N,0,"HOLA "+STR(N))
NEXT N

SUB Imprime(Y AS UBYTE, X AS UBYTE, TEXTO AS STRING)
     DIM N, COLOR AS UBYTE
     COLOR=0
     PRINT AT Y,X;"";
     FOR N=0 TO LEN(TEXTO)-1
          PRINT INK COLOR;TEXTO(N);
          COLOR=COLOR+1
     NEXT N
END SUB

Este ejemplo imprime el texto “HOLA 0”, “HOLA 1”, “HOLA 2”, hasta “HOLA 10”, en diferentes filas. Pero lo más curioso es que tenemos un primer bucle que usa la variable “N” (que se trata de una variable global), y otra variable con el mismo nombre “N” dentro del SUB (que se denomina variable local), pero estas son diferentes y no se mezcla su valor.

 

FUNCTION

Una FUNCTION es una SUBrutina que devuelve un valor. Veamos un ejemplo:
PRINT Suma(10,3)

FUNCTION Suma(A AS INTEGER, B AS INTEGER) AS INTEGER
     DIM R AS INTEGER
     R=A+B
     RETURN R
END FUNTION

En este ejemplo estamos definir una función llamada “Suma”, que suma dos valores del tipo INTEGER, y devuelve el resultado en forma de INTEGER. Evidentemente se puede, y debería, simplificar con un simple RETURN A+B, pero así vemos que también se pueden crear variables “locales”.

 

Integración con assembler

Aunque sea una característica avanzada, explicaremos de forma sencilla como añadir unas líneas de assembler a nuestro código. Veamos este ejemplo sacado de NextLib:

function GetReg(byval nextRegister as ubyte) as ubyte
asm
     push bc
     ld bc,$243B                                        ; Register Select
     out(c),a
     ld bc,$253B                                        ; reg access
     in a,(c)
     pop bc
end asm
end function

Este ejemplo implementa el comando GetReg, que lee el valor de un registro de Next (NextReg). Se puede observar que dentro de la función el código assembler se incluye entre las líneas marcadas por “ASM” y “END ASM”.

En este caso el parámetro “nextRegisterse coloca en el registro A del procesador, y el valor que retorna la función también es el que se alm en dicho registro A.

El código assembler hace un OUT en el puerto $243B y acto seguido hace un IN en el puerto $253B. Como resultado, el valor del registro NextReg indicado por el parámetro “nextRegister”

Esto también nos permite añadir datos a nuestro código, por ejemplo las definiciones de una fuente o unos Sprites, como se puede ver en este ejemplo:

Bola:
ASM
db  $E3, $E3, $E3, $E3, $E3, $F5, $F5, $F4, $F4, $F5, $F5, $E3, $E3, $E3, $E3, $E3;
db  $E3, $E3, $E3, $FA, $F4, $F4, $F4, $F4, $F4, $F4, $F0, $ED, $F6, $E3, $E3, $E3;
db  $E3, $E3, $F9, $F8, $F4, $F4, $F4, $F4, $F4, $F4, $F4, $ED, $E9, $F2, $E3, $E3;
db  $E3, $FA, $F8, $F8, $F8, $F8, $F4, $F4, $F4, $F4, $F4, $EC, $E9, $E9, $F6, $E3;
db  $E3, $F8, $F8, $F8, $F8, $F8, $F8, $F4, $F4, $F4, $F4, $ED, $E9, $E9, $ED, $E3;
db  $F9, $F8, $F8, $F8, $F8, $F8, $F8, $F8, $F4, $F4, $F0, $ED, $E9, $E9, $E9, $F2;
db  $F9, $F8, $F8, $D8, $B8, $B8, $D8, $F8, $F4, $F0, $ED, $E9, $E9, $E9, $E9, $ED;
db  $F8, $D8, $98, $78, $58, $78, $78, $B4, $F0, $ED, $E9, $E9, $E9, $E9, $E9, $CE;
db  $D8, $98, $58, $58, $59, $58, $55, $52, $CE, $E9, $E9, $E9, $E9, $E9, $CE, $AE;
db  $B9, $58, $59, $58, $58, $55, $37, $53, $AF, $CE, $CE, $CE, $CE, $AE, $AE, $AF;
db  $99, $58, $58, $58, $59, $36, $33, $33, $AF, $AE, $AE, $AE, $AE, $AE, $AE, $D3;
db  $E3, $79, $58, $58, $55, $37, $33, $37, $6F, $AE, $AE, $AE, $AE, $AE, $AE, $E3;
db  $E3, $9A, $58, $58, $55, $37, $33, $33, $33, $8F, $AF, $AE, $AE, $AE, $D7, $E3;
db  $E3, $E3, $99, $58, $55, $37, $33, $33, $33, $37, $53, $8F, $8F, $B3, $E3, $E3;
db  $E3, $E3, $E3, $99, $79, $36, $33, $33, $33, $33, $33, $37, $77, $E3, $E3, $E3;
db  $E3, $E3, $E3, $E3, $E3, $7A, $37, $33, $33, $37, $77, $E3, $E3, $E3, $E3, $E3;
end asm

La diferencia entre usas este sistema y usar DATA es que con DB los datos ya están “pokeados” en la memoria. No hace falta leerlos con READ y hacer el POKE. Lo veremos más adelante, pero es una gran ventaja.

Otro detalle muy importante es que no hace falta poner ORG en assembler. Boriel coloca el código en la memoria ocupando todos los huecos y recolocando el código a medida que vamos programando. Esto nos permite olvidarnos un poco del mapa de memoria, pero no del todo si queremos jugar con el mapeo de memoria.

 

Uso de librerías

Una librería es un fichero de código que contiene una serie de SUBrutinas y FUNCTIONes que amplían los comandos disponibles.

Boriel incluye un paquete bastante nutrido de librerías, que incluyen, entro otras muchas, funciones para leer teclas pulsadas de forma simultánea, funciones para trabajar con cadenas, sprites, acceso a disco y tarjetas SD, scroll, etc…

Para usar una librería integrada utilizamos la directiva “#include” indicando el nombre de la librería entre signos de mayor y menor ( < > )

#include <Keys.bas>

IF MultiKeys(KEYSYMBOL)<>0 THEN PRINT "Se ha pulsado Symbol Shift"

En este ejemplo, se incluye la librería “Keys.bas”, que implementa los métodos “GetKey”, “MultiKeys” y “GetKeyScanCode”, además de definir una serie de constantes que nos permite leer cada una de las teclas de nuestro querido Spectrum.

Otro ejemplo:

#include <Scroll.bas>
DIM Y, X, N AS UBYTE
FOR Y=0 TO 23
     FOR X=0 TO 31
          PRINT AT Y,X;"#";
     NEXT X
NEXT Y
WHILE INKEY$=""
    PRINT AT 10,11;N;
    N=N+1
    ScrollRight(80,40,200,120)
WEND

Este ejemplo llena la pantalla con el símbolo “#” y después hace un scroll pixel a pixel en una ventana hasta que pulsemos una tecla.

El bucle formado por WHILE .. WEND hace que se repita todo lo que hay dentro mientras se cumple una condición. En este caso se hará el scroll hasta que se pulse una tecla, es decir, cuando INKEY$ no sea igual a una cadena vacía.

 

Si lo que queremos hacer es usar una librería que no está integrada en Boriel, la podemos incluir con el nombre entre comillas:

#include "MiLibreria.bas"

 

WHILE…WEND

Este tipo de bucle hace que se repita su contenido mientras se cumpla una condición. Por ejemplo:

10 LET A=0
20 PRINT AT 0,0;A
30 LET A=A+1
40 IF INKEY$="" THEN GOTO 20

Esto se traduce como:

DIM A AS UINTEGER
A=0
WHILE INKEY$=""
     PRINT AT 0,0;A
     A=A+1
WEND

A primera vista se ve que no ganamos en líneas, tecleamos lo mismo o incluso más, pero si que ganamos en visibilidad. El código se ve más limpio, no hay que buscar la línea 20 del GOTO y además podemos “indentar” el contenido del bucle.

WHILE 1
     PRINT "Bucle infinito ";
WEND

Esto hace que el bucle se repita de forma infinita. Tambien podemos forzar la salida del bucle con un BREAK WHILE, por ejemplo:

WHILE 1
     PRINT "Bucle infinito ";
     IF INKEY$<>"" THEN BREAK WHILE
WEND

Aunque poco útil, este ejemplo nos sirve para ver como se puede salir de este bucle.

 

DO…LOOP

Este tipo de bucle es una variante del anterior. Al igual que WHILE…WEND, el contenido se ejecuta mientras se cumple una condición. La diferencia es que la condición puede ponerse al principio del bucle, al final o no ponerse.
DO
     PRINT "Bucle infinito ";
LOOP

Otro ejemplo:
DO
     PRINT "Hasta que se pulse una tecla… ";
LOOP WHILE INKEY$=""
Hay más posibilidades, como que se repitas hasta que se cumpla una condición, pero lo dejaremos para otro día…

Quedan muchas cosas en el tintero, pero esto solo pretende ser una pequeña introducción a Boriel ZX Basic.

 

Agradecimientos y atribuciones

Algunos ejemplos de código están sacados de los ejemplos que vienen con NextBuild de David Saphier AKA em00K

Gracias a José Rodriguez AKA Boriel por su ayuda y por su trabajo con el compilador Boriel ZX Basic.

Imagen principal del artículo diseñada por vectorpouch / Freepik 

 

Recordad, para dudas me podéis encontrar en twitter “@Duefectu” o Facebook, o en el canal de “Boriel ZX Basic” en Telegram.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *