cursos:basic:zxmines_ejemplo_comentado

ZXMines: Juego Completo en BASIC comentado

ZXMines es un juego en BASIC que se presentó al concurso de programación de Juegos en Basic 2003 de ByteManiacos. ZXMines implementa en BASIC un sencillo juego de Buscaminas en el cual debemos destapar todo el tablero sin levantar las casillas en que se alojan las minas (destapar una mina finaliza el juego). Para poder destapar totalmente el tablero sin levantar casillas con minas disponemos de una información providencial: cada casilla que no contiene una mina nos indica numéricamente cuántas minas hay en las 8 casillas de alrededor de la casilla actual.

Así, si destapamos una casilla y contiene el número 1, sabemos que alguna de las 8 casillas de alrededor de la misma contiene una mina. Utilizando la información numérica que nos proporcionan las diferentes casillas que vamos destapando podemos ser capaces de averiguar qué casillas contienen minas y evitarlas. El juego acaba cuando destapamos una mina (y perdemos) o bien cuando destapamos todas las casillas del tablero quedando sólo por destapar las casillas con minas (ganando el juego). Por último, una cosa a destacar es que si destapamos una casilla sin minas alrededor, se abre todo un área de juego a la vista, para acelerar el ritmo de juego.


ZXMines: el clásico juego Buscaminas, en BASIC


El objetivo del presente artículo es mostrar y explicar el código BASIC utilizado para programar ZXMINES, mostrando así algunos trucos que en BASIC proporcionan una mayor velocidad de ejecución.


Lo primero que hacemos es definir el juego mediante lenguaje humano, para posteriormente adaptar ese lenguaje humano a código en BASIC. Es muy importante hacer esto antes de escribir una sóla línea en BASIC. El programa se adapta al diseño y al pseudocódigo, y no al revés.

Veamos el pseudocódigo para nuestro juego buscaminas:

Inicio ZXMINES
 - Inicio del programa (declaracion de variables, etc.).
 - Dibujado de la pantalla
 - Bucle principal del juego:
   - Comprobacion de fin de partida:
     - Si quedan tantas casillas como minas:
       - Llamar a Victoria_Fin_Juego
   - Leer teclado
     - Si tecla es 1, 2, ó 3:
        - Cambiar la dificultad
        - Nueva partida, saltando a "Dibujado de la pantalla"
     - Si tecla es 4 Entonces Fin del juego.
     - Si tecla es ARRIBA:
         - Mover cursor arriba (ycursor = ycursor-1)
     - Si tecla es ABAJO:
         - Mover cursor abajo (ycursor = ycursor+1)
     - Si tecla es IZQUIERDA:
         - Mover cursor izquierda (xcursor = xcursor-1)
     - Si tecla es DERECHA:
         - Mover cursor derecha (xcursor = xcursor-1)
     - Si tecla es DISPARO:
         - Destapar la casilla bajo (xcursor,ycursor)
           llamando a DestaparMina


FUNCIÓN DestaparMina:
 - Si debajo de (xcursor,ycursor) hay una casilla por destapar:
   - Si debajo de (xcursor,ycursor) hay una mina:
     Muerte_Fin_Juego
   - Si debajo de (xcursor,ycursor) no hay mina:
     Blanquear_Cuadros_Alrededor
   - Actualizar puntos y numero de casillas y minas restantes.


FUNCIÓN Muerte_Fin_Juego:
 - Imprimir por pantalla mensajes, esperar tecla.
 - Restart del juego.


FUNCIÓN Victoria_Fin_Juego:
 - Imprimir por pantalla mensajes, esperar tecla.
 - Restart del juego.


FUNCIÓN Blanquear_Cuadros_Alrededor:
 - Rutina que recibe una posición CX,CY y destapa
   todas las casillas no minas a partir de CX,CY.


FUNCIÓN Generacion_de_tablero:
 - Definir una array bidimensional de 12x12 (dificultad maxima)
   para almacenar las minas (V).
 - Definir una array bidimensional de 12x12 (dificultad maxima)
   para almacenar si una casilla está destapada o no, 0/1 (T).
 - Poner a cero todos los elementos de los arrays.
 - Segun la dificultad actual, llenar el tablero con minas de
   forma aleatoria (tablero de 8x8, 10x10 o 12x12). Las minas
   se colocan en V() mediante un número "9".
 - Actualizar todas las casillas del tablero recontando el
   número de minas aldededor, y colocando ese número en la
   casilla correspondiente.


