import type { ServerWebSocket } from "bun"; export interface RoomSettings { mode: "individual" | "teams"; numTeams: number; teamNames: string[]; playerPickTeam: boolean; showBuzzOrder: boolean; buzzerLockout: boolean; timerSeconds: number; } export interface Player { id: string; name: string; teamIndex: number | null; ws: ServerWebSocket | null; isConnected: boolean; } export interface BuzzerState { roundOpen: boolean; buzzOrder: string[]; buzzTimes: Map; } export interface Room { id: string; moderatorSecret: string; modWs: ServerWebSocket | null; settings: RoomSettings; players: Map; buzzerState: BuzzerState; locked: boolean; teamLocked: boolean; } export const rooms = new Map(); export const wsToPlayer = new Map, { roomId: string; playerId: string }>(); // Infinite Greek alphabet: Alpha, Beta, ... Omega, Alpha II, Beta II, ... const GREEK = ["Alpha","Beta","Gamma","Delta","Epsilon","Zeta","Eta","Theta","Iota","Kappa","Lambda","Mu","Nu","Xi","Omicron","Pi","Rho","Sigma","Tau","Upsilon","Phi","Chi","Psi","Omega"]; export function greekName(i: number): string { const cycle = Math.floor(i / GREEK.length); const name = GREEK[i % GREEK.length]; return cycle === 0 ? name : `${name} ${toRoman(cycle + 1)}`; } function toRoman(n: number): string { const vals = [1000,900,500,400,100,90,50,40,10,9,5,4,1]; const syms = ["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"]; let out = ""; for (let i = 0; i < vals.length; i++) while (n >= vals[i]) { out += syms[i]; n -= vals[i]; } return out; } export function genId(len = 8): string { const c = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; let s = ""; for (let i = 0; i < len; i++) s += c[Math.floor(Math.random() * c.length)]; return s; } export function sanitize(v: unknown, max = 32): string { return String(v ?? "").trim().replace(/[<>"'`]/g, "").slice(0, max); } export function freshBuzzer(): BuzzerState { return { roundOpen: false, buzzOrder: [], buzzTimes: new Map() }; } export function publicRoom(room: Room) { return { id: room.id, settings: room.settings, locked: room.locked, teamLocked: room.teamLocked, modOnline: room.modWs !== null, buzzerState: { roundOpen: room.buzzerState.roundOpen, buzzOrder: room.buzzerState.buzzOrder, }, players: Array.from(room.players.values()).map(p => ({ id: p.id, name: p.name, teamIndex: p.teamIndex, isConnected: p.isConnected, })), }; } export function broadcast(room: Room, msg: object, exclude?: ServerWebSocket) { const d = JSON.stringify(msg); for (const p of room.players.values()) if (p.ws && p.isConnected && p.ws !== exclude) try { p.ws.send(d); } catch {} if (room.modWs && room.modWs !== exclude) try { room.modWs.send(d); } catch {} } export function toMod(room: Room, msg: object) { if (room.modWs) try { room.modWs.send(JSON.stringify(msg)); } catch {} }