From e23ca3d1ff71fa2f6d07b6c9bb14cc75b52454cf Mon Sep 17 00:00:00 2001 From: Tiago Ramos <2222055@my.ipleiria.pt> Date: Fri, 2 Jan 2026 13:24:20 +0000 Subject: [PATCH] Issues Websockets #48 #49 #51 e #54 --- websockets/classes/BiscaGame.js | 45 +++++++++ websockets/events/connection.js | 3 - websockets/events/game.js | 165 +++++++++++++++++++++++++++++--- websockets/server.js | 21 ++-- websockets/state/game.js | 8 +- 5 files changed, 219 insertions(+), 23 deletions(-) diff --git a/websockets/classes/BiscaGame.js b/websockets/classes/BiscaGame.js index 967a696..8ee4574 100644 --- a/websockets/classes/BiscaGame.js +++ b/websockets/classes/BiscaGame.js @@ -6,6 +6,9 @@ class BiscaGame { this.deck = []; this.trumpCard = null; this.trumpSuit = null; + this.timer = null; + this.turnDeadline = null; + this.timeoutCallback = null; this.table = { playerCard: null, @@ -50,6 +53,44 @@ class BiscaGame { this.status = "playing"; } + startTurnTimer(callback) { + if (this.timer) clearTimeout(this.timer); + + this.timeoutCallback = callback; + const DURATION_MS = 20000; + this.turnDeadline = Date.now() + DURATION_MS; + + console.log(`[Game ${this.id}] Timer started for User ${this.currentTurn}`); + + this.timer = setTimeout(() => { + this.handleTimeout(); + }, DURATION_MS); + } + + stopTimer() { + if (this.timer) clearTimeout(this.timer); + this.timer = null; + this.turnDeadline = null; + } + + handleTimeout() { + console.log(`[Game ${this.id}] Timeout! Auto-playing for ${this.currentTurn}`); + + const lowestCard = this.getLowestCard(this.currentTurn); + + if (lowestCard && this.timeoutCallback) { + this.timeoutCallback(this.currentTurn, lowestCard.id); + } + } + + getLowestCard(userId) { + const hand = this.players[userId].hand; + if (!hand || hand.length === 0) return null; + + const sortedHand = [...hand].sort((a, b) => a.strength - b.strength); + return sortedHand[0]; + } + createDeck() { const suits = ["c", "e", "o", "p"]; const ranks = [1, 2, 3, 4, 5, 6, 7, 11, 12, 13]; @@ -105,6 +146,8 @@ class BiscaGame { } } + this.stopTimer(); + player.hand.splice(cardIndex, 1); if (!this.table.playerCard) { @@ -152,6 +195,7 @@ class BiscaGame { if (this.players[winnerId].hand.length === 0) { this.status = "finished"; + this.stopTimer(); const pIds = Object.keys(this.players); const p1Obj = this.players[pIds[0]]; @@ -205,6 +249,7 @@ class BiscaGame { points: this.players[oppId].points, cardCount: this.players[oppId].hand.length, }, + turnDeadline: this.turnDeadline, }; } diff --git a/websockets/events/connection.js b/websockets/events/connection.js index db4fe03..b4f914b 100644 --- a/websockets/events/connection.js +++ b/websockets/events/connection.js @@ -27,20 +27,17 @@ export const handleConnectionEvents = (io, socket) => { socket.on("join", (user) => { addUser(socket, user); console.log(`[Connection] User ${user.name} has joined the server`); - console.log(`[Connection] ${getUserCount()} users online`); }); socket.on("leave", () => { const user = removeUser(socket.id); if (user) { console.log(`[Connection] User ${user.name} has left the server`); - console.log(`[Connection] ${getUserCount()} users online`); } }); socket.on("disconnect", () => { console.log("Connection Lost:", socket.id); removeUser(socket.id); - console.log(`[Connection] ${getUserCount()} users online`); }); }; diff --git a/websockets/events/game.js b/websockets/events/game.js index a1a5c87..28aaa44 100644 --- a/websockets/events/game.js +++ b/websockets/events/game.js @@ -28,6 +28,78 @@ async function saveGameResult(gameId, result, token) { } } +const handleGameMove = (io, gameId, userId, cardId) => { + const game = getGame(gameId); + if (!game) return; + + const result = game.playCard(userId, cardId); + + if (result.error) { + console.log(`[Game Error] ${result.error}`); + return; + } + + const roomName = `game_${gameId}`; + const socketsInRoom = io.sockets.adapter.rooms.get(roomName); + + if (socketsInRoom) { + for (const socketId of socketsInRoom) { + const s = io.sockets.sockets.get(socketId); + const u = getUser(socketId); + if (u && game.players[u.id]) { + s.emit("game-state", game.getStateForPlayer(u.id)); + } + } + } + + if (result.action === "trick_resolved") { + io.to(roomName).emit("trick-end", result.trickResult); + } + + if (result.action === "game_ended") { + io.to(roomName).emit("game-over", { + winnerId: result.winnerId, + isDraw: result.isDraw, + p1Points: result.player1_points, + p2Points: result.player2_points, + }); + + let tokenToUse = null; + const playerIds = Object.keys(game.players); + + for (const pid of playerIds) { + if (game.players[pid] && game.players[pid].token) { + tokenToUse = game.players[pid].token; + break; + } + } + + if (tokenToUse) { + saveGameResult(gameId, result, tokenToUse); + console.log(`[DB] Jogo salvo usando o token de um dos jogadores.`); + } else { + console.error( + `[DB Error] CRÍTICO: Nenhum jogador tem token. O jogo ${gameId} não foi salvo.` + ); + } + } + + if (result.action === "next_turn" || result.action === "trick_resolved") { + game.startTurnTimer((timeoutUserId, timeoutCardId) => { + handleGameMove(io, gameId, timeoutUserId, timeoutCardId); + }); + + if (socketsInRoom) { + for (const socketId of socketsInRoom) { + const s = io.sockets.sockets.get(socketId); + const u = getUser(socketId); + if (u && game.players[u.id]) + s.emit("game-state", game.getStateForPlayer(u.id)); + } + } + } +}; + export default (io, socket) => { socket.on("join-game", async ({ gameId }) => { const user = getUser(socket.id); @@ -47,6 +119,39 @@ export default (io, socket) => { const gameData = response.data.data || response.data; + console.log( + `[Join Debug] API Data recebida para Jogo ${gameId}:`, + gameData ? "OK" : "NULL" + ); + + if ( + gameData.player2_user_id == 1 && + user.id != gameData.player1_user_id + ) { + try { + console.log( + `[API Sync] A oficializar User ${user.id} como Player 2 na BD...` + ); + await axios.post( + `${API_URL}/games/${gameId}/join`, + {}, + { + headers: { Authorization: token }, + } + ); + console.log(`[API Sync] Sucesso! O jogo passou a 'Playing' na BD.`); + + gameData.player2_user_id = user.id; + gameData.player2 = { name: user.name }; + gameData.status = "Playing"; + } catch (joinError) { + console.error( + `[API Sync Error] Falha ao fazer join na BD:`, + joinError.message + ); + } + } + if (gameData.status === "Ended") { socket.emit("error", { message: "Game already finished." }); return; @@ -71,7 +176,18 @@ export default (io, socket) => { { id: gameData.player1_user_id, name: p1Name }, { id: gameData.player2_user_id, name: p2Name } ); + console.log( + `[Join Debug] Jogo criado?`, + game ? "SIM" : "NÃO (Undefined)" + ); } catch (error) { + if (error.response) { + console.error(`[Join Error] Status: ${error.response.status}`); + console.error(`[Join Error] Data:`, error.response.data); + } else { + console.error(`[Join Error] Message:`, error.message); + } + socket.emit("error", { message: "Failed to join game." }); return; } @@ -80,17 +196,37 @@ export default (io, socket) => { const GHOST_ID = 1; if (game && game.players) { - if (!game.players[user.id]) { - if (game.players[GHOST_ID]) { - delete game.players[GHOST_ID]; - game.players[user.id] = { - id: user.id, - name: user.name, - hand: [], - points: 0, - tricks: 0, - }; - } + if (game.players[user.id]) { + game.players[user.id].token = socket.handshake.auth.token; + } + + if (!game.players[user.id] && game.players[GHOST_ID]) { + console.log(`[Ghost Swap] User ${user.name} substituiu o Fantasma.`); + + delete game.players[GHOST_ID]; + game.players[user.id] = { + id: user.id, + name: user.name, + hand: [], + points: 0, + tricks: 0, + token: socket.handshake.auth.token, + }; + + const token = socket.handshake.auth.token; + + axios + .post( + `${API_URL}/games/${gameId}/join`, + {}, + { + headers: { Authorization: token }, + } + ) + .then(() => + console.log(`[API Sync] Sucesso! BD atualizada para 'Playing'.`) + ) + .catch((err) => console.error(`[API Sync Error]`, err.message)); } } @@ -99,6 +235,11 @@ export default (io, socket) => { if (game && game.players[user.id]) { socket.emit("game-state", game.getStateForPlayer(user.id)); console.log(`--> Estado enviado para User ${user.id}`); + game.players[user.id].token = socket.handshake.auth.token; + if (!game.timer && game.status === "playing") { + game.startTurnTimer((uId, cId) => handleGameMove(io, gameId, uId, cId)); + } + socket.emit("game-state", game.getStateForPlayer(user.id)); } else { socket.emit("error", { message: "Error joining game state." }); } @@ -117,6 +258,8 @@ export default (io, socket) => { return; } + handleGameMove(io, gameId, user.id, cardId); + const roomName = `game_${gameId}`; const socketsInRoom = io.sockets.adapter.rooms.get(roomName); if (socketsInRoom) { diff --git a/websockets/server.js b/websockets/server.js index b59c8ec..e4960b6 100644 --- a/websockets/server.js +++ b/websockets/server.js @@ -18,15 +18,22 @@ export const serverStart = (port) => { }); server.io.use(async (socket, next) => { - const token = socket.handshake.auth.token; + let token = socket.handshake.auth.token; if (!token) { return next(new Error("Authentication error: No token provided")); } + if (!token.startsWith("Bearer ")) { + token = "Bearer " + token; + } + try { const response = await axios.get(`${API_URL}/users/me`, { - headers: { Authorization: token }, + headers: { + Authorization: token, + Accept: "application/json" + }, }); const user = response.data.data || response.data; @@ -35,7 +42,10 @@ export const serverStart = (port) => { return next(new Error("Authentication error: User data not found")); } - addUser(socket.id, user); + socket.user = user; + socket.handshake.auth.token = token; + + addUser(socket.id, user); next(); } catch (error) { @@ -53,15 +63,12 @@ export const serverStart = (port) => { }); server.io.on("connection", (socket) => { - console.log("New connection:", socket.id); - gameEvents(server.io, socket); handleConnectionEvents(server.io, socket); socket.on("disconnect", () => { removeUser(socket.id); - console.log("Disconnected:", socket.id); }); }); -}; +}; \ No newline at end of file diff --git a/websockets/state/game.js b/websockets/state/game.js index c98d8fa..2a3f7d8 100644 --- a/websockets/state/game.js +++ b/websockets/state/game.js @@ -18,8 +18,12 @@ const getGame = (id) => { }; const removeGame = (id) => { - activeGames.delete(id); - console.log(`[Game State] Game removed: ${id}`); +const game = activeGames.get(id); + if (game) { + game.stopTimer(); + activeGames.delete(id); + console.log(`[Game State] Game removed: ${id}`); + } }; module.exports = { -- 2.54.0