cursos:basic:zxsnake_ejemplo_comentado

ZXSnake: Un juego completo BASIC comentado

Aprovechando la reciente convocatoria del concurso de BASIC 2004 de la mano de Radastan, vamos a retomar el curso que dejamos en suspenso hace más de un año. En esta entrega, aprovecharemos la disección de uno de los programas presentados en la edición del concurso del año pasado para ver cómo dibujar gráficos en movimiento y cómo efectuar la lectura del teclado para transmitir las órdenes del jugador.


El juego no es más que una versión más del clásico Snake, últimamente muy de moda gracias a su inclusión en los teléfonos móviles. Es un juego muy sencillo que nos va a servir para comprender la estructura general de cualquier juego y tocar temas como lectura del teclado, impresión de gráficos en movimiento y detección de choques.

El juego está planteado en modo texto, tal y como disponían las normas del concurso, y sólo se usan un par de GDUs, para representar la cabeza de la serpiente y las frutas.

ZXSnake

Vamos a dejar aparte todo el tema de presentación, opciones, redefinición de teclas y demás aderezos y nos vamos a centrar en lo que es el núcleo del programa, que seguirá este esquema. Al final del artículo podréis encontrar el código fuente completo.

REPETIR MIENTRAS la serpiente esté viva
    Mover serpiente
    Si hay colisión:
        - colisión con fruta :
            * La serpiente crece
            * Dibujamos una nueva fruta
        - colisión con pared :
            * La serpiente muere
        - colisión con ella misma :
            * La serpiente muere
    Leer teclado

El teclado se puede leer antes o después de mover la serpiente, la verdad es que no notaremos diferencia debido a la velocidad a la que se ejecuta el programa (sí, incluso en BASIC). Lo único diferente que notaremos será que el primer movimiento de la serpiente siempre será hacia la derecha desde su posición inicial, ya que lo habremos definido así en las condiciones iniciales.


La serpiente tiene una cabeza (un carácter) y una cola (compuesta por varios caracteres). La cola de la serpiente siempre va siguiendo el movimiento de la cabeza.

Una primera aproximación que se nos podría ocurrir sería calcular hacia dónde se mueve la cabeza y, a continuación, redibujar toda la serpiente. Esa solución es inaceptable por la cantidad de proceso que requeriría, y más hablando de un lenguaje interpretado, como BASIC, corriendo sobre un microprocesador a algo más de 3MHz. Debemos plantear la solución de otra manera.

Si nos fijamos bien, veremos que sólo necesitamos mover la cabeza y el último elemento de la cola. Esto es así debido a que todos los componentes de la cola son iguales gráficamente hablando. Por tanto, deberemos hacer lo siguiente:

  • Dibujar la nueva cabeza en la posición correspondiente
  • Sustituir la antigua cabeza por un elemento de la cola
  • Borrar el último elemento de la cola

Para conseguir modelizar este comportamiento de manera sencilla vamos a usar unas cuantas variables auxiliares. Por un lado necesitamos conocer dónde está la cabeza y dónde el último elemento de la cola de la serpiente. Por otro lado, para evitarnos tener que andar recalculando cada vez, almacenaremos para cada elemento de la serpiente, dónde está el elemento que la precede (de esta forma, al borrar el último elemento de la cola sabremos quién pasa a ocupar ese último lugar). Este dato lo almacenaremos en las matrices x e y.

Las orientaciones las hemos codificado de la siguiente manera (a estas alturas ya os habréis dado cuenta de que lo que se le da bien al Spectrum, como cualquier ordenador, es trabajar con números):

eje horizontal (matriz x):

    1 - derecha
   -1 - izquierda
   
eje vertical (matriz y):

    1 - abajo
   -1 - arriba

Estos valores no los hemos determinado así al azar, sino debido a que el origen de coordenadas de la pantalla del Spectrum en modo texto, el punto (0,0) se encuentra en la esquina superior izquierda de la pantalla.

