Juego de memorización

Introducción

captura de pantalla

En este proyecto se propone la creación de un juego de memorización. En este juego se presentan varias parejas de cartas boca abajo y el jugador debe localizar todas las parejas de cartas dando la vuelta una a una a las cartas, pero con la condición de que solo pueden estar boca arriba dos cartas que aún no hayan sido emparejadas. De esta forma, el jugador debe memorizar dónde están las cartas que ya ha visto para en cuanto destape una carta nueva, ser capaz de dar la vuelta a su pareja.

En este proyecto se utilizarán seis cartas Pokémon recortadas de Pokémon Trading Card Database (© Pokémon TM, ® Nintendo); y para su reverso, una versión reducida de la imagen Pokemon Card Backside in High Resolution (© AtomicmonkeyTCG). Todas estas imágenes se utilizan tan solo a efectos ilustrativos y con fines exclusivamente docentes. En caso de querer publicar una aplicación partiendo de este proyecto, ten en cuenta que deberías utilizar imágenes para las que tengas permisos de uso.

Paso 1: disposición de la interfaz gráfica

captura de pantalla

En este primer paso, se diseñará la interfaz gráfica de la aplicación y se dotará de una funcionalidad elemental a las tres cartas superiores.

La interfaz gráfica está formada por diversos componentes de disposición gráfica, que servirán para alinear las cartas en la pantalla, y por 12 botones, que harán las veces de las cartas. A continuación se describen los distintos componentes de la interfaz gráfica y los parámetros que se han variado con respecto a sus valores por defecto.

En primer lugar, con respecto al componente Screen1, se han modificado las siguientes propiedades:

A continuación, y como se puede ver en la figura de la derecha, en un primer nivel debajo de Screen1, se han dispuesto:

Las propiedades que se han modificado de los componentes HAFilaN han sido:

Por otra parte en los componentes VA_VSpacerN, tan solo se ha modificado la siguiente propiedad:

A continuación, dentro de cada componente HAFilaN se han dispuesto:

También se han añadido al proyecto las siguientes 6 imágenes (pulsa en cada una de ellas para descargarlas):
01.png 02.png 03.png 04.png 05.png 06.png

Una vez preparada la interfaz gráfica, el siguiente paso consiste en escribir el código necesario para que cada vez que el usuario pulse sobre el botón correspondiente a la primera carta, Carta1, su imagen alterne entre «reverso.png» y «01.png».

Cuando el código anterior funcione correctamente, cópialo y modifícalo para que también se aplique cuando se pulse sobre las cartas 2 y 3 (¡no lo hagas para todas las cartas!).

Descargar solución.

Paso 2: reproducción de sonidos

Todo juego que se precie debe ser capaz de reproducir sonidos. En este paso se añadirá un efecto de sonido de volteo de una carta, que como es de esperar, se reproducirá cada vez que el usuario voltee una carta (aunque por el momento solo en las 3 cartas superiores).

En primer lugar será necesario conseguir este sonido. Para ello, podríamos grabarlo nosotros mismos o acudir a un banco de sonidos que proporcione efectos de sonidos (ya sean gratuitos o de pago). En el caso de este proyecto, hemos utilizado el banco de sonidos freesound para conseguir los efectos sonoros que se añadirán al juego. freesound permite realizar búsquedas por nombre, por etiquetas y por tipo de licencia. Una vez hayamos localizado un sonido que nos guste, será necesario tener en cuenta su tipo de licencia para saber cómo podemos utilizarlo.

También hay que tener en cuenta que una vez descargado un sonido, es posible que haya que cambiarlo de su formato original a uno de los formatos de sonido soportados por Android. De hecho, los sonidos que se utilizarán en este proyecto han tenido que convertirse de wav a ogg (utilizando el comando GNU/Linux: lame -V 5 fichero.wav fichero.ogg).

Para el efecto de volteo de una carta se propone utilizar el fichero flip_card.ogg, que es una versión recortada y al doble de velocidad del fichero flip_card.wav del usuario Splashdust.

Para poder utilizar este sonido, en primer lugar habrá que añadir en la interfaz gráfica un componente Sound (sección Media), renombrarlo por ejemplo a «Voltear» y modificar su propiedad Source para que haga referencia al fichero flip_card.ogg (pulsa en el anterior enlace para descargarlo).

Una vez hecho lo anterior, añade el código necesario para que se reproduzca el sonido Voltear cada vez que se de la vuelta a una de las tres cartas superiores.

Descargar solución.

Paso 3: creando un procedimiento

