Jeu du Morpion avec IA invincible :

Publié le

Mis à jour le

Vidéo de présentation :



Présentation du projet :

Langages utilisés : JavaScript / HTML / CSS

Dans ce projet, on code un Jeu du Morpion en JavaScript. En comprenant l'algo Minimax, on pourra implémenter une IA imbattable.

2 modes de jeu sont disponibles : Joueur contre Joueur et Joueur contre IA. En jouant contre l'IA, le joueur a le choix entre 2 modes de difficultés : Facile et Extrême. En Facile, l'IA joue aléatoirement. Alors qu'en Extrême, l'IA choisit toujours le meilleur coup : elle est donc invincible.

Le joueur peut changer le mode de jeu et / ou la difficulté à tout moment.

Les scores sont mis à jour à chaque nouvelle partie. Le joueur a la possibilité de réinitialiser les scores en cliquant sur un bouton.

Enfin, le joueur qui commence change à chaque nouvelle partie.


Accéder au projet :

Jouer au Morpion



Code Source :

Structure HTML


        <!--Fichier : morpion.html-->

                    <!DOCTYPE html>
                    <html lang="fr">
                    <head>
                        <meta charset="UTF-8">
                        <meta name="viewport" content="width=device-width, initial-scale=1.0">
                        <title>Jeu du Morpion </title>
                        <link rel="stylesheet" href="style.css"> <!--Intégration du style CSS.-->
                    </head>
                    <body>
                        <script src="IArandom.js" defer></script> <!--Intégration de l'IA aléatoire JS.-->
                        <script src="IAminimax.js" defer></script> <!--Intégration de l'IA Minimax JS.-->
                        <script src="game.js" defer></script> <!--Intégration du moteur de jeu.-->


                        <h1>Jeu du Morpion : </h1>
                        <div class="contenu">
                            <div id="Score" class="menuInvisible">
                                <h2>Score :</h2>
                                <p id="score1">X :  <span></span></p>
                                <p id="score2">O :  <span></span></p>
                                <p id="scoreEgalite">Nul : <span></span></p>
                        
                                <button id="score">Réinitialiser le score </button>
                                <button id="mode"></button>
                                <button id="difficulte"></button>
                                <button id="sortir">Sortir</button>
                            </div>
                        
                            <div id="game">
                                <br>
                                <button id="menu">Menu</button>
                                <table> <!--Le tableau sert de grille du Morpion, de plateau de jeu.-->
                                <tr> <!--Chaque case (td) est identifiée par un id.-->
                                    <td id="C00"></td> <!--C00 : en haut à gauche.-->
                                    <td id="C01"></td>
                                    <td id="C02"></td>
                                </tr>
                                <tr>
                                    <td id="C10"></td>
                                    <td id="C11"></td>
                                    <td id="C12"></td>
                                </tr>
                                <tr>
                                    <td id="C20"></td>
                                    <td id="C21"></td>
                                    <td id="C22"></td> <!--C22 : en bas à droite.--> 
                                </tr>
                                </table>
                            </div>

                        </div>

                    </body>
                    </html>
        

Moteur de jeu en JS

