Merge pull request 'feature/websockets-matches' (#95) from feature/websockets-matches into develop

Reviewed-on: https://gitea.fernandovideira.com/SyntaxSquad/DADProject/pulls/95
Reviewed-by: Afonso Clerigo Mendes de Sousa <[email protected]>
Reviewed-by: Edd <[email protected]>
This commit was merged in pull request #95.
This commit is contained in:
Edd
2026-01-02 22:23:20 +00:00
9 changed files with 512 additions and 69 deletions
+24 -11
View File
@@ -55,42 +55,55 @@ class GameController extends Controller
$validated = $request->validate([ $validated = $request->validate([
"type" => "required|in:3,9", "type" => "required|in:3,9",
"player2_user_id" => "nullable|integer|exists:users,id",
"match_id" => "nullable|integer|exists:matches,id",
"status" => "nullable|in:Pending,Playing",
"began_at" => "nullable|date"
]); ]);
$user = Auth::user(); $user = Auth::user();
$activeGame = Game::where(function ($q) use ($user) { $activeGame = Game::where(function ($q) use ($user) {
$q->where("player1_user_id", $user->id) $q->where("player1_user_id", $user->id)
->orWhere("player2_user_id", $user->id); ->orWhere("player2_user_id", $user->id);
}) })
->whereIn("status", ["Pending", "Playing"]) ->whereIn("status", ["Pending", "Playing"])
->exists(); ->exists();
if ($activeGame) { if ($activeGame) {
return response()->json(["message" => "You already have an active game."], 400); return response()->json(["message" => "You already have an active game."], 400);
}
$player2Id = $request->input('player2_user_id', self::GHOST_ID);
$initialStatus = $request->input('status', 'Pending');
if ($player2Id !== self::GHOST_ID && !$request->has('status')) {
$initialStatus = 'Playing';
} }
$game = Game::create([ $game = Game::create([
"type" => $validated["type"], "type" => $validated["type"],
"status" => "Pending", "status" => $initialStatus,
"player1_user_id" => $user->id, "player1_user_id" => $user->id,
"player2_user_id" => self::GHOST_ID, "player2_user_id" => $player2Id,
"winner_user_id" => self::GHOST_ID, "winner_user_id" => null,
"loser_user_id" => self::GHOST_ID, "loser_user_id" => null,
"began_at" => now(), "match_id" => $request->input('match_id', null),
"began_at" => $request->input('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" => "Game created",
"game" => $game, "game" => $game,
], 201); ], 201);
} }
public function join(Request $request, Game $game) public function join(Request $request, Game $game)
{ {
// Prevent joining if already full or started // Prevent joining if already full or started
if ($game->status !== 'Pending' || $game->player2_user_id !== 1) { if ($game->status !== 'Pending' || $game->player2_user_id !== 1) {
return response()->json(['message' => 'Game is not available'], 400); return response()->json(['message' => 'Game is not available'], 400);
+5 -7
View File
@@ -86,7 +86,6 @@ class GamePolicy
return true; return true;
} }
/** /**
* Determine whether the user can update the model. * Determine whether the user can update the model.
*/ */
@@ -96,17 +95,16 @@ class GamePolicy
return false; return false;
} }
if ( if (!in_array($game->status, ['Playing', 'Pending'])) {
$game->status !== "Playing" || return false;
($game->player1_user_id !== $user->id && }
$game->player2_user_id !== $user->id)
) { if ($game->player1_user_id !== $user->id && $game->player2_user_id !== $user->id) {
return false; return false;
} }
return true; return true;
} }
/** /**
* Determine whether the user can delete the model. * Determine whether the user can delete the model.
*/ */
+20 -13
View File
@@ -1,7 +1,9 @@
class BiscaGame { class BiscaGame {
constructor(id, type, player1, player2) { constructor(id, type, player1, player2, dbId = null, onGameEnd = null) {
this.id = id; this.id = id;
this.dbId = dbId;
this.type = type; this.type = type;
this.onGameEnd = onGameEnd;
this.deck = []; this.deck = [];
this.trumpCard = null; this.trumpCard = null;
@@ -17,7 +19,7 @@ class BiscaGame {
}; };
this.currentTurn = null; this.currentTurn = null;
this.status = 'pending'; this.status = "pending";
this.startTime = null; this.startTime = null;
this.players = { this.players = {
@@ -50,7 +52,7 @@ class BiscaGame {
this.currentTurn = playerIds.find((id) => id != 1) || playerIds[0]; this.currentTurn = playerIds.find((id) => id != 1) || playerIds[0];
this.startTime = Date.now(); this.startTime = Date.now();
this.status = 'playing'; this.status = "playing";
} }
startTurnTimer(callback) { startTurnTimer(callback) {
@@ -75,7 +77,7 @@ class BiscaGame {
handleTimeout() { handleTimeout() {
console.log( console.log(
`[Game ${this.id}] Timeout! Auto-playing for ${this.currentTurn}`, `[Game ${this.id}] Timeout! Auto-playing for ${this.currentTurn}`
); );
const lowestCard = this.getLowestCard(this.currentTurn); const lowestCard = this.getLowestCard(this.currentTurn);
@@ -94,7 +96,7 @@ class BiscaGame {
} }
createDeck() { createDeck() {
const suits = ['c', 'e', 'o', 'p']; const suits = ["c", "e", "o", "p"];
const ranks = [1, 2, 3, 4, 5, 6, 7, 11, 12, 13]; const ranks = [1, 2, 3, 4, 5, 6, 7, 11, 12, 13];
let deck = []; let deck = [];
@@ -130,7 +132,7 @@ class BiscaGame {
} }
playCard(userId, cardId) { playCard(userId, cardId) {
if (this.currentTurn != userId) return { error: 'Not your turn!' }; if (this.currentTurn != userId) return { error: "Not your turn!" };
const player = this.players[userId]; const player = this.players[userId];
const cardIndex = player.hand.findIndex((c) => c.id === cardId); const cardIndex = player.hand.findIndex((c) => c.id === cardId);
@@ -144,7 +146,7 @@ class BiscaGame {
const suitToFollow = this.table.playerCard.suit; const suitToFollow = this.table.playerCard.suit;
const hasSuit = player.hand.some((c) => c.suit === suitToFollow); const hasSuit = player.hand.some((c) => c.suit === suitToFollow);
if (hasSuit && card.suit !== suitToFollow) { if (hasSuit && card.suit !== suitToFollow) {
return { error: 'You must follow the suit!' }; return { error: "You must follow the suit!" };
} }
} }
@@ -156,7 +158,7 @@ class BiscaGame {
this.table.playerCard = card; this.table.playerCard = card;
this.table.firstPlayerId = userId; this.table.firstPlayerId = userId;
this.currentTurn = this.getOpponentId(userId); this.currentTurn = this.getOpponentId(userId);
return { action: 'next_turn' }; return { action: "next_turn" };
} else { } else {
this.table.opponentCard = card; this.table.opponentCard = card;
return this.resolveTrick(); return this.resolveTrick();
@@ -196,7 +198,7 @@ class BiscaGame {
this.table = { playerCard: null, opponentCard: null, firstPlayerId: null }; this.table = { playerCard: null, opponentCard: null, firstPlayerId: null };
if (this.players[winnerId].hand.length === 0) { if (this.players[winnerId].hand.length === 0) {
this.status = 'finished'; this.status = "finished";
this.stopTimer(); this.stopTimer();
const pIds = Object.keys(this.players); const pIds = Object.keys(this.players);
@@ -216,11 +218,11 @@ class BiscaGame {
const finalLoser = isDraw const finalLoser = isDraw
? null ? null
: gameWinnerId == p1Obj.id : gameWinnerId == p1Obj.id
? p2Obj.id ? p2Obj.id
: p1Obj.id; : p1Obj.id;
return { return {
action: 'game_ended', action: "game_ended",
trickResult, trickResult,
winnerId: finalWinner, winnerId: finalWinner,
loserId: finalLoser, loserId: finalLoser,
@@ -231,15 +233,20 @@ class BiscaGame {
player2_points: p2Obj.points, player2_points: p2Obj.points,
totalTime: totalTime, totalTime: totalTime,
}; };
if (this.onGameEnd) {
this.onGameEnd(result);
}
return result;
} }
return { action: 'trick_resolved', trickResult }; return { action: "trick_resolved", trickResult };
} }
getStateForPlayer(userId) { getStateForPlayer(userId) {
const oppId = this.getOpponentId(userId); const oppId = this.getOpponentId(userId);
return { return {
id: this.id, id: this.id,
dbId: this.dbId,
trumpCard: this.trumpCard, trumpCard: this.trumpCard,
trumpSuit: this.trumpSuit, trumpSuit: this.trumpSuit,
deckCount: this.deck.length + (this.trumpCard ? 1 : 0), deckCount: this.deck.length + (this.trumpCard ? 1 : 0),
+275
View File
@@ -0,0 +1,275 @@
import BiscaGame from "../classes/BiscaGame.js";
import axios from "axios";
const API_URL = "http://localhost:8000/api/v1";
class BiscaMatch {
constructor(apiData, emitStateCallback, token) {
this.id = apiData.id;
this.emitStateCallback = emitStateCallback;
this.token = token;
this.stake = apiData.stake;
this.type = apiData.type;
this.status = apiData.status;
this.player1Id = apiData.player1_user_id;
this.player2Id = apiData.player2_user_id;
this.marks = {
[this.player1Id]: apiData.player1_marks || 0,
[this.player2Id]: apiData.player2_marks || 0,
};
this.points = {
[this.player1Id]: apiData.player1_points || 0,
[this.player2Id]: apiData.player2_points || 0,
};
this.currentGame = null;
this.GHOST_ID = 1;
if (this.status === "Playing" && this.player2Id !== this.GHOST_ID) {
this.startNewGame();
}
}
async joinPlayer(userId) {
if (this.player2Id !== this.GHOST_ID) return false;
if (userId === this.player1Id) return false;
this.player2Id = userId;
this.marks[userId] = 0;
this.points[userId] = 0;
this.status = "Playing";
await this.startNewGame();
return true;
}
getAuthHeaders() {
return {
headers: { Authorization: this.token },
};
}
async startNewGame() {
console.log(`[Match ${this.id}] A iniciar nova mão (Game DB)...`);
let newGameDbId = null;
try {
let payloadP1 = this.player1Id;
let payloadP2 = this.player2Id;
if (this.player2Id !== this.GHOST_ID) {
console.log(
"🔄 Multiplayer detetado: Trocando ordem para satisfazer a API..."
);
payloadP1 = this.player2Id;
payloadP2 = this.player1Id;
}
const payload = {
type: this.type,
player1_user_id: payloadP1,
player2_user_id: payloadP2,
match_id: this.id,
status: "Playing",
began_at: new Date().toISOString(),
};
console.log("📤 [Payload Adaptado]:", JSON.stringify(payload, null, 2));
const res = await axios.post(
`${API_URL}/games`,
payload,
this.getAuthHeaders()
);
const gameData = res.data.game || res.data.data || res.data;
newGameDbId = gameData.id;
console.log(`[Match ${this.id}] Game DB criado: #${newGameDbId}`);
const apiP1 = gameData.player1_user_id;
const apiP2 = gameData.player2_user_id;
const p1 = { id: apiP1, name: `User ${apiP1}` };
const p2 = { id: apiP2, name: `User ${apiP2}` };
this.currentGame = new BiscaGame(
this.id,
this.type,
p1,
p2,
newGameDbId,
(result) => {
this.handleGameEnd(result);
}
);
this.currentGame.init();
if (this.emitStateCallback) {
this.emitStateCallback("match:new-round", this.getState());
}
} catch (e) {
if (e.response) {
console.error(
`❌ Erro API (${e.response.status}):`,
JSON.stringify(e.response.data)
);
} else {
console.error(`❌ Erro Código: ${e.message}`);
}
}
}
async handleGameEnd(result) {
console.log(
`[Match ${this.id}] Mão terminada. Vencedor: ${result.winnerId}`
);
this.points[result.player1_id] += result.player1_points;
this.points[result.player2_id] += result.player2_points;
if (this.currentGame && this.currentGame.dbId) {
try {
const payload = {
status: "Ended",
winner_user_id: result.winnerId,
loser_user_id: result.loserId,
is_draw: result.isDraw,
player1_points: result.player1_points,
player2_points: result.player2_points,
ended_at: new Date().toISOString(),
};
console.log(
`📤 A enviar update para Game #${this.currentGame.dbId}...`
);
// --- CORREÇÃO IMPORTANTE: Forçar o header aqui ---
const config = {
headers: {
Authorization: this.token,
"Content-Type": "application/json",
Accept: "application/json",
},
};
await axios.put(
`${API_URL}/games/${this.currentGame.dbId}`,
payload,
config
);
console.log(
`✅ Game #${this.currentGame.dbId} atualizado com sucesso.`
);
} catch (e) {
// --- LOG DETALHADO PARA VER O MOTIVO DO 403 ---
if (e.response) {
console.error(`❌ ERRO CRÍTICO API (${e.response.status}):`);
// Isto vai imprimir o JSON exato que o Laravel devolve
console.error(JSON.stringify(e.response.data, null, 2));
} else {
console.error("Erro Axios:", e.message);
}
}
} else {
console.warn("⚠️ Ignorado: Não há ID de jogo na DB para atualizar.");
}
// ... (resto da lógica de atribuir marcas e finishMatch mantém-se igual)
let marksToAdd = 0;
let roundWinnerId = result.winnerId;
if (roundWinnerId) {
const winningScore =
roundWinnerId == result.player1_id
? result.player1_points
: result.player2_points;
if (winningScore === 120) marksToAdd = 4;
else if (winningScore > 90) marksToAdd = 2;
else if (winningScore >= 60) marksToAdd = 1;
this.marks[roundWinnerId] += marksToAdd;
}
if (this.marks[this.player1Id] >= 4 || this.marks[this.player2Id] >= 4) {
await this.finishMatch();
if (this.emitStateCallback) {
this.emitStateCallback("match:ended", this.getState());
}
} else {
console.log(`[Match ${this.id}] Ninguém ganhou 4 marcas. A baralhar...`);
await this.startNewGame();
}
}
async finishMatch() {
this.status = "Ended";
const winnerId =
this.marks[this.player1Id] >= 4 ? this.player1Id : this.player2Id;
const loserId =
winnerId == this.player1Id ? this.player2Id : this.player1Id;
try {
await axios.put(
`${API_URL}/matches/${this.id}`,
{
status: "Ended",
winner_user_id: winnerId,
loser_user_id: loserId,
player1_marks: this.marks[this.player1Id],
player2_marks: this.marks[this.player2Id],
player1_points: this.points[this.player1Id],
player2_points: this.points[this.player2Id],
},
this.getAuthHeaders()
);
console.log(`[Match ${this.id}] Encerrado com sucesso na API.`);
} catch (e) {
console.error(`Erro ao fechar Match API: ${e.message}`);
}
}
playCard(userId, cardId) {
if (!this.currentGame || this.status !== "Playing") return;
return this.currentGame.playCard(userId, cardId);
}
getState() {
let gameState = null;
if (this.currentGame) {
const p1Id = this.player1Id;
const p2Id = this.player2Id;
if (this.currentGame.players && this.currentGame.players[p1Id]) {
gameState = this.currentGame.getStateForPlayer(p1Id);
} else if (this.currentGame.players && this.currentGame.players[p2Id]) {
gameState = this.currentGame.getStateForPlayer(p2Id);
} else if (this.currentGame.players) {
const availableIds = Object.keys(this.currentGame.players);
if (availableIds.length > 0) {
console.warn(
`⚠️ [Match ${this.id}] IDs desalinhados! A usar vista do user ${availableIds[0]}`
);
gameState = this.currentGame.getStateForPlayer(availableIds[0]);
}
}
}
return {
matchId: this.id,
status: this.status,
marks: this.marks,
points: this.points,
game: gameState,
};
}
}
export default BiscaMatch;
+7 -8
View File
@@ -1,6 +1,5 @@
import { addUser, removeUser } from "../state/connection.js"; import { addUser, removeUser } from "../state/connection.js";
const HEARTBEAT_INTERVAL = 30000; // 30 seconds const HEARTBEAT_INTERVAL = 30000; // 30 seconds
const HEARTBEAT_TIMEOUT = 10000; // 10 seconds const HEARTBEAT_TIMEOUT = 10000; // 10 seconds
@@ -9,36 +8,36 @@ export const handleConnectionEvents = (io, socket) => {
let heartbeatTimeout; let heartbeatTimeout;
const startHeartbeat = () => { const startHeartbeat = () => {
heartbeatInterval = setInterval(() => { heartbeatInterval = setInterval(() => {
socket.emit('heartbeat'); socket.emit("heartbeat");
heartbeatTimeout = setTimeout(() => { heartbeatTimeout = setTimeout(() => {
console.log( console.log(
`[Heartbeat] No response from ${socket.id}, disconnecting...`, `[Heartbeat] No response from ${socket.id}, disconnecting...`
); );
socket.disconnect(true); socket.disconnect(true);
}, HEARTBEAT_TIMEOUT); }, HEARTBEAT_TIMEOUT);
}, HEARTBEAT_INTERVAL); }, HEARTBEAT_INTERVAL);
}; };
socket.on('heartbeat_ack', () => { socket.on("heartbeat_ack", () => {
clearTimeout(heartbeatTimeout); clearTimeout(heartbeatTimeout);
console.log(`[Heartbeat] Received pong from ${socket.id}`); console.log(`[Heartbeat] Received pong from ${socket.id}`);
}); });
socket.on('join', (user) => { socket.on("join", (user) => {
addUser(socket, user); addUser(socket, user);
console.log(`[Connection] User ${user.name} has joined the server`); console.log(`[Connection] User ${user.name} has joined the server`);
}); });
socket.on('leave', () => { socket.on("leave", () => {
const user = removeUser(socket.id); const user = removeUser(socket.id);
if (user) { if (user) {
console.log(`[Connection] User ${user.name} has left the server`); console.log(`[Connection] User ${user.name} has left the server`);
} }
}); });
socket.on('disconnect', () => { socket.on("disconnect", () => {
console.log('Connection Lost:', socket.id); console.log("Connection Lost:", socket.id);
removeUser(socket.id); removeUser(socket.id);
}); });
}; };
+138
View File
@@ -0,0 +1,138 @@
import { getMatch, addMatch } from "../state/match.js";
import BiscaMatch from "../classes/BiscaMatch.js";
import axios from "axios";
const API_URL = "http://localhost:8000/api/v1";
export default (io, socket) => {
const createMatchEmitter = (matchId) => (eventName, payload) => {
io.to(`match_${matchId}`).emit(eventName, payload);
};
socket.on("match:enter", async (data) => {
const { matchId } = data;
if (!socket.user || !socket.user.id) {
return socket.emit("error", { message: "User not authenticated" });
}
const userId = socket.user.id;
const room = `match_${matchId}`;
let match = getMatch(matchId);
if (!match) {
try {
console.log(`A pedir match ${matchId} à API...`);
const res = await axios.get(`${API_URL}/matches/${matchId}`, {
headers: { Authorization: socket.token },
});
const matchData = res.data.data || res.data;
match = new BiscaMatch(
matchData,
createMatchEmitter(matchId),
socket.token
);
addMatch(match);
console.log(`Match ${matchId} carregado em memória.`);
} catch (e) {
console.error("Erro ao carregar Match da API:", e.message);
socket.emit("error", "Match not found or API error");
return;
}
} else {
match.token = socket.token;
}
if (match) {
match.token = socket.token;
}
match.emitStateCallback = createMatchEmitter(matchId);
if (userId !== match.player1Id && match.player2Id === 1) {
console.log(`User ${userId} a substituir o Ghost no Match ${matchId}`);
await match.joinPlayer(userId);
}
socket.join(room);
console.log(`Socket ${socket.id} entrou na sala ${room}`);
if (match.status === "Playing") {
socket.emit("match:update", match.getState());
} else {
socket.emit("match:waiting", { msg: "Waiting for opponent..." });
}
});
socket.on("game:play-card", (data) => {
const { matchId, cardId } = data;
const match = getMatch(matchId);
if (!match || match.status !== "Playing") {
console.log(
"Tentativa de jogar sem match ativo ou match não encontrado."
);
return;
}
const result = match.playCard(socket.user.id, cardId);
if (result && result.error) {
console.log(
`❌ [Erro Jogo] User ${socket.user.id} tentou jogar carta ${cardId} mas: ${result.error}`
);
socket.emit("error", { message: result.error });
return;
}
if (match.currentGame) {
io.to(`match_${matchId}`).emit(
"game:update",
match.currentGame.getStateForPlayer(socket.user.id)
);
}
});
socket.on("debug:end-game", async (data) => {
const { matchId } = data;
const match = getMatch(matchId);
if (match) {
console.log(`[DEBUG] User ${socket.user.id} a forçar fim de jogo.`);
// Simula que o user que clicou ganhou 120 pontos
const winnerId = socket.user.id;
const loserId =
winnerId == match.player1Id ? match.player2Id : match.player1Id;
// Objeto de resultado falso para fechar o jogo
const fakeResult = {
winnerId: winnerId,
loserId: loserId,
isDraw: false,
player1_points: winnerId == match.player1Id ? 120 : 0,
player2_points: winnerId == match.player2Id ? 120 : 0,
player1_id: match.player1Id,
player2_id: match.player2Id,
};
await match.handleGameEnd(fakeResult);
}
});
// DEBUG: Forçar fim do Match Completo
socket.on("debug:end-match", async (data) => {
const { matchId } = data;
const match = getMatch(matchId);
if (match) {
console.log(`[DEBUG] User ${socket.user.id} a forçar fim de MATCH.`);
// Dá 4 marcas a quem clicou
match.marks[socket.user.id] = 4;
await match.finishMatch();
// Emite o estado final
io.to(`match_${matchId}`).emit("match:ended", match.getState());
}
});
};
+4
View File
@@ -3,6 +3,7 @@ import axios from "axios";
import { handleConnectionEvents } from "./events/connection.js"; import { handleConnectionEvents } from "./events/connection.js";
import { addUser, removeUser } from "./state/connection.js"; import { addUser, removeUser } from "./state/connection.js";
import gameEvents from "./events/game.js"; import gameEvents from "./events/game.js";
import matchEvents from "./events/match.js";
export const server = { export const server = {
io: null, io: null,
@@ -43,6 +44,7 @@ export const serverStart = (port) => {
} }
socket.user = user; socket.user = user;
socket.token = token;
socket.handshake.auth.token = token; socket.handshake.auth.token = token;
addUser(socket.id, user); addUser(socket.id, user);
@@ -65,6 +67,8 @@ export const serverStart = (port) => {
server.io.on("connection", (socket) => { server.io.on("connection", (socket) => {
gameEvents(server.io, socket); gameEvents(server.io, socket);
matchEvents(server.io, socket);
handleConnectionEvents(server.io, socket); handleConnectionEvents(server.io, socket);
socket.on("disconnect", () => { socket.on("disconnect", () => {
+23 -30
View File
@@ -1,41 +1,34 @@
import BiscaGame from '../classes/BiscaGame.js'; import BiscaGame from "../classes/BiscaGame.js";
const activeGames = new Map(); const activeGames = new Map();
const createGame = (id, type, player1, player2) => { export const createGame = (id, type, player1, player2) => {
const existingGame = activeGames.get(id); const existingGame = activeGames.get(id);
if (existingGame) { if (existingGame) {
console.log( console.log(
`[Game State] Game ${id} already exists, returning existing instance`, `[Game State] Game ${id} already exists, returning existing instance`
); );
return existingGame; return existingGame;
} }
const game = new BiscaGame(id, type, player1, player2); const game = new BiscaGame(id, type, player1, player2);
game.init(); game.init();
activeGames.set(id, game); activeGames.set(id, game);
console.log(`[Game State] Game created: ${id} (Type: ${type})`); console.log(`[Game State] Game created: ${id} (Type: ${type})`);
return game; return game;
}; };
const getGame = (id) => { export const getGame = (id) => {
return activeGames.get(id); return activeGames.get(id);
}; };
const removeGame = (id) => { export const removeGame = (id) => {
const game = activeGames.get(id); const game = activeGames.get(id);
if (game) { if (game) {
game.stopTimer(); game.stopTimer();
activeGames.delete(id); activeGames.delete(id);
console.log(`[Game State] Game removed: ${id}`); console.log(`[Game State] Game removed: ${id}`);
} }
}; };
module.exports = {
createGame,
getGame,
removeGame,
};
+16
View File
@@ -0,0 +1,16 @@
const matches = new Map();
export const addMatch = (match) => {
matches.set(parseInt(match.id), match);
return match;
};
export const getMatch = (id) => {
return matches.get(parseInt(id));
};
export const removeMatch = (id) => {
matches.delete(parseInt(id));
};
export { matches };