miércoles, 17 de octubre de 2012

UCBLogo: videojuego de la serpiente

El videojuego de la serpiente es un videojuego clásico (ahora en muchos móviles) que consiste en empezar siendo una serpiente pequeñita (un cuadrado blanco en nuestro caso) y al ir comiendo comida (bola roja) va aumentando el tamaño de su cuerpo. Se pierde si la cabeza de la serpiente choca contra cualquier pared (rectángulo rojo) o bien contra cualquier parte de su propio cuerpo. Además, iremos poniendo un contador según el número de comidas que logre. Una captura de pantalla del juego es la siguiente:



La dificultad de este videojuego (y de otros posteriores) es que tendremos que trabajar con tablas y llevar un doble juego de coordenadas: uno para la tabla y otro para la representación visual del juego en función del contenido de la tabla.

1.- Trabajando con tablas (vectores multidimensionales): aunque la representación visual es la que es, este juego está pensado sobre una tabla de 21 columnas y 15 filas. Para ello primero llenamos todas las celdas de la tabla con el valor 0. A la celda donde hay comida le damos el valor -1 (marcado en rojo). Y la celda que tiene la serpiente le damos un valor con el que comienza la cabeza de la serpiente: 1 (marcado en azul). Un ejemplo de esto (con una tabla de 8x8) sería así:

0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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
1
0
0
0
0
0
0
0
0
0
0
0

Para definir una tabla se usa mdarray y podríamos hacerlo con esta sintaxis:

make "tabla (mdarray (21 16) 1)

de esta manera definimos una variable que se llama tabla que va a ser una variable multidimensional (tendrá una composición de 21 columnas y 16 filas). El último número (el 1) sirve para indicar que no empiece en 0, sino en 1. Trabajar con tablas (o vectores multidimensionales) es algo engorroso y a veces no obtenemos el resultado deseado. En este caso, puede verse en la programación que en el procedimiento comienzo se define lo siguiente:

make "size_x 21
make "size_y 16
make "tabla (mdarray (list :size_x :size_y) 1)

es decir, en vez de usar directamente números usamos variables que contienen esos números y las metemos como una lista ordenada (es lo que hace el comando list).

Una vez definida ya nuestra tabla podemos rellenar el valor de la celdas de la misma:

mdsetitem (list :cx :cy) :tabla -1  ;cx y cy son coordenadas de la comida

el comando mdsetitem sirve para dar valores dentro del vector multidimensional, en este caso haremos que el contenido de tabla, en su celda cx, cy, valga -1.

Para llenar toda la tabla de valores 0:

for [i 1 21 1] [for [j 1 16 1] [mdsetitem (list :i :j) :tabla 0]]

hacemos lo mismo que en el caso anterior, pero usando dos bucles for anidados para llenen toda la tabla con el valor 0 (obviamente este paso se hace sólo al principio antes de llenarlo con el valor de la comida escrito anteriormente).

Finalmente escribimos el contenido de la celda en la que irá la cabeza de la serpiente:

mdsetitem (list :xx :yy) :tabla :tamanio

siendo :tamanio una variable cuyo primer valor será 1 y que irá aumentando según “coma” la serpiente.


2.- Trabajando con dos sistemas simultáneos de coordenadas, matriz y visual: tendremos dos sistemas de coordenadas: (xx,yy) serán las coordenadas de la cabeza de la serpiente correspondientes a la celda que ocupa en la tabla y (x,y) serán las coordenadas de la cabeza de la serpiente correspondiente a la posición en la pantalla. De igual manera tendremos (cx,cy) para la situación en la tabla de la comida y (ccx, ccy) para su representación en la pantalla. Esta situación es debido a que los cálculos se realizan sobre la tabla y, obviamente, no vamos a jugar con un único píxel que se mueve, queremos algo más grande. En nuestro caso algo 8 veces más grande. Ajustando cambio de escala y origen en función de dónde hemos dibujado el campo, tenemos algo como esto para las coordenadas de la cabeza de la serpiente:

make "x -80
make "y -80
make "xx ((x/8)+30)
make "yy ((y/8)+16)

esto nos deja (-80,-80) para dibujar la serpiente cuya posición en la tabla será de (20,6).

De igual manera, para la comida:

make "ccx (random 90)-180
make "ccy (random 40)-70
make "cx int ((ccx/8)+30)
make "cy int ((ccy/8)+16)

el cambio de escala es el mismo (dividir entre 8 y sumar un valor constante) pero introduciendo que la comida aparece de manera aleatoria.

En resumen, vamos a manejar dos conjuntos de coordenadas: una para los cálculos de lo que debe hacer el juego (sobre la tabla) y el otro para poder visualizar dónde están los elementos en la pantalla.