//Fichier : game.js

            
        let grilleAffichage = document.querySelector("table");
        grilleAffichage.addEventListener('click',jouer);

        // INITIALISATION :

        let grille =
                [[0,0,0],       // 0 : case vide
                [0,0,0],        // 1 : joueur 1
                [0,0,0]]        // 2 : joueur 2

        let idJoueur;
        let boutonMode = document.querySelector("#mode"); // Bouton pour le choix du mode de jeu

        // Permet de stocker le mode de jeu entre les parties :
        if (!localStorage.getItem('modeJeu'))
            localStorage.setItem('modeJeu', "2J");
        let GAMEMODE = localStorage.getItem('modeJeu') // "IA" ou "2J"


        let boutonDifficulte = document.querySelector("#difficulte");
        // Permet de stocker la difficulté entre les parties
        if (!localStorage.getItem('difficulte'))
            localStorage.setItem('difficulte', "Facile");
        let DIFFICULTE = localStorage.getItem('difficulte') // "facile" ou "extrême" 
        boutonDifficulte.textContent = "Difficulté : "+DIFFICULTE;


        // Initialisation du texte du bouton 
        //+ inversion 1er joueur (celui qui débute la partie) pour le mode 2J :
        
        // ou faire Jouer l'IA au 1er tour, 1 partie sur 2.
        
        if (GAMEMODE === "2J") // Mode joueur contre joueur
        {
            boutonDifficulte.style.display = "none"; // Cache le bouton de la difficulté
            boutonMode.textContent = "Mode de jeu : 1 VS 1"; // Initialisation du texte du bouton

            // Inversion du 1er joueur :
            console.log(parseInt(localStorage.getItem('idJoueur')));
            if (!localStorage.getItem('idJoueur'))
                localStorage.setItem('idJoueur', "2");
            
            if (parseInt(localStorage.getItem('idJoueur')) === 1)
                localStorage.setItem('idJoueur', "2");
            else
                localStorage.setItem('idJoueur', "1");
            
            
            idJoueur = parseInt(localStorage.getItem('idJoueur'));
        }
        else // Mode joueur contre l'IA
        {
            // Si IAcommence n'est pas défini dans le stockage local,
            if (!localStorage.getItem('IAcommence'))
            // alors on l'initialise à 1 (utile seulement au 1er chargement)
            localStorage.setItem('IAcommence',1);

            // On récupère la valeur (0 : le joueur commence, 1 : l'IA commence.) :
            let IAcommence = parseInt(localStorage.getItem('IAcommence')); 
            boutonMode.textContent = "Mode de jeu : IA"; // Mise à jour du texte du bouton
            
            if (IAcommence == 1) // L'IA commence :
            {
                idJoueur = 2; // On fixe idJoueur = 2 -> l'IA jourra avec les "O".
                // On passe IAcommence à 0, pour qu'elle joue en 2ème lors de la prochaine partie :
                localStorage.setItem('IAcommence',0); 
                jouerIA(); // L'IA joue et inverse l'idJoueur
            }
            else // Le joueur commence :
            {
                idJoueur = 1; // On fixe idJoueur = 1 -> le joueur jouera avec les "X".
                // On passe IAcommence à 1, pour qu'elle commence lors de la prochaine partie :
                localStorage.setItem('IAcommence',1);
            }       
        }
       
        // Initialise les scores et le compteur d'égalités à zéro lors de la première partie :
        if (!localStorage.getItem('scoreJoueur1')) {
            localStorage.setItem('scoreJoueur1', 0);
        }
        if (!localStorage.getItem('scoreJoueur2')) {
            localStorage.setItem('scoreJoueur2', 0);
        }
        if (!localStorage.getItem('scoreEgalite')) {
            localStorage.setItem('scoreEgalite', 0);
        }

        // Récupère les scores des joueurs depuis le localStorage à chaque nouvelle partie
        let scoreJoueur1 = parseInt(localStorage.getItem('scoreJoueur1'));
        let scoreJoueur2 = parseInt(localStorage.getItem('scoreJoueur2'));
        let scoreEgalite = parseInt(localStorage.getItem('scoreEgalite'));

        // Variable du score pour l'affichage du score (span HTML) :
        let score1 = document.querySelector("#score1 span");
        let score2 = document.querySelector("#score2 span");
        let scoreEgaliteAffichage = document.querySelector("#scoreEgalite span");

        // Mise à jour de l'affichage du score : 
        score1.textContent = scoreJoueur1;
        score2.textContent = scoreJoueur2;
        scoreEgaliteAffichage.textContent = scoreEgalite;


        // FONCTIONS :

        function resetScore()
        {
            //localStorage.clear(); efface tout : pas bon -> on veut garder le mode de jeu, même après le reset
            localStorage.removeItem('scoreJoueur1'); // Permet de supprimer seulement l'item passé en paramètre
            localStorage.removeItem('scoreJoueur2');
            localStorage.removeItem('scoreEgalite');
            scoreJoueur1 = 0;
            scoreJoueur2 = 0;
            scoreEgalite = 0;
            score1.textContent = scoreJoueur1; 
            score2.textContent = scoreJoueur2;
            scoreEgaliteAffichage.textContent = scoreEgalite; 
        }

        // Bouton pour le reset du score :
        let boutonScore = document.querySelector("#score");
        // Configure le boutonScore pour qu'il exécute la fonction resetScore, au clic :
        boutonScore.addEventListener('click',resetScore);

        
       
        function ChangerModeJeu()
        {
            resetScore(); // On reset le score, avant d'inverser le mode de jeu.
            if (GAMEMODE === "2J")
            {
                GAMEMODE = "IA";
                localStorage.setItem('modeJeu', "IA");
                window.location.reload(); // permet d'actualiser la page, pour lancer une nouvelle partie.
            }
            else
            {
                GAMEMODE = "2J";
                localStorage.setItem('modeJeu', "2J");
                window.location.reload(); // permet d'actualiser la page, pour lancer une nouvelle partie.
            }
        }
        // Configure le boutonMode pour qu'il exécute la fonction ChangerModeJeu, au clic :
        boutonMode.addEventListener('click',ChangerModeJeu);


        function ChangerDifficulte()
        {
            resetScore(); // On reset le score, avant de changer la difficulté.
            if (DIFFICULTE === "Facile")
            {
                DIFFICULTE = "Extrême";
                localStorage.setItem('difficulte', "Extrême");
                window.location.reload(); // permet d'actualiser la page, pour lancer une nouvelle partie.
            }
            else
            {
                DIFFICULTE = "Facile";
                localStorage.setItem('difficulte', "Facile");
                window.location.reload(); // permet d'actualiser la page, pour lancer une nouvelle partie.
            }
        }
        // Configure le boutonDifficulte pour qu'il exécute la fonction ChangerDifficulte, au clic  :
        boutonDifficulte.addEventListener('click',ChangerDifficulte);

        // RESPONSIVE :
        // Menu mobile :
        function afficherMasquerMenu(event) // Permet d'afficher/masquer le menu sur mobile
        {
            let nomBoutonClique = event.target.id;
            // Autant utiliser classList.toggle avec 1 class à la place des 2 class,
            // Avec par défaut, l'élément en display:none; 
            // Exemple : element.classList.toggle("menuVisible");

            // Sinon, autre manière :
            if (nomBoutonClique === "menu") 
                document.querySelector("#Score").className = "menuVisible"; 
            else
                document.querySelector("#Score").className = "menuInvisible";
        }

        let boutonMenu = document.querySelector("#menu");
        // Configure le boutonMenu pour qu'il exécute la fonction afficherMasquerMenu, au clic :
        boutonMenu.addEventListener('click',afficherMasquerMenu);

        let boutonSortir = document.querySelector("#sortir");
        // Configure le boutonSortir pour qu'il exécute la fonction afficherMasquerMenu, au clic :
        boutonSortir.addEventListener('click',afficherMasquerMenu);


        function egalite() // Vérifie l'égalité (= la grille est pleine et personne n'a gagné)
        {
            // Il ne doit pas rester de case vide (0) :
            if (!grille[0].includes(0) && !grille[1].includes(0) && !grille[2].includes(0))
                return true;
            else
                return false;
        }

        function gagner() // Vérifie si un joueur a gagné : renvoie l'id du gagnant le cas échéant, null sinon.
        {
            let idGagnant = null;
            let i=0;
            while (i<3 && idGagnant == null)
            {
                //Lignes :
                if ( grille[i][0] !=0 && grille[i][0] == grille[i][1] && grille[i][1] == grille[i][2] )
                    idGagnant = grille[i][0];
                //Colonnes :
                else if ( grille[0][i] != 0 && grille[0][i] == grille[1][i] && grille[1][i] == grille[2][i] )
                    idGagnant = grille[0][i];
                i++
            }
            //Diagonales
            if(grille[1][1] != 0) // grille[1][1] == 0 : Pas de diagonales
            {

                if( (grille[0][0] == grille[1][1] && grille[1][1] == grille[2][2]))
                    idGagnant = grille[0][0];
                else if(grille[0][2] == grille[1][1] && grille[1][1] == grille[2][0]) 
                    idGagnant = grille[0][2];    
            }    

            return idGagnant;
        }


        function alerteGagner(idGagnant) // Affichage d'une alerte en cas de victoire.
        {
            let message = `Le ${idGagnant} joueur a gagné !`; 
            setTimeout(function()
            { // setTimeout : permet d'attendre (de continuer le code) 
              // et donc d'afficher le symbole, avant l'alerte.
                alert(message);
                // Sauvegarde du score dans le stockage local avant de recharger la page
                if (idGagnant == 1)
                {
                    scoreJoueur1++
                    localStorage.setItem('scoreJoueur1', scoreJoueur1);
                }
                else
                {    
                    scoreJoueur2++
                    localStorage.setItem('scoreJoueur2', scoreJoueur2);
                }
                window.location.reload();
        }, 100); // Attend 100 millisecondes avant d'afficher l'alerte   
            
        }

        function alerteEgalite() // Affichage d'une alerte en cas d'égalité.
        {
            console.log("égalité");
            scoreEgalite++;
            localStorage.setItem('scoreEgalite', scoreEgalite);
            setTimeout(function() {
                alert("Égalité !");
                window.location.reload();
            }, 100); // Attend 100 millisecondes avant d'afficher l'alerte

        } 

        function jouer(event)
        {
            //console.log(grille);
            let CASE = event.target; // récupération de la case cliquée

            let x = CASE.id[1]; // récupération de x (grâce à l'id)
            let y =   CASE.id[2]; // récupération de y (grâce à l'id)
            //console.log( x+ " " + y);

            if (grille[x][y] == 0) // Si la case est vide
            {
                grille[x][y] = idJoueur; // Mise à jour de la grille avec le numéro du joueur

                let idGagnant = gagner(); 
                if (idGagnant != null) // Vérifier la victoire après chaque coup
                    alerteGagner(idGagnant);
                
                else if(egalite()) // Vérifier l'égalité après chaque coup
                    alerteEgalite();
                
                // Pas de victoire, ni d'égalité : la partie continue
                if (idJoueur === 1)
                {
                    CASE.textContent = "X"; // MAJ de l'affichage
                    CASE.style.color = "green";
                    idJoueur = 2; // Changement de joueur
                }
                else if (idJoueur === 2)
                {
                    CASE.textContent = "O"; // MAJ de l'affichage
                    CASE.style.color = "red";
                    idJoueur = 1; // Changement de joueur
                }
                
                if (GAMEMODE=="IA") // Faire jouer l'IA, si le mode est activé
                    jouerIA();    
                            
            }
            else // Si la case est déjà occupée
                alert("Impossible !")
        }


        function jouerIA() {
            
            if (!egalite() && gagner()==null) //Evite que l'IA joue après une égalité ou la victoire du joueur
            {
                let coup;
                
                if (DIFFICULTE === "Facile")
                    coup = coupAleatoire(); // Jouer un coup pour l'IA
                else
                    coup = meilleurCoup(); // Jouer le meilleur coup pour l'IA
                grille[coup.i][coup.j] = 2; 

                // Accès à la case TD (i,j) grâce à l'id :
                let CASE = document.querySelector(`#C${coup.i}${coup.j}`); 
                
                setTimeout(function() // ajoute un délai à l'affichage, pour que l'IA ne joue pas instantanément 
                {
                    // Si l'IA gagne ou en cas d'égalité : afficher le résultat et recharger la page
                    let idGagnant = gagner();
                    if (idGagnant != null)
                        alerteGagner(idGagnant);
                    // Vérifier l'égalité après chaque coup
                    else if(egalite())
                        alerteEgalite();

                    CASE.textContent = "O"; // MAJ de l'affichage
                    CASE.style.color = "red";
                    idJoueur = 1; // Changement de joueur
                }
                ,700) 
            }
            
        }

