¡Buenas!

Se me ha ocurrido la idea de crear una sección en donde iré intentando desarrollar diferentes tipos de juegos o programas interactivos utilizando JavaScript. Obviamente, la gracia de esta sección será hacer este tipo de cosas sin el uso de librerías o frameworks, porque es más divertido así.

 

En esta primera entrada de la sección, iré haciendo y explicando un juego de estos de memoria, para encontrar pares de imágenes. No soy fan de este tipo de juegos, realmente, pero pensé que podría ser una buena idea para ingurar la sección.

 

 


Ahora bien, vamos a comenzar.

 

Funciones útiles

Antes de programar el juego en sí, debemos crear algunas funciones que nos irán sirviendo en varios aspectos del juego. Ya que esta mecánica se basa especialmente en la aleatoriedad, nos vendrán bien dos funciones: Una que nos permita obtener un número entero aleatorio, y otra que nos permita desordenar un arreglo.

 

Este código lo colocaremos en un archivo llamado game.js.

 

Primero, la función de aleatoriedad. Es bastante sencilla, en realidad; ocupa sólo una línea de código.

function irandom(n) { //Devolver un número aleatorio entre 0 y n - 1
    return Math.floor( Math.random() * n );
}

Como es muy simple, no hay necesidad de explicar su funcionamiento, así que pasemos a la siguiente función:

function shuffle( array ) { //Devolver una copia desordenada de el arreglo dado
    var copy = array.slice(); //Creamos una copia del arreglo dado, para no modificar el original.
    var shuf = []; //Un arreglo vacío donde irán los valores desordenados.

    while(copy.length) { //Mientras que el arreglo original tenga valores
        var pos = irandom(copy.length); //Obtener una posición aleatoria del arreglo
        shuf.push(copy[ pos ]); //Añadir el valor en esta posición al vacío,
        copy.splice(pos, 1); //Eliminar el valor del original
    }
    return shuf; //Devolver el arreglo nuevo (desordenado)
}

Tampoco es muy complejo este código. Simplemente toma al azar elementos de un arreglo original para crear uno nuevo.

 

El HTML

Esta parte tampoco es muy difícil. Para este tipo de cosas, el archivo HTML base no suele tener mucho contenido, y ésta no es la excepción.

 

Nuestro documento contendrá lo siguiente: La estructura básica, una hoja de estilos y script incluídos (que serán los realmente importantes), un par de elementos para mostrar el estado del juego (parejas armadas y tiempo transcurrido) y un div que actuará como contenedor.

 

El HTML que usaremos será éste:

<!DOCTYPE html>
<html>
<head>
    <title>Un juego de memoria hecho en JavaScript</title>
    <link rel="stylesheet" type="text/css" href="style.css">
    <meta charset="utf-8">
</head>
<body>


<div style="text-align: center;">
    <h1>Juego de memoria</h1>
    ¡Arma las parejas en el menor tiempo posible!<br>
    Parejas encontradas <span id="score">0</span><br>
    Tiempo: <span id="time">00: 00</span>
</div>


<div id="game"><!-- Aquí irán todas las parejas --></div>


<script type="text/javascript" src="game.js"></script>
</body>
</html>

Como vemos, no es nada del otro mundo… Los archivos que usaremos serán game.js y style.css, como dice el código de arriba.

 

Las imágenes

Otra cosa indispensable: Las imágenes que usaremos. En mi caso, usaré una imágenes libres de kenney.nl, que encontrarán aquí.

 

Antes de continuar, es importante mirar la carpeta de las imágenes que acabo de poner, para ver cómo es que funcionará nuestro juego: Las parejas se formarán con imágenes con el mismo nombre en directorios distintos. Además, hay una imagen extra, que será la que estará visible cuando las parejas están “ocultas”.

 

El CSS

Una pequeña hoja de estilos para que no se vea tan mal el juego, estéticamente hablando.

* {box-sizing: border-box;}
body {
	margin:0;
	font:13pt Calibri;
	color: #333;
}
h1 {
	margin:0;
	padding:0.5em;
}
#game {
	border:1px solid #333;
	margin:3em;
	padding: 1em;
}
#game .image-box {
	display: inline-block;
	border:1px solid #666;
}
#game .image-box:hover {
	background-color: #DDD;
}

 

