Morpion
Projet Morpion ⭕❌⭕ — ton premier jeu en JavaScript
Tu vas programmer le morpion (tic-tac-toe ou encore OXO) : deux joueurs posent tour à tour leurs pions sur une grille 3 × 3, le premier qui aligne trois symboles gagne. Puis tu iras plus loin : ton programme jouera contre toi.
C'est le projet idéal pour apprendre à construire un jeu, parce que toute la difficulté est dans la logique, pas dans la technique. En chemin, tu vas pratiquer :
- la manipulation du DOM (créer du HTML depuis JavaScript, réagir aux clics)
- les tableaux 2D (la grille de jeu)
- la séparation entre l'état du jeu et son affichage — LE concept de ce chapitre
- la logique de jeu : victoire, match nul, tours de jeu
- tes premiers algorithmes d'IA
Temps estimé : 4 à 6 heures, selon jusqu'où tu pousses l'IA.
Comment travailler
L'énoncé est découpé en étapes. Chaque étape te donne un objectif, des explications sur les concepts nécessaires, et parfois un coup de pouce. On ne te donne pas le code : à toi de le trouver. Mais si tu suis les étapes dans l'ordre, chacune est un petit pas faisable.
Règle d'or : ne passe pas à l'étape suivante tant que l'actuelle ne fonctionne pas. Teste après chaque modification, et utilise console.log dès que tu ne comprends pas ce qui se passe.
Le point de départ
Crée un projet Vite (vanilla JS). Remplace le contenu de main.js par ceci :
import "./style.css";
const app = document.querySelector("#app");
app.innerHTML = `
<h1>Morpion</h1>
<div id="message"></div>
<div id="jeu"></div>
<div>
<button id="recommencer">Recommencer</button>
</div>
`;
let board = [];
let joueurActuel = "";
function initialiserBoard() {
return [
["", "", ""],
["", "", ""],
["", "", ""],
];
}
function choisirPremierJoueur() {
return Math.random() < 0.5 ? "X" : "O";
}
function afficherBoard(board) {
const jeu = document.querySelector("#jeu");
jeu.innerHTML = `
<div class="board-grid">
<div class="case" data-row="0" data-col="0">${board[0][0]}</div>
<div class="case" data-row="0" data-col="1">${board[0][1]}</div>
<div class="case" data-row="0" data-col="2">${board[0][2]}</div>
<div class="case" data-row="1" data-col="0">${board[1][0]}</div>
<div class="case" data-row="1" data-col="1">${board[1][1]}</div>
<div class="case" data-row="1" data-col="2">${board[1][2]}</div>
<div class="case" data-row="2" data-col="0">${board[2][0]}</div>
<div class="case" data-row="2" data-col="1">${board[2][1]}</div>
<div class="case" data-row="2" data-col="2">${board[2][2]}</div>
</div>
`;
}
function demarrerJeu() {
board = initialiserBoard();
joueurActuel = choisirPremierJoueur();
afficherBoard(board);
}
// lancement du programme
demarrerJeu();
Et le contenu de style.css par ceci :
.board-grid {
display: grid;
grid-template-columns: auto auto auto;
gap: 10px;
width: fit-content;
}
.board-grid > div {
display: flex;
align-items: center;
justify-content: center;
height: 100px;
width: 100px;
background-color: aqua;
font-size: 3rem;
cursor: pointer;
}
Lance le projet : une grille de 9 cases vides s'affiche. Elle ne réagit pas encore aux clics — c'est tout l'objet de l'étape 1.
Concept clé : l'état et l'affichage

