VueJs – le jeu MEMORY

DE NOMBREUSES EXPLICATIONS SUPPLEMENTAIRES SE TROUVENT DANS LES VIDEOS DONC N’HESITEZ PAS A LES CONSULTER EN PLUS DE L’ARTICLE TEXTE.

Ce que vous allez apprendre

J’ai décidé de rendre l’apprentissage un peu plus ludique en créant ce jeu Memory en VueJs. Grâce à ce petit tuto, vous apprendrez les notions suivantes :

  • Quelques notions de Bootstrap pour placer les cartes du jeu (notions utilisées mais pas très détaillées)
  • Quelques notions de CSS pour le design et les animations de retournement des cartes(notions utilisées mais pas très détaillées)
  • Le conditionnement des styles CSS en fonction de données stockées dans notre modèle VueJs
  • L’affichage de données VueJs dans l’HTML
  • L’utilisation des tableaux de données
  • L’utilisation de LODASH pour la copie complète et le mélange de données présentes dans un tableau javascript
  • L’utilisation des timers en javascript et donc en VueJs
  • L’utilisation d’un bouton RESTART en overlay à la fin du jeu

Les règles du jeu

Avant tout, définissons les règles du jeu :

  • Plusieurs cartes apparaissent cachées (dos affiché) à l’écran.
  • Parmis elles, il y a à chaque fois 2x la même carte.
  • Cliquer sur une carte la retourne. Si une autre carte non matchée est déjà retournée, on vérifie si c’est la même que celle qu’on vient de retourner. Si c’est le cas, ces cartes sont marquées comme matchées sinon elles se recachent à nouveau toutes les deux.
  • Lorsque toutes les cartes en double sont « matchées » le jeu est terminé.

Les ressources

Pour faire ce jeu, vous aurez besoin de Visual Studio Code (éditeur de code) : voir le tuto suivant pour l’installation et configuration : Formation de base VueJs.

Vous aurez également besoin des images pour les cartes du jeu (mais vous pouvez très bien en télécharger d’autres sur le Web) :

Création du jeu

1. Le projet vide

On va donc commencer par créer notre projet avec Visual Studio Code via la commande habituelle :

vue create vue-memory

Une fois le projet créé, on le charge dans Visual Studio Code et on ajoute dans le dossier PUBLIC un sous-dossier « images » avec les images que vous avez téléchargés en ressources. Ainsi, on pourra les utiliser pour afficher nos cartes.

Quand on fait un vue create, par défaut, il nous ajoute un composant HelloWorld. On va donc supprimer celui-ci dans le dossier src\components et du coup, on va également nettoyer notre App.vue pour qu’il ressemble à ceci :

<template>
  <div id="app">
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
</style>

Pour notre projet on aura besoin de Boostrap (placement des cartes) et de Lodash (manipulation des données du tableau). On va donc les installer avec les commandes suivantes :

vue add bootstrap-vue
npm install --save lodash

Dernière petite chose : pour pouvoir utiliser les méthodes présentes dans lodash, on va devoir modifier notre main.js et ajouter ceci :

import _ from 'lodash';    

Object.defineProperty(Vue.prototype, '$_', { value: _ });

2. Créer le tableau de cartes en mémoire

On va à présent créer notre tableau de cartes disponibles. Dans la partie data de notre App.vue, on va donc ajouter ceci :

