[LEM 1] Jacques-a-dit

Vous avez tous sûrement déjà joué à « Jacques-a-dit » lorsque vous étiez plus jeune. Ce jeu où vous devez faire une action si le maître du jeu a prononcé les mots « Jacques a dit » en début de phrase.
Mais connaissez-vous la version américaine de ce jeu ? Non ? Eh bien cela s’appelle le « Simon ». C’est un jeu électronique sorti à la fin des années ’70. Il possède quatre grosses touches de couleurs sur lesquelles il faut appuyer selon une séquence aléatoire générée par le jeu. Le tout s’accompagne bien entendu d’un son pour faire une mélodie plus ou moins agréable 😀 . Mémoire et réflexes sont au rendez-vous !
Je vous propose dans cet exercice de réaliser notre propre version de ce jeu avec une Arduino et quelques composants électroniques !

Consigne et Matériel

Matériel nécessaire

Pour réaliser ce petit jeu, vous aurez besoin dans une version simple des composants suivants :

  • Une Arduino !
  • 4 LEDs (et leur résistance de limitation de courant)
  • 4 boutons poussoirs (éventuellement un condensateur de filtrage chacun)
  • Un buzzer piezo (avec une résistance de 1k pour limiter le courant
  • Une breadboard et des fils

Objectifs

Consigne

Si vous avez lu le Wikipédia proposé plus haut, vous avez compris le principe. Il faut réaliser un jeu de mémoire basé sur quatre sons et quatre couleurs.
Le jeu réalise une séquence aléatoire puis donne ensuite la main au joueur qui doit répéter cette même séquence. Si ce dernier se trompe ou met trop de temps à répondre, il peut recommencer la séquence jusqu’à trois fois. Au bout de trois tentatives perdues, c’est la fin du jeu ! S’il réussit la manche, le compteur d’essais ratés est réinitialisé.
Pour gagner, le joueur doit réaliser la séquence jusqu’au bout. C’est au programmeur de définir la difficulté en décidant du nombre maximum de manches à faire (cf. « améliorations » ci-dessous).
Si le joueur parvient à réaliser toutes les manches, c’est gagné !

Comme une vidéo vaut mieux qu’une longue explication, voici en image le résultat attendu !

Vidéo de démonstration

Informations sur le buzzer

Pour utiliser un buzzer c’est très simple. Il suffit de le brancher comme une LED sur l’Arduino, avec une résistance entre 300 et 1000 ohms pour limiter le courant. Ensuite, pour le faire biper il faudra lui envoyer un signal avec une fréquence. Pour cela, Arduino met à disposition deux fonctions : tone() et noTone().
La première prend en arguments la broche sur laquelle est connecté le buzzer puis la fréquence à laquelle il doit biper. Éventuellement un troisième argument indique la durée pendant laquelle il doit biper.
La seconde fonction fait cesser le son si vous utilisez tone() sans durée (sans le 3ème argument). Elle prend simplement en paramètre la broche sur laquelle est connecté le buzzer.
Voici la doc de chacune des fonctions, tone() et noTone().

Axes d’améliorations

Tout cela était trop facile pour vous ? Vous en voulez encore ? Voici quelques axes d’améliorations pour votre superbe jeu !

  • Ajout d’un compteur de points avec un (ou plusieurs) afficheur(s) 7 segments
  • Sauvegarde du meilleur score dans l’EEPROM (avec affichage en voie série ou LCD)
  • Utilisation d’un potentiomètre (ou voie série) pour définir la vitesse (difficulté) de jeu
  • Utilisation d’un potentiomètre (ou voie série) pour définir la durée (nombre de manche) de la partie
  • Mode « sans lumière », uniquement avec un buzzer !

Si vous avez encore d’autres idées pour rendre les choses plus amusantes, n’hésitez pas à les partager 🙂

À vous de jouer !

Correction

Montage

Le câblage est assez simple, il n’y a aucune surprise à rencontrer, il suffit juste de brancher 4 boutons et 4 LEDs à l’Arduino plus un buzzer.
Pour ma part, j’ai branché les 4 boutons sur les pins 8, 9, 10, 11 en utilisant la pull-up interne à l’Arduino pour limiter le nombre de composants externes. Je me suis aussi passé de condensateur de filtrage, je détecte et filtre l’appui via le code.
Les LEDs sont branchées sur les pins 4, 5, 6, 7. Idéalement, j’aurais utilisé 4 couleurs différentes. Étant un peu court en LED, j’ai utilisé deux couleurs en alternance. Elles sont branchées en mode « actif à l’état bas », ce qui signifie qu’il faudra un 0 pour les allumer et donc un 1 pour les éteindre. L’anode est reliée au +5V, la cathode est reliée à Arduino.
Le buzzer est branché à la broche 2. Une résistance de 330 ohms est utilisée pour protéger l’Arduino d’un trop fort courant.

Le code

Passons aux choses sérieuses. Bien que pas forcément trop compliqué, le code peut-être assez long. Il faut donc s’organiser correctement pour ne pas se perdre et risquer des bugs et des comportements étranges.
Avant toutes choses je vous propose de faire le tour des choses simples, les variables globales et le setup. Nous verrons la boucle et les fonctions qui vont avec ensuite.

Les globales et le setup

Les variables globales

Pour commencer, j’ai défini quelques constantes sur les paramètres du jeu :

  • La longueur maximale de la séquence : SIZE_MAX
  • La limite de temps entre deux appuis avant que le joueur ne perde : LIMITE_TEMPS
  • La vitesse de l’animation lorsqu’on montre la séquence au joueur (durée entre chaque note) : VITESSE

De manière programmatoire, cela se traduit par 3 define que voila :

Ensuite, afin de bien s’organiser, j’ai décidé d’utiliser à outrance des tableaux pour regrouper les choses logiques entres elles.
J’ai ainsi 6 tableaux différents.

  • Un tableau de char (constante) qui regroupe les numéros de broches des LEDs : leds[]
  • Un tableau de char (constante) qui regroupe les numéros de broches des boutons : boutons[]
  • Un tableau d’entiers (constante) énumérant les fréquences jouées pour chaque note : frequences[]
  • Un tableau de char portant les états des boutons à un instant t (appuyé (1) ou relâché (0)) : etats[]
  • Un tableau de char portant les états des boutons à un instant t-1 : mem[]
  • Un tableau de char possédant la séquence à jouer : sequence[]

Là encore, dans le code nous aurions les données suivantes :

Il ne nous manque plus qu’une constante pour la broche du buzzer (char) et une variable pour indiquer à quelle étape nous sommes rendus dans la séquence (nombre de points du joueur en quelque sorte) :

Le setup

Maintenant que nous avons toutes nos variables globales, nous allons pouvoir écrire notre setup.
Ce dernier aura 3 tâches principales à faire :

  • Paramétrer les entrées/sorties
  • Initialiser le générateur aléatoire
  • Démarrer la voie série (pour debug et communication éventuelle avec l’utilisateur)

Pour la voie série et le générateur d’aléatoire, pas de grande surprise. Il suffit de faire un Serial.begin() pour l’un et un randomSeed() pour l’autre.

Maintenant, pour initialiser les entrées/sorties, on va tirer parti du fait que tout cela est regroupé dans des tableaux de même taille. Ainsi, il nous suffit d’explorer les indices 0 à 3 (inclus) pour mettre en place les 4 LEDs et les 4 boutons en une seule boucle !
Le buzzer quant à lui sera réglé en une instruction après.

Et voilà ! Notre initialisation est terminée !
Voici le code complet de cette dernière :

Secret: Réponse SelectionnerAfficher

Le corps du programme

La loop()

Afin de garder légère la loop(), nous allons la découper en de nombreuses fonctions « perso » afin de garder le code simple et lisible. Nous les verrons au fur et à mesure à la suite.

Le programme va posséder deux boucles imbriquées. L’une sert à vérifier que le joueur n’a pas perdu/gagné le jeu et l’autre qu’il n’a pas perdu/gagné la manche.

Comme vous pouvez le voir, la première boucle sert aussi à ajouter une étape à la séquence de jeu SI le joueur n’a pas perdu (booléen perdu). Cette fonction renvoie elle-même un booléen qui indique si on a pu ajouter une étape au tableau de la séquence ou non. Si elle renvoie false cela signifie que le joueur a gagné puisqu’on ne peut plus ajouter d’étape.

Comme vous pouvez vous en douter, la majeure partie du programme va se dérouler dans la deuxième boucle qui testera une manche.
Cette boucle fera plusieurs choses :

  1. Affiche des informations au joueur (nombre de points…)
  2. Affiche la séquence à répéter sur les LEDs
  3. Teste si le joueur répond correctement ou non
  4. Joue des animations en fonction du point précédent
  5. Repart au point 1 en cas de défaite

Voici comment moi je vois les choses version code :

Et voici ce que cela donne lorsque tout est intégré dans la boucle principale :

Si jamais vous n’avez pas réussi à résoudre l’exercice, vous pouvez toujours essayer de copier cette loop et de compléter en écrivant vos propres fonctions 😉 .

ajouterEtape()

Dans cette fonction, nous allons ajouter une étape à la séquence jouée par « l’IA ». Pour cela, on va faire un tirage aléatoire entre 0 (inclus) et 4 (exclu) pour obtenir notre nouvelle valeur, l’ajouter au tableau puis incrémenter notre compteur de points qui nous sert aussi à indiquer combien de cases du tableau sont remplies pour la séquence.

playSequence()

Cette fonction est une des plus simples à réaliser. Son objectif est de lire la séquence à réaliser pour montrer au joueur ce qu’il doit répéter. Nous allons donc faire une boucle for() qui ira de 0 jusqu’à « cpt » qui indique la longueur de la séquence. À chaque itération dans la séquence, la case courante du tableau « sequence » nous retournera la note concernée (0, 1, 2, 3) et on pourra alors l’afficher via le tableau de LEDs (pour allumer la bonne LED) et aussi jouer la bonne fréquence via le tableau idoine.
Voici le code de cette fonction.

checkEtape()

Cette fonction est un peu la clé de voute du programme. Elle va nous servir à tester si le joueur répond correctement ou non. Elle va retourner un booléen, qui sera true si le joueur répond correctement et false s’il s’est trompé de touche ou à mis trop de temps à répondre.
Pour tester ces deux cas de figures, on procède de la manière suivante.
On commence par englober le contenu de la fonction par un while() qui teste à chaque passage si LIMITE_TEMPS s’est écoulé depuis le moment ou l’on est entré dans la fonction :

Ensuite, dans le corps du while on va tester si le joueur appuie sur la bonne touche ou non.
Pour cela, on va faire appel à une fonction perso nommée lectureBoutons() qui remplira le tableau d’état des boutons. Ensuite, on testera l’état de chaque bouton via un for. Si un bouton est appuyé, on allume sa LED et fait sonner le buzzer avec sa fréquence. Si ce bouton est le bon (correspond à l’objectif courant dans la séquence) alors on retourne true, sinon on retourne false. Si aucun bouton n’était appuyé, on retourne au début du while.

lectureBoutons()

Afin de pouvoir vérifier les étapes, il faut pouvoir vérifier si les boutons sont appuyés ou non. Cette fonction s’en occupe. Elle s’occupera de remplir le tableau « état » qui donne l’état des boutons à un moment [t]. Si le bouton est appuyé et qu’il était relaché au passage précédent dans la boucle, alors l’état sera 1, sinon il sera 0. Cela signifie que si un bouton reste appuyé lors de deux passages consécutifs dans la fonction, son état passera de 1 à 0. En effet, si l’on ne faisait pas cela et laissait l’état du bouton à 1, la fonction précédente détecterait deux appuis conséquents si le joueur reste appuyé et/ou ne retire pas son doigt assez vite, ce qui pourrait poser des problèmes.

animationReponseFausse()

Quand l’utilisateur se trompe ou met trop de temps à répondre, il perd une chance et doit recommencer la séquence actuelle. On le lui signale en faisant une petite animation qui va faire clignoter toutes les LEDs en même temps et qui fera sonner le buzzer en rythme.
Rien de réellement compliqué, juste une simple boucle for() pour faire un clignotement.

animationReponseBonne()

Comme pour le cas précédent, on va signaler à l’utilisateur si sa réponse est bonne avant de passer à la réponse suivante. Pour cela, on fait un chenillard avec les LEDs et un crescendo avec le buzzer. Là encore l’utilisation d’une boucle for est faite.

ecranFin()

Lorsque l’utilisateur a fini, on lui signale s’il a perdu ou gagné via un message sur la liaison série. En cas de défaite, on lui propose de revoir la dernière séquence jouée s’il appuie sur un bouton. En cas de victoire, on ne fait rien. Dans les deux cas, le joueur est invité à rejouer en redémarrant la carte.
La fonction définie ici prend un seul argument, un booléen indiquant la défaite (true) ou la victoire (false).

Code complet

Comme je suis sympa, voici le code complet que j’ai utilisé pour réaliser la vidéo que l’on peut voir ci-dessus 😉 .

Secret: Le code complet SelectionnerAfficher

Et voilà, c’est terminé pour cet exercice. J’espère qu’il vous aura plu et que vous aurez réussi à le réaliser par vous même sans attendre la correction.
Comme c’était le premier exercice, j’aimerais entendre votre opinion sur ce dernier pour m’aider à préparer les prochains 😉 .

  • Était-ce trop difficile ? Trop simple ?
  • Les explications (consigne) sont trop/pas assez détaillées ?
  • La correction est trop/pas assez détaillée ?

Merci d’avance pour vos retours !

À bientôt pour un nouvel exercice !

Héhé, pas encore 😉 !!

13 commentaires

    • Bin, à part sur le sujet de présentation du blog sur OC je n’ai pas trop fait de com’… Mais j’y avais pas pensé… je vais aller faire un sujet dans la catégorie « Électronique » 🙂
      Merci du relais en tout cas !

    • colle le lien du listing ici et comme ca on pourra aller voir le code 😉

      Je viens de regarder la vidéo, c’est très bien tout ca ! Belle résolution de l’exercice 🙂
      Juste une remarque, quand le joueur appuie sur une touche pour résoudre, tu ne joues pas le son/lumière en même temps. C’est voulu pour faire un rendu différent ou c’est un oubli ?
      En tout cas bravo pour la réalisation (et le montage propre, qui n’a pas des fils dans tous les sens 🙂 )

  1. Pingback: Correction de l'exercice disponible ! | Le blog d'EskimonLe blog d'Eskimon

  2. Merci . C’est super pour apprendre. J’espère que vous ferrez d’autre exercices corrigés.
    On peut aller chercher dans votre code quand on bloque et vos tutos sont vraiment super clair.

    merci encore et bonne soirée

    mon code final a donné ça:

    //Code pour jeu « Jacque a dis »

    const int latchPin = 12;//Déclaration du ST pour l’affichage
    const int clockPin = 13;//Délclaration du SH pour l’affichage
    const int dataPin = 11; //Déclaration du DS pour l’affichage

    const char leds[]={2,3,4,5}; //Déclaration leds en tableau
    const int bouton[] ={6,7,8,9};//Déclaration des boutons en tableau
    int memoire[100] ; // Déclaration du tableau qui va mémoriser la séquence à retrouver
    int affArray[10] = {252, 96, 218, 242, 102, 182, 190, 224, 254, 246};// Petit tableau pour la correspondance binaire de 0 à 9

    const int frequences[]={494,440,392,349};// Les notes (Si,la,sol ,Fa)
    const int buzzer =10; //Déclaration de la pin du Buzzer

    int nbrseq =1; // Déclaration du nombre de coup à jouer ,ça servira aussi pour le nombre de point
    int ledaj; // Déclaration de numéro de la led à allumer
    int nbrtape; // Déclaration du nombre de coup joué par l’opérateur

    void setup()
    {

    pinMode(latchPin,OUTPUT); // set up du compteur
    pinMode(clockPin,OUTPUT); // set up du compteur
    pinMode(dataPin,OUTPUT); // set up du compteur

    for (int i=0;i<4;i++)//Initialisations des sortie et entrées
    {
    pinMode (leds[i],OUTPUT);// on déclare les led en sortie
    pinMode (bouton[i],INPUT);// et les boutons comme entrées
    }
    pinMode(buzzer,OUTPUT);// le buzzer, une sortie qui sera modulé en PWM

    Serial.begin (9600);// on déclare le moniteur série- ça sert pour le debuggage
    randomSeed(analogRead(1)); //Lecture d'un bruit pour faire un nombre aléatoire
    }

    void loop() {

    //on commence par afficher le score.
    digitalWrite(latchPin,LOW);
    shiftOut(dataPin,clockPin,MSBFIRST,affArray[nbrseq-1]);
    digitalWrite(latchPin,HIGH);

    //—————— étape 1 : faire la séquence aléatoire et la mémoriser——————-

    nbrtape=0; // on remet le nombre de coup joué à zéro

    for (int i=0;i<4;i++) // on étteints toutes les leds
    {
    digitalWrite (leds[i],LOW);
    }

    for (int i=0;i<nbrseq;i++) // on joue la séquence aléatoire
    {
    ledaj = random(4);
    digitalWrite(leds[ledaj],HIGH);
    analogWrite (buzzer,frequences[ledaj]);
    delay(1000);
    digitalWrite(leds[ledaj],LOW);
    analogWrite (buzzer,0);
    delay (1000);
    memoire[i]=ledaj;//on enregistre la séquence
    }

    //——————————étape 2 : On attends que le joueur reproduise la séquence——————————

    while (nbrtape < nbrseq)
    {

    for (int ii=0;ii<4;ii++)
    {
    if (digitalRead (bouton[ii])==HIGH)
    {
    digitalWrite(leds[ii],HIGH);
    analogWrite (buzzer,frequences[ii]);
    delay(500);

    Serial.println (" le bon bouton tappe");
    Serial.println (ii);
    Serial.println ("la mémoire" );
    Serial.println (memoire[nbrtape]);
    Serial.println (nbrtape);
    Serial.println (nbrseq);
    digitalWrite(leds[ii],LOW);
    analogWrite (buzzer,0);

    if (memoire[nbrtape]==ii) {

    delay (300); // petite pause aprés avoir appuyer sur une touche

    if (nbrtape+1==nbrseq){// si le dernier coup est bon
    nbrseq=nbrseq+1;
    nbrtape=nbrtape+1; // pour éviter le bug d'avoir un coup en plus à jouer

    digitalWrite(latchPin,LOW); //ground latchPin and hold low for as long as you are transmitting
    shiftOut(dataPin,clockPin,MSBFIRST,affArray[nbrseq-1]);
    digitalWrite(latchPin,HIGH); //pull the latchPin to save the data

    delay (1000); // un petit temps entre deux parties
    }

    }
    else {

    perdu();// pour s'entrainer a faire des fonctions
    }

    nbrtape=nbrtape+1;
    }
    }
    }
    }// la fin

    //—————————Les Fonctions————————————————————————-//

    void perdu(){

    for (int ii=0;ii<4;ii++)
    {

    digitalWrite (leds[0],HIGH);
    digitalWrite (leds[2],HIGH);
    digitalWrite (leds[3],HIGH);
    digitalWrite (leds[1],HIGH);
    delay (400);
    digitalWrite (leds[1],LOW);
    digitalWrite (leds[2],LOW);
    digitalWrite (leds[3],LOW);
    digitalWrite (leds[0],LOW);
    delay (400);

    nbrtape=0;
    nbrseq=1;
    }
    }

Laisser un commentaire