Como se ha podido ver en los pasos anteriores, cada vez que se quiera añadir o cambiar el código relacionado con la pulsación de una carta, será necesario replicar y adaptar el código hecho para esa carta a los códigos de las 11 cartas restantes. Proceder de esta forma no es conveniente. No solo es tedioso, si no que es muy fácil cometer errores en algunos de los cambios realizados. Afortunadamente, existe una solución: encapsular el fragmento de código que debe ejecutarse cuando se voltea una carta en un procedimiento, al que se le pasarán los parámetros necesarios para que pueda realizar acciones distintas en función de qué carta se ha volteado en un momento dado. Una vez creado el procedimiento, el código que gestiona el evento de pulsar sobre en una carta se reducirá a una única instrucción: la llamada a este procedimiento con los parámetros adecuados.

Para ver qué parámetros requiere este procedimiento, que se podría llamar Voltea_carta, conviene fijarse en los códigos actuales para dos de las cartas y ver en qué se diferencian:

código

Como se puede ver en el código anterior, los bloques que gestionan el evento Click de los botones Carta1 y Carta2 se diferencian en:

  1. modifican y leen propiedades de los botones Carta1 y Carta2, respectivamente, y
  2. asignan una imagen distinta, 01.png y 02.png.

Así pues, una primera aproximación consistiría en crear un procedimiento, Voltea_carta, al que se le pasarían como parámetros: la carta y la imagen asociada a dicha carta. Conviene saber que MIT App Inventor proporciona bloques no ligados a un componente en particular que permiten leer o escribir los parámetros del componente que se especifique en la entrada of component. Estos bloques están en la sección Blocks, bajo la entrada Any component. A continuación se muestra cómo quedarían los manejadores del evento Click de las primeras dos cartas y el procedimiento Voltea_carta(Carta, Imagen).

código

Como se puede ver en el código anterior, para acceder en el procedimiento Voltea_carta al parámetro Image del botón correspondiente al parámetro de entrada Carta, se han utilizado los siguientes bloques genéricos: set Button.Image of component y Button.Image of component.

Descargar solución.

Paso 4: organizando los datos

Aunque en el paso anterior se llamaba a Voltea_carta desde el manejador del evento Click de cada botón indicando la carta que debía girarse (botón al que debía cambiarse su imagen) y la imagen del anverso de la carta en cuestión, esta última información no tiene sentido que se proporcione directamente en el código, ya que no habría manera de cambiar la asignación de las parejas de cartas: siempre estarían en la misma posición. Así pues, es conveniente que la información correspondiente a las imágenes asociadas a cada carta estén almacenadas en una estructura de datos que pueda modificarse en tiempo de ejecución. La estructura de datos idónea sería una lista de imágenes de tal forma que el elemento 1 de la lista indicaría la imagen de la carta 1, etc.

Por otro lado, para cada carta también será necesario saber si ya se ha encontrado la pareja a la que pertenece. Al igual que antes, la estructura de datos idónea para este caso sería una lista de elementos lógicos de tal forma que el elemento 1 de esta lista indicaría si la carta 1 forma parte de una pareja ya encontrada o no, etc.

Es más, también sería conveniente que los botones CartaN también estuvieran almacenados en una lista similar a las anteriores, ya que de esta forma, tan solo sería necesario indicar al procedimiento Voltea_carta el número de la carta que se quiere voltear y, una vez en el procedimiento, este número serviría para extraer tanto el componente Carta en cuestión, como su imagen y si ya ha sido encontrada su pareja o no.

Una vez vista la conveniencia de disponer de estas tres listas, se puede ir un paso más allá y utilizar una única lista, Cartas, que proporcione en cada una de sus entradas: una carta, su imagen y si ya ha sido encontrada. Esto se puede conseguir en MIT App Inventor utilizando una lista en la que cada uno de sus elementos es a su vez otra lista. Para el caso que nos ocupa, habría que construir una lista con 12 entradas, que a su vez serían listas de 3 entradas cada una. Así pues, los datos de una carta n vendrían dados por la lista de 3 elementos que se encuentre en la posición n de la lista Cartas.