El JavaScript

Ahora sí viene lo bueno xD.

 

Usaremos el mismo archivo game.js, y (como es de esperarse) comenzaremos con añadir el evento load al documento.

window.addEventListener("load", function() {
    var game = document.getElementById('game'); //Obtenemos el DIV del juego
    if(!game) //Si no se ha encontrado el div
        return; //cerrar el programa

    game.innerHTML = ''; //Limpiar el contenido del DIV
});

El resto de código que vayamos a añadir, lo haremos dentro del evento, para que se ejecute correctamente.

 

Lo siguiente que haremos será añadir una lista con los nombres de las imágenes que formarán las parejas:

    var images = [
    //Crearemos un arreglo donde colocaremos los nombres de todas las imágenes
    //Como las parejas tienen el mismo nombre pero están en directorios distintos,
    //sólo será necesario guardar el nombre de la imagen
    
        'imagen_0.png',
        'imagen_1.png',
        'imagen_2.png',
        'imagen_3.png',
        'imagen_4.png',
        'imagen_5.png',
        'imagen_6.png',
        'imagen_7.png',
        'imagen_8.png',
        'imagen_9.png',
        'imagen_10.png',
        'imagen_11.png',
        'imagen_12.png',
        'imagen_13.png'

    ]

Como dice el comentario, se trarará de una lista con sólo el nombre de las imágenes. La idea es que si (por ejemplo) tendremos dos imágenes por pareja, hayan dos directorios distintos con las imágenes usando el mismo nombre.

 

Continuemos… Ahora, añadiremos los directorios en donde estarán las imágenes:

    //Los directorios donde se ubican las imágenes
    //Si son 2 directorios, significa que hay dos imágenes por pareja, una en cada uno.
    var pairs = [ 'imagenes/par1', 'imagenes/par2' ];

    const max_score = images.length; //Obtener la cantidad de parejas (puntaje máximo)
    var score = 0; //Puntaje actual: 0 

Como usaremos sólo parejas (“de a dos”, :v), sólo hay dos directorios… Además, configuramos de una vez un par de variables para la puntuación del juego: La cantidad máxima de puntos es igual a la cantidad de parejas, en el caso de que al jugador se le dé un punto por pareja armada.

 

Ahora que tenemos las parejas y los directorios, es hora de unir la información: Como tenemos 14 parejas y dos directorios, habrá un total de 28 “tarjetas”, 14 en un directorio, y 14 en el otro.

    //Aquí se guardarán todas las imágenes: Si hay 14 parejas y dos imágenes por cada
    //una, entonces habrá un total de 28: 14 con el primer directorio, y 14 con el segundo
    var all_images = [];

    for(var i = 0; i < pairs.length; i++) {
        for(var j = 0; j < images.length; j++) {

            //Guardaremos en el arreglo las imágenes como objetos con dos propiedades:
            //El nombre de la imagen, y la ruta completa (el nombre más el directorio)
            all_images.push( {
                name:  images[j], 
                path: pairs[i] +  '/' + images[j]
            });
        }
    }
    all_images = shuffle(all_images); //Desordenamos la lista

De esta manera, obtendríamos un arreglo algo así (el orden puede variar aleatoriamente, esa es la gracia):

Bien. La propiedad name relacionará las parejas entre sí, y la propiedad path almacena la ruta única de cada imagen, la que veremos en pantalla.

 

Programando el juego

Ahora sí, procedamos a programar el juego, que ya va siendo hora xD.

 

Lo que vamos a hacer es recorrer el arreglo que hemos generado, y crear un elemento <img> por cada índice del mismo:

    for(var i = 0; i < all_images.length; i++) {
        //Ahora, recorremos esta lista e imprimimos en el DOM cada imagen

        var img = document.createElement( 'img' );

            //Para el estilo CSS
            img.setAttribute('class', 'image-box');

            //Un par de propiedades para guardar el nombre y ruta de la imagen
            img.img_path = all_images[i].path;
            img.img_name = all_images[i].name;

            img.src = 'imagenes/unknown.png'; //La imagen que se mostrará, será la del "signo desconocido".
            img.on = false; //¿El usuario ha abierto la imagen?

        game.appendChild(img);
    }

