Files
Transcendence/Transcendence/srcs/backend/services/socket.js
T

566 lines
16 KiB
JavaScript

import jwt from 'jsonwebtoken';
import chatService from './global_chat.js';
import friendsService from './friends.js';
import gameRoomService from './game_room.js';
import playerStatsService from './player_stats.js';
// Store game state per room
const gameRooms = new Map();
// Store tetris duel rooms { roomCode → Map<socketId, socket> }
const tetrisRooms = new Map();
// Store io instance globally for use in routes
let ioInstance = null;
export function getIO() {
return ioInstance;
}
// Broadcast rooms list to all connected clients
async function broadcastRoomsList(io) {
try {
const rooms = await gameRoomService.listActiveRooms();
io.emit('game-rooms-updated', { rooms });
} catch (err) {
console.error('Error broadcasting rooms list:', err);
}
}
// Save round points to database (only the difference from round start)
async function saveRoundPoints(currentScores, roundStartScores) {
for (const [username, currentPoints] of Object.entries(currentScores)) {
const startPoints = roundStartScores[username] || 0;
const pointsEarned = currentPoints - startPoints;
if (pointsEarned !== 0) {
try {
await playerStatsService.addPointsByUsername(username, pointsEarned);
console.log(`Saved ${pointsEarned} points for ${username}`);
} catch (err) {
console.error(`Error saving points for ${username}:`, err);
}
}
}
}
function setupSocketIO(io)
{
ioInstance = io;
io.use((socket, next) =>
{
const token = socket.handshake.auth.token;
if (!token)
return (next(new Error('Authentication error')));
try
{
const decoded = jwt.verify(token, process.env.JWT_SECRET);
socket.user = decoded;
next();
}
catch(err)
{
next(new Error('Authentication error'));
}
});
io.on('connection', async (socket) =>
{
console.log(`User connected: ${socket.user.username}`);
socket.join('general-chat');
// Send recent messages and friend IDs on connection
try {
const [recentMessages, friendIds] = await Promise.all([
chatService.getRecentMessages(50),
friendsService.getFriendIds(socket.user.userId)
]);
socket.emit('chat-init', {
messages: recentMessages,
friendIds: friendIds
});
} catch (err) {
console.error('Error fetching initial data:', err);
}
socket.on('chat-message', async(data) =>
{
try
{
const message = await chatService.saveMessage(socket.user.userId, data.content);
socket.broadcast.to('general-chat').emit('chat-message',
{
id: message.id,
sender_id: socket.user.userId,
username: socket.user.username,
content: message.content,
created_at: message.created_at
});
}
catch (err)
{
console.error('Error saving message:', err);
socket.emit('error', {message: 'Failed to send message'});
}
});
// ============================================
// GAME ROOM EVENTS
// ============================================
// Join a game room
socket.on('game-join-room', async (data) => {
console.log('Received game-join-room from', socket.user.username, 'data:', data);
const roomId = `game-room-${data.roomId}`;
socket.join(roomId);
socket.gameRoomId = roomId;
socket.gameRoomDbId = data.roomId;
console.log(`${socket.user.username} joined ${roomId}, socket.gameRoomId set to:`, socket.gameRoomId);
// Send confirmation to the socket that joined
socket.emit('game-room-joined', {
roomId: data.roomId,
success: true
});
// Get updated player list from DB
try {
const players = await gameRoomService.getRoomPlayers(data.roomId);
// Notify ALL players in the room (including the one who joined) with updated player list
io.to(roomId).emit('game-players-updated', { players });
} catch (err) {
console.error('Error getting room players:', err);
}
// Notify others in the room that someone joined
socket.to(roomId).emit('game-player-joined', {
username: socket.user.username,
userId: socket.user.userId
});
// Broadcast rooms list update to everyone
broadcastRoomsList(io);
// Send current game state if game is in progress
const gameState = gameRooms.get(roomId);
if (gameState && gameState.isPlaying) {
socket.emit('game-state-sync', {
isPlaying: gameState.isPlaying,
drawer: gameState.drawer,
wordLength: gameState.currentWord ? gameState.currentWord.length : 0,
revealedLetters: gameState.revealedLetters,
revealedWord: gameState.revealedWord || [],
guessedLetters: gameState.guessedLetters,
players: gameState.players
});
}
});
// Leave a game room
socket.on('game-leave-room', async () => {
if (socket.gameRoomId) {
const roomId = socket.gameRoomId;
const dbRoomId = socket.gameRoomDbId;
socket.to(roomId).emit('game-player-left', {
username: socket.user.username,
userId: socket.user.userId
});
socket.leave(roomId);
console.log(`${socket.user.username} left ${roomId}`);
// Get updated player list and broadcast to remaining players
if (dbRoomId) {
try {
const players = await gameRoomService.getRoomPlayers(dbRoomId);
io.to(roomId).emit('game-players-updated', { players });
} catch (err) {
// Room may have been deleted
console.log('Room may have been deleted:', err.message);
}
}
socket.gameRoomId = null;
socket.gameRoomDbId = null;
// Broadcast updated rooms list
broadcastRoomsList(io);
}
});
// Start the game
socket.on('game-start', (data) => {
console.log('Received game-start event from', socket.user.username);
console.log('socket.gameRoomId:', socket.gameRoomId);
const gameStartedData = {
drawer: data.drawer,
players: data.players
};
const roomId = socket.gameRoomId;
// If no roomId, still start the game for this socket only
if (!roomId) {
console.log('WARNING: No roomId for socket, starting game for this socket only');
socket.emit('game-started', gameStartedData);
return;
}
// Initialize scores for all players
const scores = {};
data.players.forEach(p => scores[p] = 0);
const gameState = {
isPlaying: true,
currentWord: '',
revealedLetters: [],
drawer: data.drawer,
players: data.players,
currentPlayerIndex: 0,
guessedLetters: [],
scores: scores,
roundStartScores: { ...scores }
};
gameRooms.set(roomId, gameState);
// Emit to OTHER players in the room
socket.to(roomId).emit('game-started', gameStartedData);
// Emit directly to this socket (the one who started the game)
socket.emit('game-started', gameStartedData);
console.log(`Game started in ${roomId} by ${socket.user.username}`);
});
// Drawer sets the word
socket.on('game-set-word', (data) => {
const roomId = socket.gameRoomId;
if (!roomId) return;
const gameState = gameRooms.get(roomId);
if (!gameState) return;
gameState.currentWord = data.word.toLowerCase();
gameState.revealedLetters = new Array(data.word.length).fill(false);
gameState.revealedWord = new Array(data.word.length).fill('_');
gameState.guessedLetters = [];
gameState.wrongGuesses = 0;
// Initialize scores if not already done
if (!gameState.scores) {
gameState.scores = {};
gameState.players.forEach(p => gameState.scores[p] = 0);
}
// Notify all players (without revealing the word)
io.to(roomId).emit('game-word-set', {
wordLength: data.word.length,
drawer: socket.user.username,
revealedWord: gameState.revealedWord,
scores: gameState.scores
});
});
// Drawing data (real-time)
socket.on('game-draw', (data) => {
const roomId = socket.gameRoomId;
if (!roomId) return;
// Broadcast drawing to all other players in the room
socket.to(roomId).emit('game-draw', {
x1: data.x1,
y1: data.y1,
x2: data.x2,
y2: data.y2,
color: data.color,
lineWidth: data.lineWidth
});
});
// Clear canvas
socket.on('game-clear-canvas', () => {
const roomId = socket.gameRoomId;
if (!roomId) return;
socket.to(roomId).emit('game-clear-canvas');
});
// Player makes a guess
socket.on('game-guess', (data) => {
const roomId = socket.gameRoomId;
if (!roomId) return;
const gameState = gameRooms.get(roomId);
if (!gameState || !gameState.currentWord) return;
const guess = data.guess.toLowerCase();
const isLetter = guess.length === 1;
let success = false;
let points = 0;
const username = socket.user.username;
// Initialize scores if needed
if (!gameState.scores) {
gameState.scores = {};
gameState.players.forEach(p => gameState.scores[p] = 0);
}
if (!gameState.scores[username]) {
gameState.scores[username] = 0;
}
if (isLetter) {
// Check if letter was already guessed
if (gameState.guessedLetters.includes(guess)) {
socket.emit('game-guess-result', {
guess,
success: false,
type: 'letter',
message: 'Lettre deja proposee',
username: username,
scores: gameState.scores
});
return;
}
gameState.guessedLetters.push(guess);
// Check each position and reveal the actual letter
let lettersFound = 0;
for (let i = 0; i < gameState.currentWord.length; i++) {
if (gameState.currentWord[i] === guess) {
gameState.revealedLetters[i] = true;
gameState.revealedWord[i] = guess;
success = true;
lettersFound++;
}
}
// Points: 10 per letter found, -5 for wrong guess
if (success) {
points = lettersFound * 10;
gameState.scores[username] += points;
} else {
points = -5;
gameState.scores[username] += points;
gameState.wrongGuesses++;
}
} else {
// Full word guess
success = guess === gameState.currentWord;
if (success) {
gameState.revealedLetters = gameState.revealedLetters.map(() => true);
gameState.revealedWord = gameState.currentWord.split('');
// Bonus points for guessing the whole word
const remainingLetters = gameState.revealedLetters.filter(r => !r).length;
points = 50 + (remainingLetters * 5);
gameState.scores[username] += points;
} else {
points = -10;
gameState.scores[username] += points;
gameState.wrongGuesses++;
}
}
// Broadcast result to all players with the revealed word (actual letters)
io.to(roomId).emit('game-guess-result', {
guess,
success,
type: isLetter ? 'letter' : 'word',
username: username,
revealedLetters: gameState.revealedLetters,
revealedWord: gameState.revealedWord,
points: points,
scores: gameState.scores
});
// Check if word is complete
if (gameState.revealedLetters.every(r => r)) {
// Bonus points for the drawer
const drawerBonus = Math.max(0, 30 - (gameState.wrongGuesses * 5));
if (gameState.scores[gameState.drawer]) {
gameState.scores[gameState.drawer] += drawerBonus;
}
// Save points to database for all players
saveRoundPoints(gameState.scores, gameState.roundStartScores || {});
// Update round start scores for next round
gameState.roundStartScores = { ...gameState.scores };
io.to(roomId).emit('game-word-found', {
word: gameState.currentWord,
winner: username,
scores: gameState.scores,
drawerBonus: drawerBonus
});
}
});
// Next round
socket.on('game-next-round', (data) => {
const roomId = socket.gameRoomId;
if (!roomId) return;
const gameState = gameRooms.get(roomId);
if (!gameState) return;
gameState.currentWord = '';
gameState.revealedLetters = [];
gameState.guessedLetters = [];
gameState.drawer = data.drawer;
io.to(roomId).emit('game-new-round', {
drawer: data.drawer
});
});
// End game
socket.on('game-end', () => {
const roomId = socket.gameRoomId;
if (!roomId) return;
gameRooms.delete(roomId);
io.to(roomId).emit('game-ended');
});
// ============================================
// TETRIS DUEL EVENTS
// ============================================
socket.on('tetris:join', ({ roomCode }) => {
const code = String(roomCode).toUpperCase().slice(0, 8);
// Quitter l'ancienne room tetris si besoin
if (socket.tetrisRoomCode) {
_tetrisLeave(socket);
}
if (!tetrisRooms.has(code)) {
tetrisRooms.set(code, new Map());
}
const room = tetrisRooms.get(code);
if (room.size >= 2) {
socket.emit('tetris:room-status', { status: 'full', players: [] });
return;
}
room.set(socket.id, socket);
socket.tetrisRoomCode = code;
const players = [...room.values()].map(s => s.user.username);
if (room.size === 1) {
socket.emit('tetris:room-status', { status: 'waiting', players });
} else {
// Notifier les deux joueurs
for (const s of room.values()) {
s.emit('tetris:room-status', { status: 'ready', players });
}
// Notifier l'adversaire qu'un nouveau joueur a rejoint
for (const [id, s] of room) {
if (id !== socket.id) {
s.emit('tetris:opponent-joined', { username: socket.user.username });
}
}
}
});
socket.on('tetris:leave', () => {
_tetrisLeave(socket);
});
// Relay pur : grid-update → adversaire uniquement
socket.on('tetris:grid-update', (data) => {
_tetrisRelayToOpponent(socket, 'tetris:grid-update', data);
});
// Relay pur : lines-cleared → adversaire uniquement
socket.on('tetris:lines-cleared', (data) => {
_tetrisRelayToOpponent(socket, 'tetris:lines-cleared', data);
});
// start-duel → relayé aux DEUX joueurs de la room (inclut l'émetteur)
socket.on('tetris:start-duel', () => {
const code = socket.tetrisRoomCode;
if (!code) return;
const room = tetrisRooms.get(code);
if (!room || room.size < 2) return;
for (const s of room.values()) {
s.emit('tetris:start-duel');
}
});
// game-over → relayé en opponent-game-over chez l'adversaire
socket.on('tetris:game-over', (data) => {
_tetrisRelayToOpponent(socket, 'tetris:opponent-game-over', data);
});
socket.on('disconnect', async () =>
{
// Nettoyage room tetris
if (socket.tetrisRoomCode) {
_tetrisLeave(socket);
}
console.log(`User disconnected: ${socket.user.username}`);
// Notify game room if player was in one
if (socket.gameRoomId) {
const roomId = socket.gameRoomId;
const dbRoomId = socket.gameRoomDbId;
socket.to(roomId).emit('game-player-left', {
username: socket.user.username,
userId: socket.user.userId
});
// Get updated player list and broadcast
if (dbRoomId) {
try {
const players = await gameRoomService.getRoomPlayers(dbRoomId);
io.to(roomId).emit('game-players-updated', { players });
} catch (err) {
console.log('Room may have been deleted on disconnect:', err.message);
}
}
// Broadcast updated rooms list
broadcastRoomsList(io);
}
});
});
}
// ── Helpers tetris duel ──────────────────────────────────────────────────
function _tetrisLeave(socket) {
const code = socket.tetrisRoomCode;
if (!code) return;
const room = tetrisRooms.get(code);
if (room) {
room.delete(socket.id);
// Notifier l'adversaire restant
for (const s of room.values()) {
s.emit('tetris:opponent-left');
s.emit('tetris:room-status', { status: 'waiting', players: [s.user.username] });
}
if (room.size === 0) tetrisRooms.delete(code);
}
socket.tetrisRoomCode = null;
}
function _tetrisRelayToOpponent(socket, event, data) {
const code = socket.tetrisRoomCode;
if (!code) return;
const room = tetrisRooms.get(code);
if (!room) return;
for (const [id, s] of room) {
if (id !== socket.id) s.emit(event, data);
}
}
export { broadcastRoomsList };
export default setupSocketIO;