const { DEFAULT_SETTINGS, BOT_NAMES, AREA_CLUES } = require("../utils/constants"); const { randomCode, clone } = require("../utils/format"); const ROOMS_KEY = "zmc_rooms"; const CURRENT_ROOM_KEY = "zmc_current_room"; const USER_ID = "player-me"; const ROOM_SCHEMA_VERSION = 2; const ROOM_STATUSES = ["waiting", "preparing", "playing", "paused", "finished", "cancelled"]; const PLAYER_ROLES = ["", "seeker", "hider"]; const PLAYER_STATUSES = ["idle", "ready", "hiding", "seeking", "caught", "spectating", "offline", "quit"]; const DEFAULT_RUNTIME = { demoAccelerated: true, prepareSecondsPerMinute: 1, maxGameSeconds: 180, demoClueIntervalSeconds: 15 }; function isRecord(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function asNumber(value, fallback) { const number = Number(value); return Number.isFinite(number) ? number : fallback; } function assertIntegerInRange(value, min, max, message) { if (!Number.isInteger(value) || value < min || value > max) { throw new Error(message); } } function validateRoomPayload(payload) { const source = isRecord(payload) ? payload : {}; const name = String(source.name || "").trim(); if (!name) throw new Error("请填写房间名称"); const settings = normalizeSettings(source); assertIntegerInRange(settings.durationMinutes, 15, 90, "游戏时长应为 15~90 分钟"); assertIntegerInRange(settings.prepareMinutes, 1, 10, "准备时长应为 1~10 分钟"); assertIntegerInRange(settings.maxPlayers, 4, 30, "房间人数应为 4~30 人"); assertIntegerInRange(settings.seekerCount, 1, 29, "初始寻找者人数应为 1~29 人"); assertIntegerInRange(settings.clueIntervalMinutes, 3, 10, "线索间隔应为 3~10 分钟"); assertIntegerInRange(settings.radiusMeters, 200, 1500, "活动半径应为 200~1500 米"); if (settings.seekerCount >= settings.maxPlayers) { throw new Error("初始寻找者必须少于房间人数上限"); } return { ...settings, name: name.slice(0, 16) }; } function normalizeSettings(settings) { const source = isRecord(settings) ? settings : {}; const merged = { ...DEFAULT_SETTINGS, ...source }; return { mode: merged.mode === "classic" ? "classic" : "infection", durationMinutes: asNumber(merged.durationMinutes, DEFAULT_SETTINGS.durationMinutes), prepareMinutes: asNumber(merged.prepareMinutes, DEFAULT_SETTINGS.prepareMinutes), maxPlayers: asNumber(merged.maxPlayers, DEFAULT_SETTINGS.maxPlayers), seekerCount: asNumber(merged.seekerCount, DEFAULT_SETTINGS.seekerCount), clueIntervalMinutes: asNumber(merged.clueIntervalMinutes, DEFAULT_SETTINGS.clueIntervalMinutes), radiusMeters: asNumber(merged.radiusMeters, DEFAULT_SETTINGS.radiusMeters), skillsEnabled: merged.skillsEnabled !== false }; } function normalizeRuntime(runtime) { const source = isRecord(runtime) ? runtime : {}; return { demoAccelerated: source.demoAccelerated !== false, prepareSecondsPerMinute: Math.max(1, asNumber( source.prepareSecondsPerMinute, DEFAULT_RUNTIME.prepareSecondsPerMinute )), maxGameSeconds: Math.max(1, asNumber( source.maxGameSeconds, DEFAULT_RUNTIME.maxGameSeconds )), demoClueIntervalSeconds: Math.max(1, asNumber( source.demoClueIntervalSeconds, DEFAULT_RUNTIME.demoClueIntervalSeconds )) }; } function getPrepareDurationSeconds(room) { if (room.runtime.demoAccelerated) { return room.settings.prepareMinutes * room.runtime.prepareSecondsPerMinute; } return room.settings.prepareMinutes * 60; } function getGameDurationSeconds(room) { const configured = room.settings.durationMinutes * 60; return room.runtime.demoAccelerated ? Math.min(configured, room.runtime.maxGameSeconds) : configured; } function getClueIntervalSeconds(room) { return room.runtime.demoAccelerated ? room.runtime.demoClueIntervalSeconds : room.settings.clueIntervalMinutes * 60; } function normalizePlayer(player, index, roomCreatedAt) { const source = isRecord(player) ? player : {}; const fallbackName = `玩家${index + 1}`; const name = String(source.name || fallbackName).slice(0, 10); const ready = Boolean(source.ready); return { ...source, id: String(source.id || `legacy-player-${index}`), name, avatar: String(source.avatar || name.slice(0, 1) || "?"), isHost: Boolean(source.isHost), isBot: Boolean(source.isBot), ready, role: PLAYER_ROLES.includes(source.role) ? source.role : "", status: PLAYER_STATUSES.includes(source.status) ? source.status : ready ? "ready" : "idle", caughtAt: source.caughtAt || null, caughtBy: source.caughtBy || null, captures: Math.max(0, asNumber(source.captures, 0)), initialRole: PLAYER_ROLES.includes(source.initialRole) ? source.initialRole : source.role || "", skillsUsed: { radar: Math.max(0, asNumber(source.skillsUsed && source.skillsUsed.radar, 0)), silent: Math.max(0, asNumber(source.skillsUsed && source.skillsUsed.silent, 0)) }, silentPending: Boolean(source.silentPending), joinedAt: asNumber(source.joinedAt, roomCreatedAt) }; } function normalizeEvent(event, index, roomCreatedAt) { const source = isRecord(event) ? event : {}; return { ...source, id: String(source.id || `legacy-event-${roomCreatedAt}-${index}`), type: String(source.type || "system"), message: String(source.message || "游戏状态已更新"), createdAt: asNumber(source.createdAt, roomCreatedAt) }; } function normalizeRoom(room, codeHint = "") { if (!isRecord(room)) return null; const code = String(room.code || codeHint || ""); if (!/^\d{6}$/.test(code)) return null; const createdAt = asNumber(room.createdAt, Date.now()); const players = Array.isArray(room.players) ? room.players.map((player, index) => normalizePlayer(player, index, createdAt)) : []; const events = Array.isArray(room.events) ? room.events.slice(0, 20).map((event, index) => normalizeEvent(event, index, createdAt)) : []; return { ...room, schemaVersion: ROOM_SCHEMA_VERSION, id: String(room.id || `room-${code}`), code, name: String(room.name || "未命名游戏").slice(0, 16), hostId: String(room.hostId || (players[0] && players[0].id) || ""), status: ROOM_STATUSES.includes(room.status) ? room.status : "waiting", settings: normalizeSettings(room.settings), runtime: normalizeRuntime(room.runtime), players, events, captureTokensUsed: Array.isArray(room.captureTokensUsed) ? room.captureTokensUsed.slice(-50).map(String) : [], winner: room.winner === "hiders" || room.winner === "seekers" ? room.winner : "", phaseStartedAt: room.phaseStartedAt || null, phaseEndsAt: room.phaseEndsAt || null, nextClueAt: room.nextClueAt || null, finishedAt: room.finishedAt || null, pausedAt: room.pausedAt || null, pausedRemainingMs: Math.max(0, asNumber(room.pausedRemainingMs, 0)), pausedClueRemainingMs: Math.max(0, asNumber(room.pausedClueRemainingMs, 0)), finishReason: String(room.finishReason || ""), resultSnapshot: isRecord(room.resultSnapshot) ? room.resultSnapshot : null, createdAt, updatedAt: asNumber(room.updatedAt, createdAt) }; } function readRooms() { try { const rooms = wx.getStorageSync(ROOMS_KEY); return isRecord(rooms) ? rooms : {}; } catch (error) { return {}; } } function writeRooms(rooms) { wx.setStorageSync(ROOMS_KEY, rooms); } function saveRoom(room) { const rooms = readRooms(); const normalized = normalizeRoom(room); if (!normalized) throw new Error("房间数据无效"); normalized.updatedAt = Date.now(); rooms[normalized.code] = normalized; writeRooms(rooms); return clone(normalized); } function makePlayer(id, name, isHost = false, isBot = false) { return { id, name, avatar: name.slice(0, 1), isHost, isBot, ready: isHost, role: "", initialRole: "", status: "idle", caughtAt: null, caughtBy: null, captures: 0, skillsUsed: { radar: 0, silent: 0 }, silentPending: false, joinedAt: Date.now() }; } function makeDemoRoom() { const room = { schemaVersion: ROOM_SCHEMA_VERSION, id: "room-demo", code: "731204", name: "周末公园追逃局", hostId: "demo-host", status: "waiting", settings: { ...DEFAULT_SETTINGS, durationMinutes: 20, prepareMinutes: 1 }, runtime: { ...DEFAULT_RUNTIME }, players: [ makePlayer("demo-host", "阿树", true, true), makePlayer("demo-2", "小满", false, true), makePlayer("demo-3", "木木", false, true) ], events: [], createdAt: Date.now() }; room.players.forEach((player) => { player.ready = true; }); return room; } function ensureSeedData() { const rooms = readRooms(); let changed = false; Object.keys(rooms).forEach((code) => { const normalized = normalizeRoom(rooms[code], code); if (normalized) { rooms[code] = normalized; } else { delete rooms[code]; } changed = true; }); if (!rooms["731204"]) { rooms["731204"] = makeDemoRoom(); changed = true; } if (changed) { writeRooms(rooms); } } function createRoom(payload) { const validated = validateRoomPayload(payload); const rooms = readRooms(); let code = randomCode(); while (rooms[code]) code = randomCode(); const room = { schemaVersion: ROOM_SCHEMA_VERSION, id: `room-${Date.now()}`, code, name: validated.name, hostId: USER_ID, status: "waiting", settings: { ...DEFAULT_SETTINGS, ...validated }, runtime: { ...DEFAULT_RUNTIME }, players: [makePlayer(USER_ID, "我", true, false)], events: [ { id: `event-${Date.now()}`, type: "system", message: "房间已创建,邀请好友加入吧", createdAt: Date.now() } ], createdAt: Date.now() }; saveRoom(room); wx.setStorageSync(CURRENT_ROOM_KEY, code); return clone(room); } function getRoom(code) { const room = normalizeRoom(readRooms()[code], code); return room ? clone(room) : null; } function getCurrentRoom() { let code = ""; try { code = wx.getStorageSync(CURRENT_ROOM_KEY); } catch (error) { return null; } return code ? getRoom(code) : null; } function setCurrentRoom(code) { wx.setStorageSync(CURRENT_ROOM_KEY, code); } function joinRoom(code, displayName = "我") { const normalizedCode = String(code || "").trim(); if (!/^\d{6}$/.test(normalizedCode)) { throw new Error("请输入正确的六位房间码"); } const normalizedName = String(displayName || "").trim().slice(0, 10); if (!normalizedName) throw new Error("请输入游戏昵称"); const rooms = readRooms(); const room = normalizeRoom(rooms[normalizedCode], normalizedCode); if (!room) { throw new Error("没有找到这个房间,请检查房间码"); } if (room.status === "finished") throw new Error("这场游戏已经结束"); if (room.status === "cancelled") throw new Error("房间已被房主取消"); if (room.status !== "waiting") throw new Error("游戏已经开始,暂时无法加入"); const existing = room.players.find((player) => player.id === USER_ID); if (!existing) { if (room.players.length >= room.settings.maxPlayers) { throw new Error("房间人数已满"); } const player = makePlayer(USER_ID, normalizedName, false, false); if (room.code === "731204") { player.ready = true; player.status = "ready"; } room.players.push(player); } else { existing.name = normalizedName; existing.avatar = normalizedName.slice(0, 1); } saveRoom(room); setCurrentRoom(normalizedCode); return clone(room); } function addDemoPlayers(code, targetCount = 6) { const room = getRoom(code); if (!room || room.status !== "waiting") return room; const usedNames = room.players.map((player) => player.name); const available = BOT_NAMES.filter((name) => !usedNames.includes(name)); while ( room.players.length < targetCount && room.players.length < room.settings.maxPlayers && available.length ) { const name = available.shift(); const player = makePlayer(`bot-${Date.now()}-${room.players.length}`, name, false, true); player.ready = true; room.players.push(player); } return saveRoom(room); } function toggleReady(code, playerId = USER_ID) { const room = getRoom(code); if (!room) throw new Error("房间不存在"); if (room.status !== "waiting") throw new Error("游戏已开始,不能修改准备状态"); const player = room && room.players.find((item) => item.id === playerId); if (!player) throw new Error("你不在当前房间中"); player.ready = !player.ready; player.status = player.ready ? "ready" : "idle"; return saveRoom(room); } function startGame(code, operatorId = USER_ID) { const room = getRoom(code); if (!room) throw new Error("房间不存在"); if (room.status !== "waiting") throw new Error("当前房间不能重复开始"); const canControlDemo = room.code === "731204" && operatorId === USER_ID; if (room.hostId !== operatorId && !canControlDemo) { throw new Error("只有房主可以开始游戏"); } if (room.players.length < 4) throw new Error("至少需要 4 名玩家"); if (room.settings.seekerCount >= room.players.length) { throw new Error("寻找者人数必须少于当前玩家人数"); } if (room.players.some((player) => !player.ready)) { throw new Error("还有玩家尚未准备好"); } const seekerCount = room.settings.seekerCount; const sorted = [...room.players].sort((a, b) => { if (a.id === USER_ID) return -1; if (b.id === USER_ID) return 1; return a.joinedAt - b.joinedAt; }); sorted.forEach((player, index) => { player.role = index < seekerCount ? "seeker" : "hider"; player.initialRole = player.role; player.status = player.role === "seeker" ? "seeking" : "hiding"; player.ready = true; player.skillsUsed = { radar: 0, silent: 0 }; player.silentPending = false; }); room.players = sorted; room.status = "preparing"; room.phaseStartedAt = Date.now(); room.phaseEndsAt = room.phaseStartedAt + getPrepareDurationSeconds(room) * 1000; room.gameDurationSeconds = getGameDurationSeconds(room); room.captureTokensUsed = []; room.events.unshift({ id: `event-${Date.now()}`, type: "system", message: "身份已分配,躲藏者请立即出发", createdAt: Date.now() }); return saveRoom(room); } function beginPlaying(code) { const room = getRoom(code); if (!room) throw new Error("房间不存在"); if (room.status !== "preparing") throw new Error("当前房间不能进入追逃阶段"); const scheduledStartAt = room.phaseEndsAt || Date.now(); room.status = "playing"; room.phaseStartedAt = scheduledStartAt; room.gameDurationSeconds = room.gameDurationSeconds || getGameDurationSeconds(room); room.phaseEndsAt = scheduledStartAt + room.gameDurationSeconds * 1000; room.nextClueAt = scheduledStartAt + getClueIntervalSeconds(room) * 1000; room.events.unshift({ id: `event-${Date.now()}`, type: "system", message: "追逃开始!请留意安全边界", createdAt: Date.now() }); return saveRoom(room); } function addEvent(code, message, type = "system") { const room = getRoom(code); if (!room) return null; room.events.unshift({ id: `event-${Date.now()}-${Math.random()}`, type, message, createdAt: Date.now() }); room.events = room.events.slice(0, 20); return saveRoom(room); } function revealClue(code) { const room = getRoom(code); if (!room) throw new Error("房间不存在"); if (room.status !== "playing") throw new Error("当前阶段不能广播线索"); const silentPlayers = room.players.filter( (player) => player.role === "hider" && player.status === "hiding" && player.silentPending ); silentPlayers.forEach((player) => { player.silentPending = false; }); const area = AREA_CLUES[Math.floor(Math.random() * AREA_CLUES.length)]; const message = silentPlayers.length ? `${silentPlayers.length} 名目标的静默已生效;${area}` : area; room.events.unshift({ id: `event-${Date.now()}-${Math.random()}`, type: "clue", message, createdAt: Date.now() }); room.events = room.events.slice(0, 20); room.nextClueAt = Date.now() + getClueIntervalSeconds(room) * 1000; return saveRoom(room); } function useSkill(code, skill, playerId = USER_ID) { const room = getRoom(code); if (!room) throw new Error("房间不存在"); if (room.status !== "playing") throw new Error("当前阶段不能使用技能"); if (!room.settings.skillsEnabled) throw new Error("本局未启用角色技能"); const player = room.players.find((item) => item.id === playerId); if (!player) throw new Error("你不在当前房间中"); const expectedSkill = player.role === "seeker" ? "radar" : "silent"; if (skill !== expectedSkill) throw new Error("当前身份不能使用这个技能"); if (player.status !== (player.role === "seeker" ? "seeking" : "hiding")) { throw new Error("当前玩家状态不能使用技能"); } if (player.skillsUsed[skill] >= 1) throw new Error("本局技能已经使用过"); player.skillsUsed[skill] += 1; if (skill === "radar") { const nearby = Math.random() > 0.45; room.events.unshift({ id: `event-${Date.now()}-${Math.random()}`, type: "skill", message: nearby ? "雷达回应:附近 150 米内可能有人" : "雷达回应:附近暂未检测到目标", createdAt: Date.now() }); } else { player.silentPending = true; room.events.unshift({ id: `event-${Date.now()}-${Math.random()}`, type: "skill", message: "有目标已开启静默信号", createdAt: Date.now() }); } room.events = room.events.slice(0, 20); return saveRoom(room); } function captureTokenKey(targetId, timeWindow) { return `${targetId}:${timeWindow}`; } function hashCaptureToken(value) { let hash = 2166136261; for (let index = 0; index < value.length; index += 1) { hash ^= value.charCodeAt(index); hash = Math.imul(hash, 16777619); } return String(Math.abs(hash) % 10000).padStart(4, "0"); } function getCaptureCode(code, targetId = USER_ID, now = Date.now()) { const room = getRoom(code); if (!room) return ""; const target = room.players.find( (player) => player.id === targetId && player.role === "hider" && player.status === "hiding" ); if (!target) return ""; const timeWindow = Math.floor(now / 30000); return hashCaptureToken(`${room.code}:${target.id}:${timeWindow}`); } function capture(code, targetId, submittedCode, seekerId = USER_ID, now = Date.now()) { const room = getRoom(code); if (!room || room.status !== "playing") { throw new Error("当前不能提交抓捕"); } const seeker = room.players.find((player) => player.id === seekerId); if (!seeker || seeker.role !== "seeker" || seeker.status !== "seeking") { throw new Error("只有正在游戏的寻找者可以提交抓捕"); } const target = room.players.find((player) => player.id === targetId); if (!target) throw new Error("没有找到抓捕目标"); const timeWindow = Math.floor(now / 30000); const tokenKey = captureTokenKey(target.id, timeWindow); if (room.captureTokensUsed.includes(tokenKey)) throw new Error("口令已经使用过"); if (target.role !== "hider" || target.status !== "hiding") { throw new Error("目标已不在躲藏状态"); } if (String(submittedCode) !== getCaptureCode(code, target.id, now)) { throw new Error("口令不正确或已经失效"); } room.captureTokensUsed.push(tokenKey); room.captureTokensUsed = room.captureTokensUsed.slice(-50); target.caughtAt = now; target.caughtBy = seeker.id; seeker.captures += 1; if (room.settings.mode === "infection") { target.role = "seeker"; target.status = "seeking"; } else { target.status = "caught"; } room.events.unshift({ id: `event-${Date.now()}`, type: "capture", message: `${seeker.name} 抓到了 ${target.name}`, createdAt: Date.now() }); const survivors = room.players.filter( (player) => player.role === "hider" && player.status === "hiding" ); if (!survivors.length) { finishGameObject(room, "seekers"); } return saveRoom(room); } function makeResultSnapshot(room, winner, finishedAt, reason) { const gameStartedAt = room.phaseStartedAt || room.createdAt; const elapsedSeconds = Math.max(0, Math.floor((finishedAt - gameStartedAt) / 1000)); const players = room.players.map((player) => { const survivedUntil = player.caughtAt || finishedAt; return { id: player.id, name: player.name, avatar: player.avatar, initialRole: player.initialRole || player.role, finalRole: player.role, finalStatus: player.status, captures: player.captures, caughtAt: player.caughtAt, survivedSeconds: player.initialRole === "hider" ? Math.max(0, Math.floor((survivedUntil - gameStartedAt) / 1000)) : 0, joinedAt: player.joinedAt }; }); players.sort((a, b) => { if (b.captures !== a.captures) return b.captures - a.captures; if (b.survivedSeconds !== a.survivedSeconds) return b.survivedSeconds - a.survivedSeconds; if (a.joinedAt !== b.joinedAt) return a.joinedAt - b.joinedAt; return a.id.localeCompare(b.id); }); return { winner, reason, finishedAt, elapsedSeconds, eventCount: room.events.length, players }; } function finishGameObject(room, winner, reason = "completed") { const finishedAt = Date.now(); room.status = "finished"; room.finishedAt = finishedAt; room.winner = winner; room.finishReason = reason; room.phaseEndsAt = finishedAt; room.resultSnapshot = makeResultSnapshot(room, winner, finishedAt, reason); } function finishGame(code, winner, operatorId = USER_ID, reason = "manual") { const room = getRoom(code); if (!room) throw new Error("房间不存在"); if (!["preparing", "playing", "paused"].includes(room.status)) { throw new Error("当前房间不能结束游戏"); } const canControlDemo = room.code === "731204" && operatorId === USER_ID; if (operatorId && room.hostId !== operatorId && !canControlDemo) { throw new Error("只有房主可以提前结束游戏"); } const survivors = room.players.filter( (player) => player.role === "hider" && player.status === "hiding" ); finishGameObject(room, winner || (survivors.length ? "hiders" : "seekers"), reason); return saveRoom(room); } function pauseGame(code, operatorId = USER_ID) { const room = getRoom(code); if (!room) throw new Error("房间不存在"); if (room.status !== "playing") throw new Error("只有追逃阶段可以暂停"); const canControlDemo = room.code === "731204" && operatorId === USER_ID; if (room.hostId !== operatorId && !canControlDemo) throw new Error("只有房主可以暂停游戏"); const now = Date.now(); room.status = "paused"; room.pausedAt = now; room.pausedRemainingMs = Math.max(0, room.phaseEndsAt - now); room.pausedClueRemainingMs = room.nextClueAt ? Math.max(0, room.nextClueAt - now) : 0; room.events.unshift({ id: `event-${now}`, type: "system", message: "房主暂停了游戏,请所有玩家留在安全位置", createdAt: now }); return saveRoom(room); } function resumeGame(code, operatorId = USER_ID) { const room = getRoom(code); if (!room) throw new Error("房间不存在"); if (room.status !== "paused") throw new Error("当前游戏没有暂停"); const canControlDemo = room.code === "731204" && operatorId === USER_ID; if (room.hostId !== operatorId && !canControlDemo) throw new Error("只有房主可以继续游戏"); const now = Date.now(); room.status = "playing"; room.phaseEndsAt = now + room.pausedRemainingMs; room.nextClueAt = room.pausedClueRemainingMs ? now + room.pausedClueRemainingMs : now + getClueIntervalSeconds(room) * 1000; room.pausedAt = null; room.pausedRemainingMs = 0; room.pausedClueRemainingMs = 0; room.events.unshift({ id: `event-${now}`, type: "system", message: "房主继续了游戏", createdAt: now }); return saveRoom(room); } function quitGame(code, playerId = USER_ID) { const room = getRoom(code); if (!room) throw new Error("房间不存在"); if (!["preparing", "playing", "paused"].includes(room.status)) { throw new Error("当前不能退出游戏"); } const player = room.players.find((item) => item.id === playerId); if (!player) throw new Error("你不在当前房间中"); if (player.status === "quit") throw new Error("玩家已经退出游戏"); player.status = "quit"; player.ready = false; room.events.unshift({ id: `event-${Date.now()}`, type: "safety", message: `${player.name} 已安全退出并前往集合点`, createdAt: Date.now() }); const survivors = room.players.filter( (item) => item.role === "hider" && item.status === "hiding" ); if (!survivors.length) finishGameObject(room, "seekers", "all_hiders_left"); return saveRoom(room); } function resetRoom(code) { const room = getRoom(code); if (!room) return null; room.status = "waiting"; room.winner = ""; room.finishReason = ""; room.phaseEndsAt = null; room.phaseStartedAt = null; room.nextClueAt = null; room.gameDurationSeconds = null; room.finishedAt = null; room.pausedAt = null; room.pausedRemainingMs = 0; room.pausedClueRemainingMs = 0; room.resultSnapshot = null; room.events = []; room.players.forEach((player) => { player.role = ""; player.initialRole = ""; player.status = "ready"; player.ready = true; player.caughtAt = null; player.caughtBy = null; player.captures = 0; player.skillsUsed = { radar: 0, silent: 0 }; player.silentPending = false; }); room.captureTokensUsed = []; return saveRoom(room); } function clearCurrentRoom() { wx.removeStorageSync(CURRENT_ROOM_KEY); } module.exports = { USER_ID, ensureSeedData, createRoom, getRoom, getCurrentRoom, setCurrentRoom, joinRoom, addDemoPlayers, toggleReady, startGame, beginPlaying, revealClue, useSkill, getCaptureCode, capture, finishGame, pauseGame, resumeGame, quitGame, resetRoom, clearCurrentRoom };