un jeu Simon avec JavaScript

Comment créer un jeu Simon avec JavaScript

ns ce didacticiel, nous allons développer le jeu Simon classique avec JavaScript. Le but du jeu est de répéter une série de clics de tuiles aléatoires créés par le jeu. Après chaque tour, la séquence devient progressivement plus longue et plus complexe, ce qui la rend plus difficile à retenir.

Simon Jeu
Jouez à une version live du jeu.

En règle générale, vous aurez quatre tuiles différentes, chacune avec une couleur et un son uniques qui s’activent lorsque vous appuyez dessus. Le son aide le joueur à se souvenir de la séquence et le jeu se termine si le joueur manque une étape de la séquence.

Conditions préalables

Ce tutoriel suppose une connaissance de base de JavaScript et du DOM .

Commencer

Récupérez les fichiers HTML et CSS du jeu sur GitHub . Les instructions pour configurer le projet se trouvent dans le fichier README inclus. Vous pouvez également suivre le tutoriel en utilisant JSFiddle si vous préférez.

JSFiddle
Voici le point de départ sur JSFiddle

Comme mentionné précédemment, un tour commence lorsque le jeu active une ou plusieurs tuiles dans un ordre aléatoire et se termine lorsque le joueur reproduit l’ordre en appuyant sur les tuiles. Au tour suivant, le nombre de tuiles dans la séquence augmente de un.

Commençons par créer un tableau pour garder une trace de la séquence originale de clics sur les tuiles et un second tableau pour la séquence humaine :

let sequence = [];
let humanSequence = [];

Ensuite, sélectionnez le bouton de démarrage et créez une nouvelle startGame()fonction qui sera exécutée lorsque ce bouton sera cliqué.

const startButton = document.querySelector('.js-start');

function startGame() {
  startButton.classList.add('hidden');
}

startButton.addEventListener('click', startGame);

À ce stade, une fois le bouton de démarrage enfoncé, il sera masqué. L’ .info élément doit également apparaître car c’est là que les messages d’état seront affichés. Il a la hiddenclasse appliquée par défaut dans le HTML qui a été stylisé display: none;dans le CSS , c’est donc ce qui doit être supprimé une fois le jeu démarré.

<span class="info js-info hidden"></span>

Mettez à jour votre fichier JavaScript comme indiqué ci-dessous :

const startButton = document.querySelector('.js-start');
const info = document.querySelector('.js-info');

function startGame() {
  startButton.classList.add('hidden');
  info.classList.remove('hidden');
  info.textContent = 'Wait for the computer';
}

Désormais, une fois le bouton de démarrage cliqué, l’ .infoélément affiche un message indiquant à l’utilisateur d’attendre la fin de la séquence.

Commencer le tour suivant

Créez une nouvelle levelvariable ci-dessous humanSequence. C’est ainsi que nous garderons une trace du nombre de tours qui ont été joués jusqu’à présent.

let level = 0;

Ensuite, créez une nouvelle nextRound()fonction juste au-dessus startGame(), comme indiqué dans l’extrait ci-dessous. Le but de cette fonction est de démarrer la prochaine séquence de clics sur les tuiles.

function nextRound() {
  level += 1;

  // copy all the elements in the `sequence` array to `nextSequence`
  const nextSequence = [...sequence];
}

A chaque nextRound()appel, la levelvariable est incrémentée de 1 et la séquence suivante est préparée. Chaque nouveau tour s’appuie sur le précédent, donc ce que nous devons faire est de copier l’ordre existant des pressions sur les boutons et d’y ajouter un nouveau aléatoire. Nous avons fait le premier sur la dernière ligne de nextRound(), alors ajoutons maintenant une nouvelle pression de bouton aléatoire à la séquence.

Créez une nouvelle nextStep()fonction juste au dessus nextRound():

function nextStep() {
  const tiles = ['red', 'green', 'blue', 'yellow'];
  const random = tiles[Math.floor(Math.random() * tiles.length)];

  return random;
}

La tilesvariable contient les couleurs de chaque bouton du plateau de jeu. Remarquez comment les valeurs correspondent aux valeurs de la data-tilepropriété dans le HTML .

<div class="tile tile-red" data-tile="red"></div>
<div class="tile tile-green" data-tile="green"></div>
<div class="tile tile-blue" data-tile="blue"></div>
<div class="tile tile-yellow" data-tile="yellow"></div>

