mirror of
https://github.com/randyjc/Rahoot.git
synced 2026-03-13 20:15:35 +01:00
feat(game): enhance game reconnection logic and improve reset messages
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
export type Player = {
|
export type Player = {
|
||||||
id: string
|
id: string
|
||||||
clientId: string
|
clientId: string
|
||||||
|
connected: boolean
|
||||||
username: string
|
username: string
|
||||||
points: number
|
points: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export interface ServerToClientEvents {
|
|||||||
"game:startCooldown": () => void
|
"game:startCooldown": () => void
|
||||||
"game:cooldown": (_count: number) => void
|
"game:cooldown": (_count: number) => void
|
||||||
"game:kick": () => void
|
"game:kick": () => void
|
||||||
"game:reset": () => void
|
"game:reset": (_message: string) => void
|
||||||
"game:updateQuestion": (_data: { current: number; total: number }) => void
|
"game:updateQuestion": (_data: { current: number; total: number }) => void
|
||||||
"game:playerAnswer": (_count: number) => void
|
"game:playerAnswer": (_count: number) => void
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ io.on("connection", (socket) => {
|
|||||||
`A user connected: socketId: ${socket.id}, clientId: ${socket.handshake.auth.clientId}`
|
`A user connected: socketId: ${socket.id}, clientId: ${socket.handshake.auth.clientId}`
|
||||||
)
|
)
|
||||||
|
|
||||||
socket.on("player:reconnect", () => {
|
socket.on("player:reconnect", ({ gameId }) => {
|
||||||
const game = registry.getPlayerGame(socket.handshake.auth.clientId)
|
const game = registry.getPlayerGame(gameId, socket.handshake.auth.clientId)
|
||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
game.reconnect(socket)
|
game.reconnect(socket)
|
||||||
@@ -34,20 +34,19 @@ io.on("connection", (socket) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit("game:reset")
|
socket.emit("game:reset", "Game not found")
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on("manager:reconnect", () => {
|
socket.on("manager:reconnect", ({ gameId }) => {
|
||||||
const game = registry.getManagerGame(socket.handshake.auth.clientId)
|
const game = registry.getManagerGame(gameId, socket.handshake.auth.clientId)
|
||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
game.reconnect(socket)
|
game.reconnect(socket)
|
||||||
registry.reactivateGame(game.gameId)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit("game:reset")
|
socket.emit("game:reset", "Game expired")
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on("manager:auth", (password) => {
|
socket.on("manager:auth", (password) => {
|
||||||
@@ -132,17 +131,18 @@ io.on("connection", (socket) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
console.log(`user disconnected ${socket.id}`)
|
console.log(`A user disconnected : ${socket.id}`)
|
||||||
|
|
||||||
const managerGame = registry.getGameByManagerSocketId(socket.id)
|
const managerGame = registry.getGameByManagerSocketId(socket.id)
|
||||||
|
|
||||||
if (managerGame) {
|
if (managerGame) {
|
||||||
|
managerGame.manager.connected = false
|
||||||
registry.markGameAsEmpty(managerGame)
|
registry.markGameAsEmpty(managerGame)
|
||||||
|
|
||||||
if (!managerGame.started) {
|
if (!managerGame.started) {
|
||||||
console.log("Reset game (manager disconnected)")
|
console.log("Reset game (manager disconnected)")
|
||||||
managerGame.abortCooldown()
|
managerGame.abortCooldown()
|
||||||
io.to(managerGame.gameId).emit("game:reset")
|
io.to(managerGame.gameId).emit("game:reset", "Manager disconnected")
|
||||||
registry.removeGame(managerGame.gameId)
|
registry.removeGame(managerGame.gameId)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -151,7 +151,7 @@ io.on("connection", (socket) => {
|
|||||||
|
|
||||||
const game = registry.getGameByPlayerSocketId(socket.id)
|
const game = registry.getGameByPlayerSocketId(socket.id)
|
||||||
|
|
||||||
if (!game || game.started) {
|
if (!game) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,12 +161,19 @@ io.on("connection", (socket) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!game.started) {
|
||||||
game.players = game.players.filter((p) => p.id !== socket.id)
|
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)
|
io.to(game.gameId).emit("game:totalPlayers", game.players.length)
|
||||||
|
|
||||||
console.log(`Removed player ${player.username} from game ${game.gameId}`)
|
console.log(`Removed player ${player.username} from game ${game.gameId}`)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.connected = false
|
||||||
|
io.to(game.gameId).emit("game:totalPlayers", game.players.length)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { Answer, Player, Quizz } from "@rahoot/common/types/game"
|
import { Answer, Player, Quizz } from "@rahoot/common/types/game"
|
||||||
import { Server, Socket } from "@rahoot/common/types/game/socket"
|
import { Server, Socket } from "@rahoot/common/types/game/socket"
|
||||||
import { Status, STATUS, StatusDataMap } from "@rahoot/common/types/game/status"
|
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 { createInviteCode, timeToPoint } from "@rahoot/socket/utils/game"
|
||||||
import sleep from "@rahoot/socket/utils/sleep"
|
import sleep from "@rahoot/socket/utils/sleep"
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from "uuid"
|
||||||
|
|
||||||
|
const registry = Registry.getInstance()
|
||||||
|
|
||||||
class Game {
|
class Game {
|
||||||
io: Server
|
io: Server
|
||||||
|
|
||||||
@@ -12,6 +15,7 @@ class Game {
|
|||||||
manager: {
|
manager: {
|
||||||
id: string
|
id: string
|
||||||
clientId: string
|
clientId: string
|
||||||
|
connected: boolean
|
||||||
}
|
}
|
||||||
inviteCode: string
|
inviteCode: string
|
||||||
started: boolean
|
started: boolean
|
||||||
@@ -46,6 +50,7 @@ class Game {
|
|||||||
this.manager = {
|
this.manager = {
|
||||||
id: "",
|
id: "",
|
||||||
clientId: "",
|
clientId: "",
|
||||||
|
connected: false,
|
||||||
}
|
}
|
||||||
this.inviteCode = ""
|
this.inviteCode = ""
|
||||||
this.started = false
|
this.started = false
|
||||||
@@ -72,6 +77,7 @@ class Game {
|
|||||||
this.manager = {
|
this.manager = {
|
||||||
id: socket.id,
|
id: socket.id,
|
||||||
clientId: socket.handshake.auth.clientId,
|
clientId: socket.handshake.auth.clientId,
|
||||||
|
connected: true,
|
||||||
}
|
}
|
||||||
this.quizz = quizz
|
this.quizz = quizz
|
||||||
|
|
||||||
@@ -109,11 +115,22 @@ class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
join(socket: Socket, username: string) {
|
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)
|
socket.join(this.gameId)
|
||||||
|
|
||||||
const playerData = {
|
const playerData = {
|
||||||
id: socket.id,
|
id: socket.id,
|
||||||
clientId: socket.handshake.auth.clientId,
|
clientId: socket.handshake.auth.clientId,
|
||||||
|
connected: true,
|
||||||
username,
|
username,
|
||||||
points: 0,
|
points: 0,
|
||||||
}
|
}
|
||||||
@@ -151,27 +168,23 @@ class Game {
|
|||||||
const { clientId } = socket.handshake.auth
|
const { clientId } = socket.handshake.auth
|
||||||
const isManager = this.manager.clientId === clientId
|
const isManager = this.manager.clientId === clientId
|
||||||
|
|
||||||
if (!isManager) {
|
if (isManager) {
|
||||||
const player = this.players.find((p) => p.clientId === clientId)
|
this.reconnectManager(socket)
|
||||||
|
} else {
|
||||||
if (!player) {
|
this.reconnectPlayer(socket)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private reconnectManager(socket: Socket) {
|
||||||
|
if (this.manager.connected) {
|
||||||
|
socket.emit("game:reset", "Manager already connected")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
socket.join(this.gameId)
|
socket.join(this.gameId)
|
||||||
|
|
||||||
const commonData = {
|
|
||||||
gameId: this.gameId,
|
|
||||||
started: this.started,
|
|
||||||
currentQuestion: {
|
|
||||||
current: this.round.currentQuestion,
|
|
||||||
total: this.quizz.questions.length,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isManager) {
|
|
||||||
this.manager.id = socket.id
|
this.manager.id = socket.id
|
||||||
|
this.manager.connected = true
|
||||||
|
|
||||||
const status = this.managerStatus ||
|
const status = this.managerStatus ||
|
||||||
this.lastBroadcastStatus || {
|
this.lastBroadcastStatus || {
|
||||||
@@ -180,20 +193,39 @@ class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
socket.emit("manager:successReconnect", {
|
socket.emit("manager:successReconnect", {
|
||||||
...commonData,
|
gameId: this.gameId,
|
||||||
|
currentQuestion: {
|
||||||
|
current: this.round.currentQuestion,
|
||||||
|
total: this.quizz.questions.length,
|
||||||
|
},
|
||||||
status,
|
status,
|
||||||
players: this.players,
|
players: this.players,
|
||||||
})
|
})
|
||||||
socket.emit("game:totalPlayers", this.players.length)
|
socket.emit("game:totalPlayers", this.players.length)
|
||||||
|
|
||||||
|
registry.reactivateGame(this.gameId)
|
||||||
console.log(`Manager reconnected to game ${this.inviteCode}`)
|
console.log(`Manager reconnected to game ${this.inviteCode}`)
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = this.players.find((p) => p.clientId === clientId)!
|
private reconnectPlayer(socket: Socket) {
|
||||||
|
const { clientId } = socket.handshake.auth
|
||||||
|
const player = this.players.find((p) => p.clientId === clientId)
|
||||||
|
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.connected) {
|
||||||
|
socket.emit("game:reset", "Player already connected")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.join(this.gameId)
|
||||||
|
|
||||||
const oldSocketId = player.id
|
const oldSocketId = player.id
|
||||||
player.id = socket.id
|
player.id = socket.id
|
||||||
|
player.connected = true
|
||||||
|
|
||||||
const status = this.playerStatus.get(oldSocketId) ||
|
const status = this.playerStatus.get(oldSocketId) ||
|
||||||
this.lastBroadcastStatus || {
|
this.lastBroadcastStatus || {
|
||||||
@@ -208,7 +240,11 @@ class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
socket.emit("player:successReconnect", {
|
socket.emit("player:successReconnect", {
|
||||||
...commonData,
|
gameId: this.gameId,
|
||||||
|
currentQuestion: {
|
||||||
|
current: this.round.currentQuestion,
|
||||||
|
total: this.quizz.questions.length,
|
||||||
|
},
|
||||||
status,
|
status,
|
||||||
player: {
|
player: {
|
||||||
username: player.username,
|
username: player.username,
|
||||||
@@ -219,8 +255,6 @@ class Game {
|
|||||||
console.log(
|
console.log(
|
||||||
`Player ${player.username} reconnected to game ${this.inviteCode}`
|
`Player ${player.username} reconnected to game ${this.inviteCode}`
|
||||||
)
|
)
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startCooldown(seconds: number): Promise<void> {
|
startCooldown(seconds: number): Promise<void> {
|
||||||
|
|||||||
@@ -37,14 +37,17 @@ class Registry {
|
|||||||
return this.games.find((g) => g.inviteCode === inviteCode)
|
return this.games.find((g) => g.inviteCode === inviteCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlayerGame(clientId: string): Game | undefined {
|
getPlayerGame(gameId: string, clientId: string): Game | undefined {
|
||||||
return this.games.find((g) =>
|
return this.games.find(
|
||||||
g.players.some((p) => p.clientId === clientId)
|
(g) =>
|
||||||
|
g.gameId === gameId && g.players.some((p) => p.clientId === clientId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getManagerGame(clientId: string): Game | undefined {
|
getManagerGame(gmageId: string, clientId: string): Game | undefined {
|
||||||
return this.games.find((g) => g.manager.clientId === clientId)
|
return this.games.find(
|
||||||
|
(g) => g.gameId === gmageId && g.manager.clientId === clientId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getGameByManagerSocketId(socketId: string): Game | undefined {
|
getGameByManagerSocketId(socketId: string): Game | undefined {
|
||||||
|
|||||||
@@ -49,10 +49,11 @@ const Game = () => {
|
|||||||
reset()
|
reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
useEvent("game:reset", () => {
|
useEvent("game:reset", (message) => {
|
||||||
router.replace("/")
|
router.replace("/")
|
||||||
reset()
|
reset()
|
||||||
toast("The game has been reset by the host")
|
setQuestionStates(null)
|
||||||
|
toast.error(message)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!gameIdParam) {
|
if (!gameIdParam) {
|
||||||
@@ -61,7 +62,7 @@ const Game = () => {
|
|||||||
|
|
||||||
let component = null
|
let component = null
|
||||||
|
|
||||||
switch (status.name) {
|
switch (status?.name) {
|
||||||
case STATUS.WAIT:
|
case STATUS.WAIT:
|
||||||
component = <Wait data={status.data} />
|
component = <Wait data={status.data} />
|
||||||
|
|
||||||
@@ -93,7 +94,7 @@ const Game = () => {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return <GameWrapper statusName={status.name}>{component}</GameWrapper>
|
return <GameWrapper statusName={status?.name}>{component}</GameWrapper>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Game
|
export default Game
|
||||||
|
|||||||
@@ -47,10 +47,11 @@ const ManagerGame = () => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
useEvent("game:reset", () => {
|
useEvent("game:reset", (message) => {
|
||||||
router.replace("/manager")
|
router.replace("/manager")
|
||||||
reset()
|
reset()
|
||||||
toast("Game is not available anymore")
|
setQuestionStates(null)
|
||||||
|
toast.error(message)
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSkip = () => {
|
const handleSkip = () => {
|
||||||
@@ -58,7 +59,7 @@ const ManagerGame = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (status.name) {
|
switch (status?.name) {
|
||||||
case STATUS.SHOW_ROOM:
|
case STATUS.SHOW_ROOM:
|
||||||
socket?.emit("manager:startGame", { gameId })
|
socket?.emit("manager:startGame", { gameId })
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ const ManagerGame = () => {
|
|||||||
|
|
||||||
let component = null
|
let component = null
|
||||||
|
|
||||||
switch (status.name) {
|
switch (status?.name) {
|
||||||
case STATUS.SHOW_ROOM:
|
case STATUS.SHOW_ROOM:
|
||||||
component = <Room data={status.data} />
|
component = <Room data={status.data} />
|
||||||
|
|
||||||
@@ -126,7 +127,7 @@ const ManagerGame = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GameWrapper statusName={status.name} onNext={handleSkip} manager>
|
<GameWrapper statusName={status?.name} onNext={handleSkip} manager>
|
||||||
{component}
|
{component}
|
||||||
</GameWrapper>
|
</GameWrapper>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import Image from "next/image"
|
|||||||
import { PropsWithChildren } from "react"
|
import { PropsWithChildren } from "react"
|
||||||
|
|
||||||
type Props = PropsWithChildren & {
|
type Props = PropsWithChildren & {
|
||||||
statusName: Status
|
statusName: Status | undefined
|
||||||
onNext?: () => void
|
onNext?: () => void
|
||||||
manager?: boolean
|
manager?: boolean
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ const GameWrapper = ({ children, statusName, onNext, manager }: Props) => {
|
|||||||
const { isConnected } = useSocket()
|
const { isConnected } = useSocket()
|
||||||
const { player } = usePlayerStore()
|
const { player } = usePlayerStore()
|
||||||
const { questionStates, setQuestionStates } = useQuestionStore()
|
const { questionStates, setQuestionStates } = useQuestionStore()
|
||||||
const next = MANAGER_SKIP_BTN[statusName] || null
|
const next = statusName ? MANAGER_SKIP_BTN[statusName] : null
|
||||||
|
|
||||||
useEvent("game:updateQuestion", ({ current, total }) => {
|
useEvent("game:updateQuestion", ({ current, total }) => {
|
||||||
setQuestionStates({
|
setQuestionStates({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { create } from "zustand"
|
|||||||
|
|
||||||
type ManagerStore<T> = {
|
type ManagerStore<T> = {
|
||||||
gameId: string | null
|
gameId: string | null
|
||||||
status: Status<T>
|
status: Status<T> | null
|
||||||
players: Player[]
|
players: Player[]
|
||||||
|
|
||||||
setGameId: (_gameId: string | null) => void
|
setGameId: (_gameId: string | null) => void
|
||||||
@@ -16,13 +16,9 @@ type ManagerStore<T> = {
|
|||||||
reset: () => void
|
reset: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialStatus = createStatus<StatusDataMap, "SHOW_ROOM">("SHOW_ROOM", {
|
|
||||||
text: "Waiting for the players",
|
|
||||||
})
|
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
gameId: null,
|
gameId: null,
|
||||||
status: initialStatus,
|
status: null,
|
||||||
players: [],
|
players: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +28,7 @@ export const useManagerStore = create<ManagerStore<StatusDataMap>>((set) => ({
|
|||||||
setGameId: (gameId) => set({ gameId }),
|
setGameId: (gameId) => set({ gameId }),
|
||||||
|
|
||||||
setStatus: (name, data) => set({ status: createStatus(name, data) }),
|
setStatus: (name, data) => set({ status: createStatus(name, data) }),
|
||||||
resetStatus: () => set({ status: initialStatus }),
|
resetStatus: () => set({ status: null }),
|
||||||
|
|
||||||
setPlayers: (players) => set({ players }),
|
setPlayers: (players) => set({ players }),
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ type PlayerState = {
|
|||||||
type PlayerStore<T> = {
|
type PlayerStore<T> = {
|
||||||
gameId: string | null
|
gameId: string | null
|
||||||
player: PlayerState | null
|
player: PlayerState | null
|
||||||
status: Status<T>
|
status: Status<T> | null
|
||||||
|
|
||||||
setGameId: (_gameId: string | null) => void
|
setGameId: (_gameId: string | null) => void
|
||||||
|
|
||||||
@@ -24,14 +24,10 @@ type PlayerStore<T> = {
|
|||||||
reset: () => void
|
reset: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialStatus = createStatus<StatusDataMap, "WAIT">("WAIT", {
|
|
||||||
text: "Waiting for the players",
|
|
||||||
})
|
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
gameId: null,
|
gameId: null,
|
||||||
player: null,
|
player: null,
|
||||||
status: initialStatus,
|
status: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePlayerStore = create<PlayerStore<StatusDataMap>>((set) => ({
|
export const usePlayerStore = create<PlayerStore<StatusDataMap>>((set) => ({
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ import { create } from "zustand"
|
|||||||
|
|
||||||
type QuestionStore = {
|
type QuestionStore = {
|
||||||
questionStates: GameUpdateQuestion | null
|
questionStates: GameUpdateQuestion | null
|
||||||
setQuestionStates: (_state: GameUpdateQuestion) => void
|
setQuestionStates: (_state: GameUpdateQuestion | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useQuestionStore = create<QuestionStore>((set) => ({
|
export const useQuestionStore = create<QuestionStore>((set) => ({
|
||||||
questionStates: null,
|
questionStates: null,
|
||||||
setQuestionStates: (state: GameUpdateQuestion) =>
|
setQuestionStates: (state) => set({ questionStates: state }),
|
||||||
set({ questionStates: state }),
|
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user