feat(games): integrate game WS handler into server

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
syntaxbullet
2026-04-02 13:28:41 +02:00
parent 33a1848096
commit 069c0b93ef
2 changed files with 117 additions and 2 deletions

View File

@@ -0,0 +1,89 @@
import { RoomManager } from "./RoomManager";
import type { GameWsClientMessage, PlayerInfo } from "./types";
import { logger } from "@shared/lib/logger";
export const roomManager = new RoomManager();
interface WsContext {
playerId: string;
username: string;
send: (data: string) => void;
subscribe: (channel: string) => void;
unsubscribe: (channel: string) => void;
publish: (channel: string, data: string) => void;
}
export function handleGameMessage(msg: GameWsClientMessage, ctx: WsContext): void {
switch (msg.type) {
case "CREATE_ROOM": {
const result = roomManager.createRoom(msg.gameType, ctx.playerId);
if (!result.ok) {
ctx.send(JSON.stringify({ type: "ERROR", message: result.error }));
return;
}
ctx.subscribe(`room:${result.roomId}`);
ctx.send(JSON.stringify({ type: "ROOM_CREATED", roomId: result.roomId, gameSlug: msg.gameType }));
ctx.publish("lobby", JSON.stringify({ type: "ROOM_LIST_UPDATE", rooms: roomManager.listRooms() }));
logger.debug("games", `Room created: ${result.roomId} (${msg.gameType}) by ${ctx.playerId}`);
break;
}
case "JOIN_ROOM": {
const result = roomManager.joinRoom(msg.roomId, ctx.playerId, msg.as);
if (!result.ok) {
ctx.send(JSON.stringify({ type: "ERROR", message: result.error }));
return;
}
ctx.subscribe(`room:${msg.roomId}`);
const playerInfo: PlayerInfo = { discordId: ctx.playerId, username: ctx.username };
ctx.publish(`room:${msg.roomId}`, JSON.stringify({ type: "PLAYER_JOINED", roomId: msg.roomId, player: playerInfo, as: msg.as }));
if (result.started) {
const spectatorView = roomManager.getSpectatorView(msg.roomId);
ctx.publish(`room:${msg.roomId}`, JSON.stringify({ type: "GAME_STARTED", roomId: msg.roomId, state: spectatorView }));
} else {
const room = roomManager.getRoom(msg.roomId);
if (room && room.status === "playing") {
const view = msg.as === "spectator"
? roomManager.getSpectatorView(msg.roomId)
: roomManager.getPlayerView(msg.roomId, ctx.playerId);
ctx.send(JSON.stringify({ type: "GAME_STATE", roomId: msg.roomId, state: view }));
}
}
ctx.publish("lobby", JSON.stringify({ type: "ROOM_LIST_UPDATE", rooms: roomManager.listRooms() }));
break;
}
case "LEAVE_ROOM": {
roomManager.leaveRoom(msg.roomId, ctx.playerId);
ctx.unsubscribe(`room:${msg.roomId}`);
ctx.publish(`room:${msg.roomId}`, JSON.stringify({ type: "PLAYER_LEFT", roomId: msg.roomId, playerId: ctx.playerId }));
ctx.publish("lobby", JSON.stringify({ type: "ROOM_LIST_UPDATE", rooms: roomManager.listRooms() }));
break;
}
case "GAME_ACTION": {
const result = roomManager.handleAction(msg.roomId, ctx.playerId, msg.action);
if (!result.ok) {
ctx.send(JSON.stringify({ type: "ERROR", message: result.error }));
return;
}
const spectatorView = roomManager.getSpectatorView(msg.roomId);
ctx.publish(`room:${msg.roomId}`, JSON.stringify({ type: "GAME_UPDATE", roomId: msg.roomId, state: spectatorView }));
if (result.gameOver) {
ctx.publish(`room:${msg.roomId}`, JSON.stringify({
type: "GAME_ENDED",
roomId: msg.roomId,
winner: result.gameOver.winner,
reason: result.gameOver.reason,
}));
ctx.publish("lobby", JSON.stringify({ type: "ROOM_LIST_UPDATE", rooms: roomManager.listRooms() }));
}
break;
}
}
}