3.- El caracter virgulilla (~): una ojeada al código nos permite ver que hacemos uso por primera vez de este caracter. Se usa cuando la línea es demasiado larga para que siga leyendo el siguiente renglón como si no hubiera un intro:

for [i 1 21 1] [for [j 1 15 1] [make "dentro mditem (list :i :j) :tabla ~
 if dentro > 0 [mdsetitem (list :i :j) :tabla dentro-1] ~
if dentro = 1 [penup setxy ((i*8)-240) ((j*8)-128) pendown borraserpiente]]]

de tal forma que estos tres renglones el compilador los interpreta como una única línea (que es en realidad lo que deben ser).

4.- Explicación del funcionamiento del videojuego: supongamos que tenemos la posición indicada en el apartado anterior y supongamos que la cabeza de la serpiente (la serpiente sólo ocupa una celda de momento) se esté moviendo hacia la izquierda. Tenemos 4 posibilidades:
-        Posibilidad 1: que choque con la pared de la izquierda (esto se hizo con coordenadas normales, no con las matrices, algo ya explicado anteriormente): if x <= -230 [fin], es decir, si la coordenada “x” de la serpiente es menor que -230 llama al procedimiento fin que acaba la partida.
-        Posibilidad 2: la cabeza de la serpiente entra en una celda cuyo valor es 0. Si esto es así, no ocurre nada, salvo que la coordenada xx disminuye en una unidad (pues iba hacia la izquierda). Para saber el contenido de una celda definimos una variable llamada contenido que precisamente mira el contenido de la tabla en las coordenadas que ocuparía la cabeza de la serpiente en el siguiente paso: make "contenido mditem (list :xx :yy) :tabla.
-        Posibilidad 3: la cabeza de la serpiente entra en una celda cuyo valor es mayor o igual a 1, esto significa que ha chocado contra su propio cuerpo y, por lo tanto, pierde. Para ello: if contenido >= 1 [fin].
-        Posibilidad 4: la cabeza de la serpiente entra en una celda cuyo valor es -1, lo que significa que ha “chocado” con la comida, de esta forma el tamaño de la serpiente aumenta en uno y la comida debe aparecer en otro lado de la pantalla. Para ello: if contenido = -1 [comer].

En el procedimiento principal juego se llama al procedimiento ciclo que recorre todas las casillas y si el contenido es mayor que uno le resta uno, es el encargado de realizar la simulación del movimiento de la serpiente. Veamos el funcionamiento con este ejemplo:

0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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
1
0
0
0
0
0
0
0
0
0
0
0

La serpiente (de tamaño 1 y en azul) se acerca (suponemos movimiento a la izquierda) a la comida. Como el cuadro en el que va a entrar tiene un 0 no ocurre nada.

0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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
0
0

Ahora la serpiente entrará en una casilla en la que hay comida, por el que su tamaño aumentará en uno y la comida aparecerá en otra casilla. Como el tamaño aumenta en uno, el valor del contenido de la casilla en la que estaba la comida aumenta en uno, es decir, vale dos, por lo que justo en ese momento:

0
0
0
0
0
0
0
0
0
0
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
2
1
1
0
0
0
0
0
0
0
0
0
0
0

Si ahora movemos hacia arriba para buscar la comida, el procedimiento ciclo resta uno a todos los contenidos mayores que cero y el procedimiento serpiente pone un contenido igual al tamaño en el punto en el que está la cabeza de la serpiente:


0
0
0
0
0
0
0
0
0
0
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
2
0
0
0
0
0
0
0
1
0
1
0
0
0
0
0
0
0
0
0
0
0

Siguiendo el procedimiento:

0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
3
0
-1
0
0
0
0
0
2
0
0
0
0
0
0
0
1
0
1
0
0
0
0
0
0
0
0
0
0
0

Si restamos uno de nuevo a todos los contenidos mayores que cero y ponemos el valor del tamaño (ahora 3) en la cabeza de la serpiente, si giramos a la derecha:

0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
2
3
-1
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
0
0

Y así sucesivamente vamos simulando el movimiento de la serpiente.

Con todo esto ya podemos entender el código del juego.

Código del juego

;
; Videojuego de la serpiente
; Raultecnologia
; el programa esta en LOGO para usar directamente en el UCBLogo
; para empezar el juego teclea el procedimiento inicio
;
: en este juego usaremos los procedimientos MDARRAY, MDITEM y MDSETITEM
; para trabajar con vectores que tienen la informacion de que hay en cada celda
;

