javascript

Juego de Memorama en JavaScript – Tutorial

Resumen: en este post te voy a explicar cómo hacer el juego de memorama en JavaScript. Pondré el código fuente y una demostración en línea. Este juego también es conocido como “Memoria“.

Características del memorama con JavaScript

Memorama programado en JavaScript – Tutorial de juego

Este juego de memorama en JS que vengo a presentar tiene las siguientes características:

  • Totalmente responsivo, es decir, se adapta a cualquier pantalla
  • Las imágenes se pueden cambiar
  • Intentos máximos modificables
  • Alerta para cuando ganas y cuando pierdes
  • Código fuente modificable

Nota: otro videojuego que desarrollé con JavaScript fue el de snake.

Demostración y código fuente

A lo largo de este post voy a explicar las partes más importantes del juego de memorama en JavaScript, pero puedes ver el código fuente en GitHub que dejaré más adelante.

He grabado un vídeo:

Para probar el juego de memorama online simplemente accede a este enlace.

Creación del juego de memorama en JavaScript

Ahora comenzaré a dar los detalles.

Dibujo de imágenes

El diseño de cuadricula y las imágenes responsivas (así como el borde de las mismas) se consigue gracias al framework de Bootstrap 4.

Las imágenes que se ven son un arreglo, dibujado gracias a Vue JS 2. Tenemos dos arreglos, comenzando por la ruta de las imágenes del memorama:

imagenes: [
            "./img/cabra.jpg",
            "./img/conejo.jpg",
            "./img/leon.jpg",
            "./img/oveja.jpg",
            "./img/perro.jpg",
            "./img/gato.jpg",
        ],

El arreglo de las imágenes tiene objetos que tienen las siguientes propiedades:

  • ruta: la ruta absoluta de la imagen
  • mostrar: indica si se debe mostrar, temporalmente, la imagen
  • acertada: indica si esta imagen ya fue adivinada, para ignorar cualquier click que se haga sobre ella

Después de que se crea ese arreglo, se mezcla (es decir, se hace que se revuelvan sus elementos) y se parte dependiendo de la constante COLUMNAS (así se puede configurar el número de columnas del memorama en JS)

Finalmente, se asigna a la instancia de Vue para que las imágenes sean dibujadas:

reiniciarJuego() {
    let memorama = [];
    this.imagenes.forEach((imagen, indice) => {
        let imagenDeMemorama = {
            ruta: imagen,
            mostrar: false, // No se muestra la original
            acertada: false, // No es acertada al inicio
        };
        // Poner dos veces la misma imagen
        memorama.push(imagenDeMemorama, Object.assign({}, imagenDeMemorama));
    });

    // Sacudir o mover arreglo; es decir, hacerlo aleatorio
    this.mezclarArreglo(memorama);

    // Dividirlo en subarreglos o columnas
    let memoramaDividido = [];
    for (let i = 0; i < memorama.length; i += COLUMNAS) {
        memoramaDividido.push(memorama.slice(i, i + COLUMNAS));
    }
    // Reiniciar intentos
    this.intentos = 0;
    this.aciertos = 0;
    // Asignar a instancia de Vue para que lo dibuje
    this.memorama = memoramaDividido;
},

En la vista, se hace un v-for de la siguiente manera:

<div v-for="(fila, indiceFila) in memorama" :key="indiceFila"
    class="row">
    <div :key="indiceFila+''+indiceImagen" class="col-3"
        v-for="(imagen, indiceImagen) in fila">
        <div class="mb-3">
            <img @click="voltear(indiceFila, indiceImagen)"
                :class="{'girar': imagen.mostrar}"
                :src="(imagen.mostrar ? imagen.ruta :
                NOMBRE_IMAGEN_OCULTA)" class="card-img-top img-fluid
                img-thumbnail">
        </div>
    </div>
</div>

El método para voltear una imagen