<script>
export default {
  name: 'App',
  data() {
    return {
      availableCards : [ 
        { key:"cat", name: "cat", isFlipped: false, isMatched: false }, 
        { key:"dog", name: "dog", isFlipped: false, isMatched: false },
        { key:"duck", name: "duck", isFlipped: false, isMatched: false },
        { key:"panda", name: "panda", isFlipped: false, isMatched: false }, 
        { key:"pig", name: "pig", isFlipped: false, isMatched: false }, 
        { key:"rabbit", name: "rabbit", isFlipped: false, isMatched: false }
        ]
...

On a ainsi une liste de cartes disponibles avec :

  • Une clé : pour pouvoir afficher les images avec un v-for
  • Un nom : pour pouvoir comparer nos cartes et savoir si on a retourné 2x la même. Ce nom va également servir pour retrouver l’image png à afficher
  • Une propriété isFlipped qui permet de savoir si la carte a sa face visible
  • Une propriété isMatched pour savoir si la carte fait partie d’une paire trouvée par la joueur (2x la même carte a déjà été retournée)

A présent, nous avons besoin de doubler chaque carte (le principe du jeu est de trouver 2x la même carte) et de tout mélanger avant de les afficher.

Pour se faire, on va ajouter une autre données qui va représenter les cartes à afficher :

cards : []

Maintenant, on va créer une fonction initGame() qui va initialiser notre jeu en doublant les cartes disponibles et en les mélangeant (les commentaires sont assez explicites). On va également l’appeler dès la création de notre composant principal VueJs donc dans le created(). Voici le code :

created() {
    this.initGame();
  },
  methods: {
    initGame() {
      // On copie 2x la liste des cartes disponibles
      var cards1 = this.$_.cloneDeep(this.availableCards);
      var cards2 = this.$_.cloneDeep(this.availableCards);

      // On vide notre liste de cartes à afficher
      this.cards = [];
      // On met dans notre tableau de cartes à afficher les 2 copies
      this.cards = this.cards.concat(cards1, cards2);
      // On mélange le tout
      this.cards = this.$_.shuffle(this.cards);
    }
  }

On a donc à présent notre tableau prêt à être affiché.

3. Affichage des cartes

Passons maintenant à l’affichage proprement parlé. Pour se faire, on va utiliser Bootstrap qui permet d’afficher des DIV qui représente des lignes (class=row) ou qui représente des cellules qui prennent un certain nombre de colonnes (class=col-xxx). Voici le code pour afficher nos cartes. On va donc avoir dans notre App.vue, pour la partie HTML :

<template>
  <div id="app">
    <div class="row">
      <div class="col-md-6 col-lg-6 col-xl-5 mx-auto">
        <div class="row justify-content-md-center">
          <div v-for="card in cards" v-bind:key="card.key" class="col-auto mb-3 flip-container">
            <div class="front border rounded shadow"><img width="100" height="150" src="/images/cardback.png"></div>
            <div class="back border rounded shadow"><img width="100" height="150" :src="'/images/'+card.name+'.png'"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

Le code HTML ci-dessus contient des classes css liées à bootstrap pour le positionnement des cartes. On indique donc à bootstrap de centrer notre jeux et on définit la taille des colonnes avec col-xxx. Pour plus d’infos sur bootstrap car le but ici n’est pas de s’étendre sur ce sujet, vous pouvez visiter le site officiel de bootstrap.

Sinon, vous pouvez voir qu’on fait un v-for pour boucler sur notre liste de carte afin d’afficher un div par carte. Chaque div va contenir 2 images :

  • Le dos de la carte (image cardback.png : class css front).
  • L’image de la carte qui sera caché pour le moment (image card.name) -> pour utiliser le code vueJs dans src, on va préfixer src avec « : » ainsi on peut utiliser ‘/images/’+card.name+’.png’ comme image qui se réfère à la propriété name de l’objet card du v-for en cours.

Et pour la partie STYLE :

<style>
    .front,
    .back {
        backface-visibility: hidden;
        transition: 0.6s;
        transform-style: preserve-3d;
        top: 0;
        width: 100%;
    }
    .back {
        transform: rotateY(-180deg);
        position: absolute;
        width: auto;
    }
</style>

Ci-dessus le style css de nos cartes.

  • backface-visibility: hidden signifie que quand on retourne une des 2 images, sa face « dos » n’est pas visible.
  • Ainsi, on retourne déjà le back qui représente la partie avec le dessin. Cela se fait avec le transform: rotateY(-180deg);
  • Le reste est du détail. Pour plus d’infos sur css, n’hésitez pas à consulter le site w3schools partie css.

4. Retourner les cartes

On va tout d’abord ajouter des classes css isFlipped et isMatched qui indiqueront si les cartes sont visibles (image visible -> isFlipped) et même matchée avec une autre (une paire trouvée -> isMatched). Pour se faire, voici les 2 styles à ajouter dans la partie style de App.vue :

    .flipped .back {
        transform: rotateY(0deg);
    }
    .flipped .front {
        transform: rotateY(180deg);
    }
    .matched{
      opacity: 0.3;
    }

On va également préparer une méthode qui sera appelée quand on cliquera sur une carte : flipCard. Cette méthode ne va pour le moment que retourner la carte cliquée. On verra plus tard le reste de la logique. Voici donc le code de notre méthode :

flipCard(card) {
      // On retourne la carte
      card.isFlipped = true;
    }

On va à présent utiliser ces classes css dans l’html et appeler cette fonction flipCard quand on va cliquer sur une carte. Du coup, on va modifier la ligne html dans le template (celle qui contient le v-for) comme ici :

<div v-for="card in cards" v-bind:key="card.key" class="col-auto mb-3 flip-container" :class="{ 'flipped': card.isFlipped, 'matched': card.isMatched }" @click="flipCard(card)">

Vous pouvez voir ci-dessus qu’on a ajouté :class pour conditionner la classe css flipped avec la donnée VueJs card.isFlipped et idem pour la classe css matched. Ensuite, le @Click indique quelle méthode VueJs sera appelée suite à un click.

5. Implémenter la logique du jeu

A présent, on va implémenter la logique du jeu :

  • Lorsqu’on clique sur une carte, on ne va agir que si elle n’est pas déjà retournée (isFlipped) et que si elle ne fait pas partie d’une paire trouvée (isMatched).
  • Ensuite, lorsqu’on retourne une carte, s’il y a déjà une carte retournée précédemment, on va comparer nos 2 cartes et vérifier qu’elle ne porte pas le même nom (que ce n’est pas une paire).
    • Si on a une paire -> on les marque toutes les 2 isMatched = true et on peut à partir de là de nouveau retourner 2 autres cartes
    • Sinon on retourne nos cartes (on les rend invisible) car on n’a pas trouvé une paire

On va donc modifier notre fonction flipCard :

    flipCard(card) {

      // Si on est déjà sur une carte matchée ou retournée : on ne fait rien
      if (card.isMatched || card.isFlipped)
      {
        return;
      }

      // On retourne la carte
      card.isFlipped = true;
      if (this.lastFlippedCards != null)
      {
        // Si on a une autre carte déjà retournée : on vérifie si on a trouvé une paire
        this.match(card);
      }
      else
      {
        // Sinon on stocke la carte ici comme dernière carte retournée
        this.lastFlippedCards = card;
      }
    },

Et pour vérifier si on a trouvé une paire, on va créer une méthode match(card) :

match(card) {

      // Si la carte précédente = la carte en cours : elles match -> isMatched = true
      if (this.lastFlippedCards.name == card.name)
      {
        card.isMatched = true;
        this.lastFlippedCards.isMatched = true;
        this.lastFlippedCards = null;
      }
      else
      {
        // Sinon on retourne simplement nos cartes (via un timer pour que la dernière carte cliquée ait le temps d'ête visible avant d'être de nouveau cachée)
        setTimeout(() => {
          card.isFlipped = false;
          this.lastFlippedCards.isFlipped = false;
          this.lastFlippedCards = null;
        }, 500);
      }
    }

6. Petits ajouts

On a à présent un jeu MEMORY fonctionnel mais ce serait intéressant d’avoir un TIMER qui se met en route dès qu’on clique sur la première carte ainsi qu’un compteur de paires réussies et un compteur de paires ratées. Et une fois le jeu terminé, ce serait également bien d’avoir un bouton permettant de démarrer un nouveau jeu.

6.a. Le timer

Commençons par le timer. On va donc ajouter dans nos variables un timer (min, sec, text -> minutes, secondes et format texte pour l’affichage en html) et une valeur qui nous indique si on a déjà cliqué sur une première carte (si le jeu a commencé) -> dans la partie data() de notre partie <script> :

timer : {
  min : 0,
  sec : 0,
  text : '00:00'
},
isStarted : false

On a nos données pour notre timer. Dans la méthode flipCard, on va donc indiquer que le jeu débute dès qu’on fait un premier flipCard donc en début de méthode :

flipCard(card) {

      // Démarrage du jeu
      if (!this.isStarted)
      {
        // Le jeu est marqué comme démarré
        this.isStarted = true;

        // On démarre un timer qui exécute le code entre parenthèses toutes les secondes
        this.interval = setInterval(() => {
          
          // On incrémente les secondes si < 59 sinon on remet les sec à 0 et on incrémente les minutes puisqu'après 59sec on passe à 1min00
          if (this.timer.sec < 59)
          {
            this.timer.sec++;
          }
          else
          {
            this.timer.sec = 0;
            this.timer.min++;
          }

          // On remet notre timer au format 00:00 pour l'affichage html
          this.timer.text = (this.timer.min < 10 ? '0' + this.timer.min : this.timer.min) + ':' + (this.timer.sec < 10 ? '0' + this.timer.sec : this.timer.sec);
        }, 1000);
      }
...

Les commentaires dans le code ci-dessus sont je crois assez explicites. Il ne reste donc plus qu’à afficher notre timer. On va pour cela aller dans la partie <template> modifier l’html et ajouter ceci :

<template>
  <div id="app">
    <div class="row">
      <div class="col-md-6 col-lg-6 col-xl-5 mx-auto">
           <div class="row justify-content-md-center">
                <div class="totalTime p-3"><span class="btn btn-info">Total Time : <span class="badge">{{this.timer.text}}</span></span></div>
...

6.b. Les compteurs de paires trouvées / ratées

Dans la partie données (data()), on va ajouter 2 variables :

countSuccess : 0,
countFailed : 0

A présent, dans la méthode match, on va les incrémenter en fonction du fait que la carte retournée = ou pas la carte retournée précédemment :

match(card) {


      if (this.lastFlippedCards.name == card.name)
      {
        this.countSuccess++;
        ...
      }
      else
      {

        setTimeout(() => {
          this.countFailed++;

Il nous reste l’affichage html dans la partie template (en dessous de notre div qu’on a ajouté pour le timer) :

<div class="row justify-content-md-center">
          <div class="totalTime p-3"><span class="btn btn-info">Total Time : <span class="badge">{{this.timer.text}}</span></span></div>
          <div class="turns p-3"><span class="btn btn-info">Success : <span class="badge">{{this.countSuccess}}</span> </span></div>
          <div class="turns p-3"><span class="btn btn-info">Failed : <span class="badge badge-danger">{{this.countFailed}}</span> </span></div>
        </div>

Pour info : j’utilise la classe css badge-danger pour que le nombre de paires ratées s’affiche avec un contour rouge.

6.c Finaliser le jeu

Lorsque vous avez trouvé toutes les paires d’images, le timer continue de tourner. Il serait bien qu’il s’arrête. Pour se faire, on va remettre notre valeur isStarted à false et arrêter le timer lorsqu’on a tout trouvé. Voici ce qu’il vous faut ajouter comme code dans la méthode match :

match(card) {

      if (this.lastFlippedCards.name == card.name)
      {
        ...

        if (this.cards.every(card => card.isMatched === true))
        {
          clearInterval(this.interval);
          this.isStarted = false;
        }
...

On fait un .every sur nos cartes pour valider que TOUTES les cartes ont bien leur propriété isMatched à true. Si c’est le cas, on arrête notre timer et on remet notre donnée isStarted à false.

6.d Le bouton Restart en overlay

Comme indiqué plus haut, une fois le jeu fini, il serait bien de pouvoir relancer une nouvelle partie sans devoir rafraichir le navigateur. On va donc afficher un bouton RESTART en overlay dès que le jeu est fini. Pour se faire, on va déjà créer notre méthode restart() :

restart() {
  // On remélange les cartes (initialisation)
  this.initGame();

  // On remet tous les compteurs à 0
  this.countSuccess = 0;
  this.countFailed = 0;
  this.timer.min = 0;
  this.timer.sec = 0;
  this.timer.text = '00:00';
}

Voici le style css d’un overlay (grisatre qui prend toute l’écran avec un contenu centré) :

    .overlay {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: rgba(0,0,0,0.5);
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      font-family: 'Rubik', sans-serif;
      color: #fff;
      -webkit-font-smoothing: antialiased;
      z-index: 3;
    }

A présent, ajoutons notre bouton en overlay lorsque le jeu est fini (donc lorsqu’on a isStarted à false et countSuccess > 0.) Voici ce que ça donne dans le code html :

<template>
  <div id="app">
    <div class="row">
      <div class="col-md-6 col-lg-6 col-xl-5 mx-auto">
        <div class="overlay" id="overlay" v-show="!this.isStarted && this.countSuccess>0"><span class="btn btn-info" style="bottom:2em" @click="restart()">Restart</span></div>
...

On peut voir un rappel des bases de vueJs ici : on utilise v-show pour indiquer quand est-ce que l’overlay doit être visible ou pas.

Pour finir

J’espère que grâce à ce tuto étape par étape, vous aurez compris chaque partie du jeu et que cela vous aura appris pas mal de nouveautés. C’était l’occasion de développer un petit jeu en parcourant des fonctionnalités qui n’ont pas toutes été reprises dans mes tutos à ce jour.

J’espère que cet article vous aura plut. N’hésitez pas à liker, commenter, partager et sutout à vous abonner au blog pour ne rien manquer. Je vous dis à très bientôt sur le blog développement web facile.com ;-).