From 5b504d10bdebbd1d85e835fcb87706fd10d133bc Mon Sep 17 00:00:00 2001 From: FernandoJVideira <03.pleaser-minster@icloud.com> Date: Sat, 3 Jan 2026 17:22:09 +0000 Subject: [PATCH] merge --- .../extra_examples/kubernetes-mysql.yml | 4 +- deployment/kubernetes-ingress.yml | 8 +- deployment/kubernetes-laravel.yml | 6 +- deployment/kubernetes-vue.yml | 6 +- deployment/kubernetes-ws.yml | 6 +- frontend/src/main.js | 4 - frontend/src/stores/socket.js | 14 +- justfile | 4 +- websockets/.env.example | 2 - websockets/classes/BiscaMatch.js | 168 +++++---- websockets/events/game.js | 318 ++++++++++-------- websockets/events/match.js | 288 ++++++++-------- websockets/server.js | 233 +++++++------ 13 files changed, 581 insertions(+), 480 deletions(-) delete mode 100644 websockets/.env.example diff --git a/deployment/extra_examples/kubernetes-mysql.yml b/deployment/extra_examples/kubernetes-mysql.yml index 420d679..05246c7 100644 --- a/deployment/extra_examples/kubernetes-mysql.yml +++ b/deployment/extra_examples/kubernetes-mysql.yml @@ -3,7 +3,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: mysql - namespace: dad-group-x + namespace: dad-group-46 spec: selector: matchLabels: @@ -37,7 +37,7 @@ apiVersion: v1 kind: Service metadata: name: mysql - namespace: dad-group-x + namespace: dad-group-46 spec: ports: - port: 3306 diff --git a/deployment/kubernetes-ingress.yml b/deployment/kubernetes-ingress.yml index 3d287e9..b1bb878 100644 --- a/deployment/kubernetes-ingress.yml +++ b/deployment/kubernetes-ingress.yml @@ -3,10 +3,10 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-ingress - namespace: dad-group-x + namespace: dad-group-46 spec: rules: - - host: web-dad-group-x-172.22.21.253.sslip.io + - host: web-dad-group-46-172.22.21.253.sslip.io http: paths: - path: / @@ -16,7 +16,7 @@ spec: name: vue-app port: number: 80 - - host: api-dad-group-x-172.22.21.253.sslip.io + - host: api-dad-group-46-172.22.21.253.sslip.io http: paths: - path: / @@ -26,7 +26,7 @@ spec: name: laravel-app port: number: 80 - - host: ws-dad-group-x-172.22.21.253.sslip.io + - host: ws-dad-group-46-172.22.21.253.sslip.io http: paths: - path: / diff --git a/deployment/kubernetes-laravel.yml b/deployment/kubernetes-laravel.yml index 6329c29..1ab5e02 100644 --- a/deployment/kubernetes-laravel.yml +++ b/deployment/kubernetes-laravel.yml @@ -3,7 +3,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: laravel-app - namespace: dad-group-x + namespace: dad-group-46 spec: replicas: 1 selector: @@ -17,7 +17,7 @@ spec: priorityClassName: high-priority containers: - name: api - image: registry-172.22.21.115.sslip.io/dad-group-x/api:v1.0.0 + image: registry-172.22.21.115.sslip.io/dad-group-46/api:v2.0.2 resources: requests: memory: "256Mi" @@ -31,7 +31,7 @@ apiVersion: v1 kind: Service metadata: name: laravel-app - namespace: dad-group-x + namespace: dad-group-46 spec: ports: - port: 80 diff --git a/deployment/kubernetes-vue.yml b/deployment/kubernetes-vue.yml index c67613f..e7c900d 100644 --- a/deployment/kubernetes-vue.yml +++ b/deployment/kubernetes-vue.yml @@ -3,7 +3,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: vue-app - namespace: dad-group-x + namespace: dad-group-46 spec: replicas: 1 selector: @@ -17,7 +17,7 @@ spec: priorityClassName: low-priority containers: - name: web - image: registry-172.22.21.115.sslip.io/dad-group-46/web:v1.0.1 + image: registry-172.22.21.115.sslip.io/dad-group-46/web:v2.0.4 resources: requests: memory: "64Mi" @@ -31,7 +31,7 @@ apiVersion: v1 kind: Service metadata: name: vue-app - namespace: dad-group-x + namespace: dad-group-46 spec: ports: - port: 80 diff --git a/deployment/kubernetes-ws.yml b/deployment/kubernetes-ws.yml index 879d370..1f3b273 100644 --- a/deployment/kubernetes-ws.yml +++ b/deployment/kubernetes-ws.yml @@ -3,7 +3,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: websocket-server - namespace: dad-group-x + namespace: dad-group-46 spec: replicas: 1 selector: @@ -17,7 +17,7 @@ spec: priorityClassName: low-priority containers: - name: web - image: registry-172.22.21.115.sslip.io/dad-group-x/ws:v1.0.0 + image: registry-172.22.21.115.sslip.io/dad-group-46/ws:v2.0.6 resources: requests: memory: "128Mi" @@ -31,7 +31,7 @@ apiVersion: v1 kind: Service metadata: name: websocket-server - namespace: dad-group-x + namespace: dad-group-46 spec: ports: - port: 3000 diff --git a/frontend/src/main.js b/frontend/src/main.js index dc040f0..9fc2f0c 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,15 +1,12 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' -import { io } from 'socket.io-client' import App from './App.vue' import router from './router' const apiDomain = import.meta.env.VITE_API_DOMAIN -const wsConnection = import.meta.env.VITE_WS_CONNECTION console.log('[main.js] api domain', apiDomain) -console.log('[main.js] ws connection', wsConnection) const app = createApp(App) @@ -20,4 +17,3 @@ app.use(createPinia()) app.use(router) app.mount('#app') - diff --git a/frontend/src/stores/socket.js b/frontend/src/stores/socket.js index b4e2e76..e007681 100644 --- a/frontend/src/stores/socket.js +++ b/frontend/src/stores/socket.js @@ -12,7 +12,7 @@ export const useSocketStore = defineStore('websocket', () => { const lastError = ref(null) const currentGameId = ref(null) - const WS_URL = 'http://localhost:3000' + const WS_URL = import.meta.env.VITE_WS_CONNECTION || 'ws://localhost:3000' const connect = () => { if (socket.value?.connected) return @@ -27,13 +27,15 @@ export const useSocketStore = defineStore('websocket', () => { auth: { token: `Bearer ${token}` }, reconnection: true, reconnectionAttempts: 5, - reconnectionDelay: 1000 + reconnectionDelay: 1000, }) + console.log('[WebSocket] Attempting to connect to', WS_URL) + socket.value.on('connect', () => { if (currentGameId.value) { - console.log("Reconnected to socket. Rejoining game..."); - socket.emit("join-game", { gameId: currentGameId.value }); + console.log('Reconnected to socket. Rejoining game...') + socket.emit('join-game', { gameId: currentGameId.value }) } connected.value = true @@ -122,8 +124,8 @@ export const useSocketStore = defineStore('websocket', () => { disconnect, joinGame, playCard, - surrender, + surrender, onTrickEnd, - onGameOver + onGameOver, } }) diff --git a/justfile b/justfile index 8da4505..c65de5d 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,6 @@ -GROUP := "dad-group-x" -VERSION := "1.0.0" +GROUP := "dad-group-46" +VERSION := "2.0.6" kubectl-pods: diff --git a/websockets/.env.example b/websockets/.env.example deleted file mode 100644 index cbe5b3f..0000000 --- a/websockets/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -PORT= -API_URL= \ No newline at end of file diff --git a/websockets/classes/BiscaMatch.js b/websockets/classes/BiscaMatch.js index a2961b7..23b9d61 100644 --- a/websockets/classes/BiscaMatch.js +++ b/websockets/classes/BiscaMatch.js @@ -1,13 +1,13 @@ import BiscaGame from "../classes/BiscaGame.js"; import axios from "axios"; -const API_URL = "http://localhost:8000/api/v1"; +const API_URL = process.env.API_URL || "http://localhost:8000/api/v1"; class BiscaMatch { constructor(apiData, emitStateCallback, token, autoPlayCallback) { this.id = apiData.id; this.emitStateCallback = emitStateCallback; - this.autoPlayCallback = autoPlayCallback; + this.autoPlayCallback = autoPlayCallback; this.token = token; this.stake = apiData.stake; this.type = apiData.type; @@ -52,10 +52,10 @@ class BiscaMatch { getAuthHeaders() { return { - headers: { - Authorization: this.token, - "Content-Type": "application/json", - "Accept": "application/json" + headers: { + Authorization: this.token, + "Content-Type": "application/json", + Accept: "application/json", }, }; } @@ -72,8 +72,12 @@ class BiscaMatch { status: "Playing", began_at: new Date().toISOString(), }; - - const res = await axios.post(`${API_URL}/games`, payload, this.getAuthHeaders()); + + const res = await axios.post( + `${API_URL}/games`, + payload, + this.getAuthHeaders() + ); const gameData = res.data.game || res.data.data || res.data; newGameDbId = gameData.id; @@ -86,17 +90,19 @@ class BiscaMatch { { id: this.player1Id, name: p1Name }, { id: this.player2Id, name: p2Name }, newGameDbId, - (result) => { this.handleGameEnd(result); } + (result) => { + this.handleGameEnd(result); + } ); this.currentGame.init(); - + this.currentGame.startTurnTimer((uid, cid) => { - if (this.autoPlayCallback) this.autoPlayCallback(uid, cid); + if (this.autoPlayCallback) this.autoPlayCallback(uid, cid); }); if (this.emitStateCallback) { - this.emitStateCallback("match:new-round-start", null); + this.emitStateCallback("match:new-round-start", null); } } catch (e) { console.error(`❌ Start Game Error: ${e.message}`); @@ -104,82 +110,104 @@ class BiscaMatch { } async abortCurrentGame(loserId) { - if (this.currentGame && this.currentGame.dbId) { - console.log(`[Match ${this.id}] Aborting Game #${this.currentGame.dbId}`); - try { - const winnerId = loserId == this.player1Id ? this.player2Id : this.player1Id; - - const payload = { - status: "Ended", - winner_user_id: winnerId, - loser_user_id: loserId, - is_draw: false, - ended_at: new Date().toISOString(), - }; - await axios.put(`${API_URL}/games/${this.currentGame.dbId}`, payload, this.getAuthHeaders()); - } catch(e) { console.error("Error aborting game:", e.message); } + if (this.currentGame && this.currentGame.dbId) { + console.log(`[Match ${this.id}] Aborting Game #${this.currentGame.dbId}`); + try { + const winnerId = + loserId == this.player1Id ? this.player2Id : this.player1Id; + + const payload = { + status: "Ended", + winner_user_id: winnerId, + loser_user_id: loserId, + is_draw: false, + ended_at: new Date().toISOString(), + }; + await axios.put( + `${API_URL}/games/${this.currentGame.dbId}`, + payload, + this.getAuthHeaders() + ); + } catch (e) { + console.error("Error aborting game:", e.message); } + } } async handleGameEnd(result) { console.log(`[Match ${this.id}] Round ended. Winner: ${result.winnerId}`); - if (this.points[result.player1_id] !== undefined) this.points[result.player1_id] += result.player1_points; - if (this.points[result.player2_id] !== undefined) this.points[result.player2_id] += result.player2_points; + if (this.points[result.player1_id] !== undefined) + this.points[result.player1_id] += result.player1_points; + if (this.points[result.player2_id] !== undefined) + this.points[result.player2_id] += result.player2_points; if (this.currentGame && this.currentGame.dbId) { - try { - const payload = { - status: "Ended", - winner_user_id: result.winnerId, - loser_user_id: result.loserId, - is_draw: result.isDraw, - player1_points: result.player1_points, - player2_points: result.player2_points, - ended_at: new Date().toISOString(), - }; - await axios.put(`${API_URL}/games/${this.currentGame.dbId}`, payload, this.getAuthHeaders()); - } catch (e) { console.error("DB Game Update Error:", e.message); } + try { + const payload = { + status: "Ended", + winner_user_id: result.winnerId, + loser_user_id: result.loserId, + is_draw: result.isDraw, + player1_points: result.player1_points, + player2_points: result.player2_points, + ended_at: new Date().toISOString(), + }; + await axios.put( + `${API_URL}/games/${this.currentGame.dbId}`, + payload, + this.getAuthHeaders() + ); + } catch (e) { + console.error("DB Game Update Error:", e.message); + } } let marksToAdd = 0; let roundWinnerId = result.winnerId; if (roundWinnerId) { - const winningScore = roundWinnerId == result.player1_id ? result.player1_points : result.player2_points; + const winningScore = + roundWinnerId == result.player1_id + ? result.player1_points + : result.player2_points; if (winningScore === 120) marksToAdd = 4; else if (winningScore > 90) marksToAdd = 2; else if (winningScore >= 60) marksToAdd = 1; - - if (this.marks[roundWinnerId] !== undefined) this.marks[roundWinnerId] += marksToAdd; + + if (this.marks[roundWinnerId] !== undefined) + this.marks[roundWinnerId] += marksToAdd; } if (this.emitStateCallback) { - this.emitStateCallback("match:round-ended", { - winnerId: roundWinnerId, - marksAdded: marksToAdd, - p1Points: result.player1_points, - p2Points: result.player2_points, - p1Marks: this.marks[this.player1Id], - p2Marks: this.marks[this.player2Id], - reason: result.reason || 'points' - }); + this.emitStateCallback("match:round-ended", { + winnerId: roundWinnerId, + marksAdded: marksToAdd, + p1Points: result.player1_points, + p2Points: result.player2_points, + p1Marks: this.marks[this.player1Id], + p2Marks: this.marks[this.player2Id], + reason: result.reason || "points", + }); } if (this.marks[this.player1Id] >= 4 || this.marks[this.player2Id] >= 4) { await this.finishMatch(); - if (this.emitStateCallback) this.emitStateCallback("match:ended-signal", null); + if (this.emitStateCallback) + this.emitStateCallback("match:ended-signal", null); } else { setTimeout(() => { - this.startNewGame(); + this.startNewGame(); }, 1000); } } async finishMatch() { this.status = "Ended"; - const winnerId = this.marks[this.player1Id] >= 4 ? this.player1Id : this.player2Id; - const loserId = winnerId == this.player1Id ? this.player2Id : this.player1Id; + const winnerId = + this.marks[this.player1Id] >= 4 ? this.player1Id : this.player2Id; + const loserId = + winnerId == this.player1Id ? this.player2Id : this.player1Id; const payload = { status: "Ended", @@ -192,7 +220,11 @@ class BiscaMatch { }; try { - await axios.put(`${API_URL}/matches/${this.id}`, payload, this.getAuthHeaders()); + await axios.put( + `${API_URL}/matches/${this.id}`, + payload, + this.getAuthHeaders() + ); console.log(`[Match ${this.id}] Match Closed in DB.`); } catch (e) { console.error(`Match Close Error: ${e.message}`); @@ -201,21 +233,25 @@ class BiscaMatch { playCard(userId, cardId) { if (!this.currentGame || this.status !== "Playing") return; - + const result = this.currentGame.playCard(userId, cardId); - - if (result && (result.action === 'next_turn' || result.action === 'trick_resolved')) { - this.currentGame.startTurnTimer((uid, cid) => { - if (this.autoPlayCallback) this.autoPlayCallback(uid, cid); - }); + + if ( + result && + (result.action === "next_turn" || result.action === "trick_resolved") + ) { + this.currentGame.startTurnTimer((uid, cid) => { + if (this.autoPlayCallback) this.autoPlayCallback(uid, cid); + }); } - + return result; } getGameState(userId) { if (!this.currentGame) return null; - if (this.currentGame.players[userId]) return this.currentGame.getStateForPlayer(userId); + if (this.currentGame.players[userId]) + return this.currentGame.getStateForPlayer(userId); return null; } @@ -226,7 +262,7 @@ class BiscaMatch { marks: this.marks, points: this.points, player1: this.player1, - player2: this.player2 + player2: this.player2, }; } } diff --git a/websockets/events/game.js b/websockets/events/game.js index ba4c27b..22380c9 100644 --- a/websockets/events/game.js +++ b/websockets/events/game.js @@ -2,69 +2,80 @@ import axios from "axios"; import { getGame, createGame } from "../state/game.js"; import { getUser } from "../state/connection.js"; -const API_URL = "http://localhost:8000/api/v1"; +const API_URL = process.env.API_URL || "http://localhost:8000/api/v1"; const getStrength = (rank) => { - const strengthMap = { 1: 10, 7: 9, 13: 8, 11: 7, 12: 6, 6: 5, 5: 4, 4: 3, 3: 2, 2: 1 }; - return strengthMap[rank] || 0; + const strengthMap = { + 1: 10, + 7: 9, + 13: 8, + 11: 7, + 12: 6, + 6: 5, + 5: 4, + 4: 3, + 3: 2, + 2: 1, + }; + return strengthMap[rank] || 0; }; const drawCard = (game, playerId) => { - const player = game.players[playerId]; - if (!player) return; + const player = game.players[playerId]; + if (!player) return; - if (game.deck && game.deck.length > 0) { - const card = game.deck.pop(); - player.hand.push(card); - } else if (game.trumpCard) { - player.hand.push(game.trumpCard); - game.trumpCard = null; - } + if (game.deck && game.deck.length > 0) { + const card = game.deck.pop(); + player.hand.push(card); + } else if (game.trumpCard) { + player.hand.push(game.trumpCard); + game.trumpCard = null; + } }; const sanitizeState = (state, game) => { - if (!state) return null; - const p1 = game.player1 || state.player1 || {}; - const p2 = game.player2 || state.player2 || {}; + if (!state) return null; + const p1 = game.player1 || state.player1 || {}; + const p2 = game.player2 || state.player2 || {}; - return { - id: state.id, - status: state.status || game.status, - player1: { id: p1.id, name: p1.name }, - player2: { id: p2.id, name: p2.name }, - players: state.players, - hand: state.hand, - points: state.points, - opponent: { - id: state.opponent?.id, - name: state.opponent?.name, - points: state.opponent?.points, - cardCount: state.opponent?.cardCount - }, - table: state.table, - trumpCard: state.trumpCard, - deckCount: game.deck ? game.deck.length : (state.deckCount || 0), - currentTurn: state.currentTurn, - turnStartedAt: state.turnStartedAt - }; + return { + id: state.id, + status: state.status || game.status, + player1: { id: p1.id, name: p1.name }, + player2: { id: p2.id, name: p2.name }, + players: state.players, + hand: state.hand, + points: state.points, + opponent: { + id: state.opponent?.id, + name: state.opponent?.name, + points: state.opponent?.points, + cardCount: state.opponent?.cardCount, + }, + table: state.table, + trumpCard: state.trumpCard, + deckCount: game.deck ? game.deck.length : state.deckCount || 0, + currentTurn: state.currentTurn, + turnStartedAt: state.turnStartedAt, + }; }; const broadcastState = (io, gameId, game) => { - 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]) { - const rawState = game.getStateForPlayer(u.id); - const cleanState = sanitizeState(rawState, game); - s.emit("game-state", cleanState); - } + 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]) { + const rawState = game.getStateForPlayer(u.id); + const cleanState = sanitizeState(rawState, game); + s.emit("game-state", cleanState); } } -} + } +}; async function saveGameResult(gameId, result, token) { try { @@ -88,53 +99,55 @@ async function saveGameResult(gameId, result, token) { } const performGameEnd = (io, game, gameId, winnerId, loserId, reason) => { - const result = { - action: "game_ended", - winnerId: parseInt(winnerId), - loserId: parseInt(loserId), - totalTime: game.startTime ? Math.floor((Date.now() - game.startTime) / 1000) : 0, - isDraw: false, - player1_points: game.players[Object.keys(game.players)[0]]?.points || 0, - player2_points: game.players[Object.keys(game.players)[1]]?.points || 0, - reason: reason - }; + const result = { + action: "game_ended", + winnerId: parseInt(winnerId), + loserId: parseInt(loserId), + totalTime: game.startTime + ? Math.floor((Date.now() - game.startTime) / 1000) + : 0, + isDraw: false, + player1_points: game.players[Object.keys(game.players)[0]]?.points || 0, + player2_points: game.players[Object.keys(game.players)[1]]?.points || 0, + reason: reason, + }; - game.status = "ended"; - if (game.timer) clearInterval(game.timer); + game.status = "ended"; + if (game.timer) clearInterval(game.timer); - io.to(`game_${gameId}`).emit("game-over", { - winnerId: result.winnerId, - isDraw: result.isDraw, - p1Points: result.player1_points, - p2Points: result.player2_points, - message: reason === "surrender" ? "Opponent Surrendered!" : "Game Over" - }); + io.to(`game_${gameId}`).emit("game-over", { + winnerId: result.winnerId, + isDraw: result.isDraw, + p1Points: result.player1_points, + p2Points: result.player2_points, + message: reason === "surrender" ? "Opponent Surrendered!" : "Game Over", + }); - const anyId = Object.keys(game.players)[0]; - const token = game.players[anyId]?.token; - if (token) saveGameResult(gameId, result, token); -} + const anyId = Object.keys(game.players)[0]; + const token = game.players[anyId]?.token; + if (token) saveGameResult(gameId, result, token); +}; const handleTimeout = (io, gameId, userId) => { const game = getGame(gameId); - if (!game || game.status === 'ended') return; + if (!game || game.status === "ended") return; console.log(`[Timeout] User ${userId} auto-playing lowest card.`); - + const player = game.players[userId]; if (!player || !player.hand || player.hand.length === 0) return; - const trumpSuit = game.trumpCard?.suit || ''; - + const trumpSuit = game.trumpCard?.suit || ""; + const sortedHand = [...player.hand].sort((a, b) => { - const strengthA = getStrength(a.rank) + (a.suit === trumpSuit ? 100 : 0); - const strengthB = getStrength(b.rank) + (b.suit === trumpSuit ? 100 : 0); - return strengthA - strengthB; + const strengthA = getStrength(a.rank) + (a.suit === trumpSuit ? 100 : 0); + const strengthB = getStrength(b.rank) + (b.suit === trumpSuit ? 100 : 0); + return strengthA - strengthB; }); const cardToPlay = sortedHand[0]; if (cardToPlay) { - handleGameMove(io, gameId, userId, cardToPlay.id); + handleGameMove(io, gameId, userId, cardToPlay.id); } }; @@ -144,30 +157,33 @@ const handleGameMove = (io, gameId, userId, cardId) => { const result = game.playCard(userId, cardId); const roomName = `game_${gameId}`; - + broadcastState(io, gameId, game); if (result.action === "trick_resolved") { io.to(roomName).emit("trick-end", result.trickResult); - + setTimeout(() => { - if (game.status !== "ended") { - const canDraw = (game.deck && game.deck.length > 0) || game.trumpCard !== null; + if (game.status !== "ended") { + const canDraw = + (game.deck && game.deck.length > 0) || game.trumpCard !== null; - if (canDraw) { - const winnerId = result.trickResult.winnerId; - const loserId = Object.keys(game.players).find(id => String(id) !== String(winnerId)); + if (canDraw) { + const winnerId = result.trickResult.winnerId; + const loserId = Object.keys(game.players).find( + (id) => String(id) !== String(winnerId) + ); - if (winnerId) drawCard(game, winnerId); - if (loserId) drawCard(game, loserId); - } - broadcastState(io, gameId, game); + if (winnerId) drawCard(game, winnerId); + if (loserId) drawCard(game, loserId); } - }, 1500); + broadcastState(io, gameId, game); + } + }, 1500); } if (result.action === "game_ended") { - performGameEnd(io, game, gameId, result.winnerId, result.loserId, "points"); + performGameEnd(io, game, gameId, result.winnerId, result.loserId, "points"); } if (result.action === "next_turn" || result.action === "trick_resolved") { @@ -189,72 +205,76 @@ export default (io, socket) => { let gameData = null; try { - const token = socket.handshake.auth.token; - const response = await axios.get(`${API_URL}/games/${gameId}`, { - headers: { Authorization: token }, - }); - gameData = response.data.data || response.data; - } catch (e) { console.error("DB Sync Failed"); } + const token = socket.handshake.auth.token; + const response = await axios.get(`${API_URL}/games/${gameId}`, { + headers: { Authorization: token }, + }); + gameData = response.data.data || response.data; + } catch (e) { + console.error("DB Sync Failed"); + } if (!game && gameData) { - const p1Name = gameData.player1?.nickname || "Player 1"; - const p2Name = gameData.player2?.nickname || "Player 2"; - const type = gameData.type == "9" ? 9 : 3; - game = createGame( - gameId, - type, - { id: gameData.player1_user_id, name: p1Name }, - { id: gameData.player2_user_id, name: p2Name } - ); - game.status = gameData.status === 'Playing' ? 'playing' : 'pending'; + const p1Name = gameData.player1?.nickname || "Player 1"; + const p2Name = gameData.player2?.nickname || "Player 2"; + const type = gameData.type == "9" ? 9 : 3; + game = createGame( + gameId, + type, + { id: gameData.player1_user_id, name: p1Name }, + { id: gameData.player2_user_id, name: p2Name } + ); + game.status = gameData.status === "Playing" ? "playing" : "pending"; } if (!game) { - socket.emit("error", { message: "Game not found." }); - return; + socket.emit("error", { message: "Game not found." }); + return; } const myId = user.id; - const ghostKey = Object.keys(game.players).find(key => key == 1); + const ghostKey = Object.keys(game.players).find((key) => key == 1); const amIDbPlayer2 = gameData && gameData.player2_user_id == myId; if (!game.players[myId] && (ghostKey || amIDbPlayer2)) { - let ghostHand = []; - let ghostPoints = 0; - let ghostTricks = 0; - if (ghostKey && game.players[ghostKey]) { - ghostHand = game.players[ghostKey].hand || []; - ghostPoints = game.players[ghostKey].points || 0; - ghostTricks = game.players[ghostKey].tricks || 0; - delete game.players[ghostKey]; - } + let ghostHand = []; + let ghostPoints = 0; + let ghostTricks = 0; + if (ghostKey && game.players[ghostKey]) { + ghostHand = game.players[ghostKey].hand || []; + ghostPoints = game.players[ghostKey].points || 0; + ghostTricks = game.players[ghostKey].tricks || 0; + delete game.players[ghostKey]; + } - game.players[myId] = { - id: myId, - name: user.nickname, - hand: ghostHand, - points: ghostPoints, - tricks: ghostTricks, - token: socket.handshake.auth.token, - }; - game.player2 = { id: myId, name: user.nickname }; - game.status = 'playing'; - if(!game.timer) { - game.startTurnTimer((timeoutUserId) => handleTimeout(io, gameId, timeoutUserId)); - } + game.players[myId] = { + id: myId, + name: user.nickname, + hand: ghostHand, + points: ghostPoints, + tricks: ghostTricks, + token: socket.handshake.auth.token, + }; + game.player2 = { id: myId, name: user.nickname }; + game.status = "playing"; + if (!game.timer) { + game.startTurnTimer((timeoutUserId) => + handleTimeout(io, gameId, timeoutUserId) + ); + } } socket.join(`game_${gameId}`); socket.activeGameId = gameId; if (game.players[myId]) { - game.players[myId].token = socket.handshake.auth.token; - - const rawState = game.getStateForPlayer(myId); - const cleanState = sanitizeState(rawState, game); - - socket.emit("game-state", cleanState); - broadcastState(io, gameId, game); + game.players[myId].token = socket.handshake.auth.token; + + const rawState = game.getStateForPlayer(myId); + const cleanState = sanitizeState(rawState, game); + + socket.emit("game-state", cleanState); + broadcastState(io, gameId, game); } }); @@ -264,14 +284,16 @@ export default (io, socket) => { }); socket.on("surrender", ({ gameId }) => { - const user = getUser(socket.id); - const game = getGame(gameId); - - if (!game || !user || !game.players[user.id]) return; + const user = getUser(socket.id); + const game = getGame(gameId); - console.log(`[Game] User ${user.nickname} surrendered.`); - - const winnerId = Object.keys(game.players).find(id => String(id) !== String(user.id)); - performGameEnd(io, game, gameId, winnerId, user.id, "surrender"); + if (!game || !user || !game.players[user.id]) return; + + console.log(`[Game] User ${user.nickname} surrendered.`); + + const winnerId = Object.keys(game.players).find( + (id) => String(id) !== String(user.id) + ); + performGameEnd(io, game, gameId, winnerId, user.id, "surrender"); }); }; diff --git a/websockets/events/match.js b/websockets/events/match.js index ee172b6..ddf4bde 100644 --- a/websockets/events/match.js +++ b/websockets/events/match.js @@ -2,153 +2,167 @@ import { getMatch, addMatch } from "../state/match.js"; import BiscaMatch from "../classes/BiscaMatch.js"; import axios from "axios"; -const API_URL = "http://localhost:8000/api/v1"; +const API_URL = process.env.API_URL || "http://localhost:8000/api/v1"; export default (io, socket) => { - - const broadcastMatchUpdate = (matchId, match) => { - const roomName = `match_${matchId}`; - const room = io.sockets.adapter.rooms.get(roomName); - if (room) { - for (const socketId of room) { - const clientSocket = io.sockets.sockets.get(socketId); - if (!clientSocket || !clientSocket.user) continue; - const userId = clientSocket.user.id; - const publicState = match.getPublicState(); - const gameState = match.getGameState(userId); - clientSocket.emit("match:update", { ...publicState, game: gameState }); - } - } - }; - - const createMatchEmitter = (matchId) => (eventName, payload) => { - const match = getMatch(matchId); - if (!match) return; - - if (eventName === 'match:new-round-start' || eventName === 'match:ended-signal') { - broadcastMatchUpdate(matchId, match); - } else if (eventName === 'match:round-ended') { - io.to(`match_${matchId}`).emit('match:round-ended', payload); - } else { - io.to(`match_${matchId}`).emit(eventName, payload); - } - }; - - const handleAutoPlay = (matchId) => (userId, cardId) => { - const match = getMatch(matchId); - if (!match || match.status !== "Playing") return; - - console.log(`[Match ${matchId}] Auto-playing for User ${userId}`); - const result = match.playCard(userId, cardId); - - if (result && result.action === "trick_resolved") { - io.to(`match_${matchId}`).emit("game:trick-end", result.trickResult); - setTimeout(() => { broadcastMatchUpdate(matchId, match); }, 1500); - } else { - broadcastMatchUpdate(matchId, match); - } - }; - - socket.on("match:enter", async (data) => { - const { matchId } = data; - if (!socket.user || !socket.user.id) return socket.emit("error", { message: "Auth failed" }); - const userId = socket.user.id; - socket.activeMatchId = matchId; - - const room = `match_${matchId}`; - let match = getMatch(matchId); - - if (!match) { - try { - const res = await axios.get(`${API_URL}/matches/${matchId}`, { - headers: { Authorization: socket.token || socket.handshake.auth.token }, - }); - const matchData = res.data.data || res.data; - - match = new BiscaMatch( - matchData, - createMatchEmitter(matchId), - socket.token, - handleAutoPlay(matchId) - ); - addMatch(match); - } catch (e) { - return socket.emit("error", "Match not found"); - } - } - match.emitStateCallback = createMatchEmitter(matchId); - if (userId !== match.player1Id && match.player2Id === 1) await match.joinPlayer(userId, socket.user); - - socket.join(room); + const broadcastMatchUpdate = (matchId, match) => { + const roomName = `match_${matchId}`; + const room = io.sockets.adapter.rooms.get(roomName); + if (room) { + for (const socketId of room) { + const clientSocket = io.sockets.sockets.get(socketId); + if (!clientSocket || !clientSocket.user) continue; + const userId = clientSocket.user.id; const publicState = match.getPublicState(); const gameState = match.getGameState(userId); - socket.emit("match:update", { ...publicState, game: gameState }); - if (match.status === 'Playing') broadcastMatchUpdate(matchId, match); - }); + clientSocket.emit("match:update", { ...publicState, game: gameState }); + } + } + }; - socket.on("game:play-card", (data) => { - const { matchId, cardId } = data; - const match = getMatch(matchId); - if (!match || match.status !== "Playing") return; - const result = match.playCard(socket.user.id, cardId); - if (result && result.error) return socket.emit("error", { message: result.error }); - if (result && result.action === "trick_resolved") { - io.to(`match_${matchId}`).emit("game:trick-end", result.trickResult); - setTimeout(() => { broadcastMatchUpdate(matchId, match); }, 1500); - } else { - broadcastMatchUpdate(matchId, match); - } - }); + const createMatchEmitter = (matchId) => (eventName, payload) => { + const match = getMatch(matchId); + if (!match) return; - socket.on("game:surrender", async ({ matchId }) => { - const match = getMatch(matchId); - if(!match || match.status !== "Playing") return; - const userId = socket.user.id; - console.log(`[Match ${matchId}] User ${userId} surrendered ROUND.`); - const opponentId = match.player1Id == userId ? match.player2Id : match.player1Id; - const result = { - winnerId: opponentId, - loserId: userId, - isDraw: false, - player1_id: match.player1Id, - player2_id: match.player2Id, - player1_points: match.player1Id == opponentId ? 91 : 0, - player2_points: match.player2Id == opponentId ? 91 : 0, - reason: 'surrender' - }; - await match.handleGameEnd(result); - }); + if ( + eventName === "match:new-round-start" || + eventName === "match:ended-signal" + ) { + broadcastMatchUpdate(matchId, match); + } else if (eventName === "match:round-ended") { + io.to(`match_${matchId}`).emit("match:round-ended", payload); + } else { + io.to(`match_${matchId}`).emit(eventName, payload); + } + }; - socket.on("match:surrender", async ({ matchId }) => { - const match = getMatch(matchId); - if(!match || match.status !== "Playing") return; - const userId = socket.user.id; - - await match.abortCurrentGame(userId); + const handleAutoPlay = (matchId) => (userId, cardId) => { + const match = getMatch(matchId); + if (!match || match.status !== "Playing") return; - console.log(`[Match ${matchId}] User ${userId} surrendered MATCH.`); - const opponentId = match.player1Id == userId ? match.player2Id : match.player1Id; - match.marks[opponentId] = 4; - await match.finishMatch(); + console.log(`[Match ${matchId}] Auto-playing for User ${userId}`); + const result = match.playCard(userId, cardId); + + if (result && result.action === "trick_resolved") { + io.to(`match_${matchId}`).emit("game:trick-end", result.trickResult); + setTimeout(() => { broadcastMatchUpdate(matchId, match); - }); + }, 1500); + } else { + broadcastMatchUpdate(matchId, match); + } + }; - socket.on("disconnect", async () => { - if (socket.activeMatchId) { - const match = getMatch(socket.activeMatchId); - if (match && match.status === 'Playing') { - const userId = socket.user?.id; - if (userId === match.player1Id || userId === match.player2Id) { - console.log(`[Match ${match.id}] Player ${userId} disconnected.`); - - await match.abortCurrentGame(userId); + socket.on("match:enter", async (data) => { + const { matchId } = data; + if (!socket.user || !socket.user.id) + return socket.emit("error", { message: "Auth failed" }); + const userId = socket.user.id; + socket.activeMatchId = matchId; - const opponentId = match.player1Id == userId ? match.player2Id : match.player1Id; - match.marks[opponentId] = 4; - await match.finishMatch(); - broadcastMatchUpdate(match.id, match); - } - } + const room = `match_${matchId}`; + let match = getMatch(matchId); + + if (!match) { + try { + const res = await axios.get(`${API_URL}/matches/${matchId}`, { + headers: { + Authorization: socket.token || socket.handshake.auth.token, + }, + }); + const matchData = res.data.data || res.data; + + match = new BiscaMatch( + matchData, + createMatchEmitter(matchId), + socket.token, + handleAutoPlay(matchId) + ); + addMatch(match); + } catch (e) { + return socket.emit("error", "Match not found"); + } + } + match.emitStateCallback = createMatchEmitter(matchId); + if (userId !== match.player1Id && match.player2Id === 1) + await match.joinPlayer(userId, socket.user); + + socket.join(room); + const publicState = match.getPublicState(); + const gameState = match.getGameState(userId); + socket.emit("match:update", { ...publicState, game: gameState }); + if (match.status === "Playing") broadcastMatchUpdate(matchId, match); + }); + + socket.on("game:play-card", (data) => { + const { matchId, cardId } = data; + const match = getMatch(matchId); + if (!match || match.status !== "Playing") return; + const result = match.playCard(socket.user.id, cardId); + if (result && result.error) + return socket.emit("error", { message: result.error }); + if (result && result.action === "trick_resolved") { + io.to(`match_${matchId}`).emit("game:trick-end", result.trickResult); + setTimeout(() => { + broadcastMatchUpdate(matchId, match); + }, 1500); + } else { + broadcastMatchUpdate(matchId, match); + } + }); + + socket.on("game:surrender", async ({ matchId }) => { + const match = getMatch(matchId); + if (!match || match.status !== "Playing") return; + const userId = socket.user.id; + console.log(`[Match ${matchId}] User ${userId} surrendered ROUND.`); + const opponentId = + match.player1Id == userId ? match.player2Id : match.player1Id; + const result = { + winnerId: opponentId, + loserId: userId, + isDraw: false, + player1_id: match.player1Id, + player2_id: match.player2Id, + player1_points: match.player1Id == opponentId ? 91 : 0, + player2_points: match.player2Id == opponentId ? 91 : 0, + reason: "surrender", + }; + await match.handleGameEnd(result); + }); + + socket.on("match:surrender", async ({ matchId }) => { + const match = getMatch(matchId); + if (!match || match.status !== "Playing") return; + const userId = socket.user.id; + + await match.abortCurrentGame(userId); + + console.log(`[Match ${matchId}] User ${userId} surrendered MATCH.`); + const opponentId = + match.player1Id == userId ? match.player2Id : match.player1Id; + match.marks[opponentId] = 4; + await match.finishMatch(); + broadcastMatchUpdate(matchId, match); + }); + + socket.on("disconnect", async () => { + if (socket.activeMatchId) { + const match = getMatch(socket.activeMatchId); + if (match && match.status === "Playing") { + const userId = socket.user?.id; + if (userId === match.player1Id || userId === match.player2Id) { + console.log(`[Match ${match.id}] Player ${userId} disconnected.`); + + await match.abortCurrentGame(userId); + + const opponentId = + match.player1Id == userId ? match.player2Id : match.player1Id; + match.marks[opponentId] = 4; + await match.finishMatch(); + broadcastMatchUpdate(match.id, match); } - }); + } + } + }); }; diff --git a/websockets/server.js b/websockets/server.js index ba8525f..baec034 100644 --- a/websockets/server.js +++ b/websockets/server.js @@ -6,10 +6,10 @@ import gameEvents from "./events/game.js"; import matchEvents from "./events/match.js"; export const server = { - io: null, + io: null, }; -const API_URL = "http://localhost:8000/api/v1"; +const API_URL = process.env.API_URL || "http://localhost:8000/api/v1"; const disconnectTimers = new Map(); const RECONNECT_GRACE_PERIOD = 1 * 60 * 1000; @@ -17,114 +17,147 @@ const RECONNECT_GRACE_PERIOD = 1 * 60 * 1000; const userId = (socket) => socket.user && socket.user.id; export const serverStart = (port) => { - server.io = new Server(port, { - cors: { - origin: "*", + server.io = new Server(port, { + cors: { + origin: "*", + }, + }); + + console.log(`[WebSocket] API_URL is set to ${API_URL}`); + + server.io.use(async (socket, next) => { + 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, + Accept: "application/json", }, + }); + + const user = response.data.data || response.data; + + if (!user || !user.id) { + return next(new Error("Authentication error: User data not found")); + } + + socket.user = user; + socket.token = token; + socket.handshake.auth.token = token; + + if (disconnectTimers.has(user.id)) { + console.log( + `[Connection] User ${user.id} reconnected! Cancelling surrender timer.` + ); + clearTimeout(disconnectTimers.get(user.id)); + disconnectTimers.delete(user.id); + } + + addUser(socket.id, user); + + next(); + } catch (error) { + if (error.response) { + console.error( + "❌ Erro Laravel:", + error.response.status, + error.response.data + ); + } else { + console.error("❌ Erro Rede:", error.message); + } + next(new Error("Authentication error: Invalid token")); + } + }); + + server.io.on("connection", (socket) => { + gameEvents(server.io, socket); + matchEvents(server.io, socket); + + handleConnectionEvents(server.io, socket); + + socket.on("notify_disconnect", () => { + socket.isVoluntaryDisconnect = true; }); - server.io.use(async (socket, next) => { - let token = socket.handshake.auth.token; + socket.on("disconnect", () => { + // Remove from active socket list + removeUser(socket.id); - if (!token) { - return next(new Error("Authentication error: No token provided")); - } + if (socket.isVoluntaryDisconnect) { + console.log( + `[Disconnect] User ${userId} disconnected voluntarily. No surrender needed.` + ); + return; + } - if (!token.startsWith("Bearer ")) { - token = "Bearer " + token; - } + if (userId) { + console.log( + `[Disconnect] User ${userId} disconnected. Waiting ${ + RECONNECT_GRACE_PERIOD / 1000 + }s...` + ); - try { - const response = await axios.get(`${API_URL}/users/me`, { - headers: { - Authorization: token, - Accept: "application/json" - }, - }); + // ----------------------------------------------------------- + // 2. START SURRENDER TIMER + // ----------------------------------------------------------- + const timer = setTimeout(async () => { + console.log( + `[Timeout] User ${userId} did not return. Forcing surrender.` + ); - const user = response.data.data || response.data; + // A. Find the Game ID this user is playing (You need a way to look this up) + // Ideally, your 'socket' object or a global map knows which gameID the user is in. + // For this example, let's assume you stored `socket.activeGameId` when they joined. + const gameId = socket.activeGameId; - if (!user || !user.id) { - return next(new Error("Authentication error: User data not found")); + if (gameId) { + try { + // B. Call Laravel API to resign on their behalf + // We use the token we saved during the handshake + await axios.post( + `${API_URL}/games/${gameId}/resign`, + {}, + { + headers: { + Authorization: socket.handshake.auth.token, + Accept: "application/json", + }, + } + ); + + // C. Notify the room (The API likely triggers a Pusher/Socket event, + // but we can also emit locally if needed) + server.io.to(gameId).emit("game_over", { + winner: "opponent", + reason: "disconnect", + }); + + console.log( + `[Timeout] Successfully resigned game ${gameId} for user ${userId}` + ); + } catch (err) { + console.error( + `[Timeout] Failed to resign game for user ${userId}:`, + err.message + ); } + } - socket.user = user; - socket.token = token; - socket.handshake.auth.token = token; + disconnectTimers.delete(userId); + }, RECONNECT_GRACE_PERIOD); - if (disconnectTimers.has(user.id)) { - console.log(`[Connection] User ${user.id} reconnected! Cancelling surrender timer.`); - clearTimeout(disconnectTimers.get(user.id)); - disconnectTimers.delete(user.id); - } - - addUser(socket.id, user); - - next(); - } catch (error) { - if (error.response) { - console.error( - "❌ Erro Laravel:", - error.response.status, - error.response.data - ); - } else { - console.error("❌ Erro Rede:", error.message); - } - next(new Error("Authentication error: Invalid token")); - } - }); - - server.io.on("connection", (socket) => { - gameEvents(server.io, socket); - matchEvents(server.io, socket); - - handleConnectionEvents(server.io, socket); - - socket.on("notify_disconnect", () => { - socket.isVoluntaryDisconnect = true; - }); - - socket.on("disconnect", () => { - removeUser(socket.id); - - if (socket.isVoluntaryDisconnect) { - console.log(`[Disconnect] User ${userId} disconnected voluntarily. No surrender needed.`); - return; - } - - if (userId) { - console.log(`[Disconnect] User ${userId} disconnected. Waiting ${RECONNECT_GRACE_PERIOD / 1000}s...`); - const timer = setTimeout(async () => { - console.log(`[Timeout] User ${userId} did not return. Forcing surrender.`); - - const gameId = socket.activeGameId; - - if (gameId) { - try { - await axios.post(`${API_URL}/games/${gameId}/resign`, {}, { - headers: { - Authorization: socket.handshake.auth.token, - Accept: "application/json" - } - }); - - server.io.to(gameId).emit("game_over", { - winner: "opponent", - reason: "disconnect" - }); - - console.log(`[Timeout] Successfully resigned game ${gameId} for user ${userId}`); - } catch (err) { - console.error(`[Timeout] Failed to resign game for user ${userId}:`, err.message); - } - } - - disconnectTimers.delete(userId); - }, RECONNECT_GRACE_PERIOD); - - disconnectTimers.set(userId, timer); - } - }); + disconnectTimers.set(userId, timer); + } }); + }); };