Regarde bien le code de départ : il y a deux mondes.
Le premier, c'est l'état du jeu : le tableau board, un tableau 2D de 3 lignes × 3 colonnes. C'est lui la vérité : qui a joué où, quelles cases sont libres. Toute la logique du jeu (peut-on jouer ici ? quelqu'un a-t-il gagné ?) interroge ce tableau, et lui seul.
Le second, c'est l'affichage : le HTML que voit le joueur. Il n'est qu'un reflet de l'état : la fonction afficherBoard(board) lit le tableau et reconstruit le HTML qui correspond.
Le cycle de vie d'un coup sera donc toujours le même : un clic modifie le tableau board, puis on appelle afficherBoard(board) pour que l'écran rattrape la réalité. On ne modifie jamais le HTML directement, et on ne lit jamais le contenu du HTML pour savoir où en est la partie.
C'est le concept le plus important du chapitre. Il te resservira dans tous tes projets, jeux ou pas : tout Vue.js, que tu verras plus tard, est construit sur cette idée.
Vérifie que tu as compris (réponds avant de continuer) :
- Que contient
board[1][2]? Quelle case de la grille est-ce à l'écran ? - À quoi servent les attributs
data-rowetdata-coldans le HTML, à ton avis ? - Dans la console du navigateur, que donne
document.querySelector(".case").textContentaprès quelques coups ? Pourquoi ne s'en sert-on pas pour la logique ?
Étape 1 — Poser un pion au clic
Objectif : cliquer sur une case vide y place le pion du joueur actuel, et les joueurs alternent (X, O, X, O…).
Les concepts qu'il te faut
Sélectionner et écouter. querySelectorAll(".case") te donne les 9 cases ; une boucle dessus permet d'attacher un écouteur de clic (addEventListener) à chacune.
Savoir quelle case a été cliquée. Dans l'écouteur, event.currentTarget est l'élément cliqué. Ses attributs data-row et data-col (lisibles avec getAttribute("data-row") ou via dataset) te donnent les coordonnées de la case dans board. Voilà à quoi ils servaient.
Alterner les joueurs. Écris une petite fonction joueurSuivant() : si joueurActuel vaut "X", il devient "O", et inversement. Le code de départ contient déjà un opérateur ternaire dont tu peux t'inspirer.
⚠️ Le piège de l'
innerHTML. QuandafficherBoardréécritjeu.innerHTML, les anciennes cases sont détruites et remplacées par des neuves — qui n'ont aucun écouteur de clic. Il faut donc ré-attacher les écouteurs après chaque affichage. Mets ta boucle d'écouteurs dans une fonction (par exempleajouterHandlerAuxCases()) et appelle-la après chaqueafficherBoard. Si ta grille ne répond plus après le premier coup, c'est ça.
💡 Coup de pouce. Dans l'écouteur de clic, dans l'ordre : lire les coordonnées de la case → vérifier que la case est vide dans
board(sinon, ne rien faire) → écrire le pion dansboard→ réafficher → ré-attacher les écouteurs → passer au joueur suivant.
Validation : tu peux remplir toute la grille en alternant X et O, et rejouer sur une case occupée ne fait rien.
Étape 2 — Détecter la victoire
Objectif : dès qu'un joueur aligne trois pions, un message l'annonce et la partie s'arrête.
C'est l'étape star du projet. Au morpion, il n'existe que 8 façons de gagner : 3 lignes, 3 colonnes, 2 diagonales. Pas besoin d'être malin — il suffit de vérifier les 8, à chaque coup.