FUNCIÓN Preparar_Pantalla:
 - Limpiar pantalla.
 - Imprimir textos, título, teclas.
 - Definir ciertas variables (minas, dificultad, tamaño tablero)
 - Cargar los GDUs/UDGs desde los DATAs.

Fin ZXMINES

El pseudocódigo permite implementar el juego en cualquier lenguaje de una manera rápida: por ejemplo, el mismo pseudocódigo/diseño que se usó para ZXMINES 1 en BASIC se utilizó para programar ZXMINES 2 en C (mucho más rápido) en apenas un par de horas de codificación. Para que el pseudocódigo sea legible es recomendable mantenerlo conciso y no extenderse demasiado en detalles, ya que debe ser una visión general del programa.


En los siguientes apartados veremos los diferentes bloques funcionales del juego comentados. El lector podrá identificar fácilmente a qué función del pseudocódigo se corresponden. Como puede verse, la utilización de pseudocódigo permite una programación más limpia, ya que la implementación sólo tiene que ceñirse a lo que hemos diseñado, reduciendo los errores posteriores (se trata tan sólo de implementar la visión general que nos da el pseudocódigo, y esto se puede hacer de forma modular).

El código lo programaremos en un editor de texto convencional en nuestro ordenador personal y lo grabaremos como un simple fichero de texto con extensión .bas. Después, con bas2tap (un utilísimo programa) generaremos un TAP que será equivalente a haber tecleado el programa en el intérprete BASIC del Spectrum y haberlo grabado con SAVE. El tap resultante de la “compilación” se podrá ejecutar tanto en emuladores como en Spectrum reales.

Para empezar, veamos la relación entre el pseudocódigo y las diferentes líneas BASIC del programa (podéis utilizar el Listado 1 para identificar los diferentes bloques funcionales):

Inicio ZXMINES
 - Inicio del programa (líneas 48-90)
 - Dibujado de la pantalla (líneas 105-140 y 2300-9022)
 - Bucle principal del juego: (lineas 200-990)

FUNCIÓN DestaparMina: (líneas 1000-1099)
FUNCIÓN Muerte_Fin_Juego: (líneas 1300-1330)
FUNCIÓN Victoria_Fin_Juego: (líneas 1600-1630)
FUNCIÓN Blanquear_Cuadros_Alrededor: (líneas 3-14)
FUNCIÓN Generacion_de_tablero: (líneas 2000-2200)
FUNCIÓN Preparar_Pantalla: (líneas 105-140 y 2300-9022)
Fin ZXMINES

Como puede verse, cada función o bloque lógico del pseudocódigo se corresponde con un bloque de líneas BASIC, que será que lo veremos más detallado a continuación.


Aparte de los comentarios del programa (líneas 9100 a 9147), el programa (lógicamente hablando) empieza con el siguiente código:

48 INPUT "Spanish/English? (s/e)", L$ ;
49 IF L$<>"s" AND L$<>"S" AND L$<>"e" AND L$<>"E" THEN GO TO 48
 
50 REM *** Declaracion de las variables del programa ***
51 LET COL=6 : LET PAP=1
53 INK COL: PAPER PAP
54 IF L$="E" THEN LET L$="e"
55 IF L$="S" THEN LET L$="s"
60 DIM T(12,12)           : REM Campo de minas
65 DIM V(12,12)           : REM Visto o no visto (0/1)
70 LET BUCLE=1
80 LET IX=1 : LET IY=1    : REM IX, IY del tablero
85 LET DIFICULTAD=0
90 GO SUB 8000            : REM DEFINIMOS LOS GRAFICOS

Lo primero que hacemos es pues preguntar al usuario el idioma para el juego, y después declarar las variables que utilizaremos en el programa. La variable L$ almacenará el idioma del juego, pudiendo contener la cadena “e” (english) o la cadena “s” (spanish). La usaremos posteriormente a la hora de hacer PRINTs para imprimir mensajes en un idioma u otro.

