// Websocket front door. Protocol (JSON messages): // client -> server: // {type:"create", name} create a room, become host // {type:"join", roomId, name} join (or rejoin) a room // {type:"start"} host starts the game // {type:"command", command} a game Command for the engine // server -> client: // {type:"welcome"} on connect // {type:"room", roomId, players, hostId, started} // {type:"events", events} redacted for this recipient // {type:"state", view} redacted full view (after every change) // {type:"error", message} import { WebSocketServer, WebSocket } from "ws"; import type { Command, PlayerId } from "@wizwar/engine"; import { createRoom, getRoom, joinRoom, redactFor, runCommand, startGame, viewForPlayer, type Room, } from "./rooms"; const port = Number(process.env.PORT ?? 8787); const wss = new WebSocketServer({ port }); interface Session { socket: WebSocket; playerId: PlayerId | null; roomId: string | null; } const sessions = new Set(); function send(socket: WebSocket, message: unknown): void { if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(message)); } function roomInfo(room: Room) { return { type: "room", roomId: room.id, players: room.players, hostId: room.hostId, started: room.state !== null, }; } function broadcast(room: Room, makeMessage: (playerId: PlayerId) => unknown): void { for (const s of sessions) { if (s.roomId === room.id && s.playerId) { send(s.socket, makeMessage(s.playerId)); } } } function broadcastRoomState(room: Room): void { broadcast(room, (playerId) => roomInfo(room)); if (room.state) { broadcast(room, (playerId) => ({ type: "state", view: viewForPlayer(room, playerId) })); } } wss.on("connection", (socket) => { const session: Session = { socket, playerId: null, roomId: null }; sessions.add(session); send(socket, { type: "welcome", game: "wizwar" }); socket.on("close", () => sessions.delete(session)); socket.on("message", (data) => { let msg: Record; try { msg = JSON.parse(data.toString()); } catch { return send(socket, { type: "error", message: "invalid JSON" }); } try { switch (msg.type) { case "create": { const name = String(msg.name ?? "").trim(); if (!name) return send(socket, { type: "error", message: "name required" }); const { room, token } = createRoom(name); session.playerId = name; session.roomId = room.id; send(socket, { type: "seat", playerId: name, token }); broadcastRoomState(room); break; } case "join": { const name = String(msg.name ?? "").trim(); const roomId = String(msg.roomId ?? "").trim(); if (!name || !roomId) return send(socket, { type: "error", message: "name and roomId required" }); const room = getRoom(roomId); if (!room) return send(socket, { type: "error", message: "no such room" }); const result = joinRoom(room, name, typeof msg.token === "string" ? msg.token : null); if ("error" in result) return send(socket, { type: "error", message: result.error }); session.playerId = name; session.roomId = room.id; send(socket, { type: "seat", playerId: name, token: result.token }); broadcastRoomState(room); break; } case "start": { const room = session.roomId ? getRoom(session.roomId) : undefined; if (!room || !session.playerId) return send(socket, { type: "error", message: "not in a room" }); if (session.playerId !== room.hostId) return send(socket, { type: "error", message: "only the host can start" }); const result = startGame(room); if ("error" in result) return send(socket, { type: "error", message: result.error }); broadcast(room, (playerId) => ({ type: "events", events: redactFor(result.events, playerId) })); broadcastRoomState(room); break; } case "command": { const room = session.roomId ? getRoom(session.roomId) : undefined; if (!room || !session.playerId) return send(socket, { type: "error", message: "not in a room" }); const result = runCommand(room, session.playerId, msg.command as Command); if ("error" in result) return send(socket, { type: "error", message: result.error }); broadcast(room, (playerId) => ({ type: "events", events: redactFor(result.events, playerId) })); broadcast(room, (playerId) => ({ type: "state", view: viewForPlayer(room, playerId) })); break; } default: send(socket, { type: "error", message: `unknown message type: ${String(msg.type)}` }); } } catch (e) { send(socket, { type: "error", message: e instanceof Error ? e.message : "internal error" }); } }); }); console.log(`wizwar server listening on ws://localhost:${port}`);