Files
Site_web/exercices/memory-pictogrammes/assets/js/leaderboard.js
gauthier.chombart 2a85ced1e6 Passe le classement Memory en ligne (ASP.NET + SQL Server)
Le classement du jeu Memory était stocké en localStorage (visible d'un seul
joueur, sur un seul poste). Il est désormais partagé en ligne via une API
ASP.NET/SQL Server adaptée à l'hébergement bsite.net.

- API scores.ashx (C#, compilée au vol par IIS) : lecture GET et
  enregistrement POST, création automatique de la table, ne conserve que le
  top 10 de chaque mode (facile / normal / difficile).
- leaderboard.js : le classement passe par fetch vers l'API (états
  chargement/erreur/vide, surbrillance de la ligne du joueur). Les
  statistiques personnelles restent stockées localement.
- memory-game.js : rafraîchit le classement après l'enregistrement (async).
- Style .is-me pour la ligne du joueur courant.
- web.config.example fourni ; le web.config réel (mot de passe) est ignoré.
2026-07-15 22:35:24 +02:00

283 lines
9.9 KiB
JavaScript

/* ============================================
LEADERBOARD & PLAYER STATISTICS
============================================
Le CLASSEMENT est désormais partagé EN LIGNE : tous les joueurs
enregistrent leurs scores via l'API PHP (api/scores.php) et voient le
même top 10 par mode de jeu.
Les STATISTIQUES PERSONNELLES (« Vos statistiques ») restent stockées
localement dans le navigateur du joueur, car le serveur ne conserve que
le top 10 de chaque mode (et non l'historique complet de chacun).
============================================ */
/**
* Manager du classement
*/
class LeaderboardManager {
// Point d'entrée de l'API (relatif à memory.html) — ASP.NET + SQL Server
static API_URL = 'api/scores.ashx';
// Historique local des parties du joueur (pour ses stats perso)
static KEY_HISTORY = 'nextgn_player_history';
static MAX_HISTORY = 500;
// Cache mémoire du dernier classement récupéré (par difficulté)
static _cache = {};
/**
* Enregistrer une partie :
* - envoie le score au classement en ligne
* - conserve la partie dans l'historique local (stats perso)
* @returns {Promise<Object>} résultat de l'API ({ ok, isTop, rank } ou { ok:false })
*/
static async addScore(gameData) {
const record = {
playerName: getPlayerName(),
userId: getUserId(),
score: gameData.score,
moves: gameData.moves,
timeSeconds: gameData.duration,
difficulty: gameData.difficulty,
isPerfect: gameData.moves === DIFFICULTY_MODES[gameData.difficulty].paires,
won: gameData.won !== false,
date: new Date().toISOString()
};
// 1) Historique local (statistiques personnelles)
this._recordLocalHistory(record);
// 2) Classement en ligne
try {
const res = await fetch(this.API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(record)
});
const data = await res.json();
DEBUG.log('Score submitted:', data);
return data;
} catch (e) {
console.error('Leaderboard submit error:', e);
return { ok: false, error: 'network' };
}
}
/**
* Ajouter la partie à l'historique local du joueur
*/
static _recordLocalHistory(record) {
const history = this._getLocalHistory();
history.push(record);
if (history.length > this.MAX_HISTORY) {
history.splice(0, history.length - this.MAX_HISTORY);
}
try {
localStorage.setItem(this.KEY_HISTORY, JSON.stringify(history));
} catch (e) {
console.error('History save error:', e);
}
}
/**
* Historique local des parties du joueur
*/
static _getLocalHistory() {
try {
return JSON.parse(localStorage.getItem(this.KEY_HISTORY)) || [];
} catch (e) {
return [];
}
}
/**
* Récupérer le classement en ligne (top X) pour un mode donné.
* @returns {Promise<Array>}
*/
static async getLeaderboard(difficulty = 'all', limit = 10) {
const url = `${this.API_URL}?difficulty=${encodeURIComponent(difficulty)}&limit=${limit}`;
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
const data = await res.json();
if (!data || data.ok !== true || !Array.isArray(data.scores)) {
throw new Error('Réponse invalide du serveur');
}
this._cache[difficulty] = data.scores;
return data.scores;
}
/**
* Statistiques du joueur, calculées à partir de son historique LOCAL.
*/
static getPlayerStats() {
const scores = this._getLocalHistory();
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
};
}
/**
* Mettre à jour l'affichage du classement (asynchrone).
*/
static async updateDisplay(difficulty = 'all') {
const leaderboardList = document.getElementById('leaderboard-list');
if (!leaderboardList) return;
// État de chargement
leaderboardList.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⏳</div>
<div class="empty-state-text">Chargement du classement…</div>
</div>
`;
let leaderboard;
try {
leaderboard = await this.getLeaderboard(difficulty, 10);
} catch (e) {
console.error('Leaderboard load error:', e);
leaderboardList.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<div class="empty-state-text">Classement indisponible</div>
<div class="empty-state-hint">Vérifiez votre connexion et réessayez.</div>
</div>
`;
return;
}
if (leaderboard.length === 0) {
leaderboardList.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">🎮</div>
<div class="empty-state-text">Aucun score pour le moment</div>
<div class="empty-state-hint">Jouez une partie pour apparaître au classement!</div>
</div>
`;
return;
}
const myUserId = getUserId();
leaderboardList.innerHTML = '';
leaderboard.forEach((score, index) => {
const rankItem = document.createElement('div');
rankItem.className = 'rank-item';
if (score.userId === myUserId) {
rankItem.classList.add('is-me');
}
const rankNumber = this.getRankDisplay(index);
const medal = index < 3 ? `class="rank-number medal-${index + 1}"` : 'class="rank-number"';
rankItem.innerHTML = `
<div class="rank-top">
<div ${medal}>${rankNumber}</div>
<div class="rank-score">${formatNumber(score.score)}</div>
</div>
<div class="rank-info">
<div class="player-name">${this.escapeHTML(score.playerName)}</div>
<div class="rank-meta">
<span>⏱ ${formatTime(score.timeSeconds)}</span>
<span>🎯 ${score.moves}</span>
${score.isPerfect ? '<span>✨</span>' : ''}
</div>
</div>
`;
leaderboardList.appendChild(rankItem);
});
if (typeof analyticsManager !== 'undefined') {
analyticsManager.track('leaderboard_viewed', { filter: difficulty });
}
}
/**
* Mettre à jour les statistiques du joueur (locales).
*/
static updatePlayerStats() {
const stats = this.getPlayerStats();
const totalGamesEl = document.getElementById('total-games');
const bestScoreEl = document.getElementById('best-score');
const bestTimeEl = document.getElementById('best-time');
const winRateEl = document.getElementById('win-rate');
if (totalGamesEl) totalGamesEl.textContent = stats.totalGames;
if (bestScoreEl) bestScoreEl.textContent = formatNumber(stats.bestScore);
if (bestTimeEl) bestTimeEl.textContent = stats.bestTime === Infinity || stats.bestTime === 0 ? '--' : formatTime(stats.bestTime);
if (winRateEl) winRateEl.textContent = `${stats.winRate}%`;
if (bestScoreEl && stats.bestScore > 5000) {
bestScoreEl.parentElement.classList.add('high-value');
}
if (winRateEl && stats.winRate >= 50) {
winRateEl.parentElement.classList.add('high-value');
}
}
/**
* Effacer l'historique local du joueur (DEBUG).
* N'affecte PAS le classement en ligne partagé.
*/
static clearLocalHistory() {
if (confirm('Effacer VOS statistiques locales ? (le classement en ligne n\'est pas touché)')) {
localStorage.removeItem(this.KEY_HISTORY);
this.updatePlayerStats();
DEBUG.log('✓ Historique local effacé');
return true;
}
return false;
}
/**
* 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;
}
}
// ============================================
// INITIALISATION
// ============================================
// Exporter pour utilisation globale
window.LeaderboardManager = LeaderboardManager;
DEBUG.log('Leaderboard Manager initialized (online mode)');