30 septiembre 2014

Un mini-juego en BASIC de Amstrad CPC (6: símbolos, arrays bidimensionales, READ-DATA, medir tiempos)

25. Definir caracteres

Nuestro juego es feo. Indudablemente. Podemos mejorar un poco su apariencia si al menos creamos nuevos símbolos cuyo aspecto recuerde más a una pared, una roca o un diamante, por ejemplo.
En primer lugar, deberemos diseñar esos símbolos a partir de una matriz de 8x8 puntos. Usaremos un 1 para indicar un punto "encendido" en la pantalla y un 0 para un punto "apagado" (que se verá con el color de fondo).
Por ejemplo, un ladrillo de una pared podría ser algo así:
11111100
11111100
11111100
00000000
11001111
11001111
11001111
00000000
Nada más encender un CPC, podemos redefinir los últimos 16 caracteres, de la posición 240 a la 255. Podríamos redefinir más, por ejemplo si queremos cambiar todo el juego de caracteres para dar otra apariencia al juego. A cambio, perderemos algo de memoria libre.
Por ejemplo, si queremos redefinir todos los caracteres "imprimibles", que son del 32 (el espacio en blanco) en adelante, lo deberíamos indicar con
SYMBOL AFTER 32
Y para definir cualquiera de esos caracteres, usaríamos orden SYMBOL, seguida del número del carácter que queremos redefinir y de los ocho números que corresponden a la secuencia de ceros y unos que habíamos diseñado anteriormente. Esta secuencia se puede indicar tal cual, en binario, si precedemos cada número por "&x", así:
SYMBOL 240, &x11111100, &x11111100, &x11111100, &x00000000, &x11001111, &x11001111, &x11001111, &x00000000
Aunque en ocasiones será preferible escribir directamente el número decimal equivalente a esa secuencia, para ocupar menos memoria. Por ejemplo, la orden anterior ocupa 49 bytes en memoria si es parte de un programa, mientras que la siguiente versión ocupa 31 bytes:
SYMBOL 240,252,252,252,0,207,207,207,0
Y podemos comprobar cómo quedaría esa "pared" si mostramos varios ladrillos:
FOR i=1 TO 100: PRINT CHR$(240);: NEXT i

El premio a recoger podría ser una figura que recordase a un diamante:
00011000
00101100
01000110
10000011
10000011
01000110
00101100
00011000
que se definiría con
SYMBOL 241, &x00011000, &x00101100, &x01000110, &x10000011, &x10000011, &x01000110, &x00101100, &x00011000
O bien
SYMBOL 241,24,44,70,131,131,70,44,24
Los obstáculos podrían ser algo con apariencia "de pinchos":
00010000
10010010
01010100
00111000
11111111
00111000
01010100
10010010
SYMBOL 242,&x00010000, &x10010010, &x01010100, &x00111000, &x11111111, &x00111000, &x01010100, &x10010010
Y los enemigos podrían ser algo como
10111101
01111110
11011011
11111111
11100111
11011011
01100110
11000011
SYMBOL 243,&x10111101, &x01111110, &x11011011, &x11111111, &x11100111, &x11011011, &x01100110, &x11000011
Los mejores resultados se obtendrían si combináramos 2 o 4 caracteres para formar imágenes de mayor tamaño, pero no lo haremos por ahora.
Podríamos aplicar esto al juego, incluyendo las definiciones de caracteres al principio de programa y editando la parte que dibuja, para que muestre esos caracteres usando colores:
20 MODE 1: INK 0,0: INK 1,20: INK 2,6: INK 3,25
21 SYMBOL 240, &x11111100, &x11111100, &x11111100, &x00000000, &x11001111, &x11001111, &x11001111, &x00000000
22 SYMBOL 241, &x00011000, &x00101100, &x01000110, &x10000011, &x10000011, &x01000110, &x00101100, &x00011000
23 SYMBOL 242, &x00010000, &x10010010, &x01010100, &x00111000, &x11111111, &x00111000, &x01010100, &x10010010
24 SYMBOL 243, &x10111101, &x01111110, &x11011011, &x11111111, &x11100111, &x11011011, &x01100110, &x11000011
25 pared$=CHR$(240): premio$=CHR$(241): obstaculo$=CHR$(242): enemigo$=CHR$(243): jugador$=CHR$(248)
 
