python

Juego de Memorama (memoria) en Python con PyGame

Hoy te mostraré un juego que recién he programado usando el lenguaje de programación Python y la librería PyGame.

Se trata del videojuego Memorama, también conocido como Memoria. Es un juego en donde hay varias tarjetas en donde hay que voltearlas y encontrar el par de cada una. El jugador gana cuando encuentra todos los pares de tarjetas.

El juego que he desarrollado en PyGame cuenta con imágenes, música de fondo y sonidos, además de que este Memorama o juego de memoria es totalmente jugable.

Obviamente el código es open source y gratuito, así que puedes descargarlo, probarlo, usarlo y modificarlo a tu gusto.

Características del juego memoria en Python

Como lo mencioné anteriormente, este juego está desarrollado con PyGame. Utiliza imágenes tomadas de flaticon y sonidos de freesound. Muchas gracias a ambas páginas por poner a la disposición del público tan buenas creaciones.

Memorama en Python y PyGame – Juego de memoria

En el inicio del juego se muestran todas las imágenes originales y sin ser ocultadas. En cuanto se hace clic en el botón de “Iniciar juego” las imágenes son mezcladas y ahora se muestra el lado oculto de cada tarjeta o carta.

Al hacer clic en cada imagen, la misma gira o “se voltea” para mostrar la imagen real. Y después se puede buscar su pareja.

El jugador gana cuando ya no hay tarjetas ocultas; y puede volver a iniciar el juego. Cuando el juego de memoria es reiniciado, se mezclan y ocultan las imágenes. Así de manera infinita.

A través del post te explicaré los fragmentos del código más importantes de este juego de Memorama open source y gratuito programado con Python y PyGame.

Recuerda que para probarlo debes contar con Python y PIP. Además, debes instalar pygame con:

pip install pygame

Algoritmo general

Lo que hacemos es tener una matriz que representa al juego en sí. La matriz, que no es otra cosa más que un arreglo de arreglos, tiene filas. Cada fila tiene un cuadro. En esos cuadros guardamos toda la información relacionada a la tarjeta.

Por ejemplo, cada cuadro tiene propiedades como descubierto que indica si se debería mostrar porque el jugador lo descubrió antes, así como la fuente de la imagen o la propiedad mostrar que indica si la imagen original se debería mostrar porque se está buscando su par.

Luego, pintamos todos esos cuadros en la pantalla usando PyGame, escuchamos el clic del usuario y verificamos cuál clic es.

En caso de que sea el primer clic, entonces modificamos algunas variables para esperar el segundo clic (que será cuando el jugador busque la pareja de la tarjeta anteriormente seleccionada).

Cuando escuchamos el segundo clic verificamos a través de la ruta de la imagen si las imágenes son iguales, en ese caso reproducimos el sonido de éxito y ponemos ambos cuadros como descubiertos.

Si el jugador se equivoca en la tarjeta, entonces establecemos una marca de tiempo para indicarle al programa que las tarjetas deben ocultarse dentro de determinados segundos.

El jugador gana el juego de Memorama cuando todos los cuadros tienen la propiedad descubierto en True.

Recuerda que todo el proceso del juego se estará realizando en un ciclo infinito.

Por cierto, ya he hecho este juego anteriormente en JavaScript.

Clase Cuadro

Como lo dije, todo el juego es una matriz de instancias de la clase Cuadro que se ve así:

"""
Una clase que representa el cuadro. El mismo tiene una imagen y puede estar
descubierto (cuando ya lo han descubierto anteriormente y no es la tarjeta buscada actualmente)
o puede estar mostrado (cuando se voltea la imagen)
También tiene una fuente o nombre de imagen que servirá para compararlo más tarde
"""


class Cuadro:
    def __init__(self, fuente_imagen):
        self.mostrar = True
        self.descubierto = False
        """
        Una cosa es la fuente de la imagen (es decir, el nombre del archivo) y otra
        la imagen lista para ser pintada por PyGame
        La fuente la necesitamos para más tarde, comparar las tarjetas
        """
        self.fuente_imagen = fuente_imagen
        self.imagen_real = pygame.image.load(fuente_imagen)

En la línea 19 estamos cargando la imagen, de este modo cada instancia tendrá su propia imagen. Recuerda que también guardamos la fuente de la imagen (la cadena) para más tarde comparar si dos imágenes son iguales.

Una vez definida la clase, veamos también el tablero del juego:

cuadros = [
    [Cuadro("assets/coco.png"), Cuadro("assets/coco.png"),
     Cuadro("assets/manzana.png"), Cuadro("assets/manzana.png")],
    [Cuadro("assets/limón.png"), Cuadro("assets/limón.png"),
     Cuadro("assets/naranja.png"), Cuadro("assets/naranja.png")],
    [Cuadro("assets/pera.png"), Cuadro("assets/pera.png"),
     Cuadro("assets/piña.png"), Cuadro("assets/piña.png")],
    [Cuadro("assets/plátano.png"), Cuadro("assets/plátano.png"),
     Cuadro("assets/sandía.png"), Cuadro("assets/sandía.png")],
]

