From 74707749aa216c9a575bfb5c153611c7520df24e Mon Sep 17 00:00:00 2001 From: Ralex <95540504+Ralex91@users.noreply.github.com> Date: Sat, 25 Oct 2025 16:38:07 +0200 Subject: [PATCH] feat(game): enhance game reconnection logic and improve reset messages --- packages/common/src/types/game/index.ts | 1 + packages/common/src/types/game/socket.ts | 2 +- packages/socket/src/index.ts | 35 ++++--- packages/socket/src/services/game.ts | 92 +++++++++++++------ packages/socket/src/services/registry.ts | 13 ++- packages/web/src/app/game/[gameId]/page.tsx | 9 +- .../src/app/game/manager/[gameId]/page.tsx | 11 ++- .../web/src/components/game/GameWrapper.tsx | 4 +- packages/web/src/stores/manager.tsx | 10 +- packages/web/src/stores/player.tsx | 8 +- packages/web/src/stores/question.tsx | 5 +- 11 files changed, 114 insertions(+), 76 deletions(-) diff --git a/packages/common/src/types/game/index.ts b/packages/common/src/types/game/index.ts index a47afdf..447dc2d 100644 --- a/packages/common/src/types/game/index.ts +++ b/packages/common/src/types/game/index.ts @@ -1,6 +1,7 @@ export type Player = { id: string clientId: string + connected: boolean username: string points: number } diff --git a/packages/common/src/types/game/socket.ts b/packages/common/src/types/game/socket.ts index e0b5294..d526531 100644 --- a/packages/common/src/types/game/socket.ts +++ b/packages/common/src/types/game/socket.ts @@ -32,7 +32,7 @@ export interface ServerToClientEvents { "game:startCooldown": () => void "game:cooldown": (_count: number) => void "game:kick": () => void - "game:reset": () => void + "game:reset": (_message: string) => void "game:updateQuestion": (_data: { current: number; total: number }) => void "game:playerAnswer": (_count: number) => void diff --git a/packages/socket/src/index.ts b/packages/socket/src/index.ts index ae25428..d591cf0 100644 --- a/packages/socket/src/index.ts +++ b/packages/socket/src/index.ts @@ -25,8 +25,8 @@ io.on("connection", (socket) => { `A user connected: socketId: ${socket.id}, clientId: ${socket.handshake.auth.clientId}` ) - socket.on("player:reconnect", () => { - const game = registry.getPlayerGame(socket.handshake.auth.clientId) + socket.on("player:reconnect", ({ gameId }) => { + const game = registry.getPlayerGame(gameId, socket.handshake.auth.clientId) if (game) { game.reconnect(socket) @@ -34,20 +34,19 @@ io.on("connection", (socket) => { return } - socket.emit("game:reset") + socket.emit("game:reset", "Game not found") }) - socket.on("manager:reconnect", () => { - const game = registry.getManagerGame(socket.handshake.auth.clientId) + socket.on("manager:reconnect", ({ gameId }) => { + const game = registry.getManagerGame(gameId, socket.handshake.auth.clientId) if (game) { game.reconnect(socket) - registry.reactivateGame(game.gameId) return } - socket.emit("game:reset") + socket.emit("game:reset", "Game expired") }) socket.on("manager:auth", (password) => { @@ -132,17 +131,18 @@ io.on("connection", (socket) => { ) socket.on("disconnect", () => { - console.log(`user disconnected ${socket.id}`) + console.log(`A user disconnected : ${socket.id}`) const managerGame = registry.getGameByManagerSocketId(socket.id) if (managerGame) { + managerGame.manager.connected = false registry.markGameAsEmpty(managerGame) if (!managerGame.started) { console.log("Reset game (manager disconnected)") managerGame.abortCooldown() - io.to(managerGame.gameId).emit("game:reset") + io.to(managerGame.gameId).emit("game:reset", "Manager disconnected") registry.removeGame(managerGame.gameId) return @@ -151,7 +151,7 @@ io.on("connection", (socket) => { const game = registry.getGameByPlayerSocketId(socket.id) - if (!game || game.started) { + if (!game) { return } @@ -161,12 +161,19 @@ io.on("connection", (socket) => { return } - game.players = game.players.filter((p) => p.id !== socket.id) + if (!game.started) { + game.players = game.players.filter((p) => p.id !== socket.id) - io.to(game.manager.id).emit("manager:removePlayer", player.id) + io.to(game.manager.id).emit("manager:removePlayer", player.id) + io.to(game.gameId).emit("game:totalPlayers", game.players.length) + + console.log(`Removed player ${player.username} from game ${game.gameId}`) + + return + } + + player.connected = false io.to(game.gameId).emit("game:totalPlayers", game.players.length) - - console.log(`Removed player ${player.username} from game ${game.gameId}`) }) }) diff --git a/packages/socket/src/services/game.ts b/packages/socket/src/services/game.ts index cb8d3d5..d2ee4de 100644 --- a/packages/socket/src/services/game.ts +++ b/packages/socket/src/services/game.ts @@ -1,10 +1,13 @@ import { Answer, Player, Quizz } from "@rahoot/common/types/game" import { Server, Socket } from "@rahoot/common/types/game/socket" import { Status, STATUS, StatusDataMap } from "@rahoot/common/types/game/status" +import Registry from "@rahoot/socket/services/registry" import { createInviteCode, timeToPoint } from "@rahoot/socket/utils/game" import sleep from "@rahoot/socket/utils/sleep" import { v4 as uuid } from "uuid" +const registry = Registry.getInstance() + class Game { io: Server @@ -12,6 +15,7 @@ class Game { manager: { id: string clientId: string + connected: boolean } inviteCode: string started: boolean @@ -46,6 +50,7 @@ class Game { this.manager = { id: "", clientId: "", + connected: false, } this.inviteCode = "" this.started = false @@ -72,6 +77,7 @@ class Game { this.manager = { id: socket.id, clientId: socket.handshake.auth.clientId, + connected: true, } this.quizz = quizz @@ -109,11 +115,22 @@ class Game { } join(socket: Socket, username: string) { + const isAlreadyConnected = this.players.find( + (p) => p.clientId === socket.handshake.auth.clientId + ) + + if (isAlreadyConnected) { + socket.emit("game:errorMessage", "Player already connected") + + return + } + socket.join(this.gameId) const playerData = { id: socket.id, clientId: socket.handshake.auth.clientId, + connected: true, username, points: 0, } @@ -151,49 +168,64 @@ class Game { const { clientId } = socket.handshake.auth const isManager = this.manager.clientId === clientId - if (!isManager) { - const player = this.players.find((p) => p.clientId === clientId) + if (isManager) { + this.reconnectManager(socket) + } else { + this.reconnectPlayer(socket) + } + } - if (!player) { - return false - } + private reconnectManager(socket: Socket) { + if (this.manager.connected) { + socket.emit("game:reset", "Manager already connected") + + return } socket.join(this.gameId) + this.manager.id = socket.id + this.manager.connected = true - const commonData = { + const status = this.managerStatus || + this.lastBroadcastStatus || { + name: STATUS.WAIT, + data: { text: "Waiting for players" }, + } + + socket.emit("manager:successReconnect", { gameId: this.gameId, - started: this.started, currentQuestion: { current: this.round.currentQuestion, total: this.quizz.questions.length, }, + status, + players: this.players, + }) + socket.emit("game:totalPlayers", this.players.length) + + registry.reactivateGame(this.gameId) + console.log(`Manager reconnected to game ${this.inviteCode}`) + } + + private reconnectPlayer(socket: Socket) { + const { clientId } = socket.handshake.auth + const player = this.players.find((p) => p.clientId === clientId) + + if (!player) { + return } - if (isManager) { - this.manager.id = socket.id + if (player.connected) { + socket.emit("game:reset", "Player already connected") - const status = this.managerStatus || - this.lastBroadcastStatus || { - name: STATUS.WAIT, - data: { text: "Waiting for players" }, - } - - socket.emit("manager:successReconnect", { - ...commonData, - status, - players: this.players, - }) - socket.emit("game:totalPlayers", this.players.length) - - console.log(`Manager reconnected to game ${this.inviteCode}`) - - return true + return } - const player = this.players.find((p) => p.clientId === clientId)! + socket.join(this.gameId) + const oldSocketId = player.id player.id = socket.id + player.connected = true const status = this.playerStatus.get(oldSocketId) || this.lastBroadcastStatus || { @@ -208,7 +240,11 @@ class Game { } socket.emit("player:successReconnect", { - ...commonData, + gameId: this.gameId, + currentQuestion: { + current: this.round.currentQuestion, + total: this.quizz.questions.length, + }, status, player: { username: player.username, @@ -219,8 +255,6 @@ class Game { console.log( `Player ${player.username} reconnected to game ${this.inviteCode}` ) - - return true } startCooldown(seconds: number): Promise { diff --git a/packages/socket/src/services/registry.ts b/packages/socket/src/services/registry.ts index efe0fd5..e3bdc58 100644 --- a/packages/socket/src/services/registry.ts +++ b/packages/socket/src/services/registry.ts @@ -37,14 +37,17 @@ class Registry { return this.games.find((g) => g.inviteCode === inviteCode) } - getPlayerGame(clientId: string): Game | undefined { - return this.games.find((g) => - g.players.some((p) => p.clientId === clientId) + getPlayerGame(gameId: string, clientId: string): Game | undefined { + return this.games.find( + (g) => + g.gameId === gameId && g.players.some((p) => p.clientId === clientId) ) } - getManagerGame(clientId: string): Game | undefined { - return this.games.find((g) => g.manager.clientId === clientId) + getManagerGame(gmageId: string, clientId: string): Game | undefined { + return this.games.find( + (g) => g.gameId === gmageId && g.manager.clientId === clientId + ) } getGameByManagerSocketId(socketId: string): Game | undefined { diff --git a/packages/web/src/app/game/[gameId]/page.tsx b/packages/web/src/app/game/[gameId]/page.tsx index df8ed01..0872129 100644 --- a/packages/web/src/app/game/[gameId]/page.tsx +++ b/packages/web/src/app/game/[gameId]/page.tsx @@ -49,10 +49,11 @@ const Game = () => { reset() }) - useEvent("game:reset", () => { + useEvent("game:reset", (message) => { router.replace("/") reset() - toast("The game has been reset by the host") + setQuestionStates(null) + toast.error(message) }) if (!gameIdParam) { @@ -61,7 +62,7 @@ const Game = () => { let component = null - switch (status.name) { + switch (status?.name) { case STATUS.WAIT: component = @@ -93,7 +94,7 @@ const Game = () => { break } - return {component} + return {component} } export default Game diff --git a/packages/web/src/app/game/manager/[gameId]/page.tsx b/packages/web/src/app/game/manager/[gameId]/page.tsx index ab4f86e..c8976c0 100644 --- a/packages/web/src/app/game/manager/[gameId]/page.tsx +++ b/packages/web/src/app/game/manager/[gameId]/page.tsx @@ -47,10 +47,11 @@ const ManagerGame = () => { }, ) - useEvent("game:reset", () => { + useEvent("game:reset", (message) => { router.replace("/manager") reset() - toast("Game is not available anymore") + setQuestionStates(null) + toast.error(message) }) const handleSkip = () => { @@ -58,7 +59,7 @@ const ManagerGame = () => { return } - switch (status.name) { + switch (status?.name) { case STATUS.SHOW_ROOM: socket?.emit("manager:startGame", { gameId }) @@ -83,7 +84,7 @@ const ManagerGame = () => { let component = null - switch (status.name) { + switch (status?.name) { case STATUS.SHOW_ROOM: component = @@ -126,7 +127,7 @@ const ManagerGame = () => { } return ( - + {component} ) diff --git a/packages/web/src/components/game/GameWrapper.tsx b/packages/web/src/components/game/GameWrapper.tsx index dff2b4e..8ff12c2 100644 --- a/packages/web/src/components/game/GameWrapper.tsx +++ b/packages/web/src/components/game/GameWrapper.tsx @@ -12,7 +12,7 @@ import Image from "next/image" import { PropsWithChildren } from "react" type Props = PropsWithChildren & { - statusName: Status + statusName: Status | undefined onNext?: () => void manager?: boolean } @@ -21,7 +21,7 @@ const GameWrapper = ({ children, statusName, onNext, manager }: Props) => { const { isConnected } = useSocket() const { player } = usePlayerStore() const { questionStates, setQuestionStates } = useQuestionStore() - const next = MANAGER_SKIP_BTN[statusName] || null + const next = statusName ? MANAGER_SKIP_BTN[statusName] : null useEvent("game:updateQuestion", ({ current, total }) => { setQuestionStates({ diff --git a/packages/web/src/stores/manager.tsx b/packages/web/src/stores/manager.tsx index 2918b6a..fd13297 100644 --- a/packages/web/src/stores/manager.tsx +++ b/packages/web/src/stores/manager.tsx @@ -5,7 +5,7 @@ import { create } from "zustand" type ManagerStore = { gameId: string | null - status: Status + status: Status | null players: Player[] setGameId: (_gameId: string | null) => void @@ -16,13 +16,9 @@ type ManagerStore = { reset: () => void } -const initialStatus = createStatus("SHOW_ROOM", { - text: "Waiting for the players", -}) - const initialState = { gameId: null, - status: initialStatus, + status: null, players: [], } @@ -32,7 +28,7 @@ export const useManagerStore = create>((set) => ({ setGameId: (gameId) => set({ gameId }), setStatus: (name, data) => set({ status: createStatus(name, data) }), - resetStatus: () => set({ status: initialStatus }), + resetStatus: () => set({ status: null }), setPlayers: (players) => set({ players }), diff --git a/packages/web/src/stores/player.tsx b/packages/web/src/stores/player.tsx index a05b1e4..66c762e 100644 --- a/packages/web/src/stores/player.tsx +++ b/packages/web/src/stores/player.tsx @@ -10,7 +10,7 @@ type PlayerState = { type PlayerStore = { gameId: string | null player: PlayerState | null - status: Status + status: Status | null setGameId: (_gameId: string | null) => void @@ -24,14 +24,10 @@ type PlayerStore = { reset: () => void } -const initialStatus = createStatus("WAIT", { - text: "Waiting for the players", -}) - const initialState = { gameId: null, player: null, - status: initialStatus, + status: null, } export const usePlayerStore = create>((set) => ({ diff --git a/packages/web/src/stores/question.tsx b/packages/web/src/stores/question.tsx index 033ba9b..cc06fd8 100644 --- a/packages/web/src/stores/question.tsx +++ b/packages/web/src/stores/question.tsx @@ -3,11 +3,10 @@ import { create } from "zustand" type QuestionStore = { questionStates: GameUpdateQuestion | null - setQuestionStates: (_state: GameUpdateQuestion) => void + setQuestionStates: (_state: GameUpdateQuestion | null) => void } export const useQuestionStore = create((set) => ({ questionStates: null, - setQuestionStates: (state: GameUpdateQuestion) => - set({ questionStates: state }), + setQuestionStates: (state) => set({ questionStates: state }), }))