Merge pull request 'feature/initial_gameplay_websockets' (#77) from feature/initial_gameplay_websockets into develop

Reviewed-on: https://gitea.fernandovideira.com/SyntaxSquad/DADProject/pulls/77
Reviewed-by: Edd <[email protected]>
Reviewed-by: FernandoJVideira <[email protected]>
This commit was merged in pull request #77.
This commit is contained in:
2025-12-28 18:05:52 +00:00
9 changed files with 474 additions and 831 deletions
+4 -1
View File
@@ -101,5 +101,8 @@ Desktop.ini
# If you have any local secrets or files not to track, add them here: # If you have any local secrets or files not to track, add them here:
# /path/to/some/file # /path/to/some/file
package-lock.json /frontend/package-lock.json
/websockets/package-lock.json
/api/package-lock.json
.vscode .vscode
+20 -59
View File
@@ -10,6 +10,8 @@ use App\Models\MatchGame;
class GameController extends Controller class GameController extends Controller
{ {
const GHOST_ID = 1;
public function index(Request $request) public function index(Request $request)
{ {
$query = Game::query()->with(["winner", "player1", "player2"]); $query = Game::query()->with(["winner", "player1", "player2"]);
@@ -47,51 +49,34 @@ class GameController extends Controller
$user = Auth::user(); $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) { $activeGame = Game::where(function ($q) use ($user) {
$q->where("player1_user_id", $user->id)->orWhere( $q->where("player1_user_id", $user->id)
"player2_user_id", ->orWhere("player2_user_id", $user->id);
$user->id,
);
}) })
->whereIn("status", ["Pending", "Playing"]) ->whereIn("status", ["Pending", "Playing"])
->exists(); ->exists();
if ($activeMatch || $activeGame) { if ($activeGame) {
return response()->json( return response()->json(["message" => "You already have an active game."], 400);
["message" => "You already have an active game or match."],
400,
);
} }
$game = Game::create([ $game = Game::create([
"type" => $validated["type"], "type" => $validated["type"],
"status" => "Pending", "status" => "Pending",
"player1_user_id" => $user->id, "player1_user_id" => $user->id,
"player2_user_id" => null, "player2_user_id" => self::GHOST_ID,
"winner_user_id" => null, "winner_user_id" => self::GHOST_ID,
"loser_user_id" => null, "loser_user_id" => self::GHOST_ID,
"began_at" => now(), "began_at" => now(),
"player1_points" => 0, "player1_points" => 0,
"player2_points" => 0, "player2_points" => 0,
"custom" => null, "custom" => null,
]); ]);
return response()->json( return response()->json([
[
"message" => "Multiplayer game room created", "message" => "Multiplayer game room created",
"game" => $game, "game" => $game,
], ], 201);
201,
);
} }
public function join(Game $game) public function join(Game $game)
@@ -99,35 +84,14 @@ class GameController extends Controller
$user = Auth::user(); $user = Auth::user();
if ($game->player1_user_id == $user->id) { if ($game->player1_user_id == $user->id) {
return response()->json( return response()->json([
[ "message" => "You are the host. Waiting for opponent...",
"message" => "Waiting for opponent...", "game" => $game
"game" => $game, ], 200);
],
200,
);
} }
if ($game->status !== "Pending") { if ($game->player2_user_id !== self::GHOST_ID) {
return response()->json( return response()->json(["message" => "Game is full"], 400);
["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,
);
} }
$game->update([ $game->update([
@@ -135,13 +99,10 @@ class GameController extends Controller
"player2_user_id" => $user->id, "player2_user_id" => $user->id,
]); ]);
return response()->json( return response()->json([
[ "message" => "Joined successfully!",
"message" => "Joined successfully! Game starts now.",
"game" => $game, "game" => $game,
], ], 200);
200,
);
} }
public function show(Game $game) public function show(Game $game)
+235
View File
@@ -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;
+136 -9
View File
@@ -1,19 +1,146 @@
import axios from "axios"; import axios from "axios";
import { getGame, createGame } from "../state/game.js";
import { getUser } from "../state/connection.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) => { async function saveGameResult(gameId, result, token) {
socket.on("create-game", async (data) => {
try { try {
const user = getUser(socket.id); const payload = {
const game = createGame(data.type, user); status: "Ended",
socket.join(`game_${game.match_id}`); is_draw: result.isDraw,
total_time: result.totalTime,
player1_points: result.player1_points,
player2_points: result.player2_points,
};
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) { } catch (error) {
console.error("Error creating game:", error); console.error(`[DB Error] Game ${gameId}:`, error.message);
socket.emit("error", { message: "Failed to create game." }); }
}
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);
} }
}); });
}; };
-573
View File
@@ -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
}
}
}
}
}
+1
View File
@@ -11,6 +11,7 @@
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"axios": "^1.13.2",
"nodemon": "^3.1.11", "nodemon": "^3.1.11",
"socket.io": "^4.8.1" "socket.io": "^4.8.1"
} }
+48
View File
@@ -1,19 +1,67 @@
import { Server } from "socket.io"; import { Server } from "socket.io";
import axios from "axios";
import { handleConnectionEvents } from "./events/connection.js"; import { handleConnectionEvents } from "./events/connection.js";
import { addUser, removeUser } from "./state/connection.js";
import gameEvents from "./events/game.js";
export const server = { export const server = {
io: null, io: null,
}; };
const API_URL = "http://localhost:8000/api/v1";
export const serverStart = (port) => { export const serverStart = (port) => {
server.io = new Server(port, { server.io = new Server(port, {
cors: { cors: {
origin: "*", 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) => { server.io.on("connection", (socket) => {
console.log("New connection:", socket.id); console.log("New connection:", socket.id);
gameEvents(server.io, socket);
handleConnectionEvents(server.io, socket); handleConnectionEvents(server.io, socket);
socket.on("disconnect", () => {
removeUser(socket.id);
console.log("Disconnected:", socket.id);
});
}); });
}; };
+6 -8
View File
@@ -1,17 +1,15 @@
const users = new Map(); const users = new Map();
export const addUser = (socket, user) => { export const addUser = (socketId, userData) => {
users.set(socket.id, user); users.set(socketId, userData);
}; };
export const removeUser = (socketID) => { export const getUser = (socketId) => {
const userToDelete = { ...users.get(socketID) }; return users.get(socketId);
users.delete(socketID);
return userToDelete;
}; };
export const getUser = (socketID) => { export const removeUser = (socketId) => {
return users.get(socketID); users.delete(socketId);
}; };
export const getUserCount = () => { export const getUserCount = () => {
+16 -173
View File
@@ -1,186 +1,29 @@
const games = new Map(); const BiscaGame = require("../classes/BiscaGame");
let currentGameID = 0;
// Bisca deck configuration const activeGames = new Map();
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 createDeck = () => { const createGame = (id, type, player1, player2) => {
const deck = []; const game = new BiscaGame(id, type, player1, player2);
suits.forEach((suit) => {
cards.forEach((card) => {
deck.push({ ...card, suit });
});
});
return deck;
};
const shuffleDeck = (deck) => { game.init();
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;
};
const dealCards = (deck, cardsPerPlayer = 3) => { activeGames.set(id, game);
const player1Hand = deck.slice(0, cardsPerPlayer);
const player2Hand = deck.slice(cardsPerPlayer, cardsPerPlayer * 2);
const trumpCard = deck.pop();
return {
player1Hand,
player2Hand,
trumpCard,
remainingDeck: deck,
};
};
export const createGame = (type, host) => { console.log(`[Game State] Game created: ${id} (Type: ${type})`);
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; return game;
}; };
export const joinGame = (gameID, player2) => { const getGame = (id) => {
const game = games.get(gameID); return activeGames.get(id);
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 removeGame = (id) => {
const game = games.get(gameID); activeGames.delete(id);
if (game.current_player !== playerID) { console.log(`[Game State] Game removed: ${id}`);
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) => { module.exports = {
const [first, second] = game.current_trick; createGame,
let winner; getGame,
removeGame,
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());