El constructor de Cuadro recibe únicamente una cosa: la ruta de la imagen. En este caso definimos dos cuadros por cada imagen, pues recuerda que debe buscarse el par de cada una. Más tarde veremos cómo mezclarlas.

Fíjate bien en la matriz, pues es un arreglo que tiene arreglos. Cada arreglo interno tiene 4 imágenes o mejor dicho, 4 instancias de Cuadro.

En total hay 16 imágenes en este juego de Memoria en Python, aunque recordemos que se repiten, así que solo hay 8 imágenes únicas.

Colores y sonidos

Definimos algunos colores del juego (para el color del botón, color de fondo, etcétera) y también los sonidos que vamos a reproducir a lo largo del mismo:

# Colores
color_blanco = (255, 255, 255)
color_negro = (0, 0, 0)
color_gris = (206, 206, 206)
color_azul = (30, 136, 229)

# Los sonidos
sonido_fondo = pygame.mixer.Sound("assets/fondo.wav")
sonido_clic = pygame.mixer.Sound("assets/clic.wav")
sonido_exito = pygame.mixer.Sound("assets/ganador.wav")
sonido_fracaso = pygame.mixer.Sound("assets/equivocado.wav")
sonido_voltear = pygame.mixer.Sound("assets/voltear.wav")

Botón para iniciar juego

El usuario debe hacer clic en un botón para iniciar el juego. En este caso el botón no es más que un rectángulo. Primero definimos la fuente que vamos a usar para escribir texto dentro del botón, y luego el botón:

# La fuente que estará sobre el botón
tamanio_fuente = 20
fuente = pygame.font.SysFont("Arial", tamanio_fuente)
xFuente = int((anchura_boton / 2) - (tamanio_fuente / 2))
yFuente = int(altura_pantalla - altura_boton)

# El botón, que al final es un rectángulo
boton = pygame.Rect(0, altura_pantalla - altura_boton,
                    anchura_boton, altura_pantalla)

Los cálculos que ves son para posicionar el botón en la parte inferior. Al analizar el código fuente completo verás a lo que me refiero.

Para dibujar el botón hacemos lo siguiente, dependiendo de si el juego está o no iniciado:

# También dibujamos el botón
if juego_iniciado:
    # Si está iniciado, entonces botón blanco con fuente gris para que parezca deshabilitado
    pygame.draw.rect(pantalla_juego, color_blanco, boton)
    pantalla_juego.blit(fuente.render(
        "Iniciar juego", True, color_gris), (xFuente, yFuente))
else:
    pygame.draw.rect(pantalla_juego, color_azul, boton)
    pantalla_juego.blit(fuente.render(
        "Iniciar juego", True, color_blanco), (xFuente, yFuente))

Funciones útiles

Aunque el juego no reside en una clase separada, y todo está de manera global, tenemos algunas funciones útiles que nos permiten separar el código.


# Ocultar todos los cuadros
def ocultar_todos_los_cuadros():
    for fila in cuadros:
        for cuadro in fila:
            cuadro.mostrar = False
            cuadro.descubierto = False


def aleatorizar_cuadros():
    # Elegir X e Y aleatorios, intercambiar
    cantidad_filas = len(cuadros)
    cantidad_columnas = len(cuadros[0])
    for y in range(cantidad_filas):
        for x in range(cantidad_columnas):
            x_aleatorio = random.randint(0, cantidad_columnas - 1)
            y_aleatorio = random.randint(0, cantidad_filas - 1)
            cuadro_temporal = cuadros[y][x]
            cuadros[y][x] = cuadros[y_aleatorio][x_aleatorio]
            cuadros[y_aleatorio][x_aleatorio] = cuadro_temporal


def comprobar_si_gana():
    if gana():
        pygame.mixer.Sound.play(sonido_exito)
        reiniciar_juego()


# Regresa False si al menos un cuadro NO está descubierto. True en caso de que absolutamente todos estén descubiertos
def gana():
    for fila in cuadros:
        for cuadro in fila:
            if not cuadro.descubierto:
                return False
    return True


def reiniciar_juego():
    global juego_iniciado
    juego_iniciado = False


def iniciar_juego():
    pygame.mixer.Sound.play(sonido_clic)
    global juego_iniciado
    # Aleatorizar 3 veces
    for i in range(3):
        aleatorizar_cuadros()
    ocultar_todos_los_cuadros()
    juego_iniciado = True

