Bien, así que ahora tenemos una cuadrícula de fichas que podemos mostrar boca arriba o boca abajo. Pero no tenemos forma de jugar el juego en realidad. Para recordarte, aquí está cómo funciona el juego:
  • Cuando empieza el juego, todas las fichas están volteadas boca abajo.
  • El jugador entonces voltea dos fichas, seleccionándolas al hacer clic sobre ellas.
  • Si las dos fichas tienen la misma imagen, permanecen boca arriba. Si no, deben ser volteadas boca abajo otra vez después de un breve retraso.

Voltear fichas con un clic

Empezaremos por voltear nuestras fichas boca abajo de nuevo:
for (var i = 0; i < tiles.length; i++) { //tiles es fichas en inglés
    tiles[i].drawFaceDown(); //drawFaceDown es la función que 
                             //dibuja las fichas boca abajo
}
Para voltear una ficha, el jugador debe hacer clic sobre ella. Para responder a los clics en programas de ProcessingJS, podemos definir una función mouseClicked, y ese código se ejecutará cada vez que se haga clic con el ratón.
mouseClicked = function() {
  // procesar el clic de alguna manera
};
Cuando nuestro programa vea que el jugador hizo clic en algún lugar, queremos revisar si hizo clic en una ficha, usando mouseX y mouseY. Empecemos por agregarle un método isUnderMouse a Tile que regresa true si una x y y dadas se encuentran dentro del área de una ficha. Por como dibujamos las fichas, la x y la y de la ficha corresponden a la esquina superior izquierda de la ficha, así que debemos regresar true solo si la x dada está entre this.x y this.x + this.width, y si la y dada está entre this.y y this.y + this.width:
Tile.prototype.isUnderMouse = function(x, y) {
    return x >= this.x && x <= this.x + this.width &&
        y >= this.y && y <= this.y + this.width;  
};
Ahora que tenemos ese método, podemos usar un bucle for en mouseClicked para revisar si cada ficha está debajo de mouseX y mouseY. Si es así, las dibujamos boca arriba:
mouseClicked = function() {
    for (var i = 0; i < tiles.length; i++) {  //tiles es fichas en inglés
        if (tiles[i].isUnderMouse(mouseX, mouseY)) {
            tiles[i].drawFaceUp(); //drawFaceUp es la función que
                                   //dibuja las fichas boca arriba
        }
    }
};
Así es como se ve. Haz clic en varias fichas y ve qué pasa:
¿Observaste algo? Implementamos un aspecto del modo de juego: que el jugador sea capaz de voltear las fichas. Pero nos falta una restricción importante: no deberían poder voltear más de dos fichas a la vez.
De alguna manera necesitaremos hacer un seguimiento de la cantidad de fichas volteadas. Una forma sencilla sería una variable global numFlipped que aumentamos cada vez que el jugador voltea una ficha boca arriba. Entonces podríamos saltarnos la comprobación de cada ficha si numFlipped es 2.
var numFlipped = 0; //numFlipped es la variable que nos dice cuántas 
                    //fichas se han volteado
mouseClicked = function() {
    for (var i = 0; i < tiles.length; i++) {
        if (tiles[i].isUnderMouse(mouseX, mouseY)) {
            if (numFlipped < 2) {
              tiles[i].drawFaceUp();  //drawFaceUp es la función que
                                      //dibuja las fichas boca arriba
              numFlipped++;
            }
        }
    }
};
Sin embargo, eso no está del todo bien: con ese código, ¿qué pasaría si hacemos clic en la misma ficha dos veces seguidas? Piensa en ello: la voltearía "dos veces", haría numFlipped igual a 2 y evitaría voltear más fichas. En cambio, solo queremos que voltee la ficha si no ha sido volteada boca arriba.
¿Cómo podemos saber si la ficha actual en el bucle for está boca arriba o boca abajo? Podríamos haber llamado al método drawFaceUp sobre la ficha en algún momento anterior, pero nunca hicimos un seguimiento de eso. ¡Ba da bum, es momento de un valor booleano! Vamos a modificar los métodos de dibujar las fichas para usar un valor booleano que nos diga el estado de una ficha.
Tile.prototype.drawFaceDown = function() {
    // ...
    this.isFaceUp = false;
};

Tile.prototype.drawFaceUp = function() {
    // ...
    this.isFaceUp = true;
};
Ahora podemos modificar la revisión para asegurarnos de que la ficha en realidad esté boca abajo:
if (tiles[i].isUnderMouse(mouseX, mouseY)) {
            if (numFlipped < 2 && !tiles[i].isFaceUp) {
              tiles[i].drawFaceUp();
              numFlipped++;
            }
        }