IA Aléatoire en JS

//Fichier : IArandom.js

          function coupAleatoire()
          {
              if (!egalite() && gagner()==null)
              {
                  let i, j;
                  do {
                      i = Math.floor(Math.random() * 3); // Génère un chiffre entre 0 et 2 inclus
                      j = Math.floor(Math.random() * 3); // Génère un chiffre entre 0 et 2 inclus
                  } while (grille[i][j] !== 0); 
                  // Tant que la case générée aléatoirement n'est pas vide, on recommence
                  
                  return {i,j};
              }
          }

IA MiniMax en JS

L'explication détaillée de ce code en vidéo !

//Fichier : IAminimax.js

function meilleurCoup() {
  
    let meilleurScore = -Infinity;
    let coup;

    for (let i = 0; i < 3; i++) {
        for (let j = 0; j < 3; j++) {
            
            if (grille[i][j] == 0) {
                grille[i][j] = 2; // 2 représente l'IA
                let score = minimax(grille, false);
                grille[i][j] = 0; // Annuler le coup
                
                if (score > meilleurScore) {
                    meilleurScore = score;
                    coup = { i, j };
                }
            }
        }
    }
    return coup;
}

let scores = {
    "true" : 0,  // Égalité
    1: -1, // Score pour le joueur : -1 (score à minimiser) 
    2: 1,  // Score pour l'IA : 1 (score à maximiser)
};