;
; el procedimiento inicio establece la pantalla de presentacion
;
to inicio
clearscreen
hideturtle
setpencolor 7
penup
setxy -200 140
label "Videojuego_de_la_serpiente
setxy -200 110
label "Usa_las_teclas_q_a_o_p_para_mover_a_la_serpiente
setxy -200 0
setpencolor 3
label "Pulsa_cualquier_tecla_para_empezar
make "tecla rc
comienzo
end

;
; el procedimiento comida genera al azar las coordenadas para
; que apareza la comida y la dibuja
;
to comida
make "ccx (random 90)-180
make "ccy (random 40)-70
make "cx int ((ccx/8)+30)
make "cy int ((ccy/8)+16)
make "haydentro mditem (list :cx :cy) :tabla
  ;miramos el contenido de la celda para la comida, si esta ocupada reiniciamos
if haydentro <> 0 [comida]
mdsetitem (list :cx :cy) :tabla -1
  ;la comida se marca con el numero -1
penup
setxy (cx*8)-236 (cy*8)-124
pendown
setpensize 1
setpencolor 4
for [i 1 3 1] [arc 360 i]
end
;

; el procedimiento comienzo establece las condiciones iniciales
;
to comienzo
clearscreen
campo
penup
hideturtle
make "tamanio 1
make "x -80
make "y -80
make "xx ((x/8)+30)
make "yy ((y/8)+16)
  ;manejamos dos tipos de coordenadas. x, y son las de la pantalla para dibujar
  ;xx, yy son las de las celdas correspondientes
make "puntos 0
make "vx -8
make "vy 0
 ;vx, vy son las velocidades iniciales
setxy -200 40
label "Puntos
setxy -100 40
label puntos
make "size_x 21
make "size_y 16
make "tabla (mdarray (list :size_x :size_y) 1)
  ;definimos el tamanio de la tabla
for [i 1 21 1] [for [j 1 16 1] [mdsetitem (list :i :j) :tabla 0]]
comida
mdsetitem (list :cx :cy) :tabla -1
serpiente
juego
end

;
; el procedimiento ciclo recorre todas las casillas y si el contenido de la celda es positivo,
; resta uno, y si es 1 borra la serpiente, pues es la cola
;
to ciclo
for [i 1 21 1] [for [j 1 15 1] [make "dentro mditem (list :i :j) :tabla ~
 if dentro > 0 [mdsetitem (list :i :j) :tabla dentro-1] ~
if dentro = 1 [penup setxy ((i*8)-240) ((j*8)-128) pendown borraserpiente]]]
end

;
; el procedimiento campo sencillamente dibuja el campo de juego
;
to campo
setpencolor 4
penup
setxy -228 -117
setpensize 8
pendown
fd 121
rt 90
fd 161
rt 90
fd 121
rt 90
fd 161
rt 90
penup
end

;
; el procedimiento serpiente dibuja la cabeza de la serpiente
;
to serpiente
penup
setxy x y
mdsetitem (list :xx :yy) :tabla :tamanio
pendown
setpensize 1
setpencolor 7
for [i 1 8 1] [fd i rt 90 fd i rt 90 fd i rt 90 fd i rt 90]
end
;
; el procedimiento borraserpiente borra el cuadro anterior
;

to borraserpiente
setpencolor 0
setpensize 1
pendown
for [i 1 8 1] [fd i rt 90 fd i rt 90 fd i rt 90 fd i rt 90]
end

;
; el procedimiento juego controla las condiciones del juego
;
to juego
mdsetitem (list :xx :yy) :tabla :tamanio
if keyp [make "tecla rc]
if tecla = "q [make "vx 0 make "vy 8]
if tecla = "a [make "vx 0 make "vy -8]
if tecla = "p [make "vx 8 make "vy 0]
if tecla = "o [make "vx -8 make "vy 0]
if x > -78 [fin]
if x <= -230 [fin]
if y >= -4 [fin]
if y <= -116 [fin]
make "x x+vx
make "y y+vy
make "xx ((x/8)+30)
make "yy ((y/8)+16)
make "contenido mditem (list :xx :yy) :tabla
if contenido = -1 [comer]
if contenido >= 1 [fin]
serpiente
ciclo
juego
end

to comer
penup
setxy x y
borraserpiente
make "tamanio tamanio+1
make "puntos puntos+10
penup
setxy -100 40
setpencolor 4
label puntos
comida
end

;
; el procedimiento fin establece el final de la partida
;
to fin
clearscreen
setpencolor 7
penup
setxy 0 0
label "has_terminado_la_partida
setxy 0 -30
label "puntos=
setxy 70 -30
label puntos
setxy 0 -100
label "pulsa_cualquier_tecla_para_comenzar
make "tecla2 rc
inicio
end