Modifica el código del paso anterior para que:

  1. Se defina una variable global Cartas y se inicialice con una lista vacía. (No se puede inicializar la lista aquí ya que hasta que no se haya inicializado la pantalla, los componentes CartaN aún no habrán sido creados.)
  2. Cuando se produzca el evento Initialize de la pantalla, se asigne a la lista Cartas una lista con 12 entradas, donde cada entrada de esta lista será a su vez una lista de 3 elementos con: un botón CartaN, la imagen asociada a esa carta y un valor lógico false.
    Cuando hagas la asignación de imágenes, hazlo de tal forma que cada una de las imágenes 01.png a 06.png esté en dos cartas. No es necesario que te entretengas mucho en realizar la asignación, ya que al tratarse de permutaciones con repetición de 6 pares de elementos, hay 12!/2^6 = 7.484.400 (casi 7 millones y medio) de combinaciones posibles.
  3. Modifica el procedimiento Voltea_carta para que en lugar de necesitar dos parámetros, Carta e Imagen tan solo acepte uno: N, que indicará el número de carta que se quiere voltear.
    Teniendo en cuenta la estructura de datos que se ha escogido para almacenar los datos de todas las cartas, la forma más conveniente de explicitar dónde están los datos correspondiente a la carta N es utilizando el bloque initialize local Nombre_variable to. Por ejemplo, de la siguiente forma:

    código

    De esta forma, el código de Voltear_carta podrá utilizar las variables locales Carta, Imagen y Encontrada en lugar de tener que acceder, cada vez que se requiera una de estas variables, al elemento 1, 2, o 3, respectivamente, de la posición N de la lista Cartas.
  4. Completa el interior del procedimiento Voltear_carta a partir del código mostrado en el ítem anterior y el código al que se había llegado en el paso 3.
  5. Modifica el código asociado al evento Click de cada CartaN, para que llame al procedimiento Voltea_carta(N), indicando su número de carta. Crea ya los bloques de respuesta al evento Click para todas las cartas (hasta ahora solo se habían creado estos bloques para las 3 primeras cartas).

Descargar solución.

Paso 5: las reglas del juego

El código realizado en el paso anterior permite voltear todas las cartas. Este paso del proyecto consiste en añadir el código necesario para implementar las reglas del juego. Para ello, se deberá implementar el siguiente algoritmo dentro del procedimiento Voltear_carta(N):

Variables globales requeridas
=============================
 Cara_arriba: número de cartas cara arriba sin pareja. (Valor inicial: 0)
 Encontradas: número de cartas encontradas. (Valor inicial: 0)
 Carta1_n:    índice de la 1ª carta puesta cara arriba. (Valor inicial: 0)
 Carta2_n:    índice de la 2ª carta puesta cara arriba. (Valor inicial: 0)
 Cartas:      lista con los datos de cada una de las cartas.

Algoritmo
=========
/-----
|Si la carta N no está marcada como encontrada y cartas cara arriba < 2:
|  Voltea la carta (lo que ya se hacía: sonido y cambiar imagen)
| /-----
| |Si se ha mostrado el anverso de la carta:
| |  Añade uno al número de cartas cara arriba
| |Si no:
| |  Resta uno al número de cartas cara arriba
| \-----
| /-----
| | Si hay una carta cara arriba:
| |   Guarda el número de esta carta en Carta1_n
| | Si no y hay dos cartas cara arriba:
| |  /-----
| |  |Si son iguales (si la imagen de esta carta es igual a la última guardada):
| |  |  Reproduce sonido acierto
| |  |  Marca ambas cartas como encontradas
| |  |  Pon a 0 el número de cartas cara arriba
| |  |  Incrementa en 2 el número de cartas encontradas (Encontradas)
| |  |  Si el número de cartas encontradas es 12:
| |  |    Reproduce sonido de fin de juego
| |  |Si no (si no son iguales):
| |  |  Reproduce sonido fallo
| |  |  Guarda el número de esta segunda carta en Carta2_n
| |  |  Activa el temporizador de medio segundo
| |  \-----
| \-----
\-----
		

Conviene tener en cuenta que hasta que no se salga del manejador del evento Click, no se mostrarán los cambios hechos a los componentes gráficos. Por este motivo, no es posible que en el mismo procedimiento se muestre la imagen de una carta, se espere un tiempo y se vuelva a mostrar su reverso. Por mucho que se intente, siempre se verá el reverso (ya que cuando se sale del procedimiento la imagen que habrá será la del reverso). Una posible solución para conseguir dar la vuelta a las dos últimas cartas volteadas una vez transcurrido medio segundo desde que el jugador pulso en la carta, es crear un manejador de un temporizador que voltee las dos cartas que están boca arriba y no forman pareja, y cuando en el procedimiento Voltear_carta se detecte que ha ocurrido este caso, se active el temporizador programado a medio segundo.

Habiendo añadido a la intefaz gráfica un componente del tipo Clock, TemporizadorVoltear, con el temporizador desactivado inicialmente y el tiempo del temporizador fijado a 500 milisegundos, el código del manejador del temporizador podría ser, por ejemplo:

código

Por último, para los los sonidos de acierto, fallo y fin de juego, puedes utilizar los siguientes ficheros:

Descargar solución.

Paso 6: posibles extensiones y mejoras

El juego ya es completamente funcional, pero aún está en pañales, se podría mejorar implementando cualquiera de las siguientes ampliaciones: