From 09c4732e19aa0202e07002da3e74c4e8d9a91395 Mon Sep 17 00:00:00 2001 From: Tiago Ramos <2222055@my.ipleiria.pt> Date: Sat, 27 Dec 2025 19:52:46 +0000 Subject: [PATCH] SyntaxSquad/DADProject#46 SyntaxSquad/DADProject#47 Added initial setup for websockets --- websockets/classes/BiscaGame.js | 192 ++++++++++++++++++++++++++++ websockets/events/game.js | 150 ++++++++++++++++++++-- websockets/state/game.js | 219 ++++++-------------------------- 3 files changed, 370 insertions(+), 191 deletions(-) create mode 100644 websockets/classes/BiscaGame.js diff --git a/websockets/classes/BiscaGame.js b/websockets/classes/BiscaGame.js new file mode 100644 index 0000000..c54506a --- /dev/null +++ b/websockets/classes/BiscaGame.js @@ -0,0 +1,192 @@ +class BiscaGame { + constructor(id, type, player1, player2) { + this.id = id; + this.type = type; + this.deck = []; + this.trumpCard = null; + this.trumpSuit = null; + + this.table = { + playerCard: null, + opponentCard: null, + firstPlayerId: null, + }; + + this.currentTurn = null; + this.status = "pending"; + + this.players = { + [player1.id]: { + id: player1.id, + name: player1.name, + hand: [], + points: 0, + tricks: 0, + }, + [player2.id]: { + id: player2.id, + name: player2.name, + hand: [], + points: 0, + tricks: 0, + }, + }; + } + + init() { + this.deck = this.createDeck(); + const lastCard = this.deck.pop(); + this.trumpCard = lastCard; + this.trumpSuit = lastCard.suit; + + this.dealInitialCards(); + + this.currentTurn = Object.keys(this.players)[0]; + this.status = "playing"; + } + + createDeck() { + const suits = ["c", "e", "o", "p"]; + const ranks = [1, 2, 3, 4, 5, 6, 7, 11, 12, 13]; + let deck = []; + for (const suit of suits) { + for (const rank of ranks) { + deck.push({ + id: `${suit}-${rank}`, + suit, + rank, + strength: this.getStrength(rank), + points: this.getPoints(rank), + }); + } + } + return deck.sort(() => Math.random() - 0.5); // Baralhar + } + + dealInitialCards() { + const cardsToDeal = this.type == 9 ? 9 : 3; + const pIds = Object.keys(this.players); + for (let i = 0; i < cardsToDeal; i++) { + pIds.forEach((id) => this.drawCard(id)); + } + } + + drawCard(userId) { + if (this.deck.length > 0) { + this.players[userId].hand.push(this.deck.pop()); + } else if (this.trumpCard) { + this.players[userId].hand.push(this.trumpCard); + this.trumpCard = null; + } + } + + playCard(userId, cardId) { + if (this.currentTurn != userId) return { error: "Não é a tua vez!" }; + + const player = this.players[userId]; + const cardIndex = player.hand.findIndex((c) => c.id === cardId); + + if (cardIndex === -1) return { error: "Não tens essa carta!" }; + + const card = player.hand[cardIndex]; + + player.hand.splice(cardIndex, 1); // Remove da mão (Server-side) + + if (!this.table.playerCard) { + this.table.playerCard = card; + this.table.firstPlayerId = userId; + this.currentTurn = this.getOpponentId(userId); // Passa a vez + return { action: "next_turn" }; + } else { + this.table.opponentCard = card; + return this.resolveTrick(); + } + } + + resolveTrick() { + const p1Id = this.table.firstPlayerId; + const p2Id = this.getOpponentId(p1Id); + const card1 = this.table.playerCard; + const card2 = this.table.opponentCard; + + let winnerId = p2Id; + let p1Wins = false; + + if (card2.suit === this.trumpSuit && card1.suit !== this.trumpSuit) + p1Wins = false; + else if (card1.suit === this.trumpSuit && card2.suit !== this.trumpSuit) + p1Wins = true; + else if (card1.suit === card2.suit) + p1Wins = card1.strength > card2.strength; + else p1Wins = true; + + winnerId = p1Wins ? p1Id : p2Id; + + const points = card1.points + card2.points; + this.players[winnerId].points += points; + this.players[winnerId].tricks += 1; + this.currentTurn = winnerId; + + if (this.type == 3) { + this.drawCard(winnerId); + this.drawCard(this.getOpponentId(winnerId)); + } + + const trickResult = { winnerId, points, cards: [card1, card2] }; + + this.table = { playerCard: null, opponentCard: null, firstPlayerId: null }; + + if (this.players[winnerId].hand.length === 0) { + this.status = "finished"; + return { action: "game_ended", trickResult, winnerId }; + } + + return { action: "trick_resolved", trickResult }; + } + + getStateForPlayer(userId) { + const oppId = this.getOpponentId(userId); + + return { + id: this.id, + trumpCard: this.trumpCard, + trumpSuit: this.trumpSuit, + deckCount: this.deck.length + (this.trumpCard ? 1 : 0), + currentTurn: this.currentTurn, + table: this.table, + + hand: this.players[userId].hand, + points: this.players[userId].points, + + opponent: { + points: this.players[oppId].points, + cardCount: this.players[oppId].hand.length, + }, + }; + } + + getOpponentId(id) { + return Object.keys(this.players).find((pid) => pid != id); + } + getPoints(r) { + const p = { 1: 11, 7: 10, 13: 4, 11: 3, 12: 2 }; + return p[r] || 0; + } + getStrength(r) { + const s = { + 1: 10, + 7: 9, + 13: 8, + 11: 7, + 12: 6, + 6: 5, + 5: 4, + 4: 3, + 3: 2, + 2: 1, + }; + return s[r] || 0; + } +} + +module.exports = BiscaGame; diff --git a/websockets/events/game.js b/websockets/events/game.js index e827fc5..185f6d5 100644 --- a/websockets/events/game.js +++ b/websockets/events/game.js @@ -1,19 +1,143 @@ -import axios from "axios"; -import { getUser } from "../state/connection.js"; -import { createGame, joinGame, playCard, getGame } from "../state/game.js"; +const axios = require("axios"); +const { getGame, createGame } = require("../state/game"); +const { getUser } = require("../state/connection"); -const API_URL = process.env.API_URL || "http://localhost:8000/api"; +const API_URL = "http://localhost:8000/api/v1"; -export const handleGameEvents = (io, socket) => { - socket.on("create-game", async (data) => { - try { - const user = getUser(socket.id); - const game = createGame(data.type, user); - socket.join(`game_${game.match_id}`); +module.exports = (io, socket) => { + // --- EVENTO: ENTRAR NO JOGO (Validado pela tabela GAMES) --- + socket.on("join-game", async ({ gameId }) => { + const user = getUser(socket.id); - } catch (error) { - console.error("Error creating game:", error); - socket.emit("error", { message: "Failed to create game." }); + if (!user) { + socket.emit("error", { message: "Não autenticado." }); + return; + } + + console.log(`[Socket] User ${user.id} a tentar entrar no Game ${gameId}`); + + // 1. Verificar se o jogo já está na memória do Node + let game = getGame(gameId); + + // 2. Se NÃO existe na memória, vamos buscá-lo ao Laravel + if (!game) { + try { + // Token para autenticação no Laravel + const token = socket.handshake.auth.token; + + // CHAMADA À API GAMES + const response = await axios.get(`${API_URL}/games/${gameId}`, { + headers: { Authorization: token }, + }); + + const gameData = response.data.data; + + // Validação: O user pertence a este jogo? + if ( + gameData.player1_user_id != user.id && + gameData.player2_user_id != user.id + ) { + socket.emit("error", { message: "Não pertences a este jogo!" }); + return; + } + + // Validação: O jogo já acabou? + if (gameData.status === "Ended") { + socket.emit("error", { message: "Este jogo já terminou." }); + return; + } + + console.log(`[Laravel] Game ${gameId} validado. A iniciar lógica...`); + + // Preparar jogadores + // Nota: O endpoint 'games/{id}' deve retornar info básica dos users ou pelo menos os IDs. + // Se não vier o nome, usamos um placeholder ou o nome do user atual se coincidir. + + const p1Name = + gameData.player1 && gameData.player1.name + ? gameData.player1.name + : "Player 1"; + const p2Name = + gameData.player2 && gameData.player2.name + ? gameData.player2.name + : "Player 2"; + + const p1 = { id: gameData.player1_user_id, name: p1Name }; + const p2 = { id: gameData.player2_user_id, name: p2Name }; + + // Tipo de jogo ('3' ou '9') + const type = gameData.type == "9" ? 9 : 3; + + // CRIAR O JOGO NA MEMÓRIA + game = createGame(gameId, type, p1, p2); + } catch (error) { + console.error( + `[Erro Laravel] Falha ao buscar Game ${gameId}:`, + error.message + ); + // Dica: Se der 404, o jogo não existe na BD + socket.emit("error", { message: "Erro ao buscar dados do jogo." }); + return; + } + } + + // 3. Juntar o socket à sala do jogo + socket.join(`game_${gameId}`); + + // 4. Enviar o estado do jogo ao jogador + if (game.players[user.id]) { + // Envia apenas o que este jogador pode ver (esconde mão do adversário) + const gameState = game.getStateForPlayer(user.id); + socket.emit("game-state", gameState); + } + }); + + // --- EVENTO: JOGAR CARTA --- + socket.on("play-card", ({ gameId, cardId }) => { + const user = getUser(socket.id); + const game = getGame(gameId); + + if (!game || !user) return; + + // 1. Processar jogada + const result = game.playCard(user.id, cardId); + + if (result.error) { + socket.emit("game-error", { message: result.error }); + return; + } + + const roomName = `game_${gameId}`; + + // 2. Atualizar estado visual (Solução rápida: Forçar refresh nos clientes) + // O ideal é iterar os sockets da sala e enviar 'game-state' personalizado a cada um + const socketsInRoom = io.sockets.adapter.rooms.get(roomName); + if (socketsInRoom) { + for (const socketId of socketsInRoom) { + const s = io.sockets.sockets.get(socketId); + // Precisamos saber qual User ID pertence a este socket para enviar o estado certo + // Assumindo que o teu getUser(socketId) funciona: + const u = getUser(socketId); + if (u && game.players[u.id]) { + s.emit("game-state", game.getStateForPlayer(u.id)); + } + } + } + + // 3. Eventos de Animação (Vaza / Fim de Jogo) + 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 }); + + // AQUI: Salvar o resultado na BD 'GAMES' + // axios.put(`${API_URL}/games/${gameId}`, { + // status: 'Ended', + // winner_user_id: result.winnerId, + // ...pontos... + // }); } }); }; diff --git a/websockets/state/game.js b/websockets/state/game.js index b5ab76f..c93a79d 100644 --- a/websockets/state/game.js +++ b/websockets/state/game.js @@ -1,186 +1,49 @@ -const games = new Map(); -let currentGameID = 0; +// websockets/state/game.js -// Bisca deck configuration -const suits = ["hearts", "diamonds", "clubs", "spades"]; -const cards = [ - { face: "A", value: 11, points: 11 }, - { face: "7", value: 10, points: 10 }, - { face: "K", value: 4, points: 4 }, - { face: "J", value: 3, points: 3 }, - { face: "Q", value: 2, points: 2 }, - { face: "6", value: 0, points: 0 }, - { face: "5", value: 0, points: 0 }, - { face: "4", value: 0, points: 0 }, - { face: "3", value: 0, points: 0 }, - { face: "2", value: 0, points: 0 }, -]; +// Importamos a classe que criaste no Passo 1 +const BiscaGame = require('../classes/BiscaGame'); -const createDeck = () => { - const deck = []; - suits.forEach((suit) => { - cards.forEach((card) => { - deck.push({ ...card, suit }); - }); - }); - return deck; +// Aqui guardamos todos os jogos a decorrer na memória RAM do servidor +// Chave: ID do jogo (matchId) -> Valor: Instância do Jogo (O objeto com o baralho, mãos, etc) +const activeGames = new Map(); + +/** + * Cria um novo jogo, guarda-o na memória e retorna-o. + */ +const createGame = (id, type, player1, player2) => { + // 1. Instanciar a classe (Criar o jogo com as regras) + const game = new BiscaGame(id, type, player1, player2); + + // 2. Preparar o jogo (Baralhar, dar cartas iniciais) + game.init(); + + // 3. Guardar no "Armazém" para não se perder + activeGames.set(id, game); + + console.log(`[Game State] Jogo criado: ${id} (Tipo: ${type})`); + return game; }; -const shuffleDeck = (deck) => { - const shuffled = [...deck]; - for (let i = shuffled.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; - } - return shuffled; +/** + * Busca um jogo existente pelo ID. + * Usado sempre que um jogador envia uma carta, para sabermos a qual jogo pertence. + */ +const getGame = (id) => { + return activeGames.get(id); }; -const dealCards = (deck, cardsPerPlayer = 3) => { - const player1Hand = deck.slice(0, cardsPerPlayer); - const player2Hand = deck.slice(cardsPerPlayer, cardsPerPlayer * 2); - const trumpCard = deck.pop(); - return { - player1Hand, - player2Hand, - trumpCard, - remainingDeck: deck, - }; +/** + * Remove o jogo da memória. + * Usado quando o jogo acaba para libertar RAM. + */ +const removeGame = (id) => { + activeGames.delete(id); + console.log(`[Game State] Jogo removido: ${id}`); }; -export const createGame = (type, host) => { - const gameID = currentGameID++; - const game = { - match_id: gameID, - type: type, - player1_user_id: host.id, - player2_user_id: null, - is_draw: false, - winner_user_id: null, - loser_user_id: null, - status: "pending", - began_at: null, - ended_at: null, - player1_points: 0, - player2_points: 0, - // Game-specific state - player1_hand: [], - player2_hand: [], - trump_card: null, - trump_suit: null, - current_trick: [], - current_player: null, - round_number: 0, - }; - games.set(gameID, game); - return game; -}; - -export const joinGame = (gameID, player2) => { - const game = games.get(gameID); - game.player2_user_id = player2.id; - game.status = "playing"; - game.began_at = new Date(); - - const cardCount = parseInt(game.type, 10); - if (![3, 9].includes(cardCount)) { - throw new Error("Unsupported game type"); - } - - // Initialize game state - const deck = shuffleDeck(createDeck()); - const { player1Hand, player2Hand, trumpCard, remainingDeck } = dealCards( - deck, - cardCount - ); - - game.player1_hand = player1Hand; - game.player2_hand = player2Hand; - game.trump_card = trumpCard; - game.trump_suit = trumpCard.suit; - game.remaining_deck = remainingDeck; - game.current_player = game.player1_user_id; // Player 1 starts - - return game; -}; - -export const playCard = (gameID, playerID, cardIndex) => { - const game = games.get(gameID); - if (game.current_player !== playerID) { - throw new Error("Not this player's turn"); - } - - const isPlayer1 = playerID === game.player1_user_id; - const playerHand = isPlayer1 ? game.player1_hand : game.player2_hand; - const playedCard = playerHand.splice(cardIndex, 1)[0]; - - game.current_trick.push({ playerID, card: playedCard }); - - if (game.current_trick.length === 2) { - resolveTrick(game); - } else { - game.current_player = isPlayer1 - ? game.player2_user_id - : game.player1_user_id; - } - return game; -}; - -const resolveTrick = (game) => { - const [first, second] = game.current_trick; - let winner; - - if (first.card.suit === second.card.suit) { - winner = first.card.value > second.card.value ? first : second; - } else if (second.card.suit === game.trump_suit) { - winner = second; - } else { - winner = first; // First card wins if no trump and different suits - } - - const points = first.card.points + second.card.points; - winner.playerID === game.player1_user_id - ? (game.player1_points += points) - : (game.player2_points += points); - - if (deck.length > 0) { - const winnerHand = - winner.playerID === game.player1_user_id - ? game.player1_hand - : game.player2_hand; - loserHand = - winner.playerID === game.player1_user_id - ? game.player2_hand - : game.player1_hand; - - winnerHand.push(game.deck.shift()); - if (game.deck.length > 0) { - loserHand.push(game.deck.shift()); - } - } - - game.current_trick = []; - game.current_player = winner.playerID; - game.round_number += 1; - - if (game.player1_hand.length === 0 && game.player2_hand.length === 0) { - endGame(game); - } -}; - -const endGame = (game) => { - game.status = "ended"; - game.ended_at = new Date(); - - if (game.player1_points > game.player2_points) { - game.winner_user_id = game.player1_user_id; - game.loser_user_id = game.player2_user_id; - } else if (game.player2_points > game.player1_points) { - game.winner_user_id = game.player2_user_id; - game.loser_user_id = game.player1_user_id; - } else { - game.is_draw = true; - } -}; - -export const getGame = (gameID) => games.get(gameID); -export const getAllGames = () => Array.from(games.values()); +// Exportamos estas funções para serem usadas no ficheiro de eventos +module.exports = { + createGame, + getGame, + removeGame +}; \ No newline at end of file