225 PEN 2
230 LOCATE xo1,yo1:PRINT obstaculo$
240 LOCATE xo2,yo2:PRINT obstaculo$
250 LOCATE xo3,yo3:PRINT obstaculo$
255 PEN 1
260 LOCATE x,y
265 PEN 1
270 PRINT jugador$
271 PEN 2
272 FOR n = 1 TO 10:LOCATE xE(n),yE(n):PRINT enemigo$:  NEXT n
274 PEN 3
275 LOCATE xP,yP: PRINT premio$
280 PEN 1
290 LOCATE 3,1: PRINT "Puntos "; puntos

26. Un laberinto de fondo: arrays bidimensionales, READ y DATA

En un juego clásico como Manic Miner, uno de los niveles tenía esta apariencia:

que se consigue repitiendo una serie de casillas de tamaño 8x8:

Para imitar eso, podemos almacenar toda una "apariencia de pantalla" como esta usando un array bidimensional, con algo como
DIM pantalla(25,40)
Entonces podríamos dar valores a sus elementos con
pantalla(1,1)=5: pantalla(1,2)=0: pantalla(1,1)=0 
Donde el valor 5 podría indicar que se trata de un tipo concreto de pared, el 0 podría ser un espacio en blanco (o un fragmento de cielo), etc.
Pero existe una alternativa más cómoda para dar muchos valores: detallarlos en sentencias DATA y leerlos con la orden READ, normalmente con la ayuda de un FOR, así:
1 DATA 1,0,0,0: '...
2 DATA 1,0,2,0: '...
3 FOR fila = 1 TO 25
4   FOR columna = 1 TO 40
5      READ pantalla(fila, columna)
6   NEXT columna
7 NEXT fila
(Las sentencias DATA pueden estar indistintamente al principio del programa, al final... o incluso en medio).
Sería aún más legible si no usamos números separados por comas, sino letras que representen cada uno de los símbolos que habrá en pantalla, por ejemplo así:
 
1000 DATA PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP
1010 DATA PP----PPP--------PPPP--------PP
1020 DATA PP----PPP--------PPPP--------PP
1030 DATA PP----PPP--------PPPP--------PP
1040 DATA PP----PPPPP------PPPP--------PP
1050 DATA PP--PPPPPPPPPP--PPPPP----PPPPPP
1060 DATA PP---PPPPPPPP--PPPPPPPP----PPPP
1070 DATA PPP----PPPPP---PPPPPPPPP---PPPP
1080 DATA PP--------------------------PPP
1090 DATA PPP------------------------PPPP
1100 DATA PPPPP-----------PPPPPPPPP----PP
1110 DATA PPPPPP-----------PPPPPP---P--PP
1120 DATA PPP-------------------PPP-PP-PP
1130 DATA PP-----------------------PPPPPP
1140 DATA PP--------------------------PPP
1150 DATA PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP
(Aunque la pantalla sea de 25x40 caracteres, nuestro mapa del laberinto es sólo de 16x32, para dejar espacio para información sobre los puntos, las vidas restantes... y, de paso, gastar menos memoria, que es un bien escaso en estos equipos; por eso, dejaremos un margen superior de 2 líneas y un margen izquierdo de 4 columnas en el momento de dibujar en pantalla).
Este formato tan compacto hace que podamos comprobar de un vistazo si el laberinto tiene la apariencia que deseamos, pero también complica la lectura: cada fila es una cadena de texto, de la que deberemos extraer el contenido letra a letra, usando la función MID$, a la que hay que indicar de qué cadena queremos obtener un fragmento, en qué posición y con qué longitud:
61 DIM pantalla(25,40)
63 FOR fila = 1 TO 16
64   READ linea$
65   FOR columna = 1 TO 32
67     IF MID$(linea$,columna,1) = "P" THEN pantalla(fila+2, columna+4)=1 ELSE pantalla(fila+2, columna+4)=0
68   NEXT columna
69 NEXT fila
A la vez que extraemos esa información, podemos dibujar el laberinto en pantalla:
61 DIM pantalla(25,40)
63 FOR fila = 1 TO 16
64   READ linea$
65   FOR columna = 1 TO 32
66     LOCATE columna+4, fila+2
67     IF MID$(linea$,columna,1) = "P" THEN PRINT pared$: pantalla(fila+2, columna+4)=1 ELSE pantalla(fila+2, columna+4)=0
68   NEXT columna
69 NEXT fila
El resultado sería algo como