Dos variables importántisimas son los arrays bidimensionales T(12,12) y V(12,12), que se utilizarán para representar el tablero de juego del buscaminas. Se definen de 12×12 porque éste es el tamaño máximo del juego para el nivel de dificultad máxima; en caso de utilizar menores niveles de dificultad (8×8 y 10×10) se utilizará el mismo array de 12×12, pero usando sólo una porción del mismo.

El array V(12,12) nos indica el estado de las casillas indicando si están tapadas (0), o si están destapadas (1), es decir, si vemos el contenido de la casilla o no. Inicialmente este array tendrá todos sus elementos a cero porque al comenzar el juego todas las casillas están tapadas.

Así pues, un tablero tapado tendría la siguiente forma:

V =
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

Un tablero con la casilla central destapada tendría el siguiente aspecto:

V =
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

El array T(12,12) indica lo que contiene cada casilla propiamente dicha. Si contiene un cero, la casilla está vacía. Si contiene un nueve, la casilla tiene una mina. Finalmente, cualquier valor entre 1 y 8 significa el número de minas que hay alrededor de la casilla actual.

Así, inicialmente tendremos ambos arrays (V y T) llenos de ceros porque no hay minas dentro, y porque todas las casillas están tapadas. Si queremos introducir una mina en el tablero en la posición (3,5), podemos hacerlo con:

T(3,5) = 9;

Al igual que en el caso anterior, un array sin minas estará lleno de ceros, pero un array con 4 minas y ya correctamente “rellenado” (como veremos posteriormente), podría tener un aspecto similar al siguiente:

T =
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0
0, 0, 0, 0, 1, 9, 1, 0, 0, 0, 0, 0
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0
1, 1, 1, 0, 0, 0, 0, 0, 2, 9, 2, 0
1, 9, 1, 0, 0, 0, 0, 0, 2, 9, 2, 0
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

Como puede verse, las minas se sitúan con números 9, y las casillas de alrededor de las minas contienen valores numéricos que indican la cantidad de minas circundantes.

Finalmente se produce un salto a la rutina de la dirección 8000, que prepara los UDG o gráficos definidos por el usuario:

8000 REM *** Preparacion de GDUs ***
8005 RESTORE 9012
8010 FOR I=0 TO 7: READ FILA : POKE USR "I"+I, FILA : NEXT I
8015 FOR I=0 TO 7: READ FILA : POKE USR "M"+I, FILA : NEXT I
8020 FOR I=0 TO 7: READ FILA : POKE USR "E"+I, FILA : NEXT I

Como puede verse, se define “M” como el UDG que contiene el dibujo de las minas, “E” como un UDG que contiene el dibujo de una casilla sin destapar o esquina, e “I” como el UDG que contiene el dibujo de una casilla destapada vacía (un pequeño puntito central).

Los 3 dibujos han sido creados con SevenuP de la misma forma. Por ejemplo, para crear la mina, abrimos SevenuP y creamos un nuevo dibujo de 8×8. Dibujamos la mina (en la siguiente figura la veremos a medio dibujar) y vamos a File , y luego a Output Options y seleccionamos “No attributes” y como Byte Sort Priority dejamos “X char, Char line, Y char, Mask”.


Creando nuestros gráficos en SevenuP


A continuación exportamos los datos, desde el menu File, opción Export Data. Escribimos un nombre de fichero (por ejemplo, mina.c), y en el desplegable seleccionamos “C source” en lugar de “raw Binary”. Al darle a OK habremos creado un fichero mina.c con el gráfico exportado a formato C. El aspecto del fichero será similar (comentarios aparte) al siguiente:

unsigned char Sprite[8] = {
 16, 186, 116, 254, 124, 186, 16, 0 };

Estos son los valores que podremos en nuestros DATA en BASIC:

9017 DATA 16, 186, 116, 254, 124, 186, 16, 0

Tras el inicio del programa (líneas 48 a 90) viene la preparación de la pantalla (líneas 2300 a 2450) donde simplemente se imprimen en pantalla los mensajes que veremos en el menú principal.


Aspecto de la pantalla de presentación



La rutina de las líneas 2000 a 2200 es llamada desde la rutina de preparación de la pantalla para rellenar el tablero T con minas aleatorias (números 9), y al mismo tiempo calcular los valores numéricos de cada casilla indicando el número de minas alrededor.

