Files
buzzer-site/src/rooms.ts
2026-03-20 18:22:15 -05:00

100 lines
3.0 KiB
TypeScript

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<unknown> | null;
isConnected: boolean;
}
export interface BuzzerState {
roundOpen: boolean;
buzzOrder: string[];
buzzTimes: Map<string, number>;
}
export interface Room {
id: string;
moderatorSecret: string;
modWs: ServerWebSocket<unknown> | null;
settings: RoomSettings;
players: Map<string, Player>;
buzzerState: BuzzerState;
locked: boolean;
teamLocked: boolean;
}
export const rooms = new Map<string, Room>();
export const wsToPlayer = new Map<ServerWebSocket<unknown>, { 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<unknown>) {
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 {}
}