Voltear fichas con retraso

Bien, nuestra lógica de voltear dos fichas está completa. ¿Qué sigue? Recapitulemos las reglas del juego otra vez:
Si las dos fichas tienen la misma imagen, permanecen boca arriba. De otro modo, las fichas se voltean boca abajo después de un periodo de tiempo.
Primero implementaremos la segunda parte, que voltea automáticamente las fichas, porque va a ser difícil de probar la primera parte si no podemos buscar nuevas coincidencias fácilmente.
Sabemos cómo voltear fichas de regreso al usar el método turnFaceDown, pero ¿cómo hacemos eso después de un periodo de tiempo? Cada lenguaje y entorno tiene una manera diferente de retrasar la ejecución de código, y tenemos que averiguar cómo hacerlo en ProcessingJS. Necesitamos una forma de hacer un seguimiento del tiempo (si ya pasó el periodo de retraso o no) y una forma de llamar código después de que haya transcurrido el periodo de tiempo. Aquí está mi sugerencia:
  • Creamos una variable global llamada delayStartFC, inicialmente null.
  • En la función mouseClicked, justo después de que volteamos una segunda ficha, almacenamos el valor actual de frameCount en delayStartFC. Esa variable nos dice cuántos cuadros de animación han pasado desde que el programa comenzó a ejecutarse, y es una forma de saber la hora en nuestros programas.
  • Definimos la función draw como aquella función que será llamada continuamente a medida que el programa se ejecuta, así que sabemos que será llamada para cada valor de frameCount. En esa función, revisamos si el nuevo valor de frameCount es significativamente mayor que el anterior, y si es así, volteamos todas las fichas y hacemos numFlipped igual a 0. También restablecemos delayStartFC a null.
En realidad es una buena solución que no requiere demasiado código para implementarla. Como una optimización del rendimiento, podemos usar las funciones loop y noLoop para asegurarnos de que el código de dibujar las fichas solo sea llamado cuando esté sucediendo un retraso. Aquí está todo:
var numFlipped = 0;
var delayStartFC = null;

mouseClicked = function() {
    for (var i = 0; i < tiles.length; i++) {
        if (tiles[i].isUnderMouse(mouseX, mouseY)) {
            if (numFlipped < 2 && !tiles[i].isFaceUp) {
                tiles[i].drawFaceUp();
                numFlipped++;
                if (numFlipped === 2) {
                    delayStartFC = frameCount;
                    loop();
                }
            } 
        }
    }
};

draw = function() {
    if (delayStartFC && (frameCount - delayStartFC) > 30) {
        for (var i = 0; i < tiles.length; i++) {
            tiles[i].drawFaceDown();
        }
        numFlipped = 0;
        delayStartFC = null;
        noLoop();
    }
};
Pruébalo a continuación. Es genial cómo las fichas se voltean automáticamente, ¿o no?
Pero no siempre queremos que las fichas se volteen: recuerda, si dos fichas coinciden, entonces deberían quedarse boca arriba. Eso significa que debemos revisar si hay fichas que coinciden cada vez que haya 2 volteadas, y antes de establecer el retraso. En pseudocódigo, eso sería:
si hay dos fichas volteadas:
    si la primera ficha tiene la misma imagen que la segunda:
       mantener las fichas boca arriba
Ya tenemos una revisión para ver si hay dos fichas volteadas (numFlipped === 2), entonces ¿cómo revisamos si las fichas tienen la misma imagen? Primero, necesitamos una forma de acceder a las dos fichas volteadas. Podríamos iterar sobre nuestro arreglo, encontrar todas las fichas con isFaceUp igual a true y luego almacenarlas en un arreglo. O, tengo una idea mejor: almacenemos siempre nuestras fichas volteadas en un arreglo, para facilitar el acceso. De hecho, podemos simplemente reemplazar numFlipped con un arreglo y luego usar flippedTiles.length en todos lados donde anteriormente usamos numFlipped.
Nuestra función modificada mouseClick se ve así:
var flippedTiles = [];
var delayStartFC = null;