La rutina viene a hacer algo como lo siguiente:

  • Rellenar el tablero con minas aleatorias.
  • Recalcular el valor numerico de cada casilla sin mina para todo el tablero.

En pseudocódigo:

 Desde I = 0 a NUMERO_DE_MINAS
   Poner minas aleatoriamente con T(RANDOMX,RANDOMY) = 9;
 Fin Desde

 Desde Y = 0 a ALTO_TABLERO
   Desde X = 0 a ANCHO_TABLERO
     Contar el numero de minas alrededor de T(x,y)
     T(x,y) = ese numero de minas
   Fin Desde
 Fin Desde

El código resultante es:

2000 REM *** GENERACION DE TABLERO ***
2001 IF DIFICULTAD=0 THEN RETURN
2002 IF L$="e" THEN PRINT AT 19,1 ; "Generating board..."; AT 20,1 ;
     "> Placing mines: 0%";
2003 IF L$="s" THEN PRINT AT 19,1 ; "Generando tablero.."; AT 20,1 ;
     "> Insert. minas: 0%";
2030 LET INCR=100/MINAS : LET PORCEN=0 : LET CON=1
2040 REM Primero ponemos a cero todo el tablero
2042 DIM T(12,12) : DIM V(12,12)
2045 LET RX = INT (RND*(ANCHO))+1
2046 LET RY = INT (RND*(ALTO))+1
2047 REM Ahora instalamos las minas. Si encontramos un hueco, pasamos a
     por otra mina
2050 FOR N=1 TO MINAS
2060   IF V(RX,RY)=1 THEN GO TO 2070
2065     LET V(RX,RY)=1 : POKE (59000+N),RX : POKE (59300+N),RY
2067     LET CON=CON+1 : IF CON=5+DIFICULTAD THEN PRINT AT 20,18 ;
     INT PORCEN ; "%"; : LET CON=1
2068     LET PORCEN=PORCEN+INCR : NEXT N
2069   REM Si no, generamos otro numeros y dec el indice
2070   LET RX = INT (RND*(ANCHO))+1
2080   LET RY = INT (RND*(ALTO))+1
2090   LET N = N-1
2100 NEXT N
2101 IF L$="e" THEN PRINT AT 20,1 ; "> Preparing board: 0%";
2102 IF L$="s" THEN PRINT AT 20,1 ; "> Gener.  tablero: 0%";

En esa primera parte colocamos las minas dentro del tablero, y a continuación calculamos los valores numéricos de cada celdilla del tablero:

2109 LET PORCEN=0 : LET CON=0
2110 FOR N=1 TO MINAS
2111   LET CON=CON+1 : IF CON=5+DIFICULTAD THEN PRINT AT 20,20 ;
     INT PORCEN ; "%"; : LET CON=1
2112   LET PORCEN=PORCEN+INCR
2120   LET X=PEEK (59000+N) : LET Y=PEEK (59300+N)
2131   IF X=1 THEN GO TO 2138
2132     LET T(X-1,Y) = T(X-1,Y) + 1
2133     IF Y>1          THEN LET T(X-1,Y-1)=T(X-1,Y-1)+1
2134     IF Y1         THEN LET T(X+1,Y-1)=T(X+1,Y-1)+1
2145   IF Y1    THEN LET T(X,Y-1)=T(X,Y-1)+1
2160 NEXT N
2161 PRINT AT 20,20 ; "100%";
2165 REM Ahora plantamos las minas
2170 FOR N=1 TO MINAS : LET T( PEEK (59000+N), PEEK (59300+N)) = 9 : NEXT N
2175 DIM V(12,12)
2180 PRINT AT 19,1 ; INK COL; PAPER PAP; "                    ";
2181 PRINT AT 20,1 ; INK COL; PAPER PAP; "                        ";
2200 RETURN