Nous devons obtenir une valeur aléatoire du tableau à chaque nextStep()exécution, et nous pouvons y parvenir en utilisant la Math.random()fonction en combinaison avec Math.floor(). Le premier renvoie un nombre pseudo-aléatoire à virgule flottante compris entre 0 et moins de 1.

Démonstration de la console Math.random

Cela ne nous est pas très utile pour le moment. Il doit être converti en un index valide pour le tilestableau (0, 1, 2 ou 3 dans ce cas) afin qu’une valeur aléatoire provenant du tableau puisse être récupérée à chaque fois. Multiplier la valeur de Math.random()par la longueur de tiles(qui est 4) garantit que la plage du nombre aléatoire est maintenant comprise entre 0 et moins de 4 (au lieu de 0 et moins de 1).

Action Math.random dans la console

Pourtant, ces valeurs fractionnaires ne sont pas des index de tableau valides, elles Math.floor()sont donc utilisées pour arrondir les nombres au plus grand entier inférieur ou égal à la valeur donnée. Cela nous donne des entiers entiers entre 0 et 3 qui peuvent être utilisés pour récupérer une valeur aléatoire du tilestableau.

Démonstration de Math.floor dans la console

Utilisons la valeur de retour de la nextStep()fonction dans nextRound()comme indiqué ci-dessous :

function nextRound() {
  level += 1;

  const nextSequence = [...sequence];
  nextSequence.push(nextStep());
}

Ce qui se passe ici, c’est que lorsqu’il nextStep()est exécuté, il renvoie une valeur aléatoire du tilestableau (« rouge », « bleu », « vert » ou « jaune »), et la valeur est ajoutée à la fin du nextSequence()tableau à côté de toutes les valeurs du tour précédent.

Jouer le tour suivant

L’étape suivante consiste à jouer le tour suivant en activant les tuiles à l’écran dans le bon ordre. Ajoutez les fonctions suivantes ci-dessus nextStep()dans votre fichier JavaScript :

function activateTile(color) {
  const tile = document.querySelector(`[data-tile='${color}']`);
  const sound = document.querySelector(`[data-sound='${color}']`);

  tile.classList.add('activated');
  sound.play();

  setTimeout(() => {
    tile.classList.remove('activated');
  }, 300);
}

function playRound(nextSequence) {
  nextSequence.forEach((color, index) => {
    setTimeout(() => {
      activateTile(color);
    }, (index + 1) * 600);
  });
}

La playRound()fonction prend un tableau de séquences et itère dessus. Il utilise ensuite la setTimeout()fonction pour appeler le activateTile()à des intervalles de 600 millisecondes pour chaque valeur de la séquence. La raison setTimeout()utilisée ici est d’ajouter un délai artificiel entre chaque pression sur un bouton. Sans cela, les tuiles de la séquence seront activées toutes en même temps.

Le nombre de millisecondes spécifié dans la setTimeout()fonction change à chaque itération. Le premier bouton de la séquence est activé après 600 ms, le suivant après 1200 ms (600 ms après le premier), le troisième après 1800 ms, etc.

Dans la activateTile()fonction, la valeur de colorest utilisée pour sélectionner la vignette et les éléments audio appropriés. Dans le fichier HTML , notez comment l’ data-soundattribut sur les audioéléments correspond aux couleurs des boutons.

<audio src="https://s3.amazonaws.com/freecodecamp/simonSound1.mp3" data-sound="red" ></audio>
<audio src="https://s3.amazonaws.com/freecodecamp/simonSound2.mp3" data-sound="green" ></audio>
<audio src="https://s3.amazonaws.com/freecodecamp/simonSound3.mp3" data-sound="blue" ></audio>
<audio src="https://s3.amazonaws.com/freecodecamp/simonSound4.mp3" data-sound="yellow" ></audio>

La activatedclasse est ajoutée à la vignette sélectionnée et la play()méthode est déclenchée sur l’élément audio sélectionné, provoquant la lecture du fichier mp3 lié dans l’ src attribut. Après 300 millisecondes, la activatedclasse est à nouveau supprimée. L’effet est que chaque tuile est activée pendant 300 ms, et il y a 300 ms entre les activations de tuiles dans la séquence.

