/* ============================================ LEADERBOARD & PLAYER STATISTICS ============================================ */ /** * Manager du classement */ class LeaderboardManager { static KEY_LEADERBOARD = 'nextgn_leaderboard'; static MAX_SCORES = 100; /** * Ajouter un score au classement */ static addScore(gameData) { const scores = this.getAllScores(); const newScore = { sessionId: generateUUID(), playerName: getPlayerName(), userId: getUserId(), score: gameData.score, moves: gameData.moves, timeSeconds: gameData.duration, difficulty: gameData.difficulty, date: new Date().toISOString(), isPerfect: gameData.moves === DIFFICULTY_MODES[gameData.difficulty].paires, won: gameData.won !== false }; scores.push(newScore); // Limiter à MAX_SCORES (garder les plus récents) if (scores.length > this.MAX_SCORES) { scores.sort((a, b) => new Date(b.date) - new Date(a.date)); scores.splice(this.MAX_SCORES); } localStorage.setItem(this.KEY_LEADERBOARD, JSON.stringify(scores)); DEBUG.log('Score added:', newScore); return newScore; } /** * Obtenir tous les scores */ static getAllScores() { try { const scores = JSON.parse(localStorage.getItem(this.KEY_LEADERBOARD)) || []; return scores; } catch (e) { console.error('Leaderboard error:', e); return []; } } /** * Obtenir le classement (top X) */ static getLeaderboard(difficulty = 'all', limit = 10) { let scores = this.getAllScores(); // Filtrer par difficulté if (difficulty !== 'all') { scores = scores.filter(s => s.difficulty === difficulty); } // Trier par score (décroissant) scores.sort((a, b) => { if (b.score !== a.score) { return b.score - a.score; } // En cas d'égalité, par temps return a.timeSeconds - b.timeSeconds; }); return scores.slice(0, limit); } /** * Obtenir les stats d'un joueur */ static getPlayerStats(userId) { const scores = this.getAllScores().filter(s => s.userId === userId); if (scores.length === 0) { return { totalGames: 0, bestScore: 0, averageScore: 0, averageTime: 0, bestTime: Infinity, winRate: 0, perfectGames: 0 }; } const bestScore = Math.max(...scores.map(s => s.score), 0); const totalScore = scores.reduce((a, b) => a + b.score, 0); const totalTime = scores.reduce((a, b) => a + b.timeSeconds, 0); const bestTime = Math.min(...scores.map(s => s.timeSeconds), Infinity); const perfectGames = scores.filter(s => s.isPerfect).length; const winRate = (perfectGames / scores.length) * 100; return { totalGames: scores.length, bestScore: bestScore, averageScore: Math.round(totalScore / scores.length), averageTime: Math.round(totalTime / scores.length), bestTime: bestTime === Infinity ? 0 : bestTime, winRate: Math.round(winRate), perfectGames: perfectGames }; } /** * Obtenir le rang d'un score */ static getPlayerRank(userId, difficulty = 'all') { const leaderboard = this.getLeaderboard(difficulty, Infinity); const rank = leaderboard.findIndex(s => s.userId === userId) + 1; return rank > 0 ? rank : null; } /** * Vérifier si c'est un nouveau record personnel */ static isPersonalRecord(userId, newScore) { const stats = this.getPlayerStats(userId); return newScore > stats.bestScore; } /** * Obtenir les statistiques globales */ static getGlobalStats() { const scores = this.getAllScores(); if (scores.length === 0) { return { totalGames: 0, totalPlayers: 0, averageScore: 0, highestScore: 0, fastestCompletion: Infinity }; } const uniquePlayers = new Set(scores.map(s => s.userId)); const totalScore = scores.reduce((a, b) => a + b.score, 0); const highestScore = Math.max(...scores.map(s => s.score), 0); const fastestCompletion = Math.min(...scores.map(s => s.timeSeconds), Infinity); return { totalGames: scores.length, totalPlayers: uniquePlayers.size, averageScore: Math.round(totalScore / scores.length), highestScore: highestScore, fastestCompletion: fastestCompletion === Infinity ? 0 : fastestCompletion }; } /** * Mettre à jour l'affichage du classement */ static updateDisplay(difficulty = 'all') { const leaderboard = this.getLeaderboard(difficulty, 10); const leaderboardList = document.getElementById('leaderboard-list'); if (!leaderboardList) return; if (leaderboard.length === 0) { leaderboardList.innerHTML = `
🎮
Aucun score pour le moment
Jouez une partie pour apparaître au classement!
`; return; } leaderboardList.innerHTML = ''; leaderboard.forEach((score, index) => { const rankItem = document.createElement('div'); rankItem.className = 'rank-item'; const rankNumber = this.getRankDisplay(index); const medal = index < 3 ? `class="rank-number medal-${index + 1}"` : 'class="rank-number"'; rankItem.innerHTML = `
${rankNumber}
${this.escapeHTML(score.playerName)}
⏱ ${formatTime(score.timeSeconds)} 🎯 ${score.moves} mouvements ${score.isPerfect ? '✨ Parfait' : ''}
${formatNumber(score.score)}
${DIFFICULTY_MODES[score.difficulty].name}
`; leaderboardList.appendChild(rankItem); }); analyticsManager.track('leaderboard_viewed', { filter: difficulty }); } /** * Mettre à jour les statistiques du joueur */ static updatePlayerStats() { const userId = getUserId(); const stats = this.getPlayerStats(userId); // Remplir les éléments HTML document.getElementById('total-games').textContent = stats.totalGames; document.getElementById('best-score').textContent = formatNumber(stats.bestScore); document.getElementById('best-time').textContent = stats.bestTime === Infinity ? '--' : formatTime(stats.bestTime); document.getElementById('win-rate').textContent = `${stats.winRate}%`; // Ajouter des classes pour animation si valeurs élevées if (stats.bestScore > 5000) { document.getElementById('best-score').parentElement.classList.add('high-value'); } if (stats.winRate >= 50) { document.getElementById('win-rate').parentElement.classList.add('high-value'); } } /** * Afficher le rang du joueur */ static updatePlayerRank(difficulty = 'all') { const userId = getUserId(); const rank = this.getPlayerRank(userId, difficulty); if (rank) { const rankDisplay = document.getElementById('player-rank'); if (rankDisplay) { rankDisplay.textContent = `Vous êtes #{rank}`; } } } /** * Afficher les statistiques globales */ static displayGlobalStats() { const globalStats = this.getGlobalStats(); DEBUG.log('Global Statistics:', globalStats); return globalStats; } /** * Nettoyer les scores (DEBUG) */ static clearAll() { if (confirm('Êtes-vous ABSOLUMENT certain de vouloir supprimer TOUS les scores?')) { localStorage.removeItem(this.KEY_LEADERBOARD); this.updateDisplay(); this.updatePlayerStats(); DEBUG.log('✓ Tous les scores ont été supprimés'); return true; } return false; } /** * Exporter les scores en CSV */ static exportCSV() { const scores = this.getAllScores(); let csv = 'Rang,Joueur,Score,Difficulté,Temps (s),Mouvements,Parfait,Date\n'; scores.forEach((score, index) => { csv += `${index + 1},"${score.playerName}",${score.score},"${score.difficulty}",${score.timeSeconds},${score.moves},${score.isPerfect ? 'Oui' : 'Non'},"${score.date}"\n`; }); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `leaderboard-${new Date().toISOString().slice(0, 10)}.csv`; link.click(); URL.revokeObjectURL(url); } /** * Obtenir l'affichage du rang (numéro ou médaille) */ static getRankDisplay(index) { const medals = ['🥇', '🥈', '🥉']; return medals[index] || `${index + 1}`; } /** * Échapper le HTML (sécurité) */ static escapeHTML(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } } /** * Statistiques avancées */ class AdvancedStats { /** * Obtenir la distribution des scores par difficulté */ static getDistributionByDifficulty() { const scores = LeaderboardManager.getAllScores(); const distribution = {}; Object.keys(DIFFICULTY_MODES).forEach(difficulty => { distribution[difficulty] = scores.filter(s => s.difficulty === difficulty).length; }); return distribution; } /** * Obtenir la tendance temporelle */ static getTimelineTrend(days = 7) { const scores = LeaderboardManager.getAllScores(); const now = new Date(); const timeline = {}; for (let i = 0; i < days; i++) { const date = new Date(now); date.setDate(date.getDate() - i); const dateStr = date.toISOString().slice(0, 10); timeline[dateStr] = scores.filter(s => s.date.slice(0, 10) === dateStr).length; } return timeline; } /** * Obtenir la moyenne de temps par difficulté */ static getAverageTimeByDifficulty() { const scores = LeaderboardManager.getAllScores(); const averages = {}; Object.keys(DIFFICULTY_MODES).forEach(difficulty => { const diffScores = scores.filter(s => s.difficulty === difficulty); if (diffScores.length > 0) { const totalTime = diffScores.reduce((a, b) => a + b.timeSeconds, 0); averages[difficulty] = Math.round(totalTime / diffScores.length); } }); return averages; } /** * Taux de réussite global */ static getGlobalSuccessRate() { const scores = LeaderboardManager.getAllScores(); if (scores.length === 0) return 0; const perfectGames = scores.filter(s => s.isPerfect).length; return Math.round((perfectGames / scores.length) * 100); } } // ============================================ // INITIALISATION // ============================================ // Exporter pour utilisation globale window.LeaderboardManager = LeaderboardManager; window.AdvancedStats = AdvancedStats; DEBUG.log('Leaderboard Manager initialized');