Nótese un pequeño truco que se ha utilizado para acelerar la generación y cálculos: en lugar de trabajar sobre arrays temporales, trabajamos sobre zonas de memoria (59000 y 59300) tratándolas como vectores de trabajo temporales. Escribimos en nuestros “arrays” en memoria con POKE, y leemos con PEEK. Posteriormente en la rutina de “Blanqueado de casillas” veremos su utilidad y porqué se ha realizado esto en lugar de arrays temporales. En las posiciones de memoria desde 59000 a 59000+numero_de_minas (ese numero_de_minas varía con la dificultad de juego elegida) se almacenan las posiciones X de las minas aleatorias generadas. Lo mismo ocurre en 59300, donde se almacenan las posiciones Y de las minas aleatorias generadas.

Así, la primera parte de la función coloca en 59000 y 59300 valores X e Y para las minas, y finalmente incorporamos las minas al tablero mediante:

2170 FOR N=1 TO MINAS : LET T( PEEK (59000+N), PEEK (59300+N)) = 9 :
     NEXT N

El resultado de la ejecución de la rutina de arriba es un tablero tapado (V(x,y) = 0 para todo x e y), y que tiene una serie de minas (valores numéricos 9) en T(xminas,yminas), estando el resto de casillas de T(x,y) calculadas con valores numéricos que representan las minas alrededor de la casilla en cuestión, como en el ejemplo que hemos visto antes:

T =
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0
0, 0, 0, 0, 1, 9, 1, 0, 0, 0, 0, 0
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0
1, 1, 1, 0, 0, 0, 0, 0, 2, 9, 2, 0
1, 9, 1, 0, 0, 0, 0, 0, 2, 9, 2, 0
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

Una vez tenemos definido el tablero de juego podemos continuar con el bucle principal del programa.


El bucle principal del juego se desarrolla entre las líneas 200 y 990).

Lo primero que hacemos es dibujar el tablero de juego con 2 bucles FOR para el ancho y para el alto:

100 REM *** BUCLE PRINCIPAL ***
300 LET P$= CHR$(16) + CHR$(0) + CHR$(17) + CHR$(7) :
    FOR X=1 TO ANCHO : LET P$=P$+"{E}" : NEXT X
310 FOR Y=1 TO ALTO
315   PRINT AT IY+Y,IX+1 ; P$;
370 NEXT Y

Se ha utilizado una cadena “P$” en la que introducimos toda una fila horizontal de casillas sin destapar (E). Después con un bucle pintamos tantas líneas horizontales de casillas como altura tiene nuestro tablero. Esto es más rápido que un doble bucle, donde tendríamos que recalcular las casillas horizontales cada vez. El símbolo “{E}” representa al UDG “E” (para bas2tap).

A continuación viene el bucle principal en sí mismo. Lo que se hace en este bucle principal es utilizar unas variables CX, CY que indican la posición dentro del array de la casilla “actual” o cursor que estamos manejando (de forma que cuando pulsamos espacio destapamos la casilla actual). Las variables CX y CY (posición de la casilla/cursor) se actualizan cuando pulsamos izquierda, derecha, arriba o abajo. En cada paso del bucle representamos la casilla “actual” o cursor mediante un FLASH de la posición en pantalla de la casilla (IX+CX,IY+CY). Recordad: la posición (CX, CY) es un valor entre 1 y 12 dentro de los arrays V() y T(), mientras que (CX+IX, CY+IY) representa un valor entre 1-32 y 1-24 para dibujar en pantalla el cuadro parpadeante (IX e IY son el inicio en pantalla del tablero).

400 REM Bucle principal de partida
405 IF DIFICULTAD=0 THEN GO TO 415
407 IF MINAS=QUEDAN THEN GO TO 1600
410 OVER 1: PRINT AT IY+CY, IX+CX ; FLASH 1; INK 0 ; PAPER 7;" "; :
    OVER 0 : GO SUB 991
415 LET K$=INKEY$ : IF K$="" THEN GO TO 415
421 IF K$="1" THEN LET DIFICULTAD=1 : BEEP .0015,35 : GO SUB 2400 :
    GO SUB 1460 : GO SUB 996 : GO
TO 111
422 IF K$="2" THEN LET DIFICULTAD=2 : BEEP .0015,35 : GO SUB 2400 :
    GO SUB 1460 : GO SUB 996 : GO
TO 111
423 IF K$="3" THEN LET DIFICULTAD=3 : BEEP .0015,35 : GO SUB 2400 :
    GO SUB 1460 : GO SUB 996 : GO