Enfin, appelez playRound()la nextRound()fonction et appelez nextRound() la startGame()fonction comme indiqué ci-dessous :

function nextRound() {
  level += 1;

  const nextSequence = [...sequence];
  nextSequence.push(nextStep());
  playRound(nextSequence);
}

function startGame() {
  startButton.classList.add('hidden');
  info.classList.remove('hidden');
  info.textContent = 'Wait for the computer';
  nextRound();
}

Maintenant, une fois que vous avez appuyé sur le bouton de démarrage, le premier tour commencera et un bouton aléatoire sera activé sur le tableau.

Simon Game Démonstration du tour suivant

Au tour du joueur

Une fois que l’ordinateur commence un tour en activant la prochaine séquence de tuiles, le joueur doit terminer le tour en répétant le schéma d’activation des tuiles dans le bon ordre. Si une étape est manquée en cours de route, le jeu se termine et se réinitialise.

Sélectionnez les éléments de titre et de conteneur de tuiles sous la infovariable :

const heading = document.querySelector('.js-heading');
const tileContainer = document.querySelector('.js-container');

Ensuite, créez une humanTurnfonction qui indique que l’ordinateur a terminé le tour et qu’il est temps pour le joueur de répéter la séquence :

function humanTurn(level) {
  tileContainer.classList.remove('unclickable');
  info.textContent = `Your turn: ${level} Tap${level > 1 ? 's' : ''}`;
}

La première étape consiste à supprimer la unclickableclasse du conteneur de tuiles. Cette classe empêche d’appuyer sur les boutons lorsque le jeu n’a pas démarré et lorsque l’ IA n’a pas terminé la séquence d’appuis.

.unclickable {
  pointer-events: none;
}

Sur la ligne suivante, le contenu de l’ infoélément est modifié pour indiquer que le joueur peut commencer à répéter la séquence. Il indique également le nombre de frappes à saisir.

La humanTurn()fonction doit être exécutée une fois la séquence de l’ordinateur terminée, nous ne pouvons donc pas l’appeler immédiatement. Nous devons ajouter un délai artificiel et calculer quand l’ordinateur en aura terminé avec la séquence de pressions sur les boutons.

function nextRound() {
  level += 1;

  const nextSequence = [...sequence];
  nextSequence.push(nextStep());
  playRound(nextSequence);

  sequence = [...nextSequence];
  setTimeout(() => {
    humanTurn(level);
  }, level * 600 + 1000);
}

La setTimeout()fonction ci-dessus s’exécute humanTurn()une seconde après l’activation du dernier bouton de la séquence. La durée totale de la séquence correspond au niveau actuel multiplié par 600 ms qui est la durée de chaque tuile de la séquence. La sequencevariable est également affectée à la séquence mise à jour.

Dans la prochaine mise à jour de nextRound(), la unclickableclasse est ajoutée au conteneur de vignettes au début de la partie, et le contenu des éléments infoet headingest mis à jour.

function nextRound() {
  level += 1;

  tileContainer.classList.add('unclickable');
  info.textContent = 'Wait for the computer';
  heading.textContent = `Level ${level} of 20`;

  const nextSequence = [...sequence];
  nextSequence.push(nextStep());
  playRound(nextSequence);

  sequence = [...nextSequence];
  setTimeout(() => {
    humanTurn(level);
  }, level * 600 + 1000);
}

Démo du jeu Simon
Le titre reflète maintenant le niveau actuel

L’étape suivante consiste à détecter les appuis sur les boutons du joueur et à décider s’il faut passer au tour suivant ou mettre fin à la partie. Ajoutez l’écouteur d’événement suivant juste en dessous de celui pourstartButton :

tileContainer.addEventListener('click', event => {
  const { tile } = event.target.dataset;

  if (tile) handleClick(tile);
});

Dans l’écouteur d’événement ci-dessus, la valeur de data-tilesur l’élément sur lequel on a cliqué est accessible et stockée dans la tilevariable. Si la valeur n’est pas une chaîne vide (pour les éléments sans l’ data-tileattribut), la handleClick()fonction est exécutée avec la tilevaleur comme seul argument.

Créez la handleClick()fonction juste au-dessus startGame()comme indiqué ci-dessous :