Además, usamos un par de variables, orientacionx y orientaciony, que son las que modificaremos al leer el teclado, y que indican hacia qué dirección debe moverse la cabeza de la serpiente en la siguiente iteración.

Por último, en la matriz p almacenaremos una representación abstracta de lo que vemos en pantalla, de manera que el tratamiento de colisiones sea muy sencillo. Para ello, en cada posición almacenaremos uno de los siguientes valores:

    0 - posición vacía
    1 - fruta
    2 - cabeza
    3 - elemento de la cola
    4 - pared

Éste es el código de definición de variables:

100  REM Definicion de variables
110  LET cabezax = 11 : REM coordenada x de la cabeza
120  LET cabezay = 5 : REM coordenada y de la cabeza
130  LET colax = 5 : REM coordenada x de la cola
140  LET colay = 5 : REM coordenada y de la cola
150  LET orientacionx = 1
160  LET orientaciony = 0
170  DIM p(23,34) : REM Pantalla
180  DIM x(23,34) : REM Orientacionesx
190  DIM y(23,34) : REM Orientacionesy

Partimos de que la serpiente ya está pintada. Así que, para empezar, cambiaremos la antigua cabeza por un elemento de cola:

3000 REM Movemos la serpiente
3005 INK 0
3010 REM Cambiamos la orientacion
3015 LET x(cabezay+2,cabezax+2) = orientacionx
3020 LET y(cabezay+2,cabezax+2) = orientaciony
3025 REM Borramos la antigua cabeza
3030 PRINT AT cabezay,cabezax ; "O"
3035 LET p(cabezay+2,cabezax+2) = 3
3040 LET cabezax = cabezax + orientacionx
3045 LET cabezay = cabezay + orientaciony

A continuación habría que pintar la cabeza en su nueva posición pero, ¿qué pasa si esa posición ya está ocupada? Antes de proseguir pintando, debemos ver qué hay donde vamos a dibujar.


Al mover la cabeza de la serpiente a su nueva posición, pueden ocurrir 3 cosas:

  • Que la posición de destino esté vacía.
  • Que la posición de destino esté ocupada por una fruta.
  • Que la posición de destino esté ocupada por la pared o la cola de la serpiente.

En el primero de los casos no haremos nada, simplemente proseguiremos con el redibujado de la serpiente.