La fuente o src de la imagen depende. Si la propiedad mostrar está en true, se muestra la ruta. Si no, se muestra el signo de interrogación (cuya ruta está definida en la constante NOMBRE_IMAGEN_OCULTA.

En el click de la imagen, se llama al método voltear. Es muy largo para poner aquí así que pondré lo que se hace dentro del mismo.

  1. Comprobamos si estamos esperando un timeOut y en caso de que sí, detenemos la función. Este timeOut se encarga de girar las imágenes cuando no las acertamos, pero las muestra por un cierto tiempo. Es decir, se giran en unos segundos (pero mientras se van a girar, no se debe hacer click)
  2. Comprobamos si la imagen ya fue acertada, y si es así, entonces detenemos la función
  3. Si es la primera imagen seleccionada (es decir, que no se está buscando el par de otra) se muestra la imagen original y sus coordenadas se guardan en ultimasCoordenadas.
  4. En caso de que ya hubiera una imagen seleccionada anteriormente (se comprueba con ultimasCoordenadas) se hacen dos cosas:
    1. Verificar si la imagen es exactamente la misma que fue seleccionada anteriormente, si es así, simplemente se voltea de nuevo. Un intento es aumentado.
    2. Verificar si esta imagen es el par de la anterior seleccionada.
      1. Se aumenta el intento
      2. Si es par, se agrega un acierto
      3. Si no es su par, entonces se pone un timeOut (a esto me refería con la comprobación de la primera variable) y se ocultan ambas imágenes

Esto en código fuente se traduce a lo siguiente:

// Se desencadena cuando se hace click en la imagen
voltear(indiceFila, indiceImagen) {
    // Si se está regresando una imagen a su estado original, detener flujo
    if (this.esperandoTimeout) {
        return;
    }
    // Si es una imagen acertada, no nos importa que la intenten voltear
    if (this.memorama[indiceFila][indiceImagen].acertada) {
        return;
    }
    // Si es la primera vez que la selecciona
    if (this.ultimasCoordenadas.indiceFila === null && this.ultimasCoordenadas.indiceImagen === null) {
        this.memorama[indiceFila][indiceImagen].mostrar = true;
        this.ultimasCoordenadas.indiceFila = indiceFila;
        this.ultimasCoordenadas.indiceImagen = indiceImagen;
        return;
    }
    // Si es el que estaba mostrada, lo ocultamos de nuevo
    let imagenSeleccionada = this.memorama[indiceFila][indiceImagen];
    let ultimaImagenSeleccionada = this.memorama[this.ultimasCoordenadas.indiceFila][this.ultimasCoordenadas.indiceImagen];
    if (indiceFila === this.ultimasCoordenadas.indiceFila &&
        indiceImagen === this.ultimasCoordenadas.indiceImagen) {
        this.memorama[indiceFila][indiceImagen].mostrar = false;
        this.ultimasCoordenadas.indiceFila = null;
        this.ultimasCoordenadas.indiceImagen = null;
        this.aumentarIntento();
        return;
    }

    // En caso de que la haya encontrado, ¡acierta!
    // Se basta en ultimaImagenSeleccionada
    this.memorama[indiceFila][indiceImagen].mostrar = true;
    if (imagenSeleccionada.ruta === ultimaImagenSeleccionada.ruta) {
        this.aciertos++;
        this.memorama[indiceFila][indiceImagen].acertada = true;
        this.memorama[this.ultimasCoordenadas.indiceFila][this.ultimasCoordenadas.indiceImagen].acertada = true;
        this.ultimasCoordenadas.indiceFila = null;
        this.ultimasCoordenadas.indiceImagen = null;
        // Cada que acierta comprobamos si ha ganado
        if (this.haGanado()) {
            this.indicarVictoria();
        }
    } else {
        // Si no acierta, entonces giramos ambas imágenes
        this.esperandoTimeout = true;
        setTimeout(() => {
            this.memorama[indiceFila][indiceImagen].mostrar = false;
            this.memorama[indiceFila][indiceImagen].animacion = false;
            this.memorama[this.ultimasCoordenadas.indiceFila][this.ultimasCoordenadas.indiceImagen].mostrar = false;
            this.ultimasCoordenadas.indiceFila = null;
            this.ultimasCoordenadas.indiceImagen = null;
            this.esperandoTimeout = false;
        }, SEGUNDOS_ESPERA_VOLTEAR_IMAGEN * 1000);
        this.aumentarIntento();
    }
},

Victorias, fracasos e intentos

Cuando se aumenta un intento, se hace la siguiente comprobación en donde se indica el fracaso si los intentos han sido agotados.

// Aumenta un intento y verifica si el jugador ha perdido
aumentarIntento() {
    this.intentos++;
    if (this.intentos >= MAXIMOS_INTENTOS) {
        this.indicarFracaso();
    }
},

Para saber si el jugador ha ganado se recorre el arreglo y se comprueba que todas las imágenes tengan la propiedad acertada:

// Método que indica si el jugador ha ganado
haGanado() {
  return this.memorama.every(arreglo => arreglo.every(imagen => imagen.acertada));
},

Y si se ha ganado, se hace lo siguiente:

// Cada que acierta comprobamos si ha ganado
if (this.haGanado()) {
    this.indicarVictoria();
}

Los métodos que indican la victoria y el fracaso son simples llamadas a alertas con SweetAlert, mostrando unas imágenes y texto.

Indicar alerta o fracaso con SweetAlert

Ambas alertas quedan así:

// Método que muestra la alerta indicando que el jugador ha perdido; después
// de mostrarla, se reinicia el juego
indicarFracaso() {
    Swal.fire({
            title: "Perdiste",
            html: `
        <img class="img-fluid" src="./img/perdiste.png" alt="Perdiste">
        <p class="h4">Agotaste tus intentos</p>`,
            confirmButtonText: "Jugar de nuevo",
            allowOutsideClick: false,
            allowEscapeKey: false,
        })
        .then(this.reiniciarJuego)
},
// Mostrar alerta de victoria y reiniciar juego
indicarVictoria() {
    Swal.fire({
            title: "¡Ganaste!",
            html: `
        <img class="img-fluid" src="./img/ganaste.png" alt="Ganaste">
        <p class="h4">Muy bien hecho</p>`,
            confirmButtonText: "Jugar de nuevo",
            allowOutsideClick: false,
            allowEscapeKey: false,
        })
        .then(this.reiniciarJuego)
},

Cuando se resuelve la promesa de las mismas (es decir, el botón es presionado) se reinicia el juego; y todo vuelve a empezar.

Así se ve cuando se gana:

Lo que se muestra cuando el usuario gana la partida de memorama con JavaScript

Y así cuando se pierde:

Intentos del juego de memorama agotados

Precargar imágenes para mejorar la experiencia de usuario

La precarga de las imágenes fue hecha para que cuando el usuario gire o voltee una imagen, no se tenga que cargar en ese momento (sobre todo con eso de las conexiones lentas) así que lo que se hace es cargar todas las imágenes de manera asíncrona.

Cargar imágenes de memorama al inicio

En cada carga de una imagen, se aumenta un contador y cuando ese contador equivale a la longitud de las imágenes, se indica la finalización de la precarga.

precargarImagenes() {
    // Mostrar la alerta
    Swal.fire({
            title: "Cargando",
            html: `Cargando imágenes...`,
            allowOutsideClick: false,
            allowEscapeKey: false,
        })
        .then(this.reiniciarJuego)
        // Ponerla en modo carga
    Swal.showLoading();


    let total = this.imagenes.length,
        contador = 0;
    let imagenesPrecarga = Array.from(this.imagenes);
    // También vamos a precargar la "espalda" de la tarjeta
    imagenesPrecarga.push(NOMBRE_IMAGEN_OCULTA);
    // Cargamos cada imagen y en el evento load aumentamos el contador
    imagenesPrecarga.forEach(ruta => {
        const imagen = document.createElement("img");
        imagen.src = ruta;
        imagen.addEventListener("load", () => {
            contador++;
            if (contador >= total) {
                // Si el contador >= total entonces se ha terminado la carga de todas
                this.reiniciarJuego();
                Swal.close();
            }
        });
        // Agregamos la imagen y la removemos instantáneamente, así no se muestra
        // pero sí se carga
        document.body.appendChild(imagen);
        document.body.removeChild(imagen);
    });
},
},

Animación CSS

Para terminar, la animación de fadeIn es esta:

img.card-img-top.girar {
    animation: fadein 2s;
}

@keyframes fadein {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

Conclusión

Así es como se puede hacer un juego de memorama en JavaScript. Este código que presenté es totalmente extensible y modificable, se pueden cambiar las imágenes así como agregar más.

Sé que utiliza dependencias como Bootstrap y SweetAlert, pero las mismas podrían remplazarse fácilmente; pues de Bootstrap solo se utiliza el grid y las imágenes responsivas, y de Swal solo la alerta que en el modo más simple podría ser remplazado con alert.

No olvides que puedes ver el código en GitHub y jugar el memorama aquí.

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

Entradas recientes

Creador de credenciales web – Aplicación gratuita

Hoy te voy a presentar un creador de credenciales que acabo de programar y que…

1 semana hace

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 semanas 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 semanas 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 semanas hace

Errores de Comlink y algunas soluciones

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

2 semanas 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 semanas hace

Esta web usa cookies.