mouseClicked = function() {
    for (var i = 0; i < tiles.length; i++) {
        if (tiles[i].isUnderMouse(mouseX, mouseY)) {
            if (flippedTiles.length < 2 && !tiles[i].isFaceUp) {
                tiles[i].drawFaceUp();
                flippedTiles.push(tiles[i]);
                if (flippedTiles.length === 2) {
                    delayStartFC = frameCount;
                    loop();
                }
            } 
        }
    }
};
Ahora, tenemos que averiguar si las dos fichas en el arreglo flippedTiles en efecto tienen la misma imagen. Bueno, ¿qué es la propiedad face? Es un objeto y, en realidad, la imagen de las fichas que coinciden deben ser exactamente el mismo objeto, como en: la variable está apuntando al mismo lugar en la memoria de la computadora para ambos. Eso es porque solo creamos cada objeto de imagen una vez (como con getImage("avatars/old-spice-man") y luego empujamos el mismo objeto de imagen sobre el arreglo de imágenes dos veces:
var face = possibleFaces[randomInd]; //face es la imagen de cada ficha
    selected.push(face);
    selected.push(face);
Por lo menos en JavaScript, el operador de igualdad devolverá true si está usado en dos variables que apuntan a objetos, y ambas de esas variables se refieren al mismo objeto en memoria. Eso significa que nuestra revisión puede ser sencilla. Solo usa el operador de igualdad en la propiedad face de cada ficha:
if (flippedTiles[0].face === flippedTiles[1].face) {
  // ...
  }
Ahora que sabemos que las fichas coinciden, tenemos que mantenerlas boca arriba. Actualmente, todas son volteadas después de un retraso. No podríamos simplemente hacer la animación en este caso, pero recuerda, habrá una animación en los siguientes turnos, así que no podemos depender de eso. En su lugar, necesitamos una forma de saber "oye, cuando las volteemos todas de regreso, no deberíamos voltear estas en particular". ¡Suena como a otro buen uso para una propiedad booleana! Podríamos hacer una propiedad isMatch igual a true dentro de ese bloque if, y entonces solamente voltear las fichas si no es true:
if (flippedTiles[0].face === flippedTiles[1].face) {
    flippedTiles[0].isMatch = true;
    flippedTiles[1].isMatch = true;
}
for (var i = 0; i < tiles.length; i++) {
    if (!tiles[i].isMatch) {
        tiles[i].drawFaceDown();  //drawFaceDown es la función que
                                  //dibuja las fichas boca abajo
    }
}
Ahora, cuando encuentres dos fichas que coincidan a continuación, deberían quedarse boca arriba después del retraso (y después de turnos futuros):

Puntuación

Ahora solo nos falta una cosa: la puntuación. Aquí está un recordatorio de esa parte de las reglas del juego:
El objetivo del juego es conseguir todas las fichas volteadas boca arriba (es decir, encontrar todos los pares de imágenes que coincidan) en el menor número de intentos. Eso significa que el menor número de intentos es una mejor puntuación.
¿Cómo hacemos un seguimiento del número de intentos? Bueno, un "intento" es cada vez que volteas dos fichas, que corresponde a nuestro bloque if que revisa flippedTiles.length === 2. Podemos agregar una nueva variable global, numTries, que incrementamos dentro de esa condición.
if (flippedTiles.length === 2) {
                    numTries++;  //el número de intentos
                    // ...
               }
Podemos desplegar el marcador cuando el juego termina: cuando el jugador hace coincidir todas las fichas. ¿Cómo revisamos eso? Bueno, tenemos un arreglo de todas nuestras fichas, y cada una tiene un valor booleano que nos dice si coincide con otra, así que podemos hacer un bucle sobre eso y revisar si todas son true. Una buena manera de lograr esto en código es inicializar un valor booleano con true antes del bucle y seguir haciendo AND con el valor booleano sobre cada elemento del bucle. Mientras nunca vea un false (¡y el arreglo tenga más de un elemento!), seguirá siendo true al final. Aquí está cómo se ve eso:
var foundAllMatches = true;  //variable de si se encontraron todas las
                            // coincidencias
    for (var i = 0; i < tiles.length; i++) {
        foundAllMatches = foundAllMatches && tiles[i].isMatch;
    }
Cuando hayamos encontrado todas las coincidencias, podemos desplegar un texto de felicitación al usuario con el número de intentos, así:
if (foundAllMatches) {
    fill(0, 0, 0);
    text("Encontraste todas en " + numTries + " intentos", 20, 360);
}
Por cierto, pondremos todo ese código al final de nuestra función mouseClicked, para ejecutarlo después de haber revisado coincidencias en ese turno.
Puedes probarlo a continuación, pero puedes tardarte un poco en ganar (sin ofender, por supuesto, ¡yo también me tardo!). Aquí hay un consejo para cuando estés probando las partes del juego que son difíciles de alcanzar: modifica tu juego temporalmente para que sea más rápido llegar allí. Por ejemplo, en este juego, cambia NUM_ROWS y NUM_COLS para que sean números más pequeños y seas capaz de terminar mucho más rápido. Ahora, ¡prueba eso a continuación!
Cargando