View File

@@ -12,6 +12,8 @@ import { logger } from "@shared/lib/logger";
import { handleRequest } from "./routes"; import { handleRequest } from "./routes";
import { getFullDashboardStats } from "./routes/stats.helper"; import { getFullDashboardStats } from "./routes/stats.helper";
import { join } from "path"; import { join } from "path";
import { handleGameMessage, roomManager } from "./games/ws-handler";
import { getSession } from "./routes/auth.routes";
export interface WebServerConfig { export interface WebServerConfig {
port?: number; port?: number;
@@ -91,7 +93,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
const { port = 3000, hostname = "localhost" } = config; const { port = 3000, hostname = "localhost" } = config;
// Configuration constants // Configuration constants
const MAX_CONNECTIONS = 10; const MAX_CONNECTIONS = 200;
const MAX_PAYLOAD_BYTES = 16384; // 16KB const MAX_PAYLOAD_BYTES = 16384; // 16KB
const IDLE_TIMEOUT_SECONDS = 60; const IDLE_TIMEOUT_SECONDS = 60;
@@ -112,7 +114,8 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
return new Response("Connection limit reached", { status: 429 }); return new Response("Connection limit reached", { status: 429 });
} }
const success = server.upgrade(req); const session = getSession(req);
const success = server.upgrade(req, { data: { session } });
if (success) return undefined; if (success) return undefined;
return new Response("WebSocket upgrade failed", { status: 400 }); return new Response("WebSocket upgrade failed", { status: 400 });
} }
@@ -137,6 +140,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
*/ */
open(ws) { open(ws) {
ws.subscribe("dashboard"); ws.subscribe("dashboard");
ws.subscribe("lobby");
logger.debug("web", `Client connected. Total: ${server.pendingWebSockets}`); logger.debug("web", `Client connected. Total: ${server.pendingWebSockets}`);
// Send initial stats // Send initial stats
@@ -144,6 +148,9 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
ws.send(JSON.stringify({ type: "STATS_UPDATE", data: stats })); ws.send(JSON.stringify({ type: "STATS_UPDATE", data: stats }));
}); });
// Send initial room list
ws.send(JSON.stringify({ type: "ROOM_LIST_UPDATE", rooms: roomManager.listRooms() }));
// Start broadcast interval if this is the first client // Start broadcast interval if this is the first client
if (!statsBroadcastInterval) { if (!statsBroadcastInterval) {
statsBroadcastInterval = setInterval(async () => { statsBroadcastInterval = setInterval(async () => {
@@ -183,6 +190,24 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
if (parsed.data.type === "PING") { if (parsed.data.type === "PING") {
ws.send(JSON.stringify({ type: "PONG" })); ws.send(JSON.stringify({ type: "PONG" }));
} }
// Route game messages
const gameTypes = ["CREATE_ROOM", "JOIN_ROOM", "LEAVE_ROOM", "GAME_ACTION"];
if (gameTypes.includes(parsed.data.type)) {
const sessionData = (ws.data as any)?.session;
if (!sessionData) {
ws.send(JSON.stringify({ type: "ERROR", message: "Not authenticated" }));
return;
}
handleGameMessage(parsed.data as any, {
playerId: sessionData.discordId,
username: sessionData.username,
send: (data) => ws.send(data),
subscribe: (channel) => ws.subscribe(channel),
unsubscribe: (channel) => ws.unsubscribe(channel),
publish: (channel, data) => server.publish(channel, data),
});
}
} catch (e) { } catch (e) {
logger.error("web", "Failed to handle message", e); logger.error("web", "Failed to handle message", e);
} }
@@ -194,6 +219,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
*/ */
close(ws) { close(ws) {
ws.unsubscribe("dashboard"); ws.unsubscribe("dashboard");
ws.unsubscribe("lobby");
logger.debug("web", `Client disconnected. Total remaining: ${server.pendingWebSockets}`); logger.debug("web", `Client disconnected. Total remaining: ${server.pendingWebSockets}`);
// Stop broadcast interval if no clients left // Stop broadcast interval if no clients left