Tenemos varias funciones que, espero, se expliquen por sí mismas. Por ejemplo, una función oculta todos los cuadros del arreglo. Otra función verifica si el jugador ha ganado, existe otra que mezcla las tarjetas, etcétera.

Recuerda que usamos la palabra reservada global para modificar una variable global, pues en Python podemos leer pero no modificar las variables globales, al menos que usemos esta palabra reservada.

Eventos del juego

Ahora vamos a ver cómo detectar el clic de las imágenes o el botón, además del evento para cerrar el juego. Para cerrar el juego en la señal de Python, invocamos a sys.exit:

# Si quitan el juego, salimos
if event.type == pygame.QUIT:
    sys.exit()

Existe otro evento, que es el clic del botón. Debido a que es un rectángulo, podemos usar los métodos de PyGame para saber si el clic colisiona con el botón:

# Si hicieron clic y el usuario puede jugar...
elif event.type == pygame.MOUSEBUTTONDOWN and puede_jugar:

    """
    xAbsoluto e yAbsoluto son las coordenadas de la pantalla en donde se hizo
    clic. PyGame no ofrece detección de clic en imagen, por ejemplo. Así que
    se deben hacer ciertos trucos
    """
    # Si el click fue sobre el botón y el juego no se ha iniciado, entonces iniciamos el juego
    xAbsoluto, yAbsoluto = event.pos
    if boton.collidepoint(event.pos):
        if not juego_iniciado:
            iniciar_juego()

En este caso collidepoint recibe x e y, mismos que ya están dentro de event.pos.

Finalmente veamos cómo detectar si el usuario hizo clic en una imagen, pues en este método es en donde se lleva la mayor parte del funcionamiento del juego memorama en Python:

# Si no hay juego iniciado, ignoramos el clic
if not juego_iniciado:
    continue
"""
Ahora necesitamos a X e Y como índices del arreglo. Los índices no
son lo mismo que los pixeles, pero sabemos que las imágenes están en un arreglo
y por lo tanto podemos dividir las coordenadas entre la medida de cada cuadro, redondeando
hacia abajo, para obtener el índice.
Por ejemplo, si la medida del cuadro es 100, y el clic es en 140 entonces sabemos que le dieron
a la segunda imagen porque 140 / 100 es 1.4 y redondeado hacia abajo es 1 (la segunda posición del
arreglo) lo cual es correcto. Por poner otro ejemplo, si el clic fue en la X 50, al dividir da 0.5 y
resulta en el índice 0
"""
x = math.floor(xAbsoluto / medida_cuadro)
y = math.floor(yAbsoluto / medida_cuadro)
# Primero lo primero. Si  ya está mostrada o descubierta, no hacemos nada
cuadro = cuadros[y][x]
if cuadro.mostrar or cuadro.descubierto:
    # continue ignora lo de abajo y deja que el ciclo siga
    continue
# Si es la primera vez que tocan la imagen (es decir, no están buscando el par de otra, sino apenas
# están descubriendo la primera)
if x1 is None and y1 is None:
    # Entonces la actual es en la que acaban de dar clic, la mostramos
    x1 = x
    y1 = y
    cuadros[y1][x1].mostrar = True
    pygame.mixer.Sound.play(sonido_voltear)
else:
    # En caso de que ya hubiera una clickeada anteriormente y estemos buscando el par, comparamos...
    x2 = x
    y2 = y
    cuadros[y2][x2].mostrar = True
    cuadro1 = cuadros[y1][x1]
    cuadro2 = cuadros[y2][x2]
    # Si coinciden, entonces a ambas las ponemos en descubiertas:
    if cuadro1.fuente_imagen == cuadro2.fuente_imagen:
        cuadros[y1][x1].descubierto = True
        cuadros[y2][x2].descubierto = True
        x1 = None
        x2 = None
        y1 = None
        y2 = None
        pygame.mixer.Sound.play(sonido_clic)
    else:
        pygame.mixer.Sound.play(sonido_fracaso)
        # Si no coinciden, tenemos que ocultarlas en el plazo de [segundos_mostrar_pieza] segundo(s). Así que establecemos
        # la bandera. Como esto es un ciclo infinito y asíncrono, podemos usar el tiempo para saber
        # cuándo fue el tiempo en el que se empezó a ocultar
        ultimos_segundos = int(time.time())
        # Hasta que el tiempo se cumpla, el usuario no puede jugar
        puede_jugar = False
comprobar_si_gana()

Los comentarios son los que explican todo el código que ya expliqué anteriormente en el algoritmo. Solo fíjate en el truco que hace que sepamos en cuál imagen hicieron clic, pues debido a que todo es una cuadrícula, podemos obtener los índices a partir de las coordenadas.

Presta atención a la variable ultimos_segundos, esa variable indica si se debe ocultar alguna imagen dentro de determinados segundos. Se usa la misma lógica que el ejercicio de Semáforo en arduino sin delay.

