diff --git a/Transcendence/srcs/backend/db.js b/Transcendence/srcs/backend/db.js index 28a95d5..bbb9c63 100644 --- a/Transcendence/srcs/backend/db.js +++ b/Transcendence/srcs/backend/db.js @@ -56,6 +56,17 @@ async function runMigrations() END IF; END $$; `); + // Create tetris_game_history table if not exists + await pool.query(` + CREATE TABLE IF NOT EXISTS tetris_game_history ( + id SERIAL PRIMARY KEY, + user_id INT REFERENCES users(id) ON DELETE CASCADE, + score INT NOT NULL DEFAULT 0, + game_type VARCHAR(10) NOT NULL DEFAULT 'solo', + result VARCHAR(10) DEFAULT NULL, + played_at TIMESTAMP DEFAULT NOW() + ); + `); console.log('Migrations completed!'); } catch (err) @@ -147,6 +158,15 @@ async function createTables() started_at TIMESTAMP DEFAULT NOW(), ended_at TIMESTAMP ); + + CREATE TABLE IF NOT EXISTS tetris_game_history ( + id SERIAL PRIMARY KEY, + user_id INT REFERENCES users(id) ON DELETE CASCADE, + score INT NOT NULL DEFAULT 0, + game_type VARCHAR(10) NOT NULL DEFAULT 'solo', + result VARCHAR(10) DEFAULT NULL, + played_at TIMESTAMP DEFAULT NOW() + ); `); console.log('Tables created!'); } diff --git a/Transcendence/srcs/backend/routes/player_stats.js b/Transcendence/srcs/backend/routes/player_stats.js index 9a7dfe6..fe78e01 100644 --- a/Transcendence/srcs/backend/routes/player_stats.js +++ b/Transcendence/srcs/backend/routes/player_stats.js @@ -43,7 +43,7 @@ router.get('/leaderboard', authenticateToken, async (req, res) => { } }); -// Save tetris score (solo or duel) — updates best score if higher +// Save tetris score (solo) — updates best score if higher + saves to history router.post('/tetris/score', authenticateToken, async (req, res) => { try { const { score } = req.body; @@ -52,6 +52,7 @@ router.post('/tetris/score', authenticateToken, async (req, res) => { } const bestScore = await playerStatsService.updateTetrisBestScore(req.user.userId, score); await playerStatsService.incrementTetrisGamesPlayed(req.user.userId); + await playerStatsService.addTetrisGameHistory(req.user.userId, score, 'solo', null); res.json({ bestScore }); } catch (err) { console.error('Error saving tetris score:', err); @@ -94,6 +95,17 @@ router.get('/tetris/rank/score', authenticateToken, async (req, res) => { } }); +// Get current user's tetris game history (last 15) +router.get('/tetris/history', authenticateToken, async (req, res) => { + try { + const history = await playerStatsService.getTetrisGameHistory(req.user.userId); + res.json(history); + } catch (err) { + console.error('Error getting tetris history:', err); + res.status(500).json({ error: 'Server error' }); + } +}); + // Current user's rank by tetris duel wins router.get('/tetris/rank/wins', authenticateToken, async (req, res) => { try { diff --git a/Transcendence/srcs/backend/services/player_stats.js b/Transcendence/srcs/backend/services/player_stats.js index 4e3ed59..8663915 100644 --- a/Transcendence/srcs/backend/services/player_stats.js +++ b/Transcendence/srcs/backend/services/player_stats.js @@ -129,6 +129,38 @@ async function getTetrisDuelWinsLeaderboard(limit = 10) { return result.rows; } +// Add a game to tetris history (keep max 15 per user) +async function addTetrisGameHistory(userId, score, gameType = 'solo', result = null) { + await query( + `INSERT INTO tetris_game_history (user_id, score, game_type, result) VALUES ($1, $2, $3, $4)`, + [userId, score, gameType, result] + ); + // Keep only the 15 most recent entries + await query( + `DELETE FROM tetris_game_history + WHERE id IN ( + SELECT id FROM tetris_game_history + WHERE user_id = $1 + ORDER BY played_at DESC + OFFSET 15 + )`, + [userId] + ); +} + +// Get the last 15 games for a user +async function getTetrisGameHistory(userId) { + const result = await query( + `SELECT id, score, game_type, result, played_at + FROM tetris_game_history + WHERE user_id = $1 + ORDER BY played_at DESC + LIMIT 15`, + [userId] + ); + return result.rows; +} + // Rank of a user by tetris best score (1 = best) async function getTetrisScoreRank(userId) { const result = await query( @@ -166,5 +198,7 @@ export default { getTetrisBestScoreLeaderboard, getTetrisDuelWinsLeaderboard, getTetrisScoreRank, - getTetrisDuelWinsRank + getTetrisDuelWinsRank, + addTetrisGameHistory, + getTetrisGameHistory }; diff --git a/Transcendence/srcs/backend/services/socket.js b/Transcendence/srcs/backend/services/socket.js index 0feaa3d..6fe5e7c 100644 --- a/Transcendence/srcs/backend/services/socket.js +++ b/Transcendence/srcs/backend/services/socket.js @@ -721,6 +721,7 @@ function setupSocketIO(io) // Relay pur : grid-update → adversaire uniquement socket.on('tetris:grid-update', (data) => { + if (data.score !== undefined) socket.tetrisLastScore = data.score; _tetrisRelayToOpponent(socket, 'tetris:grid-update', data); }); @@ -779,6 +780,7 @@ function setupSocketIO(io) try { await playerStatsService.updateTetrisBestScore(loserId, data.score || 0); await playerStatsService.incrementTetrisGamesPlayed(loserId); + await playerStatsService.addTetrisGameHistory(loserId, data.score || 0, 'duel', 'loss'); } catch (err) { console.error('Error saving tetris loser stats:', err); } @@ -793,6 +795,8 @@ function setupSocketIO(io) try { await playerStatsService.incrementTetrisWins(s.user.userId); await playerStatsService.incrementTetrisGamesPlayed(s.user.userId); + const winnerScore = s.tetrisLastScore || 0; + await playerStatsService.addTetrisGameHistory(s.user.userId, winnerScore, 'duel', 'win'); } catch (err) { console.error('Error saving tetris winner stats:', err); } diff --git a/Transcendence/srcs/frontend/src/tetris.css b/Transcendence/srcs/frontend/src/tetris.css index 37ae90e..bc30260 100644 --- a/Transcendence/srcs/frontend/src/tetris.css +++ b/Transcendence/srcs/frontend/src/tetris.css @@ -524,4 +524,13 @@ button:disabled { opacity: 0.3; cursor: not-allowed; } width: 30px; } +.hist-win { + color: var(--accent); + font-weight: bold; +} + +.hist-loss { + color: var(--accent2); +} + body { overflow-y: auto; } diff --git a/Transcendence/srcs/frontend/src/tetris.html b/Transcendence/srcs/frontend/src/tetris.html index 9c4799c..ab546b7 100644 --- a/Transcendence/srcs/frontend/src/tetris.html +++ b/Transcendence/srcs/frontend/src/tetris.html @@ -127,6 +127,7 @@
+
@@ -150,6 +151,17 @@
+ +
+ + + + + + + +
#DateTypeScoreRésultat
Chargement…
+
diff --git a/Transcendence/srcs/frontend/src/ui.js b/Transcendence/srcs/frontend/src/ui.js index e284bc9..01346ae 100644 --- a/Transcendence/srcs/frontend/src/ui.js +++ b/Transcendence/srcs/frontend/src/ui.js @@ -202,6 +202,7 @@ const game = new Tetris( updateButtons(); showOverlay('GAME OVER', score); loadLeaderboards(); + loadGameHistory(); }, // onBlockPlaced — relay duel (grid) => { @@ -267,6 +268,53 @@ if (btnRestart) { }); } +// ───────────────────────────────────────────── +// GAME HISTORY +// ───────────────────────────────────────────── + +async function loadGameHistory() { + const token = localStorage.getItem('auth_token'); + if (!token) return; + + try { + const res = await fetch('/api/stats/tetris/history', { + headers: { 'Authorization': `Bearer ${token}` } + }); + if (!res.ok) return; + const history = await res.json(); + renderGameHistory(history); + } catch (err) { + console.error('Erreur chargement historique:', err); + } +} + +function renderGameHistory(history) { + const tbody = document.getElementById('lb-history-body'); + if (!tbody) return; + if (!history.length) { + tbody.innerHTML = 'Aucune partie jouée'; + return; + } + + tbody.innerHTML = history.map((entry, i) => { + const date = new Date(entry.played_at).toLocaleDateString('fr-FR', { + day: '2-digit', month: '2-digit', year: '2-digit', + hour: '2-digit', minute: '2-digit' + }); + const type = entry.game_type === 'duel' ? 'Duel' : 'Solo'; + let resultHtml = '—'; + if (entry.result === 'win') resultHtml = 'Victoire'; + if (entry.result === 'loss') resultHtml = 'Défaite'; + return ` + ${i + 1} + ${date} + ${type} + ${entry.score} + ${resultHtml} + `; + }).join(''); +} + // ───────────────────────────────────────────── // LEADERBOARDS // ───────────────────────────────────────────── @@ -349,8 +397,10 @@ document.querySelectorAll('.lb-tab').forEach(tab => { document.querySelectorAll('.lb-content').forEach(c => c.classList.remove('lb-content--active')); tab.classList.add('lb-tab--active'); document.getElementById(`lb-${tab.dataset.tab}`).classList.add('lb-content--active'); + if (tab.dataset.tab === 'history') loadGameHistory(); }); }); // Chargement initial des leaderboards loadLeaderboards(); +loadGameHistory();