En el segundo caso, la cola de la serpiente crecerá. Para simular este efecto, basta con no borrar el último elemento de la cola (por eso hemos decidido retrasar el borrado hasta después de la detección de colisiones.

En el último caso, se acaba la partida.

Aquí está el fragmento de código encargado de la detección de colisiones. Basta con consultar nuestra representación matricial de lo que hay en pantalla. Si en la nueva posición hallamos un 1 en la matriz, se trata de una fruta, con lo que sumamos la puntuación y generamos una nueva fruta. Si hallamos un valor mayor que uno (cola o pared), significará el final de la partida (el código lo hemos colocado a partir de la línea 9900).

3040 LET cabezax = cabezax + orientacionx
3045 LET cabezay = cabezay + orientaciony
3050 IF p(cabezay+2,cabezax+2) > 1 THEN GO TO 9900
3051 IF p(cabezay+2,cabezax+2) = 1 
     THEN LET puntos = puntos + 10 : PRINT AT 21,10 ;
     PAPER 1 ; INK 7 ; puntos : LET comido = 1 : GO SUB 8000

Sumamos la puntuación correspondiente, actualizamos el marcador y anotamos en la variable comido que hemos ingerido una fruta. Esto es importante a la hora de decidir si borramos la última posición de la cola o la serpiente debe crecer.

Y en la línea 8000 hemos colocado la subrutina que se encarga de generar una nueva fruta. En la línea 8030 nos estamos asegurando de que colocaremos la fruta en un lugar vacío.

8000 REM Generacion de frutas
8010 LET frutax = INT(RND*30)+1
8020 LET frutay = INT(RND*20)+1
8030 IF p(frutay+2,frutax+2) = 0 THEN GO TO 8050
8040 GO TO 8010
8050 PRINT AT frutay,frutax ; INK 2 ; "{F}"
8060 LET p(frutay+2,frutax+2) = 1
8070 RETURN

Bien, recordemos que dejamos la serpiente a medio pintar. Debemos pintar la cabeza en su nueva ubicación, ahora que sabemos que no ha ocurrido nada grave.

3055 REM Pintamos la nueva cabeza
3060 PRINT AT cabezay,cabezax ; "{S}"
3065 LET p(cabezay+2,cabezax+2) = 2

Por último, para acabar con el movimiento de la serpiente, si no ha habido ninguna colisión, recordemos que debemos borrar el último elemento de la cola. Esto lo hace la siguiente porción de código y la subrutina a la que invoca:

3070 IF comido = 0 THEN GO SUB 8100
3080 LET comido = 0
 
...
 
8100 REM Borramos la cola
8110 PRINT AT colay,colax ; " "
8120 LET nuevacolax = colax + x(colay+2,colax+2)
8130 LET nuevacolay = colay + y(colay+2,colax+2)
8140 LET p(colay+2,colax+2) = 0
8150 LET x(colay+2,colax+2) = 0
8160 LET y(colay+2,colax+2) = 0
8170 LET colax = nuevacolax
8180 LET colay = nuevacolay
8190 RETURN


La lectura del teclado la haremos mediante el uso de la sentencia INKEY$. INKEY$ devuelve el valor del carácter producido por la tecla presionada en ese momento. Es importante tener en cuenta que si, en el momento de ejecutarse la sentencia no se está pulsando ninguna tecla, INKEY$ devolverá la cadena vacía (“”) como resultado. Por lo tanto, normalmente INKEY$ se incluye dentro de un bucle que espera a la pulsación de una tecla. En nuestro caso, en ese bucle moveremos la serpiente, que sigue moviéndose en una dirección aunque no pulsemos ninguna tecla.

Por tanto, lo único que haremos al detectar la pulsación de una tecla será actualizar la orientación de la serpiente, para que se gire, si procede, en la siguiente iteración del bucle.

3210 LET a$ = INKEY$
3220 IF orientacionx < 1 AND (a$ = "O" OR a$ = "o")
     THEN LET orientacionx = -1 : LET orientaciony = 0
3230 IF orientacionx > -1 AND (a$ = "P" OR a$ = "p")
     THEN LET orientacionx = 1 : LET orientaciony = 0
3240 IF orientaciony < 1 AND (a$ = "Q" OR a$ = "q")
     THEN LET orientacionx = 0 : LET orientaciony = -1
3250 IF orientaciony > -1 AND (a$ = "A" OR a$ = "a")
     THEN LET orientacionx = 0 : LET orientaciony = 1


En este artículo hemos puesto en vuestras manos la base de funcionamiento de cualquier videojuego. Evidentemente, el esquema del ZXSnake se puede complicar muchísimo más. Hemos aprendido a pintar (pintar con el mínimo esfuerzo para obtener el mejor resultado), a detectar colisiones entre objetos (es decir, que los objetos interactúen), y a leer el teclado para que el usuario pueda introducir órdenes al juego.

Quizás la idea principal que se puede sacar es que resulta de gran utilidad mantener una representación abstracta de lo que estamos dibujando en pantalla a la hora de gestionar todos los elementos que componen el juego, esto es, separar la lógica del juego de su representación en pantalla. También que el proceso de dibujado suele ser el más costoso en términos de potencia de proceso, así que cuanto menos dibujemos, el juego se ejecutará de forma más fluida.

Por último, sentíos libres de destripar a fondo el código y jugar con él. No hay una única forma de hacer las cosas, y seguro que encontráis soluciones mejores a las aquí expuestas a la hora de programar un ZXSnake.



1  REM ********************************************************************
2  REM ZXSnake by Federico J. Alvarez Valero (05-02-2003)
10 REM This program is free software; you can redistribute it and/or modify
11 REM it under the terms of the GNU General Public License as published by
12 REM the Free Software Foundation; either version 2 of the License, or
13 REM (at your option) any later version.
14 REM This program is distributed in the hope that it will be useful,
15 REM but WITHOUT ANY WARRANTY; without even the implied warranty of
16 REM MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 REM GNU General Public License for more details.
18 REM You should have received a copy of the GNU General Public License
19 REM along with this program; if not, write to the Free Software
20 REM Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
40 REM ********************************************************************
 
50   BORDER 7 : PAPER 7 : INK 0 : CLS
51   PRINT AT 3,13 ; PAPER 1 ; INK 7 ; "ZXSnake"
52   PRINT AT 5,9 ; PAPER 7 ; INK 0 ; "Q - ARRIBA"
53   PRINT AT 6,9 ; PAPER 7 ; INK 0 ; "A - ABAJO"
54   PRINT AT 7,9 ; PAPER 7 ; INK 0 ; "O - IZQUIERDA"
55   PRINT AT 8,9 ; PAPER 7 ; INK 0 ; "P - DERECHA"
56   PRINT AT 10,3 ; PAPER 7 ; INK 0 ; "Recoge la mayor cantidad de"
57   PRINT AT 11,3 ; PAPER 7 ; INK 0 ; "frutas posible y crece"
58   PRINT AT 12,3 ; PAPER 7 ; INK 0 ; "sin chocarte..."
59   PRINT AT 15,3 ; PAPER 7 ; INK 0 ; "Pulsa una tecla para jugar"
60   LET j$ = INKEY$
61   IF j$ = "" THEN GO TO 60
 
70   REM UDG
71   RESTORE 75
72   FOR i = 0 TO 7: READ d : POKE USR "S"+i,d : NEXT i
73   FOR i = 0 TO 7: READ d : POKE USR "F"+i,d : NEXT i
75   DATA 60, 66, 129, 129, 129, 129, 66, 60 : REM serpiente (S)
76   DATA 24, 60, 60, 60, 126, 251, 247, 126 : REM fruta (F)
 
100  REM Definicion de variables
110  LET cabezax = 11 : REM coordenada x de la cabeza
120  LET cabezay = 5 : REM coordenada y de la cabeza
130  LET colax = 5 : REM coordenada x de la cola
140  LET colay = 5 : REM coordenada y de la cola
150  LET orientacionx = 1
160  LET orientaciony = 0
170  DIM p(23,34) : REM Pantalla
180  DIM x(23,34) : REM Orientacionesx
190  DIM y(23,34) : REM Orientacionesy
200  LET puntos = 0
210  LET comido = 0
220  LET maxx = 33
230  LET maxy = 22
240  LET minx = 0
250  LET miny = 0
 
1000 REM Inicializacion de la pantalla
1010 BORDER 1
1015 CLS
1020 PRINT AT 21,0 ; PAPER 1 ; INK 7 ; " PUNTOS :                       "
1030 FOR c = minx TO maxx
1040 LET p(miny+1,c+1) = 4
1050 LET p(maxy+1,c+1) = 4
1060 NEXT c
1070 FOR f = miny TO maxy
1080 LET p(f+1,minx+1) = 4
1090 LET p(f+1,maxx+1) = 4
1100 NEXT f  
 
1500 GO SUB 8000 : REM Generar la primera fruta
 
2000 REM Pintamos la serpiente (posicion inicial)
2001 PAPER 7 : INK 0
2005 REM Pintamos el cuerpo
2010 FOR c = colax TO cabezax-1
2020 PRINT AT colay,c ; INK 0 ; "O"
2025 LET p(colay+2,c+2) = 3
2026 LET x(colay+2,c+2) = 1
2027 LET y(colay+2,c+2) = 0
2030 NEXT c
2040 REM Pintamos la cabeza
2050 PRINT AT cabezay,cabezax ; INK 0 ; "{S}"
2055 LET p(cabezay+2,cabezax+2) = 2
2056 LET x(cabezay+2,cabezax+2) = 1
2057 LET y(cabezay+2,cabezax+2) = 0
 
3000 REM Movemos la serpiente
3005 INK 0
3010 REM Cambiamos la orientacion
3015 LET x(cabezay+2,cabezax+2) = orientacionx
3020 LET y(cabezay+2,cabezax+2) = orientaciony
3025 REM Borramos la antigua cabeza
3030 PRINT AT cabezay,cabezax ; "O"
3035 LET p(cabezay+2,cabezax+2) = 3
3040 LET cabezax = cabezax + orientacionx
3045 LET cabezay = cabezay + orientaciony
3050 IF p(cabezay+2,cabezax+2) > 1 THEN GO TO 9900
3051 IF p(cabezay+2,cabezax+2) = 1 
     THEN LET puntos = puntos + 10 : PRINT AT 21,10 ; 
     PAPER 1 ; INK 7 ; puntos : LET comido = 1 : GO SUB 8000
3055 REM Pintamos la nueva cabeza
3060 PRINT AT cabezay,cabezax ; "{S}"
3065 LET p(cabezay+2,cabezax+2) = 2
3070 IF comido = 0 THEN GO SUB 8100
3080 LET comido = 0
 
3200 REM Leemos el teclado
3210 LET a$ = INKEY$
3220 IF orientacionx < 1 AND (a$ = "O" OR a$ = "o") 
     THEN LET orientacionx = -1 : LET orientaciony = 0
3230 IF orientacionx > -1 AND (a$ = "P" OR a$ = "p") 
     THEN LET orientacionx = 1 : LET orientaciony = 0
3240 IF orientaciony < 1 AND (a$ = "Q" OR a$ = "q") 
     THEN LET orientacionx = 0 : LET orientaciony = -1
3250 IF orientaciony > -1 AND (a$ = "A" OR a$ = "a") 
     THEN LET orientacionx = 0 : LET orientaciony = 1
 
7998 GO TO 3000
 
8000 REM Generacion de frutas
8010 LET frutax = INT(RND*30)+1
8020 LET frutay = INT(RND*20)+1
8030 IF p(frutay+2,frutax+2) = 0 THEN GO TO 8050
8040 GO TO 8010
8050 PRINT AT frutay,frutax ; INK 2 ; "{F}"
8060 LET p(frutay+2,frutax+2) = 1
8070 RETURN
 
8100 REM Borramos la cola
8110 PRINT AT colay,colax ; " "
8120 LET nuevacolax = colax + x(colay+2,colax+2)
8130 LET nuevacolay = colay + y(colay+2,colax+2)
8140 LET p(colay+2,colax+2) = 0
8150 LET x(colay+2,colax+2) = 0
8160 LET y(colay+2,colax+2) = 0
8170 LET colax = nuevacolax
8180 LET colay = nuevacolay
8190 RETURN
 
9900 REM Fin de la partida
9910 PRINT AT 10,12 ; INK 2 ; "SE ACABO..."
9920 PRINT AT 11,10 ; INK 2 ; "PUNTUACION : " ; puntos
9930 PRINT AT 13,10 ; INK 0 ; "Pulsa una tecla"
9931 REM Pausa obligada para que se vean los letreros
9932 FOR i = 0 TO 100
9933 NEXT i
9940 LET i$ = INKEY$
9950 IF i$ <> "" THEN GO TO 100
9960 GO TO 9940


Federico J. Álvarez
Noviembre 2004
MagazineZX #10

  • cursos/basic/zxsnake_ejemplo_comentado.txt
  • Última modificación: 20-03-2009 21:40
  • por falvarez