From ceba5521dc3098d54f5af25f16a87111e1863238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Thu, 12 Mar 2026 19:51:32 +0100 Subject: [PATCH] add jury voting, bingo WS message handlers Co-Authored-By: Claude Opus 4.6 --- packages/server/src/ws/handler.ts | 146 +++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 3 deletions(-) diff --git a/packages/server/src/ws/handler.ts b/packages/server/src/ws/handler.ts index 91475db..f7efc4e 100644 --- a/packages/server/src/ws/handler.ts +++ b/packages/server/src/ws/handler.ts @@ -47,7 +47,8 @@ function sendGameState(ws: WSContext, roomCode: string, sessionId: string) { if (!gm || !playerId) return const allPlayerIds = roomManager.getAllPlayerIds(roomCode) - const gameState = gm.getGameStateForPlayer(playerId, allPlayerIds) + const displayNames = roomManager.getPlayerDisplayNames(roomCode) + const gameState = gm.getGameStateForPlayer(playerId, allPlayerIds, displayNames) sendTo(ws, { type: "game_state", gameState }) } @@ -56,7 +57,8 @@ function sendDisplayGameState(ws: WSContext, roomCode: string) { if (!gm) return const allPlayerIds = roomManager.getAllPlayerIds(roomCode) - const gameState = gm.getGameStateForDisplay(allPlayerIds) + const displayNames = roomManager.getPlayerDisplayNames(roomCode) + const gameState = gm.getGameStateForDisplay(allPlayerIds, displayNames) sendTo(ws, { type: "game_state", gameState }) } @@ -211,12 +213,15 @@ export function registerWebSocketRoutes() { type: "act_changed", newAct: result.newAct, }) - // Lock predictions when moving from pre-show to live-event + // Lock predictions and generate bingo cards when entering live-event if (result.newAct === "live-event") { const gm = roomManager.getGameManager(roomCode) if (gm) { gm.lockPredictions() broadcast(roomCode, { type: "predictions_locked" }) + const allPlayerIds = roomManager.getAllPlayerIds(roomCode) + gm.generateBingoCards(allPlayerIds) + broadcastGameStateToAll(roomCode) } } break @@ -258,6 +263,141 @@ export function registerWebSocketRoutes() { broadcastGameStateToAll(roomCode) break } + + case "open_jury_vote": { + if (!sessionId) { + sendError(ws, "Not joined") + return + } + const room = roomManager.getRoom(roomCode) + if (room?.currentAct !== "live-event") { + sendError(ws, "Jury voting is only available during Live Event") + return + } + if (!roomManager.isHost(roomCode, sessionId)) { + sendError(ws, "Only the host can open jury voting") + return + } + const gm = roomManager.getGameManager(roomCode) + if (!gm) { + sendError(ws, "Room not found") + return + } + const entry = gm.getLineup().entries.find((e) => e.country.code === msg.countryCode) + if (!entry) { + sendError(ws, "Invalid country code") + return + } + const result = gm.openJuryRound(entry.country.code, entry.country.name, entry.country.flag) + if ("error" in result) { + sendError(ws, result.error) + return + } + const round = gm.getCurrentJuryRound()! + broadcast(roomCode, { + type: "jury_vote_opened", + roundId: round.id, + countryCode: round.countryCode, + countryName: round.countryName, + countryFlag: round.countryFlag, + }) + broadcastGameStateToAll(roomCode) + break + } + + case "close_jury_vote": { + if (!sessionId) { + sendError(ws, "Not joined") + return + } + if (!roomManager.isHost(roomCode, sessionId)) { + sendError(ws, "Only the host can close jury voting") + return + } + const gm = roomManager.getGameManager(roomCode) + if (!gm) { + sendError(ws, "Room not found") + return + } + const result = gm.closeJuryRound() + if ("error" in result) { + sendError(ws, result.error) + return + } + broadcast(roomCode, { + type: "jury_vote_closed", + countryCode: result.countryCode, + countryName: result.countryName, + countryFlag: result.countryFlag, + averageRating: result.averageRating, + totalVotes: result.totalVotes, + }) + broadcastGameStateToAll(roomCode) + break + } + + case "submit_jury_vote": { + if (!sessionId) { + sendError(ws, "Not joined") + return + } + if (roomManager.getRoom(roomCode)?.currentAct !== "live-event") { + sendError(ws, "Jury voting is only available during Live Event") + return + } + const playerId = roomManager.getPlayerIdBySession(roomCode, sessionId) + const gm = roomManager.getGameManager(roomCode) + if (!playerId || !gm) { + sendError(ws, "Room not found") + return + } + const result = gm.submitJuryVote(playerId, msg.rating) + if ("error" in result) { + sendError(ws, result.error) + return + } + sendGameState(ws, roomCode, sessionId) + break + } + + case "tap_bingo_square": { + if (!sessionId) { + sendError(ws, "Not joined") + return + } + if (roomManager.getRoom(roomCode)?.currentAct !== "live-event") { + sendError(ws, "Bingo is only available during Live Event") + return + } + const playerId = roomManager.getPlayerIdBySession(roomCode, sessionId) + const gm = roomManager.getGameManager(roomCode) + if (!playerId || !gm) { + sendError(ws, "Room not found") + return + } + const result = gm.tapBingoSquare(playerId, msg.tropeId) + if ("error" in result) { + sendError(ws, result.error) + return + } + sendGameState(ws, roomCode, sessionId) + if (result.hasBingo) { + const room = roomManager.getRoom(roomCode) + const player = room?.players.find((p) => p.sessionId === sessionId) + if (player) { + const isNew = gm.addBingoAnnouncement(playerId, player.displayName) + if (isNew) { + broadcast(roomCode, { + type: "bingo_announced", + playerId, + displayName: player.displayName, + }) + broadcastGameStateToAll(roomCode) + } + } + } + break + } } },