TO 111
424 IF DIFICULTAD=0 THEN GO TO 990
425 IF K$="4" AND DIFICULTAD<>0 THEN LET DIFICULTAD=0 : BEEP .0015,35 :
    GO TO 1300
430 IF K$="o" OR K$="O" OR K$="6" THEN IF DIFICULTAD<>0 THEN
    IF CX>1 THEN GO SUB 1500 : LET CX=CX-1 : GO TO 500
440 IF K$="p" OR K$="P" OR K$="7" THEN IF DIFICULTAD<>0 THEN
    IF CX<>0 THEN
    IF CY>1 THEN GO SUB 1500 : LET CY=CY-1 : GO TO 500
460 IF K$="a" OR K$="A" OR K$="8" THEN IF DIFICULTAD<>0 THEN
    IF CY<>0 THEN GO SUB 1000
990 GO TO 400

La actualización de la posición actual del cursor se realiza en la línea 410:

410 OVER 1: PRINT AT IY+CY, IX+CX ; FLASH 1; INK 0 ; PAPER 7;" "; :
    OVER 0 : GO SUB 991

Al final de esa línea se salta a la rutina 991, que simplemente se encarga con varios PRINT de actualizar los valores numéricos de números de minas y casillas restantes en el juego:

991 REM *** Actualizar contadores minas ***
992 IF L$="e" THEN PRINT AT 14, 18; "Mines: "; INK 7 ; MINAS ;  " "
993 IF L$="s" THEN PRINT AT 14, 18; "Minas: "; INK 7 ; MINAS ;  " "
994 IF L$="e" THEN PRINT AT 15, 18; "Cells: "; INK 7 ; QUEDAN ; " " :
    RETURN
995 PRINT AT 15, 18; "Total: "; INK 7 ; QUEDAN ; " " : RETURN
996 PRINT AT 14, 18; "           "
997 PRINT AT 15, 18; "            "
998 RETURN

Como puede verse, en el bucle principal también se controla la pulsación de las teclas 1 a 4 para cambiar la dificultad y acabar el juego. Nótese también la siguiente línea:

407 IF MINAS=QUEDAN THEN GO TO 1600

La línea 407 es el control de fin de juego para saber si el jugador ha ganado: si quedan tantas casillas por destapar como minas hemos puesto en el tablero, quiere decir que hemos destapado todas las casillas del tablero excepto las minas, con lo cual el juego se ha acabado y hemos ganado. En ese caso se salta a la línea 1600 que es la rutina de Victoria_Fin_Juego. Las rutinas de Victoria_Fin_Juego y Muerte_Fin_Juego son bastante sencillas (líneas 1300-1330 y 1600-1630): simplemente muestran un mensaje en pantalla, muestran el tablero desplegado y completo (si pulsas una mina y pierdes, tienes derecho a ver el contenido del tablero y la posición de las minas), esperan la pulsación de una tecla, reinicializan las variables del juego y saltan a la línea 400 (Inicio del bucle principal) para comenzar una nueva partida. La muestra y borrado del tablero se realiza mediante las siguientes rutinas:

1400 REM *** Mostrar tablero ***
1420 FOR Y=1 TO ALTO
1425   LET P$= CHR$(16) + CHR$(0) + CHR$(17) + CHR$(7) :
     FOR X=1 TO ANCHO : LET P$=P$+C$(T(X,Y)+1) : NEXT X  :
     PRINT AT IY+Y,IX+1 ; P$;
1450 NEXT Y
1451 RETURN
 
1460 REM *** Borrar tablero ***
1461 FOR N=1 TO 12
1462 PRINT AT IY+N,IX ; INK COL; PAPER PAP ; "                ";
1463 NEXT N
1470 RETURN

El borrado se basa simplemente en pintar espacios sobre el tablero, mientras que la visualización del tablero muestra todos los valores de T(X,Y) (independientemente del valor de V(X,Y), ya que pretendemos mostrar el tablero destapado completo). Puede verse el uso de C$(), que es un array donde hemos almacenado los caracteres para los 10 valores posibles del tablero (0 a 9, desde una casilla sin mina hasta una mina), de tal forma que 0 se corresponde con una casilla vacía, los números del 1 al 8 con los caracteres del 1 al 9, y el 9 con una mina.