Gracias a esto, nuestro juego ya debería (por lo menos) crear 28 imágenes aleatoriamente en el contenedor. Pero aún no tienen ninguna funcionalidad.

 

Vamor a añadirles el comportamiento, y todo irá en el evento click de la imagen creada (sí, dentro del for todavía), y, a su vez, haremos otra cosa importante: Guardar la imagen creada en el arreglo all_images para no tener que realizar búsquedas en el DOM, sino simplemente recorrer nuevamente el arreglo.

            img.addEventListener( 'click', function() {
                //Aquí estará todo el comportamiento de las imágenes
            }
        all_images[i].image = img; //Añadimos la imagen creada al arreglo

Lo primero que debe suceder al hacer clic, es mostrar la imagen real, y dentro de un par de segundos volverla a ocultar. Para ello utilizaremos un timeout nativo de JS:

                if(!this.on) { //Sólo si la imagen no se ha desplegado
                    this.on = true;
                    this.src = this.img_path;


                    //Todo esto para desactivar la imagen dentro de 2s
                    var self = this; //Un alias de la imagen actual para poder acceder desde el timeout
 
                    //Hay que guardar el timeout en una propiedad, para poderlo detener si
                    //se llega a armar una pareja con la imagen
                    this.timeout = window.setTimeout( function() {
                        self.on = false; //Desactivar la imagen
                        self.src = 'imagenes/unknown.png'; //Volver a ícono de desconocido
                    }, 2000);
                }

De esta manera, nuestro juego iría algo así:

Ahora la última parte: Cuando se muestran dos imágenes que forman una pareja, éstas deben quedarse fijas (que no se oculten) y sumarle 1 al puntaje. Para ello, recorreremos (nuevamente) todos los elementos de all_images, y revisar imagen por imagen si el nombre (almacenado en la propiedad img_name) es igual.

for(var j = 0; j < all_images.length; j++) { //Recorrer todas las imágenes

    //Si la imagen que se está revisando NO es igual a la que está ejecutando el evento,
    //y está activado
    if( all_images[j].image !== this && all_images[j].image.on 
        && all_images[j].image.img_name == this.img_name //Y pertenece a la pareja
    ){
        //Entonces se ha armado la pareja
        //Aquí detendremos los timeout y subiremos el puntaje
    }
}

Y lo siguiente será detener los timeout, aumentar el puntaje y comprobar si se ha alcanzado el máximo (si se han armado todas las parejas):

        //Limpiar el interval que las desactiva
        window.clearTimeout(this.timeout);
        window.clearTimeout(all_images[j].image.timeout);
        score++; //Sumar 1 al puntaje
        
        span.innerText = score; //Imprimir el puntaje en el DOM

        if(score == max_score) { //Si se ha obtenido el puntaje máximo
            alert( '¡Has ganado!' );
            window.clearInterval(aug);
        }

Ahora, ¿qué es la variable aug? Pues se trata del último elemento de nuestro juego: El contador de tiempo. Para ello, usaremos un interval que le sume 1 a una variable cada segundo. Este código lo escribiremos sólo dentro del evento load del body:

    //CONTADOR DE TIEMPO
    const time = document.getElementById('time'); //Elemento donde se mostrará el tiempo
    var timer = 0;
    var aug = window.setInterval(function() {

        timer++;
        var seg = timer % 60; //Obtener los segundos
        var min = Math.floor(timer/60); //Obtener los minutos

        if(seg < 10)
            seg = "0" + seg.toString(); //Mantener siempre con dos cifras mínimo
        if(min < 10)
            min = "0" + min.toString(); //Mantener siempre con dos cifras mínimo

        time.innerText = min + ': ' + seg;

    }, 1000);

Y con este último código, el juego debería funcionar correctamente. Si quieres ver el funcionamiento en línea, puedes ir a este enlace. También dejo el código en Github.

 

Y… ¡eso ha sido todo! Espero que esta entrada haya sido interesante, o al menos de utilidad.

 

¡Saludos!