También fíjate que en cada clic de la imagen comprobamos si el usuario gana, invocando en la línea 53 a la función que previamente mostré.

Ocultar imágenes si no se encuentra pareja

Cuando se hace clic en la segunda imagen, en caso de que esta no sea el par de la anteriormente seleccionada, se ocultan de nuevo. El código que hace eso posible es:

ahora = int(time.time())
# Y aquí usamos la bandera del tiempo, de nuevo. Si los segundos actuales menos los segundos
# en los que se empezó el ocultamiento son mayores a los segundos en los que se muestra la pieza, entonces
# se ocultan las dos tarjetas y se reinician las banderas
if ultimos_segundos is not None and ahora - ultimos_segundos >= segundos_mostrar_pieza:
    cuadros[y1][x1].mostrar = False
    cuadros[y2][x2].mostrar = False
    x1 = None
    y1 = None
    x2 = None
    y2 = None
    ultimos_segundos = None
    # En este momento el usuario ya puede hacer clic de nuevo pues las imágenes ya estarán ocultas
    puede_jugar = True

Dibujar memorama

Ya vimos todos los eventos, ahora veamos cómo dibujar el juego de memoria, mostrar las imágenes ya sea ocultas o volteadas, etcétera. El dibujo de la pantalla queda así:


# Hacer toda la pantalla blanca
pantalla_juego.fill(color_blanco)
# Banderas para saber en dónde dibujar las imágenes, pues al final
# la pantalla de PyGame son solo un montón de pixeles
x = 0
y = 0
# Recorrer los cuadros
for fila in cuadros:
    x = 0
    for cuadro in fila:
        """
        Si está descubierto o se debe mostrar, dibujamos la imagen real. Si no,
        dibujamos la imagen oculta
        """
        if cuadro.descubierto or cuadro.mostrar:
            pantalla_juego.blit(cuadro.imagen_real, (x, y))
        else:
            pantalla_juego.blit(imagen_oculta, (x, y))
        x += medida_cuadro
    y += medida_cuadro

# También dibujamos el botón
if juego_iniciado:
    # Si está iniciado, entonces botón blanco con fuente gris para que parezca deshabilitado
    pygame.draw.rect(pantalla_juego, color_blanco, boton)
    pantalla_juego.blit(fuente.render(
        "Iniciar juego", True, color_gris), (xFuente, yFuente))
else:
    pygame.draw.rect(pantalla_juego, color_azul, boton)
    pantalla_juego.blit(fuente.render(
        "Iniciar juego", True, color_blanco), (xFuente, yFuente))

# Actualizamos la pantalla
pygame.display.update()

Para saber si se dibuja la imagen oculta o la real, accedemos a la propiedad mostrar y descubierto de cada cuadro. En caso de que alguna de estas esté en True, mostramos la imagen del cuadro.

Caso contrario, dibujamos la imagen oculta. Recuerda que todo esto se estará repitiendo varias veces por segundo.

Poniendo todo junto

Como lo dije anteriormente, solo expuse el código más importante. El código completo lo encuentras en mi GitHub.

Al final, solo debes instalar Python y Pip, instalar la dependencia de PyGame y ejecutar el script de memoria.py con:

python memoria.py

Acá una captura de mí jugando:

Jugando Memorama en Python – Juego gratuito y open source

Te invito a leer más sobre programación usando Python en mi blog. También te dejo el código de un memorama para la web usando JavaScript.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Ver comentarios

  • Tienes algun ejemplo para crear una mazmorra en Python, con matriz de 5 x 5 con una entrada, una salida, una trampilla, el aventurero solo tiene 4 movimientos (norte,sur,este y oeste) con el objetivo de salir de la mazmorra sin caer en la trampilla porque sino muere, puede ser un esqueleto o una trampilla.
    Es para un ejercicio que estoy haciendo con un curso de la Once. Solo me falta eso para terminarlo y no logro hacerlo. Me puede ayudar por favor

  • Dejaste los enlaces para descargar los sonidos del juego en alguna parte?, no los encontré, gracias me sirvió de mucho este tutorial

    • Hola. Gracias por sus comentarios.
      En cuanto a los sonidos, claro, están en el repositorio.
      Saludos!

  • Hola que tal !, se me hace super interesante tu implementación del memorana usando pygame. Pero en caso de que quisiéramos usar imágenes diferentes para el memorama, es decir, en una carta usar la imagen y en otra carta que sea texto, ya no se podría usar la fuente para encontrar el par ... ahi cómo se podría hacer??? ,... le agradecería me pudiese apoyar con una idea, estoy trabajando con algo similar en python.

Entradas recientes

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

2 días hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

2 días hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

2 días hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

2 días hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

2 días hace

Solución: Apache – Server unable to read htaccess file

Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…

3 días hace

Esta web usa cookies.