a) La fonction de vérification
Écris une fonction verifierVictoire(board) qui renvoie "X" ou "O" s'il y a un gagnant, et null sinon. La renvoyer ainsi (plutôt qu'un simple true/false) te resservira pour l'IA, fais-moi confiance.
💡 Coup de pouce. Liste les 8 lignes gagnantes dans un tableau, où chaque ligne est un trio de coordonnées :
const lignesGagnantes = [ [[0, 0], [0, 1], [0, 2]], // ligne du haut // ... à toi de compléter les 7 autres ];Ensuite, une boucle sur ces 8 lignes : si les trois cases d'une ligne contiennent la même valeur et que cette valeur n'est pas
"", c'est gagné. Vérifie ton tableau avec le schéma 2.
b) Arrêter la partie
Quand quelqu'un gagne : affiche un message dans le <div id="message"> (avec textContent), et fais en sorte qu'on ne puisse plus jouer.
💡 Coup de pouce. Une variable d'état
partieTerminee(un booléen) suffit : le clic ne fait plus rien quand elle est àtrue. C'est la même idée que les états d'un feu rouge : le code se comporte différemment selon la valeur d'une variable.
Validation : fais gagner X sur une ligne, une colonne et une diagonale. À chaque fois, le message s'affiche et plus aucun coup n'est possible.
Étape 3 — Le match nul
Objectif : quand les 9 cases sont pleines sans gagnant, annoncer le match nul.
À toi de trouver comment savoir si la grille est pleine. Attention à l'ordre : vérifie la victoire avant le match nul — une partie peut se gagner au 9ᵉ coup !
Validation : remplir la grille sans gagnant affiche « Match nul » ; gagner au dernier coup affiche bien la victoire, pas le nul.
Étape 4 — Recommencer
Objectif : le bouton « Recommencer » relance une partie propre, à tout moment.
Le bouton existe déjà dans le HTML du départ, et la fonction demarrerJeu() aussi : il ne reste qu'à les connecter, et à vérifier que demarrerJeu remet bien tout à zéro.
⚠️ Piège classique : tu recommences après une victoire et… impossible de jouer ? Une certaine variable d'état a échappé à ta remise à zéro.
Validation : recommencer en pleine partie, après une victoire et après un match nul fonctionne à chaque fois.
Étape 5 — Une IA qui joue au hasard
Objectif : un mode solo : tu joues X, et l'ordinateur répond en posant son O sur une case vide au hasard.
Ton premier algorithme d'IA ! Il est volontairement bête, mais le jeu devient vivant : tu joues contre ton programme.
L'algorithme
- Construis la liste de toutes les cases encore vides (un tableau d'objets
{ row, col }, rempli en parcourantboard). - Tires-en une au hasard :
const indexAleatoire = Math.floor(Math.random() * casesVides.length);
- Joue le pion de l'IA sur cette case, comme si elle avait cliqué : même logique, mêmes vérifications de victoire et de nul.
💡 Coup de pouce. Range cette logique dans une fonction
jouerCoupIA(), appelée juste après le coup du joueur humain — seulement si la partie n'est pas terminée ! Pour le confort, tu peux retarder le coup de l'IA de quelques centaines de millisecondes avecsetTimeout: l'ordinateur a l'air de « réfléchir ».
Validation : tu joues seul contre l'IA, elle ne joue que sur des cases vides, et la victoire/le nul fonctionnent toujours.
Étape 6 — Une IA qui réfléchit (avec des règles)
Objectif : remplacer le hasard par une stratégie. Ton IA devient difficile à battre.
Pas besoin d'algorithme savant : quelques règles de bon sens, appliquées dans le bon ordre, suffisent à faire une IA redoutable sur une grille 3 × 3.

