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 :
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;
}
}