101 DIM C$(10) : FOR N=1 TO 10: READ C$(N) : NEXT N
102 DIM H(10) : FOR N=1 TO 10: READ H(N) : NEXT N
103 DATA "{I}","1","2","3","4","5","6","7","8","{M}"
104 DATA 0, 1, 3, 2, 2, 2, 2, 2, 2, 0

La notación {M} significa “el UDG de M mayúscula”, y se utiliza en BAS2TAP para especificar los UDGs (ya que los teclados de los PCs no tienen las teclas de nuestro Spectrum para dibujar esos símbolos).

Por último, cuando movemos el “cursor” de una posición a otra tenemos que recuperar en pantalla lo que había bajo la posición que abandonamos. Esta tarea la realiza la siguiente función:

1500 REM *** Restaurar grafico bajo el cursor ***
1510 IF V(CX,CY)=1 THEN PRINT AT CY+IY,CX+IX ; FLASH 0; PAPER 7;
     INK H(T(CX,CY)+1) ;C$(T(CX,CY)+1); : RETURN
1520 PRINT AT CY+IY,CX+IX; FLASH 0; INK 0 ; PAPER 7; "{E}";
1530 RETURN

De nuevo podemos ver cómo se hace uso de CX+IX y CY+IY para saber dónde “escribir” la casilla que hay que recuperar, y de C$() para saber qué carácter imprimir, y H() para saber con qué atributo (tinta) imprimirlo.

Gracias a H(), por ejemplo, podemos hacer:

102 DIM H(10) : FOR N=1 TO 10: READ H(N) : NEXT N
104 DATA 0, 1, 3, 2, 2, 2, 2, 2, 2, 0
(...)
PRINT INK H( valor ) ; "X" ;

en lugar de:

IF valor="0" : PRINT INK 0; "X"
IF valor="1" : PRINT INK 1; "X"
IF valor="2" : PRINT INK 3; "X"
(...)
IF valor="9" : PRINT INK 0; "X"

Lo cual es mucho más rápido y más óptimo y legible.


Cuando en el bucle principal pulsamos espacio, se salta a una rutina que destapa la casilla actual:

470 IF K$=" " OR K$="0" THEN IF DIFICULTAD<>0 THEN GO SUB 1000

Dicha rutina DestaparMina se desarrolla desde las líneas 1000 a 1099 del programa: Si la casilla ya está destapada, no hay nada que destapar. En caso contrario, la marcamos como ya abierta, y reducimos el número de casillas, y en una tercera línea imprimimos en pantalla el carácter al que corresponde la casilla que acabamos destapar:

1000 REM *** Destapar mina ***
1010 IF V(CX,CY)=1 THEN RETURN
1020 LET V(CX,CY)=1 : LET QUEDAN=QUEDAN-1 : BEEP .0005,35 :
1030 PRINT AT CY+IY,CX+IX ; PAPER 7; INK H(T(CX,CY)+1) ;
     C$(T(CX,CY)+1) ;

Si es una mina lo que hemos destapado, hemos perdido. Si, por contra, es una casilla en blanco, tenemos que destapar todas las casillas en blanco alrededor de la casilla actual, algo que se hace en la porción de código a partir de la línea 1060. Para el resto de los casos (números 1 al 8) basta con haber destapado y mostrado la casilla, y podemos volver (saltando a 1098):

1040 IF T(CX,CY)=9 THEN GO TO 1300
1050 IF T(CX,CY)=0 THEN GO TO 1060
1055 GO TO 1098

A partir de aquí empieza el destapado de recuadros en blanco: lo primero es visualizar un mensaje de información de que se va a saltar a un cálculo que en BASIC es algo lento y costoso, para después llamar a la rutina Blanquear_Cuadros_Alrededor (llamada en línea 1075). Tras volver de la rutina se actualizan los contadores de minas, casillas y puntos y se vuelve al bucle principal.