Les priorités
- Gagner. Si une case vide me donne la victoire immédiate, je la joue.
- Bloquer. Si une case vide donne la victoire à l'adversaire au prochain tour, je la prends.
- Le centre. S'il est libre, c'est la case la plus puissante (elle participe à 4 lignes gagnantes — recompte sur le schéma 2).
- Un coin. Les coins participent à 3 lignes chacun, les bords à 2 seulement.
- Sinon, n'importe quelle case vide (ton code de l'étape 5 sert encore !).
L'astuce qui change tout : la simulation
Comment savoir si une case « me donne la victoire » ? Fais semblant d'y jouer. Pour chaque case vide : pose ton pion dans board, appelle ta fonction verifierVictoire(board), puis remets la case à "". Si la vérification a renvoyé ton symbole, cette case est gagnante. Tu vois pourquoi je t'ai fait renvoyer "X"/"O"/null plutôt qu'un booléen ? La même fonction sert à tester ta victoire et celle de l'adversaire.
⚠️ N'oublie jamais d'annuler la simulation (remettre
""), sinon ton IA pose des pions fantômes. Si ta grille se remplit toute seule, tu sais d'où ça vient. 👻💡 Coup de pouce. Écris une fonction
trouverCaseGagnante(board, symbole)qui renvoie la case gagnante pour ce symbole, ounull. Appelée avec le symbole de l'IA, elle sert pour la règle 1 ; appelée avec celui de l'adversaire, pour la règle 2. Une fonction, deux règles.
Validation : l'IA gagne dès qu'elle le peut, bloque tes alignements, et préfère le centre puis les coins. Si tu joues parfaitement, la partie doit finir nulle.
Bonus
Bonus 1 — Choisir son mode de jeu
Un petit menu au lancement (ou des boutons) : « 2 joueurs » ou « contre l'ordinateur ». Une variable modeDeJeu et le tour de l'IA ne se déclenche que dans le bon mode.
Bonus 2 — Le score sur plusieurs manches
Compte les victoires de X, de O et les nuls, affichés en permanence. « Recommencer » garde les scores ; ajoute un bouton « Tout remettre à zéro ».
Bonus 3 — Mettre en valeur la ligne gagnante
À la victoire, colore les 3 cases gagnantes (une classe CSS ajoutée aux bonnes cases). Indice : ta boucle de verifierVictoire sait déjà quelle ligne a gagné — il suffit de la faire parler.
Pour aller plus loin : Minimax, l'IA invincible
Ton IA à règles est forte, mais on peut la coincer. Les IA parfaites des jeux à deux joueurs utilisent l'algorithme Minimax : explorer tous les coups possibles, puis tous les coups de réponse, et ainsi de suite jusqu'aux fins de partie, en supposant que chacun joue au mieux. Au morpion il n'y a que quelques milliers de parties possibles : un ordinateur les examine toutes en un clin d'œil, et devient littéralement imbattable. C'est un algorithme récursif (une fonction qui s'appelle elle-même) — un beau défi pour quand tu seras à l'aise. Cherche « minimax tic tac toe » ou commence par la page Wikipédia.
Ton plan de bataille
| ✓ | Étape | Tu sauras que ça marche quand… |
|---|---|---|
| ☐ | Point de départ | la grille vide s'affiche |
| ☐ | Étape 1 | X et O se posent en alternance au clic |
| ☐ | Étape 2 | les 8 victoires sont détectées, la partie s'arrête |
| ☐ | Étape 3 | grille pleine sans gagnant → match nul |
| ☐ | Étape 4 | « Recommencer » marche dans toutes les situations |
| ☐ | Étape 5 | tu joues contre une IA aléatoire |
| ☐ | Étape 6 | l'IA gagne, bloque, et vise centre/coins |
| ☐ | Bonus | choix du mode, scores, ligne gagnante colorée |
Derniers conseils
- Une fonction = une responsabilité : afficher, vérifier la victoire, jouer un coup, trouver une case gagnante… Si une fonction fait deux choses, coupe-la en deux.
- Teste ta logique dans la console.
verifierVictoirese teste sans cliquer : construis un tableau gagnant à la main dans la console et appelle ta fonction dessus. console.log(board)est ton ami. L'affichage ment parfois (un oubli deafficherBoard), le tableau jamais.- Bloqué plus de 30 minutes sur le même problème ? Cherche sur Google ou sur MDN, demande à une IA de t'expliquer ce qui coince (pas de coder à ta place — c'est toi qui apprends), pose ta question sur Teams, ou note-la pour le prochain cours.
Documentation utile
querySelector/querySelectorAll(MDN) — sélectionner des élémentsaddEventListener(MDN) — réagir aux clics- Les attributs
data-*etdataset(MDN) — stocker les coordonnées dans le HTML - Les template strings (MDN) — écrire le HTML de la grille
- L'opérateur ternaire (MDN) — alterner les joueurs en une ligne
Math.random()(MDN) — l'aléatoiresetTimeout(MDN) — faire « réfléchir » l'IAtextContent(MDN) — afficher les messages
Bonne partie — et ne sois pas vexé le jour où ton propre programme te battra. C'est le but. 🤖
Énoncé rédigé en collaboration avec Claude Fable 5.
