Simple jeu de Memory réalisé en Javascript et CSS3 :

Nombre de paires :

Score : 0


J'ai récemment décidé de me mettre au Javascript et d'approfondir un peu mes connaissances en HTML5 et CCS3. Ce petit jeu de Memory est le premier exercice que j'ai fait pour mettre en pratique ce que j'ai appris. L'idée m'est venue en lisant HTML5 Hacks par O'Reilly, plus particulièrement le Hack #25 qui expliquait comment réaliser un retournement de carte iOS-Style.

J'ai attaqué par la création d'une carte que je pouvais tourner et retourner en cliquant dessus. Jusque là rien de bien compliqué, j'utilisais simplement classList.toggle pour ajouter/retirer la class flipped à laquelle était liée mon animation.

.card {
  transition: transform 0.6s;
  transform-style: preserve-3d;
  transform-origin: center;
}

.card.flipped {
  transform: rotateY(-180deg);
}

.front, .back {
  position: absolute;
  width: 100%;
  height: 100%;
  backface-visibility: hidden;
}

.front {
  transform: rotateY(-180deg);
}

L'étape suivante consistait à augmenter le nombre de cartes tout en pouvant les retourner indépendamment les unes des autres. Pour ceux qui s'y connaissent ça peut paraitre facile mais c'est certainement ce qui m'a donné le plus de fil à retordre. Le principal problème étant d'arriver à reconnaître sur quelle carte le clic avait été effectué. J'ai au final opté pour l'attribution d'un id unique à chacune des cartes et l'appel de ma fonction flip() depuis la carte avec onclick:

<div id="A1" class="card" onclick="flip(this.id)">
  <div class="front">A</div>
  <div class="back"></div>
</div>

Le moment était venu de créer le comportement normal du Memory, c'est à dire d'avoir à retourner deux cartes et si elles ne sont pas identiques, les retourner à nouveau.

var memory = document.getElementById('memory');
var cards = memory.getElementsByClassName('card');
var flipped = memory.getElementsByClassName('flipped');
var elem, pair, pairFound;

function isEven(x) {
  return (x % 2) == 0;
} // Renvoie true si x est pair, false si impair

function flip(x) {
  elem = document.getElementById(x); // elem = id de la carte cliquee

  if (elem.classList.contains('flipped') === false) {
  // verifie que la carte n'est pas deja retournee

    elem.classList.add('flipped'); // retourne la carte

    pair = elem.classList.item(1); // paire a laquelle la carte appartient
    pairFound = memory.getElementsByClassName(pair + ' ' + 'flipped');
    // Liste des cartes retournees appartenant a la meme paire

    if (isEven(flipped.length)) { // si nb de cartes retournees est pair
      if (pairFound.length == 2) { // si les deux cartes sont identiques
        for (i = 0; i < 2; i++) {
          pairFound[i].classList.add('found'); // ajoute la classe found
        }
      } else {
        setTimeout (unflip, 700); // appele la fonction unflip apres 0.7sec
      }
    }
  }
}

function unflip() {
  var fl = flipped.length; //nb de cartes retournees
  for (i = fl-1; i >= 0; i--) {
    if (flipped[i].classList.contains('found')) { continue; }
    // saute cette etape si la carte appartient a une paire trouvee
    flipped[i].classList.remove('flipped'); // retourne la carte
  }
}

J'ai ensuite ajouté un simple système de score, qui augmente de 1 à chaque fois que l'on retourne une carte; et un bouton Reset pour retourner toutes les cartes et les mélanger. J'ai également ajouté la possibilité de choisir le nombre de paires de cartes.

function reset() {
  draw(); // remplace l'ensemble du Memory par le nb de paires choisi
  score = 0;
  document.getElementById('score').innerHTML = score; // MaJ du score
  randomize(); // Melange les cartes
}

function randomize() {
  var cardsArray = []; // Cree un Array vide
  for (i = 0; i < cards.length; i++) {
    cardsArray.push(cards[i].outerHTML); // Ajoute les cartes a l'Array
  }
  cardsArray.sort(function(a, b){return 0.5 - Math.random()}); // Melange
  var memCards = '';
  for (i = 0; i < cardsArray.length; i++) {
    memCards += cardsArray[i]; // Distribue les cartes
  }
  memory.innerHTML = memCards;
}