Pero aún no se comporta bien: podemos pasar por encima de las paredes, pueden aparecer enemigos, obstáculos y premios fuera del laberinto o en medio de las paredes, los enemigos borran las paredes al pasar por encima de ellas... poco a poco lo iremos mejorando...
10 ' Ejemplo de juego en BASIC de CPC
20 MODE 1: INK 0,0: INK 1,20: INK 2,6: INK 3,25
21 SYMBOL 240, &x11111100, &x11111100, &x11111100, &x00000000, &x11001111, &x11001111, &x11001111, &x00000000
22 SYMBOL 241, &x00011000, &x00101100, &x01000110, &x10000011, &x10000011, &x01000110, &x00101100, &x00011000
23 SYMBOL 242, &x00010000, &x10010010, &x01010100, &x00111000, &x11111111, &x00111000, &x01010100, &x10010010
24 SYMBOL 243, &x10111101, &x01111110, &x11011011, &x11111111, &x11100111, &x11011011, &x01100110, &x11000011
25 pared$=CHR$(240): premio$=CHR$(241): obstaculo$=CHR$(242): enemigo$=CHR$(243): jugador$=CHR$(248)
30 RANDOMIZE TIME
40 x=10: y=5: terminado=0
45 puntos=0
50 xo1=INT(RND*36+2): yo1=INT(RND*22+2): xo2=INT(RND*36+2): yo2=INT(RND*22+2): xo3=INT(RND*36+2): yo3=INT(RND*22+2)
52 DIM xE(10), yE(10), velocE(10)
54 FOR n = 1 TO 10: xE(n)=INT(RND*36+2): yE(n)=INT(RND*22+2): velocE(n)=1: NEXT n
56 xP=INT(RND*36+2): yP=INT(RND*22+2)
58 IF (xP=xo1 AND y=yP1) OR (xP=xo2 AND yP=yo2) OR (xP=xo3 AND yP=yo3) THEN GOTO 56
60 arriba=0: abajo=2: derecha=1: izqda=8: salir=63: ' Teclas
61 DIM pantalla(25,40)
63 FOR fila = 1 TO 16
64   READ linea$
65   FOR columna = 1 TO 32
66     LOCATE columna+4, fila+2
67     IF MID$(linea$,columna,1) = "P" THEN PRINT pared$: pantalla(fila+2, columna+4)=1 ELSE pantalla(fila+2, columna+4)=0
68   NEXT columna
69 NEXT fila
70 ' ----- Bucle de juego -----
80 WHILE terminado = 0
90 ' -- Borrar personaje y enemigos de su posicion anterior --
92 FOR n = 1 TO 10:LOCATE xE(n),yE(n):PRINT" ": NEXT n
100 LOCATE x,y
110 PRINT " "
120 ' -- Comprobar teclas --
130 IF INKEY(arriba) <> -1 AND y > 1 THEN y=y-1
140 IF INKEY(abajo) <> -1 AND y < 25 THEN y=y+1
150 IF INKEY(derecha) <> -1 AND x < 40 THEN x=x+1
160 IF INKEY(izqda) <> -1 AND x > 1 THEN x=x-1
170 IF INKEY(salir) <> -1 THEN terminado=1
180 ' -- Mover enemigos, entorno --
185 FOR n = 1 TO 10
190 xE(n) = xE(n) + velocE(n):
192 IF xE(n)=1 OR xE(n)=40 THEN velocE(n) = -velocE(n)
195 NEXT n
200 ' -- Colisiones, perder vidas, etc --
202 IF x <> xP OR y <> yP THEN GOTO 210: 'Si no hay premio
203 puntos = puntos+10
204 xP=INT(RND*36+2): yP=INT(RND*22+2): 'Si lo hay, calculamos nueva posicion
206 IF (xP=xo1 AND y=yP1) OR (xP=xo2 AND yP=yo2) OR (xP=xo3 AND yP=yo3) THEN GOTO 204
210 IF (x=xo1 AND y=yo1) OR (x=xo2 AND y=yo2) OR (x=xo3 AND y=yo3) THEN terminado = 1
212 FOR n = 1 TO 10
214 IF x=xE(n) AND y=yE(n) THEN terminado = 1
216 NEXT n
220 ' -- Dibujar en nueva posicion --
225 PEN 2
230 LOCATE xo1,yo1:PRINT obstaculo$
240 LOCATE xo2,yo2:PRINT obstaculo$
250 LOCATE xo3,yo3:PRINT obstaculo$
255 PEN 1
260 LOCATE x,y
265 PEN 1
270 PRINT jugador$
271 PEN 2
272 FOR n = 1 TO 10:LOCATE xE(n),yE(n):PRINT enemigo$:  NEXT n
274 PEN 3
275 LOCATE xP,yP: PRINT premio$
280 PEN 1
290 LOCATE 3,1: PRINT "Puntos "; puntos
300 WEND
1000 DATA PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP
1010 DATA PP----PPP--------PPPP--------PP
1020 DATA PP----PPP--------PPPP--------PP
1030 DATA PP----PPP--------PPPP--------PP
1040 DATA PP----PPPPP------PPPP--------PP
1050 DATA PP--PPPPPPPPPP--PPPPP----PPPPPP
1060 DATA PP---PPPPPPPP--PPPPPPPP----PPPP
1070 DATA PPP----PPPPP---PPPPPPPPP---PPPP
1080 DATA PP--------------------------PPP
1090 DATA PPP------------------------PPPP
1100 DATA PPPPP-----------PPPPPPPPP----PP
1110 DATA PPPPPP-----------PPPPPP---P--PP
1120 DATA PPP-------------------PPP-PP-PP
1130 DATA PP-----------------------PPPPPP
1140 DATA PP--------------------------PPP
1150 DATA PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP
 

