diff --git a/.gitignore b/.gitignore index 957a43c..6836c95 100644 --- a/.gitignore +++ b/.gitignore @@ -101,5 +101,8 @@ Desktop.ini # If you have any local secrets or files not to track, add them here: # /path/to/some/file -package-lock.json +/frontend/package-lock.json +/websockets/package-lock.json +/api/package-lock.json + .vscode \ No newline at end of file diff --git a/api/app/Http/Controllers/GameController.php b/api/app/Http/Controllers/GameController.php index 15061f8..1a245fc 100644 --- a/api/app/Http/Controllers/GameController.php +++ b/api/app/Http/Controllers/GameController.php @@ -10,6 +10,8 @@ use App\Models\MatchGame; class GameController extends Controller { + const GHOST_ID = 1; + public function index(Request $request) { $query = Game::query()->with(["winner", "player1", "player2"]); @@ -47,51 +49,34 @@ class GameController extends Controller $user = Auth::user(); - $activeMatch = MatchGame::where(function ($q) use ($user) { - $q->where("player1_user_id", $user->id)->orWhere( - "player2_user_id", - $user->id, - ); - }) - ->whereIn("status", ["Pending", "Playing"]) - ->exists(); - $activeGame = Game::where(function ($q) use ($user) { - $q->where("player1_user_id", $user->id)->orWhere( - "player2_user_id", - $user->id, - ); + $q->where("player1_user_id", $user->id) + ->orWhere("player2_user_id", $user->id); }) - ->whereIn("status", ["Pending", "Playing"]) - ->exists(); + ->whereIn("status", ["Pending", "Playing"]) + ->exists(); - if ($activeMatch || $activeGame) { - return response()->json( - ["message" => "You already have an active game or match."], - 400, - ); + if ($activeGame) { + return response()->json(["message" => "You already have an active game."], 400); } $game = Game::create([ "type" => $validated["type"], "status" => "Pending", "player1_user_id" => $user->id, - "player2_user_id" => null, - "winner_user_id" => null, - "loser_user_id" => null, + "player2_user_id" => self::GHOST_ID, + "winner_user_id" => self::GHOST_ID, + "loser_user_id" => self::GHOST_ID, "began_at" => now(), "player1_points" => 0, "player2_points" => 0, "custom" => null, ]); - return response()->json( - [ - "message" => "Multiplayer game room created", - "game" => $game, - ], - 201, - ); + return response()->json([ + "message" => "Multiplayer game room created", + "game" => $game, + ], 201); } public function join(Game $game) @@ -99,35 +84,14 @@ class GameController extends Controller $user = Auth::user(); if ($game->player1_user_id == $user->id) { - return response()->json( - [ - "message" => "Waiting for opponent...", - "game" => $game, - ], - 200, - ); + return response()->json([ + "message" => "You are the host. Waiting for opponent...", + "game" => $game + ], 200); } - if ($game->status !== "Pending") { - return response()->json( - ["message" => "Game is full or already started"], - 400, - ); - } - - $activeGame = Game::where(function ($query) use ($user) { - $query - ->where("player1_user_id", $user->id) - ->orWhere("player2_user_id", $user->id); - }) - ->whereIn("status", ["Pending", "Playing"]) - ->exists(); - - if ($activeGame) { - return response()->json( - ["message" => "You are already in another active game."], - 400, - ); + if ($game->player2_user_id !== self::GHOST_ID) { + return response()->json(["message" => "Game is full"], 400); } $game->update([ @@ -135,13 +99,10 @@ class GameController extends Controller "player2_user_id" => $user->id, ]); - return response()->json( - [ - "message" => "Joined successfully! Game starts now.", - "game" => $game, - ], - 200, - ); + return response()->json([ + "message" => "Joined successfully!", + "game" => $game, + ], 200); } public function show(Game $game) diff --git a/websockets/classes/BiscaGame.js b/websockets/classes/BiscaGame.js new file mode 100644 index 0000000..967a696 --- /dev/null +++ b/websockets/classes/BiscaGame.js @@ -0,0 +1,235 @@ +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.startTime = null; + + 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(); + + const playerIds = Object.keys(this.players); + this.currentTurn = playerIds.find((id) => id != 1) || playerIds[0]; + + this.startTime = Date.now(); + 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); + } + + 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: "Not your turn!" }; + + const player = this.players[userId]; + const cardIndex = player.hand.findIndex((c) => c.id === cardId); + + if (cardIndex === -1) return { error: "You don't own that card!" }; + + const card = player.hand[cardIndex]; + + const deckEmpty = this.deck.length === 0 && this.trumpCard === null; + if (this.table.playerCard && deckEmpty) { + const suitToFollow = this.table.playerCard.suit; + const hasSuit = player.hand.some((c) => c.suit === suitToFollow); + if (hasSuit && card.suit !== suitToFollow) { + return { error: "You must follow the suit!" }; + } + } + + player.hand.splice(cardIndex, 1); + + if (!this.table.playerCard) { + this.table.playerCard = card; + this.table.firstPlayerId = userId; + this.currentTurn = this.getOpponentId(userId); + 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"; + + const pIds = Object.keys(this.players); + const p1Obj = this.players[pIds[0]]; + const p2Obj = this.players[pIds[1]]; + + const totalTime = (Date.now() - this.startTime) / 1000; + + let gameWinnerId = null; + let isDraw = false; + + if (p1Obj.points > p2Obj.points) gameWinnerId = p1Obj.id; + else if (p2Obj.points > p1Obj.points) gameWinnerId = p2Obj.id; + else isDraw = true; + + const finalWinner = isDraw ? null : gameWinnerId; + const finalLoser = isDraw + ? null + : gameWinnerId == p1Obj.id + ? p2Obj.id + : p1Obj.id; + + return { + action: "game_ended", + trickResult, + winnerId: finalWinner, + loserId: finalLoser, + isDraw: isDraw, + player1_id: pIds[0], + player1_points: p1Obj.points, + player2_id: pIds[1], + player2_points: p2Obj.points, + totalTime: totalTime, + }; + } + + 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..a1a5c87 100644 --- a/websockets/events/game.js +++ b/websockets/events/game.js @@ -1,19 +1,146 @@ import axios from "axios"; +import { getGame, createGame } from "../state/game.js"; import { getUser } from "../state/connection.js"; -import { createGame, joinGame, playCard, getGame } from "../state/game.js"; -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}`); +async function saveGameResult(gameId, result, token) { + try { + const payload = { + status: "Ended", + is_draw: result.isDraw, + total_time: result.totalTime, + player1_points: result.player1_points, + player2_points: result.player2_points, + }; - } catch (error) { - console.error("Error creating game:", error); - socket.emit("error", { message: "Failed to create game." }); + if (!result.isDraw && result.winnerId) { + payload.winner_user_id = result.winnerId; + payload.loser_user_id = result.loserId; + } + + await axios.put(`${API_URL}/games/${gameId}`, payload, { + headers: { Authorization: token }, + }); + console.log(`[DB] Game ${gameId} saved.`); + } catch (error) { + console.error(`[DB Error] Game ${gameId}:`, error.message); + } +} + +export default (io, socket) => { + socket.on("join-game", async ({ gameId }) => { + const user = getUser(socket.id); + if (!user) { + socket.emit("error", { message: "Not authenticated." }); + return; + } + + let game = getGame(gameId); + + if (!game) { + try { + const token = socket.handshake.auth.token; + const response = await axios.get(`${API_URL}/games/${gameId}`, { + headers: { Authorization: token }, + }); + + const gameData = response.data.data || response.data; + + if (gameData.status === "Ended") { + socket.emit("error", { message: "Game already finished." }); + return; + } + + if ( + gameData.player1_user_id != user.id && + gameData.player2_user_id != user.id && + gameData.player2_user_id !== 1 + ) { + socket.emit("error", { message: "You are not in this game." }); + return; + } + + const p1Name = gameData.player1?.name || "Player 1"; + const p2Name = gameData.player2?.name || "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 } + ); + } catch (error) { + socket.emit("error", { message: "Failed to join game." }); + return; + } + } + + 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, + }; + } + } + } + + socket.join(`game_${gameId}`); + + if (game && game.players[user.id]) { + socket.emit("game-state", game.getStateForPlayer(user.id)); + console.log(`--> Estado enviado para User ${user.id}`); + } else { + socket.emit("error", { message: "Error joining game state." }); + } + }); + + socket.on("play-card", ({ gameId, cardId }) => { + const user = getUser(socket.id); + const game = getGame(gameId); + + if (!game || !user) return; + + const result = game.playCard(user.id, cardId); + + if (result.error) { + socket.emit("game-error", { message: 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, + }); + saveGameResult(gameId, result, socket.handshake.auth.token); } }); }; diff --git a/websockets/package-lock.json b/websockets/package-lock.json deleted file mode 100644 index f21f125..0000000 --- a/websockets/package-lock.json +++ /dev/null @@ -1,573 +0,0 @@ -{ - "name": "websockets", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "websockets", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "nodemon": "^3.1.11", - "socket.io": "^4.8.1" - } - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "24.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", - "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "license": "MIT", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", - "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", - "license": "MIT", - "dependencies": { - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "license": "ISC" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/nodemon": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", - "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/socket.io": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", - "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", - "license": "MIT", - "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "license": "MIT" - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - } - } -} diff --git a/websockets/package.json b/websockets/package.json index 7d5eacc..1dc9741 100644 --- a/websockets/package.json +++ b/websockets/package.json @@ -11,6 +11,7 @@ "license": "ISC", "description": "", "dependencies": { + "axios": "^1.13.2", "nodemon": "^3.1.11", "socket.io": "^4.8.1" } diff --git a/websockets/server.js b/websockets/server.js index 3067c37..b59c8ec 100644 --- a/websockets/server.js +++ b/websockets/server.js @@ -1,19 +1,67 @@ import { Server } from "socket.io"; +import axios from "axios"; import { handleConnectionEvents } from "./events/connection.js"; +import { addUser, removeUser } from "./state/connection.js"; +import gameEvents from "./events/game.js"; export const server = { io: null, }; +const API_URL = "http://localhost:8000/api/v1"; + export const serverStart = (port) => { server.io = new Server(port, { cors: { origin: "*", }, }); + + server.io.use(async (socket, next) => { + const token = socket.handshake.auth.token; + + if (!token) { + return next(new Error("Authentication error: No token provided")); + } + + try { + const response = await axios.get(`${API_URL}/users/me`, { + headers: { Authorization: token }, + }); + + const user = response.data.data || response.data; + + if (!user || !user.id) { + return next(new Error("Authentication error: User data not found")); + } + + 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) => { 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); + }); }); }; diff --git a/websockets/state/connection.js b/websockets/state/connection.js index 5f3b49b..574712a 100644 --- a/websockets/state/connection.js +++ b/websockets/state/connection.js @@ -1,19 +1,17 @@ const users = new Map(); -export const addUser = (socket, user) => { - users.set(socket.id, user); +export const addUser = (socketId, userData) => { + users.set(socketId, userData); }; -export const removeUser = (socketID) => { - const userToDelete = { ...users.get(socketID) }; - users.delete(socketID); - return userToDelete; +export const getUser = (socketId) => { + return users.get(socketId); }; -export const getUser = (socketID) => { - return users.get(socketID); +export const removeUser = (socketId) => { + users.delete(socketId); }; export const getUserCount = () => { return users.size; -}; +}; \ No newline at end of file diff --git a/websockets/state/game.js b/websockets/state/game.js index b5ab76f..c98d8fa 100644 --- a/websockets/state/game.js +++ b/websockets/state/game.js @@ -1,186 +1,29 @@ -const games = new Map(); -let currentGameID = 0; +const BiscaGame = require("../classes/BiscaGame"); -// 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 }, -]; +const activeGames = new Map(); -const createDeck = () => { - const deck = []; - suits.forEach((suit) => { - cards.forEach((card) => { - deck.push({ ...card, suit }); - }); - }); - return deck; -}; +const createGame = (id, type, player1, player2) => { + const game = new BiscaGame(id, type, player1, player2); -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; -}; + game.init(); -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, - }; -}; + activeGames.set(id, game); -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); + console.log(`[Game State] Game created: ${id} (Type: ${type})`); 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; +const getGame = (id) => { + return activeGames.get(id); }; -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 removeGame = (id) => { + activeGames.delete(id); + console.log(`[Game State] Game removed: ${id}`); }; -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); - } +module.exports = { + createGame, + getGame, + removeGame, }; - -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());