function minimax(grille, maximisation) {
    
    let resultat;

    if ((resultat = gagner()) != null) {
        return scores[resultat];
    } 
    else if (egalite()) {
        return scores["true"];
    }
    
    if (maximisation) {
          let meilleurScore = -Infinity;
          for (let i = 0; i < 3; i++) {
              for (let j = 0; j < 3; j++) {

                  if (grille[i][j] == 0) {

                      grille[i][j] = 2; // 2 représente l'IA
                      let score = minimax(grille, false);
                      grille[i][j] = 0; // Annuler le coup
                      meilleurScore = Math.max(score, meilleurScore);
                  }
              }
          }
          return meilleurScore;
    } 
    else {
          let meilleurScore = Infinity;
          for (let i = 0; i < 3; i++) {
              for (let j = 0; j < 3; j++) {

                  if (grille[i][j] == 0) {

                      grille[i][j] = 1; // 1 représente le joueur
                      let score = minimax(grille, true);
                      grille[i][j] = 0; // Annuler le coup
                      meilleurScore = Math.min(score, meilleurScore);
                      
                  }
              }
          }
          return meilleurScore;
      }
}

Style CSS

 /*Fichier : style.css */
  
    body, html {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
}

body {
    color: white;
    background-color: black;
    font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
}

h1 {
    text-align: center;
    font-size: 4em;
    text-decoration: underline;
    margin: 0;
}