function handleClick(tile) {
  const index = humanSequence.push(tile) - 1;
  const sound = document.querySelector(`[data-sound='${tile}']`);
  sound.play();

  const remainingTaps = sequence.length - humanSequence.length;

  if (humanSequence.length === sequence.length) {
    humanSequence = [];
    info.textContent = 'Success! Keep going!';
    setTimeout(() => {
      nextRound();
    }, 1000);
    return;
  }

  info.textContent = `Your turn: ${remainingTaps} Tap${
    remainingTaps > 1 ? 's' : ''
  }`;
}

Cette fonction pousse la valeur de tuile dans le humanSequencetableau et stocke son index dans la indexvariable. Le son correspondant au bouton est joué et les étapes restantes de la séquence sont calculées et mises à jour à l’écran.

Le ifbloc compare la longueur du humanSequencetableau au sequence tableau. S’ils sont égaux, cela signifie que le tour est terminé et que le tour suivant peut commencer. À ce stade, le humanSequencetableau est réinitialisé et la nextRound() fonction est appelée après une seconde. Le délai permet à l’utilisateur de voir le message de réussite, sinon il n’apparaîtra pas du tout car il sera immédiatement écrasé.

Démo du jeu Simon

Comparez les séquences

Nous devons comparer l’ordre dans lequel le joueur appuie sur les boutons à l’ordre de la séquence générée par le jeu. Si l’ordre ne correspond pas, le jeu se réinitialise et un message s’affiche pour alerter le joueur de l’échec.

Créez une nouvelle resetGame()fonction à cet effet ci-dessus humanTurn():

function resetGame(text) {
  alert(text);
  sequence = [];
  humanSequence = [];
  level = 0;
  startButton.classList.remove('hidden');
  heading.textContent = 'Simon Game';
  info.classList.add('hidden');
  tileContainer.classList.add('unclickable');
}

Cette fonction affiche une alerte et restaure le jeu à son état d’origine. Utilisons-le handleClickcomme indiqué ci-dessous :

function handleClick(tile) {
  const index = humanSequence.push(tile) - 1;
  const sound = document.querySelector(`[data-sound='${tile}']`);
  sound.play();

  const remainingTaps = sequence.length - humanSequence.length;

  if (humanSequence[index] !== sequence[index]) {
    resetGame('Oops! Game over, you pressed the wrong tile');
    return;
  }

  if (humanSequence.length === sequence.length) {
    humanSequence = [];
    info.textContent = 'Success! Keep going!';
    setTimeout(() => {
      nextRound();
    }, 1000);
    return;
  }

  info.textContent = `Your turn: ${remainingTaps} Tap${
    remainingTaps > 1 ? 's' : ''
  }`;
}

Si la valeur de l’élément récupéré par le indexdans les tableaux sequenceet humanSequencene correspond pas, cela signifie que le joueur a fait un mauvais tour. À ce stade, une alerte s’affiche et le jeu se réinitialise.

Préparer un état final

Le jeu fonctionne principalement mais nous devons introduire un état final où le joueur gagne la partie. J’ai choisi 20 tours, mais vous pouvez utiliser n’importe quel nombre que vous aimez. Le Simon Game classique s’est terminé après 35 tours.

Voici le bit qui met fin au jeu si l’utilisateur atteint et termine le 20e tour :

function handleClick(tile) {
  const index = humanSequence.push(tile) - 1;
  const sound = document.querySelector(`[data-sound='${tile}']`);
  sound.play();

  const remainingTaps = sequence.length - humanSequence.length;

  if (humanSequence[index] !== sequence[index]) {
    resetGame('Oops! Game over, you pressed the wrong tile');
    return;
  }

  if (humanSequence.length === sequence.length) {
    if (humanSequence.length === 20) {
      resetGame('Congrats! You completed all the levels');
      return
    }

    humanSequence = [];
    info.textContent = 'Success! Keep going!';
    setTimeout(() => {
      nextRound();
    }, 1000);
    return;
  }

  info.textContent = `Your turn: ${remainingTaps} Tap${
    remainingTaps > 1 ? 's' : ''
  }`;
}

Une fois le 20e tour terminé, un message de félicitations s’affiche et le jeu se réinitialise. Assurez-vous de l’essayer et voyez si vous pouvez atteindre le niveau 20 sans échouer.

Retour en haut