27. Midiendo tiempos

Para mejorar el comportamiento de un programa, necesitamos poder comparar el "antes" y el "después". En la parte visual, es fácil notar los cambios, pero no tanto en la parte lógica. Aun así, podemos medir el tiempo de ejecución o el gasto de memoria antes y después de un cambio, para ver si está dentro de valores aceptables y si hemos mejorado algo.
Por ejemplo, la última versión de nuestro juego, dibujaba el fondo del laberinto fuera de la parte repetitiva. Eso va a complicar la lógica interna, pero es un mal necesario, porque no podremos redibujar la pantalla completa de un CPC a 25 fotogramas por segundo... ni a 10 fps... ni siquiera a un fotograma por segundo. Lo podemos comprobar con un programa auxiliar (independiente del juego) que dibuje 1000 asteriscos en pantalla y mida el tiempo empleado. Para eso usaremos la función, TIME, que nos dice el tiempo que hace que se encendió el equipo, medido en 1/300 de segundo:
1 t=TIME
2 FOR n = 1 TO 1000: PRINT"*";: NEXT n
3 PRINT TIME-t
El resultado de este programa es... 1961. Es decir, se tarda más de 6 segundos (1961/300=6.54) en llenar la pantalla de asteriscos. Y eso sin contar con que en un juego real habrá que realizar cálculos adicionales durante el dibujado. Claramente no es viable, no podemos esperar más de 6 segundos para "fotograma" del juego. Por eso hemos sacado el dibujado del laberinto fuera de la parte repetitiva del programa. (Se podría mejorar ligeramente la velocidad con técnicas que veremos más adelante, pero seguiría siendo demasiado lento como para permitir redibujar toda la pantalla en cada pasada).