function incrLetter(x) {
  return String.fromCharCode(x.charCodeAt(0) + 1); // Incremente une lettre
}

function draw() {
  var pairs = document.getElementById('pairs').value; // Recupere le nb de
  var letter = 'A';                                   // paires voulu
  var memCards = '';
  for (i = 0; i < pairs; i++) {
    memCards += '<div id="' + letter + '1" class="card mem' + letter +
      '" onclick="flip(this.id)">' + '<div class="front">' + letter +
      '</div><div class="back"></div></div>' +
      '<div id="' + letter + '2" class="card mem' + letter +
      '" onclick="flip(this.id)">' + '<div class="front">' + letter +
      '</div><div class="back"></div></div>';
    letter = incrLetter(letter);
  } // Cree les cartes pour chaque paire
  memory.innerHTML = memCards;
}
Code final

MàJ 21/04/17 : Remplacement de la méthode pour réarranger aléatoirement l'array contenant les cartes par la méthode Fisher-Yates-Durstenfeld Shuffle.

var memory = document.getElementById('memory');
var cards = memory.getElementsByClassName('card');
var flipped = memory.getElementsByClassName('flipped');
var score = 0;
var elem, pair, pairFound;

draw();
randomize();

function isEven(x) {
  return (x % 2) == 0;
}

function incrLetter(x) {
  return String.fromCharCode(x.charCodeAt(0) + 1);
}

function randomNumber(n) {
  return Math.floor(Math.random() * n);
}

function shuffle(array) { // Fisher-Yates-Durstenfeld Shuffle
  var shufArray = array;
  var lcv = array.length - 1;
  var n, nElem, lastElem;
  for (; lcv >= 0; lcv--) {
    n = randomNumber(lcv + 1);
    nElem = shufArray[n];
    lastElem = shufArray[lcv];
    shufArray.splice(n, 1, lastElem);
    shufArray.splice(lcv, 1, nElem);
  }
  return shufArray;
}

function flip(x) {
  elem = document.getElementById(x);

  if (elem.classList.contains('flipped') === false) {

    elem.classList.add('flipped');
    score += 1;
    document.getElementById('score').innerHTML = score;

    pair = elem.classList.item(1);
    pairFound = memory.getElementsByClassName(pair + ' ' + 'flipped');

    if (isEven(flipped.length)) {
      if (pairFound.length == 2) {
        for (i = 0; i < 2; i++) {
          pairFound[i].classList.add('found');
        }
      } else {
        setTimeout (unflip, 700);
      }
    }
  }
}

function unflip() {
  var fl = flipped.length;
  for (i = fl-1; i >= 0; i--) {
    if (flipped[i].classList.contains('found')) { continue; }
    flipped[i].classList.remove('flipped');
  }
}

function reset() {
  draw();
  score = 0;
  document.getElementById('score').innerHTML = score;
  randomize();
}

function randomize() {
  var cardsArray = [];
  for (i = 0; i < cards.length; i++) {
    cardsArray.push(cards[i].outerHTML);
  }
  cardsArray = shuffle(cardsArray);
  var memCards = '';
  for (i = 0; i < cardsArray.length; i++) {
    memCards += cardsArray[i];
  }
  memory.innerHTML = memCards;
}

function draw() {
  var pairs = document.getElementById('pairs').value;
  var letter = 'A';
  var memCards = '';
  for (i = 0; i < pairs; i++) {
    memCards += '<div id="' + letter + '1" class="card mem' + letter +
      '" onclick="flip(this.id)">' + '<div class="front">' + letter +
      '</div><div class="back"></div></div>' +
      '<div id="' + letter + '2" class="card mem' + letter +
      '" onclick="flip(this.id)">' + '<div class="front">' + letter +
      '</div><div class="back"></div></div>';
    letter = incrLetter(letter);
  }
  memory.innerHTML = memCards;
}

Evidemment, puisqu'il s'agit de mon tout premier exercice de Javascript c'est certainement loin d'être parfait. Mais cela m'a permis de mettre en pratique une partie des choses que j'avais apprises et d'en découvrir d'autres (comme les node-lists par exemple).

La prochaine étape sera certainement de créer des images en SVG (Scalable Vector Graphics) pour chacune des paires et de proposer un choix entre lettres et images, ainsi qu'une option pour afficher ou non le fond coloré.