1060 REM Destapar todos los cuadros de alrededor que sean blancos
1066 LET PP=1                    : REM Puntero a la pila
1069 LET PD=1                    : REM Puntero a casillas destapadas
1070 IF L$="e" THEN PRINT AT 19,9 ; "...Working...";
1071 IF L$="s" THEN PRINT AT 19,9 ; "Calculando...";
1075 LET OX=CX: LET OY=CY : GO SUB 3 : LET CX=OX : LET CY=OY
1080 FOR N=1 TO PD-1
1090 LET X=PEEK (59000+N) : LET Y=PEEK (59300+N) :
     PRINT AT Y+IY,X+IX ; PAPER 7; INK H(T(X,Y)+1) ; C$(T(X,Y)+1) ;
1095 NEXT N
1096 LET QUEDAN=QUEDAN-PD+1 : LET PUNTOS=PUNTOS+PD-2
1097 PRINT AT 19,9 ; "             ";
1098 LET PUNTOS=PUNTOS+1
1099 RETURN

La rutina Blanquear_Cuadros_Alrededor se encarga de destapar todos los recuadros en blanco si el recuadro que acabamos de destapar está en blanco. Se usa para evitar que en zonas grandes de recuadros sin minas ni números tengamos que destapar uno a uno todos los recuadros que no contienen nada. Si no se llamara a esta función, al pulsar en un recuadro de un área vacía, simplemente se destaparía ese recuadro y ningún otro más. Lo que hace esta función es destapar todo el área vacía, para agilizar el juego.

Esta rutina se ha implementado aparte y de una forma optimizada por razones de velocidad. Para empezar, se ha situado en las líneas del 2 al 14 porque las primeras líneas del programa son las que BASIC ejecuta con mayor velocidad (pequeños trucos de BASIC). Además, se ha intentado reducir el número de líneas agregando muchos comandos a cada línea mediante “:”, ya que eso también acelera la ejecución (digamos que el pasar de una línea a la siguiente requiere un tiempo en BASIC, y acumulando comandos en la misma línea lo reducimos). Como es la función más crítica (y lenta) del programa, se ha implementado aprovechando estos truquillos de BASIC, para acelerarla. Aún así hay que decir que tal y como está escrita la función es lenta (y esto es una invitación al lector a escribir una rutina más rápida, aprovechando la modularidad del programa).

Por último, esta función tiene una optimización extra que ha añadido algo de velocidad a la ejecución de la misma: se han cambiado los accesos al array ( V(x,y) = valor ) por accesos a memoria (con PEEK y POKE), al igual que se ha hecho en la rutina de generación del tablero y posicionamiento de las minas.

Lo que se hace, al igual que en el caso de la generación del tablero, es usar buffers de memoria como vectores de trabajo, empleando PEEK y POKE. Tras las pruebas realizadas, resulta que A(10)=1 es más lento que POKE 59000+10,1 . Así pues, en las direcciones 59000, 59300, 59500 y 59700 establecemos 4 buffers temporales donde trabajar.

El algoritmo de la rutina de las líneas 3 a 14 lo que hace es lo siguiente:

  • Si la casilla actual es cero, destaparla (V(X,Y) = 1) y pintarla en pantalla.
  • Comprobar si las 8 casillas de alrededor son cero.
    Si lo son, ejecutar este mismo algoritmo sobre cada una de las 8 casillas.

La implementación es bastante ilegible (al estar optimizada) y es por eso que os animamos a mejorarla y hacerla más rápida utilizando un algoritmo diferente.


Bas2tap es una utilidad muy interesante que permite teclear nuestros programas en BASIC en nuestro ordenador personal y después generar un tap tal y como si lo hubieramos tecleado en el editor del Spectrum.

Si grabamos el Listado 1 como zxmines.bas, podemos generar un tap del juego mediante:

 bas2tap -a1 -szxmines zxmines.bas

La sintaxis (explicada) es:

 -a1 : línea de autoarranque (en qué línea autocomenzará el programa sin
       necesidad de poner RUN tras el LOAD "").
 -szxmines : título del programa BASIC.
 zxmines.bas : programa a "compilar" o "traducir".

Podéis descargar BAS2TAP de la sección de Utilidades de WorldOfSpectrum.



Santiago Romero
Septiembre 2005
Magazine ZX #12

  • cursos/basic/zxmines_ejemplo_comentado.txt
  • Última modificación: 24-03-2009 09:29
  • por sromero