Un memory game, c’est le genre de projet “détente” que tu lances en te disant que ça va être plié avant le café. Et puis tu te prends une réalité très web dans la face : le layout qui doit rester propre sur mobile, l’audio qui refuse de jouer tant que l’utilisateur n’a pas touché l’écran, et les petits feedbacks qui transforment un truc mou en mini-jeu satisfaisant.
Je te montre une version pragmatique, pas un framework, pas une usine. Juste du vanilla propre, avec les pièges qui reviennent tout le temps quand tu veux ship un jeu jouable sur de vrais navigateurs.
1) Le modèle de données : si tu le rates, tout le reste devient pénible
La tentation classique, c’est de manipuler le DOM comme une source de vérité. Tu crées des cartes, tu “lis” leur état en regardant leurs classes, et tu finis avec des cas limites impossibles à débugger. Sur un memory, je préfère être sec : les données décident, le DOM affiche.
Le modèle le plus simple, c’est une liste de paires. Chaque carte a un id unique (pour différencier les deux cartes identiques), un key (la valeur de matching), et un état implicite piloté par le jeu (révélée, matchée). Ensuite tu dupliques les paires, tu mélanges, et tu rends.
// 8 paires = 16 cartes
const icons = ['🍋','🍒','🍇','🥝','🍉','🍓','🍍','🥥'];
function makeDeck() {
const cards = icons.flatMap((key) => ([
{ id: crypto.randomUUID(), key },
{ id: crypto.randomUUID(), key },
]));
// Fisher-Yates : pas de "sort random" foireux
for (let i = cards.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[cards[i], cards[j]] = [cards[j], cards[i]];
}
return cards;
}Oui, je force Fisher-Yates. Le array.sort(() => Math.random() - 0.5) c’est marrant en démo, mais c’est biaisé et ça fait le job “un coup sur deux”. Là on veut un comportement stable, parce que sinon tu vas accuser ton code de matching alors que c’est juste ton shuffle.
Sponsorisé par Le Scribouillard
Besoin de contenu optimisé SEO ?
Utilisez la meilleure plateforme française de création de contenu assistée par IA ! Et générez des articles pour moins de 1€ !
2) CSS Grid : la grille “qui s’adapte au nombre de cartes” sans se battre
Le layout est l’endroit où tu peux perdre un temps absurde. Le memory est un jeu de grille, donc si la grille est moche ou instable, l’expérience s’écroule. CSS Grid est parfait ici, mais pas en mode “je fixe 4 colonnes et basta”. Tu veux une grille qui respire sur mobile, qui ne fait pas des cartes microscopiques, et qui garde des cartes à peu près carrées.
Mon approche préférée : laisser Grid décider le nombre de colonnes avec auto-fit, et forcer un minimum de taille de carte. Tu obtiens un truc responsive sans media queries dans tous les sens. Et pour éviter les cartes écrasées, tu bloques le ratio.
.board {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
padding: 12px;
max-width: 720px;
margin: 0 auto;
}
.card {
aspect-ratio: 1 / 1;
border-radius: 14px;
background: #14151a;
transform-style: preserve-3d;
transition: transform 180ms ease;
will-change: transform;
}
.card.is-flipped { transform: rotateY(180deg); }
@media (prefers-reduced-motion: reduce) {
.card { transition: none; }
}Le minmax(84px, 1fr), tu l’ajustes selon ton design. L’idée, c’est que sur petit écran tu auras moins de colonnes, mais des cartes touchables. Et l’aspect-ratio te sauve la vie pour éviter une grille “en accordéon”.
3) Flips, locks, et état de jeu : le piège des doubles clics
Le cœur du memory, c’est une micro machine à états. Tu retournes une carte, tu retournes la deuxième, tu compares, puis tu verrouilles si c’est un match ou tu retournes après un délai. Ça a l’air trivial, mais la plupart des bugs viennent de la même chose : l’utilisateur clique pendant que tu es en train d’animer ou d’attendre un timeout.
Je pose une règle simple : tant que le jeu est en “résolution” (quand deux cartes sont ouvertes et qu’on attend de décider), on ignore les clics. Ça évite aussi le fameux bug où tu peux retourner une troisième carte pendant le délai et casser la comparaison.
Concrètement, tu gardes deux références en mémoire (les deux cartes retournées) et un booléen de lock. Quand la deuxième carte est retournée, tu lock, tu compares, tu appliques le résultat, puis tu unlock. Si tu veux un jeu qui “sonne” propre, c’est non négociable.
4) Web Audio API : le vrai boss final, c’est le “geste utilisateur obligatoire”
Le son, sur un jeu, ça change tout. Même un simple “tick” à chaque flip rend l’interaction plus nette. Sauf que les navigateurs (surtout mobile) ne te laissent pas démarrer de l’audio quand ça leur chante. Il faut une interaction utilisateur (click/touch/keydown) pour autoriser le démarrage d’un AudioContext ou la lecture.
Donc si tu initialises ton audio au chargement de la page et que tu joues un son à la première animation, tu vas te retrouver avec… rien. Et en prod, tu auras juste des gens qui disent “le son marche pas”, sans stacktrace, sans erreur claire.
Le pattern qui marche : tu crées ton moteur audio, mais tu le “réveilles” sur la première interaction explicite. Souvent, un bouton “Jouer” fait très bien l’affaire, parce que c’est un geste évident et tu ne te bats pas avec des subtilités de focus. Si tu veux zéro bouton, tu peux aussi écouter le premier pointerdown sur la grille et appeler audioContext.resume() à ce moment-là.
Et oui, il faut aussi penser aux cas où l’onglet repasse en arrière-plan. Certains navigateurs suspendent le contexte, et tu dois être prêt à le relancer proprement au prochain geste.
5) Les micro-feedbacks : ce qui transforme un “demo” en vrai jeu
Le memory, sans feedbacks, c’est froid. Ça marche, mais c’est plat. Avec deux ou trois micro-interactions bien choisies, tu changes la sensation sans ajouter une tonne de code.
Quand la paire est fausse, j’aime bien un mini “shake” très court sur les deux cartes, juste assez pour dire “non” sans agresser. Quand la paire est bonne, un léger “pop” (scale) ou un changement de teinte fait comprendre que la carte est maintenant “résolue” et qu’elle ne reviendra plus dans le jeu. Tu peux le faire en CSS, sans te perdre dans une lib d’animation.
Le timing, c’est le détail qui fait adulte. Si tu retournes les cartes trop vite après un mismatch, le joueur n’a pas le temps de mémoriser. Si tu attends trop, c’est frustrant. En général, un délai autour de 650–900ms est un bon compromis. Et surtout, évite les délais variables bizarres. La régularité rend le jeu lisible.
Dernier point que je vois souvent oublié : l’état “disabled” doit être clair. Une carte déjà matchée ne doit plus réagir, ni au hover, ni au click, ni au focus. Pas seulement pour éviter les bugs, aussi pour éviter l’impression de “ça ne répond pas” sur mobile.
Mobile, perf, et deux vérités qui font gagner du temps
Sur mobile, ton ennemi numéro un c’est la précision du doigt. Si tes cartes sont trop petites, le jeu devient une punition. Garde une taille minimale confortable, et évite les espacements trop serrés. Tu peux aussi ajouter touch-action: manipulation; sur la grille pour limiter certains comportements de double tap selon les contextes, mais teste, parce que les surprises varient.
Côté perfs, un memory est léger, mais tu peux quand même te tirer une balle dans le pied. Les flips en 3D et les ombres lourdes sur 16–24 éléments, ça peut devenir moche sur des téléphones moyens. Je garde des transitions courtes, j’évite de mettre des gros blur partout, et je respecte prefers-reduced-motion. Ce n’est pas du luxe, c’est juste du web sérieux.
Et s’il y a une règle que je garde en tête : le jeu doit rester fun même sans audio. Le son est un bonus, pas un prérequis. Vu les contraintes de navigateur, tu n’as pas envie que ton gameplay dépende d’un “context resume” qui n’a pas eu lieu.
Mon avis (franc) : ce genre de mini-projet te rend meilleur en UI produit
Le memory game, ce n’est pas un exercice de dev “gadget”. C’est un concentré de patterns qu’on retrouve partout : gestion d’état, prévention des actions concurrentes, feedbacks, responsive, et contraintes navigateur pas très glamour. Si tu sais faire un memory propre en vanilla, tu sais déjà éviter pas mal de bugs dans une UI de vraie app.