/* ============================================ 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, isPaused: 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; if (this.gameState.isPaused) 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(() => { if (!this.gameState.isPaused) { 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 = `