Files
Site_web/exercices/memory-pictogrammes/assets/js/memory-game.js
gauthier.chombart 44be41b2b2 Renomme la marque en Gauthier Chombart & Nathan Chauwin et finalise le jeu Memory
Header, footer, titres et balises meta de toutes les pages actives : remplace NextGN Formation par Gauthier Chombart & Nathan Chauwin. Nouveau pied de page uniforme mentionnant les droits reserves et le statut de formateurs independants (dossier archives/ laisse inchange).

Integre aussi la refonte du jeu Memory (CSS/JS/HUD/leaderboard) et ajoute le rapport de veille Certibiocide semaine 28.
2026-07-14 23:02:09 +02:00

539 lines
18 KiB
JavaScript

/* ============================================
MEMORY PICTOGRAMMES - GAME ENGINE
Logique principale du jeu
============================================ */
class MemoryGameEngine {
constructor(difficulty = 'normal') {
this.difficulty = difficulty;
this.mode = DIFFICULTY_MODES[difficulty];
this.cards = [];
this.gameState = {
flipped: [],
matched: 0,
moves: 0,
score: 0,
isProcessing: false,
gameStarted: false
};
this.startTime = null;
this.timerInterval = null;
this.scoreEngine = new ScoringEngine();
this.analytics = new AnalyticsManager();
this.init();
}
/**
* Initialiser le jeu
*/
init() {
this.cards = this.initializeCards();
this.renderCards();
this.setupEventListeners();
this.updateHUD();
}
/**
* Initialiser le deck de cartes
*/
initializeCards() {
const selectedPictogrammes = PICTOGRAMMES.slice(0, this.mode.paires);
const pairs = [];
// Créer paires (image + texte pour chaque pictogramme)
selectedPictogrammes.forEach(pic => {
pairs.push({
type: 'image',
data: pic,
id: pic.id,
pairId: `${pic.id}-img`
});
pairs.push({
type: 'text',
data: pic,
id: pic.id,
pairId: `${pic.id}-txt`
});
});
// Mélanger les cartes
return this.shuffle(pairs);
}
/**
* Algorithme de shuffling (Fisher-Yates)
*/
shuffle(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
/**
* Rendre les cartes en HTML
*/
renderCards() {
const gameGrid = document.getElementById('game-grid');
gameGrid.innerHTML = '';
this.cards.forEach((card, index) => {
const cardElement = document.createElement('div');
cardElement.className = 'memory-card';
cardElement.setAttribute('data-card-index', index);
// Face avant (couverture)
const frontFace = document.createElement('div');
frontFace.className = 'card-face front';
frontFace.textContent = '?';
// Face arrière (contenu)
const backFace = document.createElement('div');
backFace.className = 'card-face back';
if (card.type === 'image') {
const img = document.createElement('img');
img.src = `assets/pictogrammes/${card.data.filename}`;
img.alt = card.data.nom;
backFace.appendChild(img);
} else {
const text = document.createElement('span');
text.textContent = card.data.texte;
backFace.appendChild(text);
}
cardElement.appendChild(frontFace);
cardElement.appendChild(backFace);
cardElement.addEventListener('click', () => this.onCardClicked(index));
gameGrid.appendChild(cardElement);
});
}
/**
* Gestion du clic sur une carte
*/
onCardClicked(cardIndex) {
// Démarrer le timer au premier clic
if (!this.gameState.gameStarted) {
this.startGame();
}
// Guard clauses
if (this.gameState.isProcessing) return;
if (this.gameState.flipped.length === 2) return;
if (this.cards[cardIndex].isMatched) return;
if (this.gameState.flipped.includes(cardIndex)) return;
// Retourner la carte
this.flipCard(cardIndex);
this.gameState.flipped.push(cardIndex);
// Analytics
this.analytics.trackFlip(cardIndex, this.gameState.moves);
// Vérifier si 2 cartes retournées
if (this.gameState.flipped.length === 2) {
this.gameState.isProcessing = true;
this.gameState.moves++;
this.updateHUD();
// Délai pour visualiser les 2 cartes
setTimeout(() => this.checkMatch(), 1200);
}
}
/**
* Retourner une carte (animation)
*/
flipCard(index) {
const cardElement = document.querySelector(`[data-card-index="${index}"]`);
cardElement.classList.add('flipped');
}
/**
* Retourner une carte (inverse l'animation)
*/
unflipCard(index) {
const cardElement = document.querySelector(`[data-card-index="${index}"]`);
cardElement.classList.remove('flipped');
}
/**
* Vérifier si les 2 cartes retournées correspondent
*/
checkMatch() {
const [idx1, idx2] = this.gameState.flipped;
const card1 = this.cards[idx1];
const card2 = this.cards[idx2];
if (card1.id === card2.id) {
this.onMatchSuccess(idx1, idx2);
} else {
this.onMatchFailure(idx1, idx2);
}
}
/**
* Paire correctement matchée
*/
onMatchSuccess(idx1, idx2) {
const card1Element = document.querySelector(`[data-card-index="${idx1}"]`);
const card2Element = document.querySelector(`[data-card-index="${idx2}"]`);
// Marquer comme matched
this.cards[idx1].isMatched = true;
this.cards[idx2].isMatched = true;
card1Element.classList.add('matched');
card2Element.classList.add('matched');
this.gameState.matched++;
// Augmenter score
const points = 100 + (50 * this.mode.scoreMultiplier);
this.gameState.score += points;
// Analytics
this.analytics.trackMatch(
this.cards[idx1].id,
this.gameState.matched,
this.gameState.moves
);
// Mettre à jour UI
this.updateHUD();
// Ajouter pulse à la barre de progression
const progressContainer = document.querySelector('.progress-container');
progressContainer.classList.add('pulse');
setTimeout(() => progressContainer.classList.remove('pulse'), 600);
// Vérifier victoire
if (this.gameState.matched === this.mode.paires) {
setTimeout(() => this.endGame(true), 600);
} else {
// Reset pour prochaine tentative
this.gameState.flipped = [];
this.gameState.isProcessing = false;
}
}
/**
* Paire incorrecte
*/
onMatchFailure(idx1, idx2) {
const card1Element = document.querySelector(`[data-card-index="${idx1}"]`);
const card2Element = document.querySelector(`[data-card-index="${idx2}"]`);
// Animation shake
card1Element.classList.add('error');
card2Element.classList.add('error');
// Analytics
this.analytics.trackMismatch(
this.cards[idx1].id,
this.cards[idx2].id,
this.gameState.moves
);
// Retourner après délai
setTimeout(() => {
this.unflipCard(idx1);
this.unflipCard(idx2);
card1Element.classList.remove('error');
card2Element.classList.remove('error');
this.gameState.flipped = [];
this.gameState.isProcessing = false;
}, 1200);
}
/**
* Démarrer le timer du jeu
*/
startGame() {
this.gameState.gameStarted = true;
this.startTime = Date.now();
this.timerInterval = setInterval(() => {
this.updateTimer();
}, 100);
this.analytics.trackGameStart(this.difficulty);
}
/**
* Mettre à jour le timer
*/
updateTimer() {
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
const timerDisplay = document.getElementById('timer-display');
timerDisplay.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
// Animation warning si peu de temps reste
if (this.mode.timeLimit && elapsed >= this.mode.timeLimit * 0.8) {
timerDisplay.style.color = '#FF9800';
timerDisplay.style.animation = 'timerWarning 0.5s infinite';
}
// Game over si temps limite dépassé
if (this.mode.timeLimit && elapsed >= this.mode.timeLimit) {
this.endGame(false);
}
}
/**
* Mettre à jour le HUD
*/
updateHUD() {
// Score
document.getElementById('score-display').textContent = this.gameState.score;
// Mouvements
document.getElementById('moves-display').textContent = this.gameState.moves;
// Paires trouvées
document.getElementById('pairs-display').textContent = `${this.gameState.matched}/${this.mode.paires}`;
// Barre de progression
const progress = (this.gameState.matched / this.mode.paires) * 100;
document.getElementById('progress-bar').style.width = `${progress}%`;
// Modifier le scroll progress bar en haut
const scrollProgress = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
document.getElementById('progress-fill').style.width = `${Math.min(scrollProgress, 100)}%`;
}
/**
* Fin du jeu
*/
endGame(isWon) {
clearInterval(this.timerInterval);
const endTime = Date.now();
const gameData = {
duration: Math.floor((endTime - this.startTime) / 1000),
moves: this.gameState.moves,
score: this.gameState.score,
difficulty: this.difficulty,
won: isWon
};
// Calculer score final avec bonus
const finalScore = this.scoreEngine.calculateFinalScore(
gameData.moves,
gameData.duration,
this.mode.paires,
this.difficulty
);
// Détecter achievements
const achievements = this.scoreEngine.detectAchievements(
gameData.moves,
gameData.duration,
this.mode.paires,
this.difficulty
);
// Analytics
this.analytics.trackGameEnd({
...gameData,
finalScore: finalScore.totalScore,
achievements: achievements.map(a => a.id)
});
// Sauvegarder score
LeaderboardManager.addScore({
...gameData,
score: finalScore.totalScore
});
// Afficher modal victoire
this.showVictoryModal(finalScore, achievements, isWon);
}
/**
* Afficher la modal de victoire
*/
showVictoryModal(finalScore, achievements, isWon) {
const modal = document.getElementById('victory-modal');
const minutes = Math.floor(this.gameState.score / 60);
const seconds = this.gameState.score % 60;
// Remplir les données
document.getElementById('victory-title').textContent = isWon ? 'Bravo! 🎉' : 'Temps écoulé! ⏰';
document.getElementById('final-score').textContent = finalScore.totalScore;
document.getElementById('modal-moves').textContent = this.gameState.moves;
document.getElementById('modal-time').textContent = `${String(Math.floor(finalScore.duration / 60)).padStart(2, '0')}:${String(finalScore.duration % 60).padStart(2, '0')}`;
document.getElementById('modal-difficulty').textContent = this.mode.name;
document.getElementById('modal-bonus').textContent = `+${finalScore.timeBonus}`;
// Achievements
const achievementsList = document.getElementById('achievements-list');
achievementsList.innerHTML = '';
if (achievements.length > 0) {
const title = document.createElement('div');
title.style.fontSize = '14px';
title.style.fontWeight = '600';
title.style.marginBottom = '12px';
title.textContent = '🏅 Achievements Débloqués';
achievementsList.appendChild(title);
achievements.forEach((ach, index) => {
const achievementEl = document.createElement('div');
achievementEl.className = 'achievement';
achievementEl.style.animationDelay = `${(index * 0.1)}s`;
achievementEl.innerHTML = `
<div class="achievement-icon">${ach.icon}</div>
<div class="achievement-text">
<div class="achievement-name">${ach.name}</div>
<div class="achievement-desc">${ach.description}</div>
</div>
`;
achievementsList.appendChild(achievementEl);
});
}
// Boutons d'action
document.getElementById('play-again-btn').onclick = () => this.restart();
document.getElementById('view-leaderboard-btn').onclick = () => {
modal.classList.add('hidden');
document.querySelector('.leaderboard-section').scrollIntoView({ behavior: 'smooth' });
};
// Afficher modal
modal.classList.remove('hidden');
}
/**
* Redémarrer le jeu
*/
restart() {
document.getElementById('victory-modal').classList.add('hidden');
clearInterval(this.timerInterval);
// Réinitialiser l'état
this.gameState = {
flipped: [],
matched: 0,
moves: 0,
score: 0,
isProcessing: false,
gameStarted: false
};
// Réinitialiser les cartes
this.cards = this.initializeCards();
this.renderCards();
this.updateHUD();
// Reset timer display
document.getElementById('timer-display').textContent = '00:00';
document.getElementById('timer-display').style.color = '';
document.getElementById('timer-display').style.animation = '';
}
/**
* Setup event listeners
*/
setupEventListeners() {
// Bouton restart
document.getElementById('restart-btn').addEventListener('click', () => this.restart());
// Sélecteur de difficulté
document.querySelectorAll('.btn-difficulty').forEach(btn => {
btn.addEventListener('click', () => {
const newDifficulty = btn.getAttribute('data-difficulty');
if (newDifficulty !== this.difficulty) {
this.difficulty = newDifficulty;
this.mode = DIFFICULTY_MODES[newDifficulty];
this.restart();
// Marquer comme actif
document.querySelectorAll('.btn-difficulty').forEach(b => {
b.classList.remove('active');
});
btn.classList.add('active');
this.analytics.trackDifficultyChange(newDifficulty);
}
});
});
// Tabs du leaderboard
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
const filter = btn.getAttribute('data-filter');
LeaderboardManager.updateDisplay(filter);
document.querySelectorAll('.tab-btn').forEach(b => {
b.classList.remove('active');
});
btn.classList.add('active');
});
});
}
}
// ============================================
// INITIALISATION AU CHARGEMENT
// ============================================
let game;
/**
* Demander le nom du joueur à la première partie
*/
function setupPlayerNameModal(onReady) {
const nameConfirmed = localStorage.getItem('nextgn_name_confirmed') === 'true';
if (nameConfirmed) {
onReady();
return;
}
const modal = document.getElementById('player-name-modal');
const input = document.getElementById('player-name-input');
const confirmBtn = document.getElementById('player-name-confirm');
modal.classList.remove('hidden');
input.focus();
const confirmName = () => {
const name = input.value.trim();
localStorage.setItem('nextgn_playerName', name || getPlayerName());
localStorage.setItem('nextgn_name_confirmed', 'true');
modal.classList.add('hidden');
onReady();
};
confirmBtn.addEventListener('click', confirmName, { once: true });
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') confirmName();
}, { once: true });
}
document.addEventListener('DOMContentLoaded', () => {
// Afficher le leaderboard initial
LeaderboardManager.updateDisplay('all');
LeaderboardManager.updatePlayerStats();
setupPlayerNameModal(() => {
LeaderboardManager.updateDisplay('all');
LeaderboardManager.updatePlayerStats();
});
// Initialiser le jeu
game = new MemoryGameEngine('normal');
// Scroll progress
window.addEventListener('scroll', () => {
game.updateHUD();
});
});