h2 {
    font-size: 3em;
    margin-top: 0;
}

.contenu {
    display: flex; /*utilisation de Flexbox pour l'agencement*/
    flex-direction: row; /*Les enfants sont alignés en ligne.*/
    align-items: center; /*Centré verticalement et horizontalement*/
    justify-content: center;
}

#game {
    flex: 2; /*L'élément occupe deux fois plus d'espace que les autres*/
}

td {
    border: 2px solid black;
    border-collapse: collapse; /*Fusionne les bordures*/
    width: 25%;
    height: 150px;
    padding: 0;
    text-align: center;
    font-size: 6em;
    
}

table {
    width: 80%; /*Occupe 80% de la largeur de l'élément parent*/
    margin: auto;
    min-width: 300px;
    max-height: 600px;
    border-collapse: collapse; /*Fusionne les bordures*/
    background-color: white;
}

#Score {
    flex: 2;
    font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
    text-align: center;
}

#Score p {
    font-size: 3em;
    display: inline;
    margin-left: 1em;
}

p span {
    color: white;
}

#score1 {
    color: lime;
}

#score2 {
    color: rgb(255, 0, 0);
}

button {
    padding: 1em;
    margin: 0.5em;
    border-radius: 0.8em; /*Bordures arrondies*/
    font-size: 1.5em;
    color: white;
    background-color: goldenrod;
    border: goldenrod;
}

button#menu, button#sortir {
    display: none; /*Cache les boutons menu et sortir*/
}

/* Media query mobile */
@media screen and (max-width: 650px) {
    h1 {
        font-size: 3em;
        margin: 0;
    }

    .contenu {
        display: block;
    }

    table {
        margin-top: 2em;
        
    }


    td {
        width: 33px;
        height: 180px;
        max-width: 33px;
        overflow: hidden;
        font-size: 4em;
     
    }

    .menuVisible {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.85);
        display: block; /*Rend visible le menu*/
    }

    .menuInvisible {
        display: none;
    }

    #Score h2 {
        margin-top: 0.5em;
    }

    #Score p {
        font-size: 2em;
        display: inline;
        margin-left: 0.6em;
    }

    button {
        width: 8em;
        padding: 0.5em;
        margin-bottom: 0em;
    }

    button:last-child {
        margin-bottom: 0; /*Pas de margin-bottom pour le dernier bouton*/
    }

    button#menu, button#sortir {
        display: block;
        margin-left: auto;
        margin-right: auto;
        margin-top: 0.5em;
        padding: 0.3em;
    }
}

/* Media query PC portable */
@media screen and (min-width: 1200px) {
    h1 {
        font-size: 4em;    
    }

    h2 {
        font-size: 3em;
    }

    td {
        width: 20%;
        height: 150px;
    }

    #Score p {
        font-size: 3em;
    }

    button {
        font-size: 1.5em;
        padding: 1em;
    }
}

/* Media query écrans 24 pouces et plus */
@media screen and (min-width: 1500px) {
    h1 {
        font-size: 6em;
        margin-bottom: 1em;
    }

    h2 {
        font-size: 5em;
    }

    td {
        width: 33%;
        height: 200px;
        font-size: 8em;
    }

    #Score p {
        font-size: 4em;
    }

    button {
        margin-top: 1em;
        font-size: 2em;
        padding: 1em;
    }
}