@zhuweiyou/dsh-holdem 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -20
- package/lib/client.js +331 -283
- package/lib/client.js.map +1 -1
- package/lib/index.js +41 -10
- package/lib/index.js.map +1 -1
- package/lib/types/client/ActionBar.d.ts +6 -1
- package/lib/types/client/ActionBar.d.ts.map +1 -1
- package/lib/types/client/Table.d.ts.map +1 -1
- package/lib/types/client/i18n.d.ts.map +1 -1
- package/lib/types/client/store.d.ts.map +1 -1
- package/lib/types/config.d.ts +5 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/invariant.d.ts +6 -1
- package/lib/types/invariant.d.ts.map +1 -1
- package/lib/types/net/ws.d.ts.map +1 -1
- package/lib/types/room/RoomManager.d.ts +10 -0
- package/lib/types/room/RoomManager.d.ts.map +1 -1
- package/lib/types/types.d.ts +2 -0
- package/lib/types/types.d.ts.map +1 -1
- package/lib/worker.js +41 -9
- package/lib/worker.js.map +1 -1
- package/package.json +1 -1
- package/worker/index.ts +3 -0
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["level","name","out: Record<string, string>","chunks: Buffer[]","resolve","reject","escapeHtml","name","headers: Record<string, string | string[]>","send","payload: Record<string, unknown>","state: SocketState","parsed: { type?: string } | null","name","candidates: WranglerCommand[]","resolvedCommand: WranglerCommand | null","httpRequest","resolve","candidates: string[]","noopStore: HoldemStore","ready: Promise<void> | null","cryptoBelow: RandomBelow","deck: Card[]","empty: LegalActions","paid","pots: Pot[]","returned: { seat: number; amount: number }[]","results: PotAward[]","best: HandValue | null","STREET_ORDER: Street[]","players: HandPlayer[]","state: HandState","shown: ShownHandView[]","handle","room","room: LiveRoom","player","out: Session[]","tickHand","seats: SeatView[]","roomView: RoomView","leaderboard: LeaderboardEntry[]","player: LivePlayer","session: Session","event: ServerEvent","lanProxy: LanProxy | null"],"sources":["../src/invariant.ts","../src/config.ts","../src/types.ts","../src/net/auth.ts","../src/net/json.ts","../src/net/join-page.ts","../src/net/play-page.ts","../src/net/routes.ts","../src/net/ws.ts","../src/net/which.ts","../src/net/cloudflare.ts","../src/net/lan-proxy.ts","../src/net/network.ts","../src/persist/noop.ts","../src/persist/store.ts","../src/room/ids.ts","../src/engine/cards.ts","../src/engine/deck.ts","../src/engine/betting.ts","../src/engine/pots.ts","../src/engine/rank.ts","../src/engine/hand.ts","../src/room/RoomManager.ts","../src/index.ts"],"sourcesContent":["/** 全局常量:协议、上限、时间与筹码档位。 */\n\nexport const PACKAGE_NAME = '@zhuweiyou/dsh-holdem'\nexport const CLIENT_ID = '@zhuweiyou/dsh-holdem'\nexport const PANEL_KEY = 'holdem'\nexport const SUBPROTOCOL = 'holdem.v1'\nexport const ROUTE_PREFIX = '/holdem'\n\nexport const MAX_SEATS = 6\nexport const MAX_SPECTATORS = 16\nexport const MAX_ROOMS = 8\nexport const MAX_TITLE = 24\nexport const MAX_DISPLAY_NAME = 16\nexport const MAX_CHAT = 80\nexport const MAX_CHAT_LINES = 80\nexport const MAX_HISTORY_HANDS = 50\nexport const MAX_CACHED_ACTIONS = 64\nexport const MAX_BODY_BYTES = 64 * 1024\nexport const MAX_FRAME_BYTES = 64 * 1024\n\nexport const TICK_MS = 250\nexport const WS_TICKET_TTL_MS = 60_000\nexport const SESSION_TTL_MS = 24 * 60 * 60 * 1000\nexport const HEARTBEAT_MS = 15_000\nexport const DISCONNECT_AFTER_MS = 45_000\nexport const LOBBY_KICK_IDLE_MS = 10 * 60 * 1000\nexport const CHAT_COOLDOWN_MS = 1200\nexport const ACTION_RATE_WINDOW_MS = 10_000\nexport const ACTION_RATE_MAX = 30\n\nexport const BUY_IN_BB = 100\nexport const ALL_IN_RUNOUT_MS = 1200\nexport const HAND_OVER_MIN_MS = 2500\n\nexport const BLIND_LEVELS = [\n { id: '10/20', sb: 10, bb: 20 },\n { id: '25/50', sb: 25, bb: 50 },\n { id: '100/200', sb: 100, bb: 200 },\n] as const\n\nexport type BlindLevelId = (typeof BLIND_LEVELS)[number]['id']\n\nexport function blindsFor(id: string): { id: BlindLevelId; sb: number; bb: number } {\n const found = BLIND_LEVELS.find((level) => level.id === id)\n const level = found ?? BLIND_LEVELS[1]!\n return { id: level.id, sb: level.sb, bb: level.bb }\n}\n\nexport function buyInFor(bb: number): number {\n return bb * BUY_IN_BB\n}\n","import z from '@deepseek-ai/schemastery'\nimport {\n BLIND_LEVELS,\n MAX_ROOMS,\n MAX_SEATS,\n ROUTE_PREFIX,\n type BlindLevelId,\n blindsFor,\n buyInFor,\n} from './invariant.ts'\n\nexport interface PluginConfig {\n enabled?: boolean\n defaultBlindLevel?: string\n /** 每个行动时限,秒。 */\n turnTimeoutSec?: number\n /** 一手结束后自动开下一手的等待,秒。 */\n autoAdvanceSec?: number\n allowSpectators?: boolean\n chatEnabled?: boolean\n maxRooms?: number\n /** 房主失联多久后自动解散并结算,分钟。 */\n hostIdleDissolveMin?: number\n routePrefix?: string\n}\n\nexport const Config = z.object({\n enabled: z.boolean().default(true),\n defaultBlindLevel: z.string().default('25/50'),\n turnTimeoutSec: z.number().default(30).min(15).max(90),\n autoAdvanceSec: z.number().default(6).min(2).max(30),\n allowSpectators: z.boolean().default(true),\n chatEnabled: z.boolean().default(true),\n maxRooms: z.number().default(MAX_ROOMS).min(1).max(64),\n hostIdleDissolveMin: z.number().default(10).min(1).max(120),\n routePrefix: z.string().default(ROUTE_PREFIX),\n})\n\nexport interface ResolvedConfig {\n enabled: boolean\n blindLevel: BlindLevelId\n sb: number\n bb: number\n buyIn: number\n turnTimeoutMs: number\n autoAdvanceMs: number\n allowSpectators: boolean\n chatEnabled: boolean\n maxRooms: number\n hostIdleDissolveMs: number\n routePrefix: string\n}\n\nexport function normalizePrefix(prefix: string): string {\n const trimmed = prefix.trim()\n if (trimmed === '') return ROUTE_PREFIX\n const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`\n return withSlash.replace(/\\/+$/, '') || ROUTE_PREFIX\n}\n\nexport function resolveConfig(config: PluginConfig): ResolvedConfig {\n const blindId = (config.defaultBlindLevel ?? '25/50') as BlindLevelId\n const known = BLIND_LEVELS.some((level) => level.id === blindId)\n const blinds = blindsFor(known ? blindId : '25/50')\n const turnTimeoutSec = clamp(config.turnTimeoutSec ?? 30, 15, 90)\n const autoAdvanceSec = clamp(config.autoAdvanceSec ?? 6, 2, 30)\n return {\n enabled: config.enabled !== false,\n blindLevel: blinds.id,\n sb: blinds.sb,\n bb: blinds.bb,\n buyIn: buyInFor(blinds.bb),\n turnTimeoutMs: turnTimeoutSec * 1000,\n autoAdvanceMs: autoAdvanceSec * 1000,\n allowSpectators: config.allowSpectators !== false,\n chatEnabled: config.chatEnabled !== false,\n maxRooms: clamp(Math.trunc(config.maxRooms ?? MAX_ROOMS), 1, 64),\n hostIdleDissolveMs: clamp(config.hostIdleDissolveMin ?? 10, 1, 120) * 60_000,\n routePrefix: normalizePrefix(config.routePrefix ?? ROUTE_PREFIX),\n }\n}\n\nexport const MAX_TABLE_SEATS = MAX_SEATS\n\nfunction clamp(value: number, min: number, max: number): number {\n if (!Number.isFinite(value)) return min\n return Math.min(max, Math.max(min, value))\n}\n","import type { BlindLevelId } from './invariant.ts'\n\nexport type RoomId = string & { readonly __brand: 'RoomId' }\nexport type PlayerId = string & { readonly __brand: 'PlayerId' }\nexport type HandId = string & { readonly __brand: 'HandId' }\nexport type SessionId = string & { readonly __brand: 'SessionId' }\n/** 牌面:'As' / 'Td' / '9h'。 */\nexport type Card = string & { readonly __brand: 'Card' }\nexport type Seat = number\n\nexport type RoomPhase = 'lobby' | 'hand' | 'handOver' | 'settling' | 'closed'\nexport type Street = 'preflop' | 'flop' | 'turn' | 'river' | 'showdown'\n\nexport interface RoomSettingsView {\n blindLevel: BlindLevelId\n sb: number\n bb: number\n buyIn: number\n turnTimeoutMs: number\n autoAdvanceMs: number\n allowSpectators: boolean\n chatEnabled: boolean\n}\n\nexport interface SeatView {\n seat: Seat\n playerId: PlayerId | null\n displayName: string | null\n ready: boolean\n connected: boolean\n sittingOut: boolean\n autoFold: boolean\n host: boolean\n stack: number\n committed: number\n handNet: number\n folded: boolean\n allIn: boolean\n lastAction: string | null\n holeCount: number\n}\n\nexport interface PotView {\n amount: number\n eligible: Seat[]\n}\n\nexport interface ShownHandView {\n seat: Seat\n cards: Card[]\n name: string\n score: number\n}\n\nexport interface HandView {\n handNo: number\n street: Street\n board: Card[]\n button: Seat\n sbSeat: Seat\n bbSeat: Seat\n toActSeat: Seat | null\n currentBet: number\n minRaiseTo: number\n potTotal: number\n pots: PotView[]\n deadlineAt: string | null\n shownHands: ShownHandView[]\n winners: Seat[]\n}\n\nexport interface HandDelta {\n seat: Seat\n playerId: PlayerId\n displayName: string\n net: number\n}\n\nexport interface HandResultView {\n handNo: number\n board: Card[]\n pots: PotView[]\n winners: Seat[]\n shownHands: ShownHandView[]\n deltas: HandDelta[]\n foldedWin: boolean\n endedAt: string\n}\n\nexport interface LeaderboardEntry {\n playerId: PlayerId\n displayName: string\n seat: Seat | null\n handsPlayed: number\n handsWon: number\n netChips: number\n biggestWin: number\n showdownsWon: number\n showdownsPlayed: number\n}\n\nexport interface ChatLine {\n playerId: PlayerId\n displayName: string\n text: string\n ts: string\n}\n\nexport interface LegalActions {\n canFold: boolean\n canCheck: boolean\n canCall: boolean\n callAmount: number\n canRaise: boolean\n minRaiseTo: number\n maxRaiseTo: number\n canAllIn: boolean\n allInTo: number\n}\n\nexport interface RoomView {\n roomId: RoomId\n roomCode: string\n title: string\n hostPlayerId: PlayerId\n hostDisplayName: string\n phase: RoomPhase\n settings: RoomSettingsView\n seats: SeatView[]\n handNo: number\n createdAt: string\n /** handOver 阶段自动开下一手的时间点;其它阶段为 null。 */\n nextHandAt: string | null\n seq: number\n /** 一条链接搞定:有空位就入座,没空位自动转观战。 */\n joinUrl: string\n /** 同一个局域网里可以直接用的地址(没有可用网卡时为空)。 */\n lanUrl: string\n}\n\nexport interface YouView {\n playerId: PlayerId\n displayName: string\n seat: Seat | null\n spectator: boolean\n ready: boolean\n sittingOut: boolean\n holeCards: Card[]\n legal: LegalActions | null\n canAct: boolean\n netChips: number\n handsPlayed: number\n}\n\nexport interface SpectatorView {\n playerId: PlayerId\n displayName: string\n}\n\nexport interface SessionPlayerView {\n rank: number\n playerId: PlayerId\n displayName: string\n handsPlayed: number\n handsWon: number\n netChips: number\n biggestWin: number\n showdownsWon: number\n showdownsPlayed: number\n}\n\nexport interface SessionSummary {\n sessionId: SessionId\n roomCode: string\n title: string\n startedAt: string\n endedAt: string\n durationMs: number\n handsPlayed: number\n players: SessionPlayerView[]\n history: HandResultView[]\n reason: 'host-dissolved' | 'host-idle'\n}\n\nexport interface PlayerView {\n room: RoomView\n you: YouView\n hand: HandView | null\n lastHand: HandResultView | null\n leaderboard: LeaderboardEntry[]\n settlement: SessionSummary | null\n chat: ChatLine[]\n spectators: SpectatorView[]\n serverNow: string\n}\n\nexport type TableAction =\n | { type: 'fold' }\n | { type: 'check' }\n | { type: 'call' }\n | { type: 'raise'; to: number }\n | { type: 'allin' }\n\nexport type ClientCommand =\n | { type: 'sit'; seat?: Seat; displayName?: string }\n | { type: 'stand' }\n | { type: 'ready'; ready: boolean }\n | { type: 'start' }\n | { type: 'action'; action: TableAction }\n | { type: 'chat'; text: string }\n | { type: 'hostPause' }\n | { type: 'hostKick'; playerId: PlayerId }\n | { type: 'hostDissolve' }\n | { type: 'ping' }\n\nexport type RejectCode =\n | 'auth'\n | 'illegal'\n | 'phase'\n | 'not-your-turn'\n | 'stale'\n | 'duplicate-nonce'\n | 'room-full'\n | 'room-not-found'\n | 'expired'\n | 'already-in-room'\n | 'not-seated'\n | 'host-only'\n | 'rate-limited'\n\nexport type ServerEvent =\n | { type: 'snapshot'; seq: number; view: PlayerView }\n | { type: 'reject'; code: RejectCode; reason: string }\n | { type: 'settled'; seq: number; summary: SessionSummary }\n | { type: 'left'; seq: number; reason: string }\n | { type: 'kicked'; seq: number; reason: string }\n | { type: 'pong'; ts: number }\n\nexport class CommandError extends Error {\n readonly code: RejectCode\n constructor(code: RejectCode, reason: string) {\n super(reason)\n this.code = code\n this.name = 'CommandError'\n }\n}\n\nexport interface RoomPreviewSeat {\n seat: Seat\n displayName: string | null\n ready: boolean\n host: boolean\n connected: boolean\n}\n\nexport interface RoomPreview {\n roomCode: string\n title: string\n hostDisplayName: string\n phase: RoomPhase\n settings: RoomSettingsView\n seated: number\n spectators: number\n seats: RoomPreviewSeat[]\n canSit: boolean\n alreadyInRoom: boolean\n inviteExpiresAt: string\n}\n\nexport function asRoomId(value: string): RoomId {\n return value as RoomId\n}\n\nexport function asPlayerId(value: string): PlayerId {\n return value as PlayerId\n}\n\nexport function asCard(value: string): Card {\n return value as Card\n}\n\nexport function asSessionId(value: string): SessionId {\n return value as SessionId\n}\n\nexport function nowIso(): string {\n return new Date().toISOString()\n}\n\nexport function isSeat(value: unknown): value is Seat {\n return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value < 6\n}\n","import type { IncomingMessage } from 'node:http'\n\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]'])\nconst LOOPBACK_REMOTES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1'])\n\nexport function header(req: IncomingMessage, name: string): string | undefined {\n const value = req.headers[name]\n return Array.isArray(value) ? value[0] : value\n}\n\nexport function isLoopbackHost(hostHeader: string | undefined, remoteAddress: string | undefined): boolean {\n const host = (hostHeader ?? '').split(':')[0]?.replace(/^\\[|\\]$/g, '').toLowerCase()\n const remote = (remoteAddress ?? '').replace(/^::ffff:/, '')\n return LOOPBACK_HOSTS.has(host ?? '') || LOOPBACK_REMOTES.has(remote)\n}\n\n/** 私有网段(局域网直连)也放行:同 Wi-Fi 的朋友用 http://192.168.x.x:端口 进来。 */\nexport function isPrivateOrigin(origin: string): boolean {\n try {\n const host = new URL(origin).hostname\n return host.startsWith('10.')\n || host.startsWith('192.168.')\n || /^172\\.(1[6-9]|2\\d|3[01])\\./.test(host)\n } catch {\n return false\n }\n}\n\nexport function isLoopbackOrigin(origin: string): boolean {\n try {\n const url = new URL(origin)\n return url.hostname === '127.0.0.1' || url.hostname === 'localhost' || url.hostname === '::1' || url.hostname === '[::1]'\n } catch {\n return false\n }\n}\n\nexport function requestOrigin(req: IncomingMessage): string | null {\n const origin = header(req, 'origin')\n if (origin) {\n try {\n return new URL(origin).origin\n } catch {\n return null\n }\n }\n const referer = header(req, 'referer')\n if (referer) {\n try {\n return new URL(referer).origin\n } catch {\n return null\n }\n }\n return null\n}\n\nexport function originAllowed(origin: string | null, browser: boolean): boolean {\n if (!origin) return !browser\n return isLoopbackOrigin(origin) || isPrivateOrigin(origin)\n}\n\nexport function isBrowserRequest(req: IncomingMessage): boolean {\n return Boolean(header(req, 'origin') || header(req, 'sec-fetch-site'))\n}\n\nexport function assertOrigin(req: IncomingMessage): void {\n if (header(req, 'sec-fetch-site') === 'cross-site') {\n throw Object.assign(new Error('cross-site forbidden'), { status: 403, code: 'auth' })\n }\n if (!originAllowed(requestOrigin(req), isBrowserRequest(req))) {\n throw Object.assign(new Error('origin not allowed'), { status: 403, code: 'auth' })\n }\n}\n\n/** 变更请求必须带 JSON 与自定义请求头(跨源页面设不了后者)。 */\nexport function assertMutatingHeaders(req: IncomingMessage): void {\n if (header(req, 'x-requested-with') === '@zhuweiyou/dsh-holdem') return\n const contentType = header(req, 'content-type') ?? ''\n if (contentType.includes('application/json') && header(req, 'x-requested-with')) return\n throw Object.assign(new Error('missing csrf header'), { status: 403, code: 'auth' })\n}\n\nexport function parseCookies(req: IncomingMessage): Record<string, string> {\n const raw = header(req, 'cookie') ?? ''\n const out: Record<string, string> = {}\n for (const part of raw.split(';')) {\n const index = part.indexOf('=')\n if (index < 0) continue\n const key = part.slice(0, index).trim()\n const value = part.slice(index + 1).trim()\n if (key) {\n try {\n out[key] = decodeURIComponent(value)\n } catch {\n out[key] = value\n }\n }\n }\n return out\n}\n\nexport function cookieHeader(\n name: string,\n value: string,\n options: { secure: boolean; sameSite: 'Lax' | 'Strict'; maxAge?: number; path: string },\n): string {\n const parts = [\n `${name}=${encodeURIComponent(value)}`,\n `Path=${options.path}`,\n 'HttpOnly',\n `SameSite=${options.sameSite}`,\n `Max-Age=${options.maxAge ?? 86400}`,\n ]\n if (options.secure) parts.push('Secure')\n return parts.join('; ')\n}\n","import type { IncomingMessage } from 'node:http'\nimport { MAX_BODY_BYTES } from '../invariant.ts'\n\nexport function encodeJson(body: unknown): string {\n return JSON.stringify(body)\n}\n\nexport async function readJson<T>(req: IncomingMessage): Promise<T> {\n const chunks: Buffer[] = []\n let size = 0\n await new Promise<void>((resolve, reject) => {\n req.on('data', (chunk: Buffer | string) => {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n size += buffer.length\n if (size > MAX_BODY_BYTES) {\n reject(Object.assign(new Error('payload too large'), { status: 413, code: 'illegal' }))\n return\n }\n chunks.push(buffer)\n })\n req.on('end', () => resolve())\n req.on('error', (error: Error) => reject(error))\n })\n if (chunks.length === 0) return {} as T\n try {\n return JSON.parse(Buffer.concat(chunks).toString('utf8')) as T\n } catch {\n throw Object.assign(new Error('invalid json'), { status: 400, code: 'illegal' })\n }\n}\n","export interface JoinPageOptions {\n prefix: string\n code: string\n invite: string\n role: 'auto' | 'sit' | 'watch'\n}\n\n/**\n * 邀请落地页:只做「这是哪一桌 / 要不要现在进去」。\n * 真正的牌桌在 /play —— 那个页面自带完整客户端,对方不用装任何东西。\n */\nexport function joinPageHtml(options: JoinPageOptions): string {\n const play = `${options.prefix}/play?code=${encodeURIComponent(options.code)}&invite=${encodeURIComponent(options.invite)}&role=${options.role}`\n const roleText = options.role === 'watch' ? '观战' : '入座(满员自动观战)'\n return `<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>加入德州扑克 · ${escapeHtml(options.code)}</title>\n<style>\n :root { color-scheme: dark; }\n * { box-sizing: border-box; }\n body {\n margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px;\n font: 14px/1.6 -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n background: #101418; color: #e9ecf2;\n background-image: radial-gradient(circle at 50% 0%, #17352c 0%, #101418 60%);\n }\n .card { width: min(460px, 100%); background: #181c22; border: 1px solid #2a313a; border-radius: 16px; padding: 24px; box-shadow: 0 24px 60px rgba(0,0,0,.45); }\n h1 { font-size: 19px; margin: 0 0 4px; }\n .meta { color: #93a0b1; font-size: 13px; margin-bottom: 16px; }\n .code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 26px; letter-spacing: 4px; color: #e8c15a; margin: 8px 0 16px; }\n .info { border: 1px solid #2a313a; border-radius: 10px; padding: 12px; background: #141a20; margin-bottom: 16px; }\n .info .row { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; }\n .info .row + .row { margin-top: 6px; }\n .info .k { color: #93a0b1; }\n .hint { color: #93a0b1; font-size: 12px; margin: 0 0 12px; }\n .error { color: #e06c75; font-size: 12px; margin: 0 0 12px; }\n button { font: inherit; border-radius: 10px; border: 1px solid transparent; padding: 11px 16px; cursor: pointer; width: 100%; }\n .primary { background: #2f9c76; color: #06130f; font-weight: 700; }\n .primary:disabled { opacity: .55; cursor: progress; }\n footer { margin-top: 14px; font-size: 11.5px; color: #7b899a; word-break: break-all; }\n</style>\n</head>\n<body>\n<div class=\"card\">\n <h1>加入德州扑克牌桌</h1>\n <div class=\"meta\">娱乐筹码 · 邀请链接直接进桌</div>\n <div class=\"code\">${escapeHtml(options.code)}</div>\n <div class=\"info\">\n <div class=\"row\"><span class=\"k\">房间</span><span id=\"title\">读取中…</span></div>\n <div class=\"row\"><span class=\"k\">盲注</span><span id=\"blinds\">—</span></div>\n <div class=\"row\"><span class=\"k\">座位</span><span id=\"seats\">—</span></div>\n <div class=\"row\"><span class=\"k\">身份</span><span>${roleText}</span></div>\n </div>\n <p class=\"hint\" id=\"hint\">点下面的按钮进入牌桌,浏览器里直接就能玩,不用安装任何东西。</p>\n <p class=\"error\" id=\"error\" hidden></p>\n <button class=\"primary\" id=\"enter\" type=\"button\">进入牌桌</button>\n <footer>牌桌地址:${escapeHtml(play)}</footer>\n</div>\n<script>\n(function () {\n var prefix = ${JSON.stringify(options.prefix)};\n var code = ${JSON.stringify(options.code)};\n var invite = ${JSON.stringify(options.invite)};\n var role = ${JSON.stringify(options.role)};\n var target = ${JSON.stringify(play)};\n var enter = document.getElementById('enter');\n var error = document.getElementById('error');\n var hint = document.getElementById('hint');\n\n fetch(prefix + '/api/preview?code=' + encodeURIComponent(code) + '&invite=' + encodeURIComponent(invite) + '&role=' + encodeURIComponent(role), { credentials: 'same-origin' })\n .then(function (response) { return response.ok ? response.json() : null; })\n .then(function (room) {\n if (!room) {\n error.hidden = false;\n error.textContent = '房间可能已解散,请向房主要一个新链接。';\n return;\n }\n document.getElementById('title').textContent = room.title;\n document.getElementById('blinds').textContent = room.settings.sb + '/' + room.settings.bb + ' · 带入 ' + room.settings.buyIn;\n document.getElementById('seats').textContent = room.seated + '/' + room.seats.length + ' 人在座';\n hint.textContent = room.alreadyInRoom\n ? '你之前已经进过这一桌,直接继续就行。'\n : '点下面的按钮进入牌桌,浏览器里直接就能玩,不用安装任何东西。';\n })\n .catch(function () {\n error.hidden = false;\n error.textContent = '连不上房主的进程,确认链接没有过期。';\n });\n\n enter.addEventListener('click', function () {\n enter.disabled = true;\n enter.textContent = '正在进入…';\n location.replace(target);\n });\n})();\n</script>\n</body>\n</html>`\n}\n\nfunction escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => ({\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n })[char] ?? char)\n}\n","/**\n * 给「不在同一台机器上」的朋友用的独立牌桌页面。\n *\n * 邀请链接指向的就是这里:宿主进程直接把这个页面发给对方浏览器,不需要对方装 DSH、\n * 也不需要 DSH Web UI 的访问令牌——页面自己调 /heldem 的 HTTP 接口(轮询)。\n */\n\nexport interface PlayPageOptions {\n prefix: string\n code: string\n invite: string\n role: 'auto' | 'sit' | 'watch'\n /** 邀请链接里带了昵称就直接进,不再弹昵称框。 */\n name?: string\n}\n\nexport function playPageHtml(options: PlayPageOptions): string {\n const role = options.role\n return `<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>德州扑克 · ${escapeHtml(options.code)}</title>\n<style>\n :root { color-scheme: dark; }\n * { box-sizing: border-box; }\n body {\n margin: 0; min-height: 100vh; display: flex; flex-direction: column;\n font: 13px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n background: #0f1512; color: #e9ecf2;\n }\n header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 10px 16px; border-bottom: 1px solid #1f2a24; }\n header h1 { margin: 0; font-size: 15px; }\n .pill { border: 1px solid #2a3a32; border-radius: 999px; padding: 1px 9px; font-size: 11px; color: #9db0a6; }\n .grow { flex: 1; }\n main { display: flex; flex: 1; min-height: 0; }\n #feltWrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; padding: 10px 12px 12px; }\n #felt {\n position: relative; flex: 1; min-height: 300px; border-radius: 24px; overflow: hidden;\n background:\n radial-gradient(ellipse at 50% 38%, rgba(255,255,255,.12), transparent 58%),\n radial-gradient(ellipse at 50% 120%, rgba(0,0,0,.5), transparent 62%),\n linear-gradient(165deg, #1f6a4f 0%, #17553f 55%, #123f30 100%);\n border: 1px solid rgba(0,0,0,.32);\n box-shadow: inset 0 0 70px rgba(0,0,0,.42);\n }\n .seat {\n position: absolute; width: 158px; padding: 7px 9px; border-radius: 12px;\n background: rgba(247,247,248,.94); color: #14181c; display: flex; flex-direction: column; gap: 4px;\n border: 1px solid rgba(0,0,0,.18);\n }\n .seat.you { border-color: #e8c15a; box-shadow: 0 0 0 1px #e8c15a; }\n .seat.actor { border-color: #e8c15a; box-shadow: 0 0 0 1px #e8c15a, 0 0 18px rgba(232,193,90,.45); }\n .seat.folded { opacity: .5; }\n .seat.empty { background: rgba(255,255,255,.06); color: rgba(255,255,255,.6); border: 1px dashed rgba(255,255,255,.28); align-items: center; justify-content: center; min-height: 64px; }\n .seatTop { display: flex; align-items: center; gap: 6px; }\n .avatar { width: 28px; height: 28px; border-radius: 50%; flex: none; display: grid; place-items: center; font-weight: 700; font-size: 12px; color: #fff; background: linear-gradient(150deg,#3f7f66,#245040); }\n .who { min-width: 0; flex: 1; }\n .name { font-weight: 600; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .stack { font-size: 11.5px; color: #4b5b52; font-variant-numeric: tabular-nums; }\n .badges { display: flex; gap: 4px; flex-wrap: wrap; font-size: 9px; color: #6c7a70; }\n .badges span { border: 1px solid currentColor; border-radius: 4px; padding: 0 3px; }\n .lastAction { font-size: 10px; color: #6c7a70; }\n .cards { display: flex; gap: 3px; justify-content: center; min-height: 46px; align-items: center; }\n .p0 { left: 50%; bottom: 8px; transform: translateX(-50%); }\n .p1 { left: 2%; bottom: 96px; }\n .p2 { left: 1%; top: 62px; }\n .p3 { left: 50%; top: 6px; transform: translateX(-50%); }\n .p4 { right: 1%; top: 62px; }\n .p5 { right: 2%; bottom: 96px; }\n #center { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); text-align: center; display: flex; flex-direction: column; gap: 6px; align-items: center; }\n #street { font-size: 10px; letter-spacing: .18em; text-transform: uppercase; color: rgba(255,255,255,.62); }\n #board { display: flex; gap: 6px; }\n #pot { font-size: 12.5px; font-weight: 700; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,.4); }\n #banner { background: #fff; color: #14181c; border-radius: 10px; padding: 7px 12px; font-size: 12px; box-shadow: 0 10px 24px rgba(0,0,0,.3); }\n #banner b { display: block; margin-bottom: 2px; }\n /* 方案 A:左上角点数 + 中央大花色 */\n .card {\n position: relative; display: block; flex: none; overflow: hidden;\n width: 50px; height: 70px; border-radius: 8px;\n background: #fcfbf7; color: #16171a; border: 1px solid #e2e0d7;\n box-shadow: 0 1px 2px rgba(0,0,0,.22), 0 3px 8px rgba(0,0,0,.12);\n font-family: ui-sans-serif, system-ui, -apple-system, \"Helvetica Neue\", sans-serif;\n font-weight: 750; font-variant-numeric: tabular-nums; line-height: 1;\n --rank: 16px; --suit: 27px;\n }\n .card .rank { position: absolute; left: 5px; top: 4px; font-size: var(--rank); line-height: 1; }\n .card .suit { position: absolute; left: 50%; top: 60%; transform: translate(-50%,-50%); font-size: var(--suit); line-height: 1; }\n .card.red { color: #d81b3c; }\n .card.sm { width: 38px; height: 54px; border-radius: 6px; --rank: 13px; --suit: 19px; }\n .card.back {\n background: radial-gradient(circle at 50% 50%, #2f6f5c 0 2.5px, transparent 2.6px) 0 0 / 9px 9px, #f6f3ea;\n border-color: #d9d6cb; box-shadow: inset 0 0 0 3px #f6f3ea, 0 1px 2px rgba(0,0,0,.22);\n }\n .card.slot { background: transparent; border: 1px dashed rgba(255,255,255,.32); box-shadow: none; }\n #me { display: flex; align-items: center; gap: 12px; border: 1px solid #22302a; background: #141c18; border-radius: 12px; padding: 8px 10px; flex: none; }\n #myCards { display: flex; gap: 6px; }\n #myMeta { display: flex; flex-direction: column; gap: 1px; min-width: 88px; }\n #myName { font-weight: 600; }\n #myStack { font-size: 11.5px; color: #9db0a6; }\n #actions { flex: 1; display: flex; flex-direction: column; gap: 6px; min-width: 0; }\n .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }\n button { font: inherit; border-radius: 8px; border: 1px solid #2f4038; background: #1b2621; color: #e9ecf2; padding: 7px 12px; cursor: pointer; }\n button:hover:not(:disabled) { border-color: #47604f; }\n button:disabled { opacity: .45; cursor: default; }\n button.primary { background: #2f9c76; border-color: transparent; color: #06130f; font-weight: 700; }\n button.warn { border-color: #d64545; color: #d64545; background: transparent; }\n button.ghost { background: transparent; }\n input[type=range] { flex: 1; min-width: 120px; accent-color: #2f9c76; }\n input[type=text] { font: inherit; border-radius: 8px; border: 1px solid #33443b; background: #101714; color: #e9ecf2; padding: 7px 10px; }\n #raiseValue { font-variant-numeric: tabular-nums; font-weight: 700; min-width: 70px; text-align: right; }\n .hint { color: #8fa197; font-size: 11.5px; }\n aside { width: 260px; flex: none; border-left: 1px solid #1f2a24; padding: 10px 12px; display: flex; flex-direction: column; gap: 12px; overflow-y: auto; }\n aside h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .1em; color: #8fa197; margin: 0; }\n .list { display: flex; flex-direction: column; gap: 3px; font-size: 11.5px; }\n .leaderRow { display: grid; grid-template-columns: 16px 1fr auto; gap: 6px; }\n .chat { display: flex; flex-direction: column; gap: 4px; font-size: 11.5px; max-height: 180px; overflow-y: auto; }\n .chat b { color: #6fd0a8; font-weight: 600; }\n #toast { position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%); background: #1b2621; border: 1px solid #2f4038; border-radius: 999px; padding: 8px 16px; display: none; z-index: 40; }\n #overlay { position: fixed; inset: 0; background: rgba(6,10,8,.72); display: none; place-items: center; padding: 20px; z-index: 30; }\n #overlayCard { width: min(680px, 100%); max-height: 100%; overflow-y: auto; background: #131a16; border: 1px solid #2a3a32; border-radius: 14px; padding: 18px; }\n #overlayCard h2 { margin: 0 0 6px; font-size: 18px; }\n table { width: 100%; border-collapse: collapse; font-size: 12.5px; margin-top: 10px; }\n th, td { text-align: left; padding: 7px 8px; border-top: 1px solid #1f2a24; }\n th { color: #8fa197; font-weight: 500; font-size: 11px; }\n .up { color: #4cc38a; }\n .down { color: #e06c75; }\n #join { position: fixed; inset: 0; display: grid; place-items: center; background: rgba(6,10,8,.78); z-index: 50; }\n #joinCard { width: min(380px, 92vw); background: #131a16; border: 1px solid #2a3a32; border-radius: 14px; padding: 18px; display: flex; flex-direction: column; gap: 10px; }\n @media (max-width: 980px) {\n aside { display: none; }\n .seat { width: 132px; }\n .p1, .p2, .p4, .p5 { top: auto; bottom: auto; }\n }\n</style>\n</head>\n<body data-prefix=\"${escapeHtml(options.prefix)}\" data-code=\"${escapeHtml(options.code)}\" data-invite=\"${escapeHtml(options.invite)}\" data-role=\"${role}\" data-name=\"${escapeHtml(options.name ?? '')}\">\n<header>\n <h1 id=\"roomTitle\">德州扑克</h1>\n <span class=\"pill\" id=\"roomCode\">房号 ${escapeHtml(options.code)}</span>\n <span class=\"pill\" id=\"phase\">连接中…</span>\n <span class=\"pill\" id=\"blinds\"></span>\n <span class=\"grow\"></span>\n <span class=\"pill\" id=\"conn\">○</span>\n <button class=\"ghost\" id=\"leave\" type=\"button\">离开</button>\n</header>\n<main>\n <div id=\"feltWrap\">\n <div id=\"felt\">\n <div id=\"seats\"></div>\n <div id=\"center\">\n <div id=\"street\"></div>\n <div id=\"board\"></div>\n <div id=\"pot\"></div>\n <div id=\"banner\" style=\"display:none\"></div>\n </div>\n </div>\n <div id=\"me\">\n <div id=\"myCards\"></div>\n <div id=\"myMeta\">\n <span id=\"myName\">—</span>\n <span id=\"myStack\"></span>\n </div>\n <div id=\"actions\"><span class=\"hint\" id=\"actionHint\">等待其他人…</span></div>\n </div>\n </div>\n <aside>\n <div><h2>排行榜</h2><div class=\"list\" id=\"leaderboard\"></div></div>\n <div><h2>桌边聊天</h2><div class=\"chat\" id=\"chat\"></div><div class=\"row\" style=\"margin-top:6px\"><input type=\"text\" id=\"chatInput\" placeholder=\"说点什么…\" style=\"flex:1\" /><button id=\"chatSend\" type=\"button\">发送</button></div></div>\n </aside>\n</main>\n<div id=\"toast\"></div>\n<div id=\"overlay\"><div id=\"overlayCard\"></div></div>\n<div id=\"join\" style=\"display:none\">\n <div id=\"joinCard\">\n <h2 style=\"margin:0;font-size:16px\">加入牌桌</h2>\n <p class=\"hint\" id=\"joinHint\" style=\"margin:0\"></p>\n <input type=\"text\" id=\"nameInput\" placeholder=\"你的昵称\" maxlength=\"16\" />\n <div class=\"row\">\n <button class=\"primary\" id=\"joinSit\" type=\"button\">进入牌桌</button>\n <button id=\"joinWatch\" type=\"button\">只看不打</button>\n </div>\n </div>\n</div>\n<script>\n(function () {\n var body = document.body\n var prefix = body.dataset.prefix || '/holdem'\n var code = body.dataset.code || ''\n var invite = body.dataset.invite || ''\n var preferWatch = body.dataset.role === 'watch'\n var presetName = body.dataset.name || ''\n var storeKey = 'holdem-guest:' + code\n var state = { token: '', roomId: '', view: null, busy: false, sig: '', ended: '', poll: 0, timer: 0 }\n var els = {\n title: document.getElementById('roomTitle'),\n code: document.getElementById('roomCode'),\n phase: document.getElementById('phase'),\n blinds: document.getElementById('blinds'),\n conn: document.getElementById('conn'),\n seats: document.getElementById('seats'),\n street: document.getElementById('street'),\n board: document.getElementById('board'),\n pot: document.getElementById('pot'),\n banner: document.getElementById('banner'),\n myCards: document.getElementById('myCards'),\n myName: document.getElementById('myName'),\n myStack: document.getElementById('myStack'),\n actions: document.getElementById('actions'),\n leaderboard: document.getElementById('leaderboard'),\n chat: document.getElementById('chat'),\n chatInput: document.getElementById('chatInput'),\n toast: document.getElementById('toast'),\n overlay: document.getElementById('overlay'),\n overlayCard: document.getElementById('overlayCard'),\n join: document.getElementById('join'),\n joinHint: document.getElementById('joinHint'),\n nameInput: document.getElementById('nameInput'),\n leave: document.getElementById('leave')\n }\n\n var SUIT = { s: '♠', h: '♥', d: '♦', c: '♣' }\n var RANK = { T: '10' }\n\n function cardHtml(card, size) {\n var rank = card.charAt(0)\n var suit = card.charAt(1)\n var red = suit === 'h' || suit === 'd'\n var cls = 'card' + (size === 'sm' ? ' sm' : '') + (red ? ' red' : '')\n return '<span class=\"' + cls + '\"><span class=\"rank\">' + (RANK[rank] || rank) + '</span><span class=\"suit\">' + (SUIT[suit] || '') + '</span></span>'\n }\n\n function backHtml(size) {\n return '<span class=\"card back' + (size === 'sm' ? ' sm' : '') + '\"></span>'\n }\n\n function slotHtml() {\n return '<span class=\"card slot\"></span>'\n }\n\n function chips(value) {\n return Math.round(value).toLocaleString('en-US')\n }\n\n function signed(value) {\n if (!value) return '0'\n return (value > 0 ? '+' : '−') + chips(Math.abs(value))\n }\n\n function toast(text, isError) {\n els.toast.textContent = text\n els.toast.style.display = 'block'\n els.toast.style.borderColor = isError ? '#d64545' : '#2f4038'\n clearTimeout(toast.timer)\n toast.timer = setTimeout(function () { els.toast.style.display = 'none' }, isError ? 4200 : 2400)\n }\n\n function headers(extra) {\n var out = { 'content-type': 'application/json', 'x-requested-with': '@zhuweiyou/dsh-holdem' }\n if (state.token) out['x-holdem-seat'] = state.token\n if (extra) for (var key in extra) out[key] = extra[key]\n return out\n }\n\n function api(path, options) {\n var init = options || {}\n return fetch(prefix + path, {\n method: init.method || 'GET',\n headers: headers(),\n credentials: 'same-origin',\n body: init.body === undefined ? undefined : JSON.stringify(init.body)\n }).then(function (response) {\n return response.text().then(function (text) {\n var payload = text ? JSON.parse(text) : {}\n if (!response.ok) {\n var error = new Error(payload.message || ('HTTP ' + response.status))\n error.status = response.status\n error.code = payload.error\n error.view = payload.view\n throw error\n }\n return payload\n })\n })\n }\n\n function actionId() {\n if (window.crypto && window.crypto.randomUUID) return window.crypto.randomUUID()\n return 'g' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8)\n }\n\n // ---------------------------------------------------------------- 加入\n function storedName() {\n try { return localStorage.getItem('holdem-name') || '' } catch (e) { return '' }\n }\n\n function saveName(name) {\n try { localStorage.setItem('holdem-name', name) } catch (e) {}\n }\n\n function askJoin() {\n els.join.style.display = 'grid'\n els.nameInput.value = storedName()\n els.joinHint.textContent = preferWatch ? '以观战身份进入房间 ' + code : '房间 ' + code + ':有空位就直接入座,满员自动观战'\n setTimeout(function () { els.nameInput.focus() }, 50)\n }\n\n function join(role) {\n var name = (els.nameInput.value || '').trim()\n if (!name) { toast('先填个昵称', true); return }\n saveName(name)\n els.join.style.display = 'none'\n api('/api/join', { method: 'POST', body: { roomCode: code, invite: invite, displayName: name, role: role } }).then(function (payload) {\n state.token = payload.sessionToken || ''\n state.roomId = payload.roomId || ''\n try { sessionStorage.setItem(storeKey, state.token) } catch (e) {}\n applyView(payload.view)\n startPolling()\n toast(role === 'watch' ? '已进入观战席' : '已入座')\n }).catch(function (error) {\n els.join.style.display = 'grid'\n els.joinHint.textContent = error.message || '加入失败'\n })\n }\n\n els.nameInput.addEventListener('keydown', function (event) { if (event.key === 'Enter') join(preferWatch ? 'watch' : 'sit') })\n document.getElementById('joinSit').addEventListener('click', function () { join('sit') })\n document.getElementById('joinWatch').addEventListener('click', function () { join('watch') })\n\n // ---------------------------------------------------------------- 视图\n function applyView(view) {\n if (!view) return\n state.view = view\n state.roomId = view.room.roomId\n if (view.room.phase === 'closed') stopPolling()\n render()\n }\n\n function startPolling() {\n if (state.poll) return\n state.poll = setInterval(tick, 1200)\n tick()\n }\n\n function stopPolling() {\n if (state.poll) clearInterval(state.poll)\n state.poll = 0\n }\n\n function tick() {\n if (!state.roomId || state.busy) return\n api('/api/rooms/' + encodeURIComponent(state.roomId) + '/state').then(function (payload) {\n els.conn.textContent = '●'\n els.conn.style.color = '#4cc38a'\n applyView(payload.view)\n }).catch(function (error) {\n if (error.status === 403 || error.status === 404) {\n els.conn.textContent = '○'\n els.conn.style.color = '#e06c75'\n if (state.ended !== 'gone') {\n state.ended = 'gone'\n stopPolling()\n toast('房间已解散', true)\n }\n return\n }\n els.conn.textContent = '◐'\n els.conn.style.color = '#c58a00'\n })\n }\n\n function command(cmd) {\n if (!state.view) return Promise.resolve()\n state.busy = true\n var baseSeq = state.view.room.seq\n return api('/api/rooms/' + encodeURIComponent(state.roomId) + '/action', {\n method: 'POST',\n body: { actionId: actionId(), baseSeq: baseSeq, command: cmd }\n }).then(function (payload) {\n applyView(payload.view)\n }).catch(function (error) {\n if (error.view) applyView(error.view)\n if (error.code === 'stale') toast('状态已更新,请重试', true)\n else if (error.code !== 'not-your-turn') toast(error.message || '操作失败', true)\n }).then(function () {\n state.busy = false\n tick()\n })\n }\n\n // ---------------------------------------------------------------- 渲染\n function phaseText(phase) {\n return phase === 'hand' ? '正在打牌' : phase === 'handOver' ? '本手结束' : phase === 'closed' ? '已结束' : '等待开局'\n }\n\n function render() {\n var view = state.view\n if (!view) return\n var room = view.room\n els.title.textContent = room.title\n els.code.textContent = '房号 ' + room.roomCode\n els.phase.textContent = phaseText(room.phase)\n els.blinds.textContent = room.settings.sb + '/' + room.settings.bb + ' · 带入 ' + chips(room.settings.buyIn)\n els.conn.textContent = '●'\n renderSeats(view)\n renderCenter(view)\n renderMe(view)\n renderRail(view)\n if (room.phase === 'closed' && view.settlement) renderSettlement(view)\n else els.overlay.style.display = 'none'\n }\n\n function renderSeats(view) {\n var room = view.room\n var hand = view.hand\n var base = view.you.seat === null ? 0 : view.you.seat\n var html = ''\n for (var i = 0; i < room.seats.length; i += 1) {\n var seat = room.seats[i]\n var rel = (seat.seat - base + 6) % 6\n var cls = 'seat p' + rel\n if (seat.seat === view.you.seat) cls += ' you'\n if (hand && hand.toActSeat === seat.seat && room.phase === 'hand') cls += ' actor'\n if (seat.folded) cls += ' folded'\n if (!seat.playerId) {\n html += '<div class=\"seat empty p' + rel + '\">入座</div>'\n continue\n }\n var shown = null\n if (hand) for (var j = 0; j < hand.shownHands.length; j += 1) if (hand.shownHands[j].seat === seat.seat) shown = hand.shownHands[j]\n html += '<div class=\"' + cls + '\">'\n html += '<div class=\"seatTop\"><div class=\"avatar\">' + escape((seat.displayName || '?').slice(0, 1)) + '</div>'\n html += '<div class=\"who\"><div class=\"name\">' + escape(seat.displayName || '—') + '</div><div class=\"stack\">' + chips(seat.stack) + '</div></div></div>'\n html += '<div class=\"badges\">'\n if (seat.host) html += '<span>房主</span>'\n if (hand && hand.button === seat.seat) html += '<span>D</span>'\n if (hand && hand.sbSeat === seat.seat) html += '<span>SB</span>'\n if (hand && hand.bbSeat === seat.seat) html += '<span>BB</span>'\n if (seat.allIn) html += '<span>全下</span>'\n html += '</div>'\n html += '<div class=\"cards\">'\n if (shown) { for (var k = 0; k < shown.cards.length; k += 1) html += cardHtml(shown.cards[k], 'sm') }\n else { for (var b = 0; b < seat.holeCount; b += 1) html += backHtml('sm') }\n html += '</div>'\n if (seat.lastAction) html += '<div class=\"lastAction\">' + escape(seat.lastAction) + '</div>'\n if (seat.committed > 0) html += '<div class=\"lastAction\">本轮 ' + chips(seat.committed) + '</div>'\n html += '</div>'\n }\n els.seats.innerHTML = html\n }\n\n function renderCenter(view) {\n var hand = view.hand\n var room = view.room\n if (hand) {\n els.street.textContent = ({ preflop: '翻牌前', flop: '翻牌', turn: '转牌', river: '河牌', showdown: '摊牌' })[hand.street] || ''\n var board = ''\n for (var i = 0; i < 5; i += 1) board += hand.board[i] ? cardHtml(hand.board[i]) : slotHtml()\n els.board.innerHTML = board\n els.pot.textContent = '底池 ' + chips(hand.potTotal)\n } else {\n els.street.textContent = ''\n els.board.innerHTML = ''\n els.pot.textContent = ''\n }\n var last = view.lastHand\n if (room.phase === 'handOver' && last) {\n var winners = last.winners.map(function (seat) {\n var found = room.seats.filter(function (entry) { return entry.seat === seat })[0]\n return found && found.displayName ? found.displayName : '座位 ' + seat\n }).join(' · ')\n var gains = last.deltas.filter(function (delta) { return delta.net !== 0 }).map(function (delta) {\n return escape(delta.displayName) + ' ' + signed(delta.net)\n }).join(' ')\n els.banner.style.display = 'block'\n els.banner.innerHTML = '<b>' + (last.foldedWin ? '无人跟注,直接收池' : escape(winners) + ' 赢得 ' + chips(last.pots.reduce(function (sum, pot) { return sum + pot.amount }, 0))) + '</b>' + gains\n } else {\n els.banner.style.display = 'none'\n }\n }\n\n function renderMe(view) {\n var you = view.you\n var room = view.room\n var hand = view.hand\n var html = ''\n if (you.holeCards.length > 0) {\n for (var i = 0; i < you.holeCards.length; i += 1) html += cardHtml(you.holeCards[i])\n } else if (room.phase === 'hand' && !you.spectator) {\n html += backHtml() + backHtml()\n }\n els.myCards.innerHTML = html\n els.myName.textContent = you.displayName\n els.myStack.textContent = you.spectator ? '观战中' : chips((room.seats.filter(function (seat) { return seat.seat === you.seat })[0] || {}).stack || 0) + ' · 净赢 ' + signed(you.netChips)\n renderActions(view)\n }\n\n function renderActions(view) {\n var you = view.you\n var room = view.room\n var hand = view.hand\n var legal = you.legal\n var canAct = you.canAct && legal && room.phase === 'hand'\n var sig = canAct\n ? ['act', hand ? hand.handNo : 0, hand ? hand.toActSeat : -1, legal.minRaiseTo, legal.maxRaiseTo, legal.canCheck, legal.canCall, legal.canRaise, legal.callAmount].join(':')\n : ['idle', room.phase, you.ready, you.spectator, you.seat, room.hostPlayerId === you.playerId]\n if (sig === state.sig) { updateTimer(); return }\n state.sig = sig\n if (canAct) {\n var isAllIn = legal.canRaise && legal.minRaiseTo >= legal.maxRaiseTo\n var raiseValue = legal.minRaiseTo\n var step = Math.max(1, Math.round(room.settings.bb / 2))\n els.actions.innerHTML =\n '<div class=\"row\"><span class=\"hint\" style=\"color:#e8c15a;font-weight:700\">轮到你</span><span class=\"hint\" id=\"timer\"></span></div>' +\n '<div class=\"row\">' +\n '<button class=\"ghost\" data-act=\"fold\"' + (legal.canFold ? '' : ' disabled') + '>弃牌</button>' +\n (legal.canCheck\n ? '<button data-act=\"check\">过牌</button>'\n : '<button data-act=\"call\"' + (legal.canCall ? '' : ' disabled') + '>跟注 ' + chips(legal.callAmount) + '</button>') +\n (legal.canRaise\n ? '<span id=\"raiseValue\">' + chips(raiseValue) + '</span>' +\n '<input type=\"range\" id=\"raiseRange\" min=\"' + legal.minRaiseTo + '\" max=\"' + legal.maxRaiseTo + '\" step=\"' + step + '\" value=\"' + raiseValue + '\" />' +\n '<button data-range=\"0.5\" class=\"ghost\">1/2 池</button>' +\n '<button data-range=\"0.66\" class=\"ghost\">2/3 池</button>' +\n '<button data-range=\"1\" class=\"ghost\">一池</button>' +\n '<button data-range=\"max\" class=\"ghost\">最大</button>' +\n '<button class=\"primary\" data-act=\"raise\">' + (isAllIn ? '全下' : '加注') + '</button>'\n : '') +\n '</div>'\n var range = document.getElementById('raiseRange')\n var value = document.getElementById('raiseValue')\n if (range) {\n range.addEventListener('input', function () { value.textContent = chips(Number(range.value)) })\n }\n els.actions.querySelectorAll('[data-range]').forEach(function (button) {\n button.addEventListener('click', function () {\n var ratio = button.dataset.range\n var next = ratio === 'max' ? legal.maxRaiseTo : Math.min(legal.maxRaiseTo, Math.max(legal.minRaiseTo, Math.round((hand ? hand.potTotal : 0) * Number(ratio))))\n range.value = String(next)\n value.textContent = chips(next)\n })\n })\n els.actions.querySelectorAll('[data-act]').forEach(function (button) {\n button.addEventListener('click', function () {\n var act = button.dataset.act\n if (act === 'fold') command({ type: 'action', action: { type: 'fold' } })\n else if (act === 'check') command({ type: 'action', action: { type: 'check' } })\n else if (act === 'call') command({ type: 'action', action: { type: 'call' } })\n else if (act === 'raise') command({ type: 'action', action: { type: 'raise', to: Number(range.value) } })\n })\n })\n updateTimer()\n return\n }\n\n // 不能行动时:按阶段给出等待/准备/开始/入座\n var parts = []\n if (you.spectator) {\n parts.push('<span class=\"hint\">你在观战席' + (room.phase === 'lobby' ? ',等待阶段可以入座' : '') + '</span>')\n if (room.phase === 'lobby') parts.push('<button class=\"primary\" data-cmd=\"sit\">入座</button>')\n } else if (room.phase === 'lobby') {\n parts.push('<span class=\"hint\">' + (you.ready ? '已准备,等房主开局' : '尚未准备') + '</span>')\n parts.push('<button data-cmd=\"' + (you.ready ? 'unready' : 'ready') + '\">' + (you.ready ? '取消准备' : '准备') + '</button>')\n if (room.hostPlayerId === you.playerId) parts.push('<button class=\"primary\" data-cmd=\"start\">开始</button>')\n parts.push('<button class=\"ghost\" data-cmd=\"stand\">离座</button>')\n } else if (room.phase === 'hand') {\n var actorName = ''\n if (hand && hand.toActSeat !== null) {\n var actorSeat = room.seats.filter(function (seat) { return seat.seat === hand.toActSeat })[0]\n actorName = actorSeat && actorSeat.displayName ? actorSeat.displayName : ''\n }\n parts.push('<span class=\"hint\">' + (actorName ? '等待 ' + escape(actorName) + ' 行动' : '等待中') + '</span>')\n } else if (room.phase === 'handOver') {\n parts.push('<span class=\"hint\">' + (room.nextHandAt ? '马上开下一手…' : '本手结束') + '</span>')\n } else {\n parts.push('<span class=\"hint\">房间已结束</span>')\n }\n els.actions.innerHTML = '<div class=\"row\">' + parts.join('') + '</div>'\n els.actions.querySelectorAll('[data-cmd]').forEach(function (button) {\n button.addEventListener('click', function () {\n var cmd = button.dataset.cmd\n if (cmd === 'ready') command({ type: 'ready', ready: true })\n else if (cmd === 'unready') command({ type: 'ready', ready: false })\n else if (cmd === 'start') command({ type: 'start' })\n else if (cmd === 'sit') command({ type: 'sit' })\n else if (cmd === 'stand') command({ type: 'stand' })\n })\n })\n }\n\n function updateTimer() {\n var view = state.view\n var node = document.getElementById('timer')\n if (!node || !view || !view.hand || !view.hand.deadlineAt) return\n var left = Math.max(0, Math.ceil((Date.parse(view.hand.deadlineAt) - Date.now()) / 1000))\n node.textContent = left + ' 秒'\n node.style.color = left <= 5 ? '#e06c75' : '#9db0a6'\n }\n\n function renderRail(view) {\n var board = ''\n for (var i = 0; i < view.leaderboard.length; i += 1) {\n var entry = view.leaderboard[i]\n board += '<div class=\"leaderRow\"><span>' + (i + 1) + '</span><span>' + escape(entry.displayName) + '</span><span class=\"' + (entry.netChips >= 0 ? 'up' : 'down') + '\">' + signed(entry.netChips) + '</span></div>'\n }\n els.leaderboard.innerHTML = board || '<span class=\"hint\">还没有记录</span>'\n var chat = ''\n for (var j = 0; j < view.chat.length; j += 1) {\n chat += '<div><b>' + escape(view.chat[j].displayName) + '</b> ' + escape(view.chat[j].text) + '</div>'\n }\n els.chat.innerHTML = chat || '<span class=\"hint\">还没有记录</span>'\n els.chatInput.disabled = !view.room.settings.chatEnabled\n }\n\n function renderSettlement(view) {\n var summary = view.settlement\n if (!summary) return\n var rows = summary.players.map(function (player) {\n return '<tr><td>' + player.rank + '</td><td>' + escape(player.displayName) + '</td><td>' + player.handsPlayed + '</td><td>' + player.handsWon + '</td><td class=\"' + (player.netChips >= 0 ? 'up' : 'down') + '\">' + signed(player.netChips) + '</td></tr>'\n }).join('')\n els.overlayCard.innerHTML =\n '<h2>本场结算</h2>' +\n '<p class=\"hint\" style=\"margin:0\">' + escape(summary.title) + ' · 房号 ' + escape(summary.roomCode) + ' · 共 ' + summary.handsPlayed + ' 手 · ' + (summary.reason === 'host-idle' ? '房主失联自动解散' : '房主解散房间') + '</p>' +\n '<table><thead><tr><th>名次</th><th>玩家</th><th>手数</th><th>胜局</th><th>净赢</th></tr></thead><tbody>' + rows + '</tbody></table>' +\n '<p class=\"hint\" style=\"margin-top:12px\">这局已经结束,房间不会重开。想再打请向房主要新链接。</p>'\n els.overlay.style.display = 'grid'\n }\n\n function escape(text) {\n return String(text).replace(/[&<>\"']/g, function (char) {\n return ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' })[char]\n })\n }\n\n // ---------------------------------------------------------------- 聊天 / 离开\n document.getElementById('chatSend').addEventListener('click', function () {\n var text = (els.chatInput.value || '').trim()\n if (!text) return\n els.chatInput.value = ''\n command({ type: 'chat', text: text.slice(0, 80) })\n })\n els.chatInput.addEventListener('keydown', function (event) {\n if (event.key === 'Enter') document.getElementById('chatSend').click()\n })\n\n els.leave.addEventListener('click', function () {\n api('/api/leave', { method: 'POST', body: { roomId: state.roomId } }).catch(function () {}).then(function () {\n try { sessionStorage.removeItem(storeKey) } catch (e) {}\n location.reload()\n })\n })\n\n // 键盘:F 弃牌 / C 过牌-跟注 / R 加注\n window.addEventListener('keydown', function (event) {\n var target = event.target\n if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) return\n var view = state.view\n if (!view || !view.you.legal || !view.you.canAct) return\n var legal = view.you.legal\n var key = event.key.toLowerCase()\n if (key === 'f' && legal.canFold) command({ type: 'action', action: { type: 'fold' } })\n else if (key === 'c') command({ type: 'action', action: legal.canCheck ? { type: 'check' } : { type: 'call' } })\n else if (key === 'r' && legal.canRaise) {\n var range = document.getElementById('raiseRange')\n command({ type: 'action', action: { type: 'raise', to: Number(range ? range.value : legal.minRaiseTo) } })\n }\n })\n\n // ---------------------------------------------------------------- 启动\n state.timer = setInterval(updateTimer, 250)\n var saved = ''\n try { saved = sessionStorage.getItem(storeKey) || '' } catch (e) {}\n if (presetName) {\n els.nameInput.value = presetName\n join(preferWatch ? 'watch' : 'sit')\n state.timer = setInterval(updateTimer, 250)\n return\n }\n if (saved) {\n state.token = saved\n api('/api/session?roomId=' + encodeURIComponent(''))\n .then(function (payload) {\n if (payload.inRoom && payload.view && payload.view.room.roomCode === code) {\n applyView(payload.view)\n startPolling()\n return\n }\n state.token = ''\n try { sessionStorage.removeItem(storeKey) } catch (e) {}\n askJoin()\n })\n .catch(function () { askJoin() })\n } else {\n askJoin()\n }\n})();\n</script>\n</body>\n</html>`\n}\n\nfunction escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => ({\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n })[char] ?? char)\n}\n","import { appendFileSync, mkdirSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { WebServer } from '@deepseek-ai/cordis'\nimport { type ClientCommand } from '../types.ts'\nimport { asRoomId } from '../types.ts'\nimport type { RoomManager } from '../room/RoomManager.ts'\nimport {\n assertMutatingHeaders,\n assertOrigin,\n cookieHeader,\n header,\n isLoopbackHost,\n isLoopbackOrigin,\n isPrivateOrigin,\n parseCookies,\n} from './auth.ts'\nimport { encodeJson, readJson } from './json.ts'\nimport { joinPageHtml } from './join-page.ts'\nimport { playPageHtml } from './play-page.ts'\n\n/** 每个房间一个座位 cookie:同一个浏览器可以同时待在不同房间的多个标签页里。 */\nconst SEAT_COOKIE_PREFIX = 'hdm_seat'\n/** 客户端同时用请求头带 token:不依赖 cookie 是否被浏览器/宿主保留。 */\nconst SEAT_HEADER = 'x-holdem-seat'\n\nfunction seatCookieName(roomId: string): string {\n return `${SEAT_COOKIE_PREFIX}_${roomId}`\n}\n\nfunction seatTokens(req: IncomingMessage, cookies: Record<string, string>): string[] {\n const fromHeader = (header(req, SEAT_HEADER) ?? '')\n .split(',')\n .map((value) => value.trim())\n .filter((value) => value !== '')\n const fromCookies = Object.entries(cookies)\n .filter(([name]) => name.startsWith(SEAT_COOKIE_PREFIX))\n .map(([, value]) => value)\n .filter((value) => value !== '')\n return [...new Set([...fromHeader, ...fromCookies])]\n}\n\nfunction sessionOf(manager: RoomManager, tokens: string[], roomId?: string) {\n const sessions = manager.sessionsForTokens(tokens)\n if (roomId) return sessions.find((session) => session.roomId === roomId)\n return sessions[0]\n}\n\n/** 临时诊断日志:把每个请求的结果写到 ~/.dsh/holdem/debug.jsonl */\nfunction trace(req: IncomingMessage, res: ServerResponse): void {\n const started = Date.now()\n res.on('finish', () => {\n try {\n const cookies = Object.keys(parseCookies(req))\n const line = JSON.stringify({\n at: new Date().toISOString(),\n ms: Date.now() - started,\n method: req.method,\n url: req.url,\n status: res.statusCode,\n origin: req.headers.origin ?? null,\n host: req.headers.host ?? null,\n fetchSite: req.headers['sec-fetch-site'] ?? null,\n cookieNames: cookies,\n })\n const dir = join(homedir(), '.dsh', 'holdem')\n mkdirSync(dir, { recursive: true })\n appendFileSync(join(dir, 'debug.jsonl'), line + '\\n')\n } catch {\n // 诊断日志失败不影响主流程\n }\n })\n}\n\nexport interface CloudflareLike {\n getStatus(): unknown\n startLogin(): unknown\n startDeploy(): unknown\n cancelLogin(): unknown\n logout(): unknown\n}\n\n/** role=auto 是默认:有空位就入座,满员自动转观战。 */\nfunction playRole(value: string | null): 'auto' | 'sit' | 'watch' {\n return value === 'watch' ? 'watch' : value === 'sit' ? 'sit' : 'auto'\n}\n\nexport function registerHoldemHttp(\n server: WebServer,\n manager: RoomManager,\n cloudflare?: CloudflareLike,\n): () => void {\n const prefix = manager.getConfig().routePrefix\n return server.register({\n kind: 'prefix',\n path: prefix,\n handler: (req, res) => {\n trace(req, res)\n // 面板可以从本机或局域网另一个 origin 调过来(例如加入云端/别人机器的房间)\n const origin = header(req, 'origin')\n const allow = origin !== undefined && (isLoopbackOrigin(origin) || isPrivateOrigin(origin))\n if (allow) {\n res.setHeader('access-control-allow-origin', origin)\n res.setHeader('access-control-allow-methods', 'GET,POST,OPTIONS')\n res.setHeader('access-control-allow-headers', 'content-type,x-requested-with,x-holdem-seat')\n res.setHeader('access-control-max-age', '600')\n res.setHeader('vary', 'Origin')\n }\n if (req.method === 'OPTIONS') {\n res.writeHead(allow ? 204 : 403)\n res.end()\n return\n }\n void handle(req, res, manager, prefix, cloudflare)\n },\n })\n}\n\nasync function handle(\n req: IncomingMessage,\n res: ServerResponse,\n manager: RoomManager,\n prefix: string,\n cloudflare?: CloudflareLike,\n): Promise<void> {\n try {\n const url = new URL(req.url ?? '/', 'http://127.0.0.1')\n const path = url.pathname\n const cookies = parseCookies(req)\n\n if (req.method === 'GET' && path === `${prefix}/play`) {\n html(res, playPageHtml({\n prefix,\n code: String(url.searchParams.get('code') ?? ''),\n invite: String(url.searchParams.get('invite') ?? ''),\n role: playRole(url.searchParams.get('role')),\n name: String(url.searchParams.get('name') ?? ''),\n }))\n return\n }\n\n if (req.method === 'GET' && (path === prefix || path === `${prefix}/` || path === `${prefix}/join`)) {\n const role = playRole(url.searchParams.get('role'))\n html(res, joinPageHtml({\n prefix,\n code: String(url.searchParams.get('code') ?? ''),\n invite: String(url.searchParams.get('invite') ?? ''),\n role,\n }))\n return\n }\n\n if (req.method === 'GET' && path === `${prefix}/api/ready`) {\n json(res, 200, {\n ok: true,\n plugin: '@zhuweiyou/dsh-holdem',\n turnTimeoutMs: manager.getConfig().turnTimeoutMs,\n })\n return\n }\n\n if (req.method === 'GET' && path === `${prefix}/api/health`) {\n if (!isLoopbackHost(header(req, 'host'), req.socket.remoteAddress)) throw auth('仅本机可查看健康状态')\n json(res, 200, manager.health())\n return\n }\n\n if (path === `${prefix}/api/cloudflare`) {\n if (!cloudflare) {\n json(res, 200, { authorized: false, account: '', workerUrl: '', pending: false, deploying: false, authorizeUrl: '', error: 'not-supported', log: '' })\n return\n }\n if (req.method === 'POST') {\n assertOrigin(req)\n assertMutatingHeaders(req)\n if (!isLoopbackHost(header(req, 'host'), req.socket.remoteAddress)) throw auth('只能在本机操作 Cloudflare 授权')\n const action = url.searchParams.get('action') ?? ''\n if (action === 'login') json(res, 200, cloudflare.startLogin())\n else if (action === 'deploy') json(res, 200, cloudflare.startDeploy())\n else if (action === 'cancel') json(res, 200, cloudflare.cancelLogin())\n else if (action === 'logout') json(res, 200, cloudflare.logout())\n else json(res, 400, { error: 'illegal', message: '未知操作' })\n return\n }\n json(res, 200, cloudflare.getStatus())\n return\n }\n\n if (req.method === 'GET' && path === `${prefix}/api/session`) {\n const wanted = url.searchParams.get('roomId') ?? ''\n const tokens = seatTokens(req, cookies)\n const session = (wanted ? sessionOf(manager, tokens, wanted) : undefined) ?? sessionOf(manager, tokens)\n const seatCookieCount = tokens.length\n if (!session) {\n json(res, 200, { inRoom: false, seatCookies: seatCookieCount })\n return\n }\n try {\n json(res, 200, { inRoom: true, roomId: session.roomId, view: manager.view(session.roomId, session.playerId) })\n } catch {\n json(res, 200, { inRoom: false, seatCookies: seatCookieCount })\n }\n return\n }\n\n if (req.method === 'POST' && path === `${prefix}/api/rooms`) {\n assertOrigin(req)\n assertMutatingHeaders(req)\n const local = isLoopbackHost(header(req, 'host'), req.socket.remoteAddress)\n if (!local) throw auth('只能在运行 Harness 的本机开房')\n const body = await readJson<{ title?: string; blindLevel?: string; displayName?: string; browserId?: string }>(req)\n const created = manager.createRoom({\n title: body.title,\n blindLevel: body.blindLevel,\n displayName: body.displayName,\n browserId: body.browserId,\n })\n json(res, 200, {\n roomId: created.roomId,\n roomCode: created.roomCode,\n hostPlayerId: created.hostPlayerId,\n joinUrl: created.joinUrl,\n wsTicket: created.wsTicket,\n sessionToken: created.sessionToken,\n view: created.view,\n }, [seatCookie(manager, created.sessionToken, created.roomId)])\n return\n }\n\n if (req.method === 'GET' && path === `${prefix}/api/preview`) {\n const preview = manager.peek({\n roomCode: String(url.searchParams.get('code') ?? ''),\n invite: String(url.searchParams.get('invite') ?? ''),\n role: url.searchParams.get('role') === 'watch' ? 'watch' : 'sit',\n local: isLoopbackHost(header(req, 'host'), req.socket.remoteAddress),\n browserId: url.searchParams.get('browser') ?? undefined,\n cookies: seatTokens(req, cookies),\n })\n json(res, 200, preview)\n return\n }\n\n if (req.method === 'POST' && path === `${prefix}/api/join`) {\n assertOrigin(req)\n assertMutatingHeaders(req)\n const body = await readJson<{\n roomCode?: string\n invite?: string\n displayName?: string\n role?: 'sit' | 'watch'\n browserId?: string\n }>(req)\n const joined = manager.join({\n roomCode: String(body.roomCode ?? ''),\n invite: String(body.invite ?? ''),\n role: body.role === 'watch' ? 'watch' : 'sit',\n displayName: String(body.displayName ?? ''),\n local: isLoopbackHost(header(req, 'host'), req.socket.remoteAddress),\n browserId: body.browserId,\n cookies: seatTokens(req, cookies),\n })\n json(res, 200, {\n playerId: joined.playerId,\n roomId: joined.roomId,\n seat: joined.seat,\n wsTicket: joined.wsTicket,\n sessionToken: joined.sessionToken,\n view: joined.view,\n }, [seatCookie(manager, joined.sessionToken, joined.roomId)])\n return\n }\n\n if (req.method === 'POST' && path === `${prefix}/api/leave`) {\n assertOrigin(req)\n assertMutatingHeaders(req)\n const body = await readJson<{ roomId?: string }>(req).catch(() => ({}) as { roomId?: string })\n const tokens = seatTokens(req, cookies)\n const session = sessionOf(manager, tokens, body.roomId) ?? sessionOf(manager, tokens)\n if (session) {\n manager.leave(session.playerId)\n json(res, 200, { ok: true }, [clearSeatCookie(manager, session.roomId)])\n return\n }\n json(res, 200, { ok: true })\n return\n }\n\n if (req.method === 'POST' && path === `${prefix}/api/ws-ticket`) {\n assertOrigin(req)\n assertMutatingHeaders(req)\n const body = await readJson<{ roomId?: string }>(req).catch(() => ({}) as { roomId?: string })\n const tokens = seatTokens(req, cookies)\n const session = sessionOf(manager, tokens, body.roomId) ?? sessionOf(manager, tokens)\n if (!session) throw auth('还没进房间')\n json(res, 200, { wsTicket: manager.issueTicket(session.playerId, session.roomId) })\n return\n }\n\n const roomMatch = /^\\/api\\/rooms\\/([^/]+)(?:\\/(state|action|history))?$/.exec(path.slice(prefix.length))\n if (roomMatch) {\n const action = roomMatch[2]\n const roomId = asRoomId(decodeURIComponent(roomMatch[1] ?? ''))\n const session = sessionOf(manager, seatTokens(req, cookies), roomId)\n if (!session) throw auth('不在这个房间里')\n if (req.method === 'GET' && (!action || action === 'state')) {\n const view = manager.view(roomId, session.playerId)\n json(res, 200, { seq: view.room.seq, view })\n return\n }\n if (req.method === 'GET' && action === 'history') {\n json(res, 200, { summary: manager.exportSummary(roomId, session.playerId) })\n return\n }\n if (req.method === 'POST' && action === 'action') {\n assertOrigin(req)\n assertMutatingHeaders(req)\n const body = await readJson<{\n actionId?: string\n baseSeq?: number\n command?: ClientCommand\n }>(req)\n if (!body.command || typeof body.command !== 'object') throw Object.assign(new Error('缺少 command'), { status: 400, code: 'illegal' })\n const result = manager.command(\n roomId,\n session.playerId,\n body.command,\n Number.isFinite(body.baseSeq) ? Number(body.baseSeq) : undefined,\n typeof body.actionId === 'string' ? body.actionId : undefined,\n )\n json(res, 200, { seq: result.seq, view: result.view, duplicate: result.duplicate === true })\n return\n }\n send(res, 404, encodeJson({ error: 'expired', message: 'not found' }), jsonHeaders())\n return\n }\n\n send(res, 404, encodeJson({ error: 'expired', message: 'not found' }), jsonHeaders())\n } catch (error) {\n writeError(res, error)\n }\n}\n\nfunction seatCookie(manager: RoomManager, token: string, roomId: string): string {\n return cookieHeader(seatCookieName(roomId), token, {\n secure: false,\n sameSite: 'Lax',\n maxAge: 86400,\n path: manager.getConfig().routePrefix,\n })\n}\n\nfunction clearSeatCookie(manager: RoomManager, roomId: string): string {\n return cookieHeader(seatCookieName(roomId), '', {\n secure: false,\n sameSite: 'Lax',\n maxAge: 0,\n path: manager.getConfig().routePrefix,\n })\n}\n\nfunction auth(message: string): Error {\n return Object.assign(new Error(message), { status: 403, code: 'auth' })\n}\n\nfunction json(res: ServerResponse, status: number, body: unknown, cookies?: string[]): void {\n const headers: Record<string, string | string[]> = jsonHeaders()\n if (cookies && cookies.length > 0) headers['set-cookie'] = cookies\n send(res, status, encodeJson(body), headers)\n}\n\nfunction html(res: ServerResponse, body: string): void {\n send(res, 200, body, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' })\n}\n\nfunction jsonHeaders(): Record<string, string> {\n return { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }\n}\n\nfunction send(res: ServerResponse, status: number, body: string, headers: Record<string, string | string[]>): void {\n if (res.headersSent) return\n res.writeHead(status, headers)\n res.end(body)\n}\n\nfunction writeError(res: ServerResponse, error: unknown): void {\n const coded = error as { code?: string; status?: number; view?: unknown }\n const code = coded.code ?? 'illegal'\n const status = coded.status ?? statusOf(code)\n const payload: Record<string, unknown> = {\n error: code,\n message: error instanceof Error ? error.message : 'error',\n }\n if (code === 'stale' && coded.view) payload.view = coded.view\n send(res, status, encodeJson(payload), jsonHeaders())\n}\n\nfunction statusOf(code: string): number {\n if (code === 'auth') return 403\n if (code === 'room-not-found' || code === 'expired') return 404\n if (code === 'rate-limited') return 429\n if (code === 'room-full' || code === 'phase' || code === 'stale' || code === 'already-in-room' || code === 'duplicate-nonce') return 409\n return 400\n}\n","import { appendFileSync, mkdirSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport { WebSocket, WebSocketServer } from 'ws'\nimport type { WebServer } from '@deepseek-ai/cordis'\nimport { DISCONNECT_AFTER_MS, MAX_FRAME_BYTES, SUBPROTOCOL } from '../invariant.ts'\nimport type { RoomManager } from '../room/RoomManager.ts'\nimport type { PlayerId, RoomId, ServerEvent } from '../types.ts'\nimport { assertOrigin } from './auth.ts'\nimport { encodeJson } from './json.ts'\n\ninterface SocketState {\n playerId: PlayerId\n roomId: RoomId\n lastSeen: number\n}\n\n/** 临时诊断日志:WS 升级成功/失败都记一条。 */\nfunction traceWs(line: Record<string, unknown>): void {\n try {\n const dir = join(homedir(), '.dsh', 'holdem')\n mkdirSync(dir, { recursive: true })\n appendFileSync(join(dir, 'debug.jsonl'), JSON.stringify({ at: new Date().toISOString(), ws: true, ...line }) + '\\n')\n } catch {\n // ignore\n }\n}\n\nexport function registerHoldemSocket(server: WebServer, manager: RoomManager): () => void {\n const wss = new WebSocketServer({\n noServer: true,\n handleProtocols: (protocols) => (protocols.has(SUBPROTOCOL) ? SUBPROTOCOL : false),\n })\n const sockets = new Map<WebSocket, SocketState>()\n\n const off = manager.onEvent((playerId, event) => {\n for (const [socket, state] of sockets) {\n if (state.playerId === playerId && socket.readyState === WebSocket.OPEN) send(socket, event)\n }\n })\n\n const heartbeat = setInterval(() => {\n const now = Date.now()\n const limit = DISCONNECT_AFTER_MS\n for (const [socket, state] of sockets) {\n if (now - state.lastSeen > limit) {\n socket.terminate()\n continue\n }\n if (socket.readyState === WebSocket.OPEN) socket.ping()\n }\n }, 15_000)\n if (typeof heartbeat === 'object' && 'unref' in heartbeat) heartbeat.unref()\n\n const disposeRoute = server.registerUpgrade({\n path: `${manager.getConfig().routePrefix}/ws`,\n handler: (req, socket, head) => {\n try {\n assertOrigin(req as IncomingMessage)\n } catch (error) {\n traceWs({ outcome: 'reject-origin', origin: req.headers.origin ?? null, host: req.headers.host ?? null, message: String(error) })\n reject(socket, '403 Forbidden')\n return\n }\n const url = new URL(req.url ?? '/', 'http://127.0.0.1')\n const ticket = url.searchParams.get('ticket') ?? ''\n const consumed = manager.consumeTicket(ticket)\n if (!consumed) {\n traceWs({ outcome: 'reject-ticket', origin: req.headers.origin ?? null, host: req.headers.host ?? null, hasTicket: ticket !== '' })\n reject(socket, '401 Unauthorized')\n return\n }\n traceWs({ outcome: 'open', origin: req.headers.origin ?? null, host: req.headers.host ?? null })\n wss.handleUpgrade(req as IncomingMessage, socket as Duplex, head, (ws) => {\n const state: SocketState = { playerId: consumed.playerId, roomId: consumed.roomId, lastSeen: Date.now() }\n sockets.set(ws, state)\n manager.counters.wsConnected += 1\n manager.markConnected(state.playerId, state.roomId, true)\n try {\n send(ws, { type: 'snapshot', seq: 0, view: manager.view(state.roomId, state.playerId) })\n } catch {\n ws.close()\n return\n }\n ws.on('pong', () => {\n state.lastSeen = Date.now()\n manager.markConnected(state.playerId, state.roomId, true)\n })\n ws.on('message', (data) => {\n state.lastSeen = Date.now()\n if (Buffer.byteLength(data.toString()) > MAX_FRAME_BYTES) {\n ws.close()\n return\n }\n let parsed: { type?: string } | null = null\n try {\n parsed = JSON.parse(data.toString()) as { type?: string }\n } catch {\n return\n }\n if (parsed?.type === 'ping') send(ws, { type: 'pong', ts: Date.now() })\n })\n ws.on('close', () => {\n sockets.delete(ws)\n manager.counters.wsConnected = Math.max(0, manager.counters.wsConnected - 1)\n const stillOpen = [...sockets.values()].some(\n (entry) => entry.playerId === state.playerId && entry.roomId === state.roomId,\n )\n if (!stillOpen) manager.markConnected(state.playerId, state.roomId, false)\n })\n ws.on('error', () => {\n ws.terminate()\n })\n })\n },\n })\n\n return () => {\n clearInterval(heartbeat)\n off()\n disposeRoute()\n for (const socket of sockets.keys()) socket.terminate()\n wss.close()\n }\n}\n\nfunction send(ws: WebSocket, event: ServerEvent): void {\n if (ws.readyState === WebSocket.OPEN) ws.send(encodeJson(event))\n}\n\nfunction reject(socket: Duplex, status: string): void {\n socket.write(`HTTP/1.1 ${status}\\r\\nConnection: close\\r\\n\\r\\n`)\n socket.destroy()\n}\n","import { existsSync } from 'node:fs'\nimport { delimiter, join } from 'node:path'\n\n/** 在 PATH 里找一个可执行文件(Node 与打包后的插件通用)。 */\nexport function whichSync(name: string): string | undefined {\n const exts = process.platform === 'win32' ? (process.env.PATHEXT ?? '.EXE').split(';') : ['']\n for (const dir of (process.env.PATH ?? '').split(delimiter)) {\n if (!dir) continue\n for (const ext of exts) {\n const candidate = join(dir, name + ext)\n if (existsSync(candidate)) return candidate\n }\n }\n return undefined\n}\n","import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process'\nimport { createRequire } from 'node:module'\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, join, resolve } from 'node:path'\nimport { whichSync } from './which.ts'\n\n/**\n * Cloudflare 授权 + 部署:直接驱动 wrangler(官方 CLI)。\n *\n * - 授权:spawn `wrangler login`,它会在本机起回调服务并打开浏览器;\n * 把 OAuth 链接截出来交给界面,浏览器没自动打开时可以复制。\n * - 部署:spawn `wrangler deploy`,从输出里抓 workers.dev 地址并记住。\n */\n\nexport interface CloudflareStatus {\n authorized: boolean\n account: string\n workerUrl: string\n pending: boolean\n deploying: boolean\n authorizeUrl: string\n error: string\n log: string\n}\n\ninterface StoredState {\n workerUrl: string\n account: string\n}\n\nconst OAUTH_PATTERN = /https:\\/\\/dash\\.cloudflare\\.com\\/oauth2\\/auth\\?[^\\s\"'`]+/\nconst WORKER_URL_PATTERN = /https:\\/\\/[a-z0-9-]+\\.[a-z0-9-]+\\.workers\\.dev/i\n\nfunction stateFile(): string {\n const home = process.env.DSH_HOME ?? join(homedir(), '.dsh')\n return join(home, 'holdem', 'cloudflare.json')\n}\n\nfunction readStored(): StoredState {\n try {\n const parsed = JSON.parse(readFileSync(stateFile(), 'utf8')) as Partial<StoredState>\n return { workerUrl: parsed.workerUrl ?? '', account: parsed.account ?? '' }\n } catch {\n return { workerUrl: '', account: '' }\n }\n}\n\nfunction writeStored(next: StoredState): void {\n const file = stateFile()\n mkdirSync(dirname(file), { recursive: true })\n writeFileSync(file, JSON.stringify(next, null, 2))\n}\n\n/**\n * 插件根目录:从当前模块位置往上找 package.json(打包后是 lib/index.js,\n * 源码里是 src/net/cloudflare.ts,层级不一样,不能写死 .. 的层数)。\n */\nfunction packageRoot(): string {\n let dir = dirname(new URL(import.meta.url).pathname)\n for (let depth = 0; depth < 5; depth += 1) {\n if (existsSync(join(dir, 'wrangler.toml'))) return dir\n const parent = resolve(dir, '..')\n if (parent === dir) break\n dir = parent\n }\n return resolve(dirname(new URL(import.meta.url).pathname), '..')\n}\n\nfunction wranglerCredentialFiles(): string[] {\n const home = homedir()\n return [\n join(home, 'Library', 'Preferences', '.wrangler', 'config', 'default.toml'),\n join(home, '.config', '.wrangler', 'config', 'default.toml'),\n join(home, '.wrangler', 'config', 'default.toml'),\n ]\n}\n\nfunction hasWranglerCredentials(): boolean {\n return wranglerCredentialFiles().some((file) => existsSync(file))\n}\n\ninterface WranglerCommand {\n command: string\n baseArgs: string[]\n env?: Record<string, string>\n label: string\n}\n\n/**\n * 找一套能跑起来的 wrangler 调用方式,并做一次 `--version` 探活:\n * 1. HOLDEM_WRANGLER 显式指定;\n * 2. PATH 里的 node + 依赖里的 wrangler(正常情况,不依赖全局 wrangler);\n * 3. Electron 自带 Node 模式(用户机器上没装 Node 时):\n * ELECTRON_RUN_AS_NODE=1 <Electron 可执行文件> wrangler/bin/wrangler.js;\n * 4. 插件目录的 .bin/wrangler 软链;\n * 5. PATH 里的全局 wrangler。\n */\nfunction wranglerCandidates(): WranglerCommand[] {\n const candidates: WranglerCommand[] = []\n const custom = process.env.HOLDEM_WRANGLER\n if (custom) candidates.push({ command: custom, baseArgs: [], label: `HOLDEM_WRANGLER=${custom}` })\n\n let cli = ''\n try {\n const require = createRequire(import.meta.url)\n const pkg = require.resolve('wrangler/package.json')\n const candidate = join(dirname(pkg), 'bin', 'wrangler.js')\n if (existsSync(candidate)) cli = candidate\n } catch {\n // 依赖里没有 wrangler\n }\n\n if (cli) {\n const node = whichSync('node')\n if (node) candidates.push({ command: node, baseArgs: [cli], label: `${node} ${cli}` })\n if (process.versions.electron) {\n candidates.push({\n command: process.execPath,\n baseArgs: [cli],\n env: { ELECTRON_RUN_AS_NODE: '1' },\n label: `ELECTRON_RUN_AS_NODE ${cli}`,\n })\n }\n }\n\n const shim = join(packageRoot(), 'node_modules', '.bin', 'wrangler')\n if (existsSync(shim)) candidates.push({ command: shim, baseArgs: [], label: shim })\n const onPath = whichSync('wrangler')\n if (onPath) candidates.push({ command: onPath, baseArgs: [], label: onPath })\n return candidates\n}\n\nlet resolvedCommand: WranglerCommand | null = null\n\nfunction wranglerCommand(): WranglerCommand {\n if (resolvedCommand) return resolvedCommand\n for (const candidate of wranglerCandidates()) {\n try {\n const probe = spawnSync(candidate.command, [...candidate.baseArgs, '--version'], {\n encoding: 'utf8',\n timeout: 15_000,\n env: candidate.env ? { ...process.env, ...candidate.env } : process.env,\n })\n if (probe.status === 0) {\n resolvedCommand = candidate\n return candidate\n }\n } catch {\n // 下一个候选\n }\n }\n return { command: 'wrangler', baseArgs: [], label: 'wrangler' }\n}\n\nfunction stripAnsi(text: string): string {\n return text.replace(/[\\u001b\\u009b][[()#;?]*(?:\\d{1,4}(?:;\\d{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '')\n}\n\nexport class CloudflareDeployer {\n private stored = readStored()\n private login: ChildProcessWithoutNullStreams | null = null\n private deploy: ChildProcessWithoutNullStreams | null = null\n private authorizeUrl = ''\n private log = ''\n private error = ''\n private checkedAt = 0\n private authorized = false\n\n getStatus(): CloudflareStatus {\n this.refreshAuth()\n return {\n authorized: this.authorized,\n account: this.stored.account,\n workerUrl: this.stored.workerUrl,\n pending: this.login !== null,\n deploying: this.deploy !== null,\n authorizeUrl: this.authorizeUrl,\n error: this.error,\n log: this.log.slice(-1500),\n }\n }\n\n /** 已经授权过就不再反复跑 whoami(60 秒缓存)。 */\n private refreshAuth(): void {\n if (this.login || this.deploy) return\n const now = Date.now()\n if (now - this.checkedAt < 60_000) return\n this.checkedAt = now\n try {\n const cli = wranglerCommand()\n const result = spawnSync(cli.command, [...cli.baseArgs, 'whoami'], {\n encoding: 'utf8',\n timeout: 15_000,\n env: cli.env ? { ...process.env, ...cli.env } : process.env,\n })\n const output = stripAnsi(`${result.stdout ?? ''}\\n${result.stderr ?? ''}`)\n const notLoggedIn = /not logged in|You are not authenticated/i.test(output)\n if (notLoggedIn) {\n this.authorized = false\n return\n }\n const email = /associated with the email\\s+([^\\s.]+(?:\\.[^\\s.]+)*)/i.exec(output)?.[1]\n if (email) {\n this.authorized = true\n this.stored = { ...this.stored, account: email }\n return\n }\n // whoami 超时或输出不认识:凭据文件在就当已授权,别让界面误报\n this.authorized = result.status === 0 || hasWranglerCredentials()\n } catch {\n this.authorized = hasWranglerCredentials()\n }\n }\n\n startLogin(): CloudflareStatus {\n if (this.login) return this.getStatus()\n this.error = ''\n this.authorizeUrl = ''\n const cli = wranglerCommand()\n this.log = `wrangler: ${cli.label}\\n`\n try {\n const child = spawn(cli.command, [...cli.baseArgs, 'login'], {\n cwd: packageRoot(),\n env: cli.env ? { ...process.env, ...cli.env } : process.env,\n })\n this.login = child\n const onData = (chunk: Buffer): void => {\n const text = stripAnsi(chunk.toString())\n this.log = `${this.log}${text}`\n const match = OAUTH_PATTERN.exec(text)\n if (match && this.authorizeUrl === '') {\n // wrangler login 自己会拉起浏览器;这里只把链接暴露给界面,\n // 万一浏览器没弹出来,用户可以复制这条链接手动打开。\n this.authorizeUrl = match[0]\n }\n }\n child.stdout.on('data', onData)\n child.stderr.on('data', onData)\n child.on('exit', (code) => {\n this.login = null\n this.checkedAt = 0\n if (code !== 0) this.error = 'Cloudflare 授权没有完成'\n })\n } catch (error) {\n this.error = error instanceof Error ? error.message : String(error)\n }\n return this.getStatus()\n }\n\n /** 等待浏览器回调时用户点了取消:直接杀掉 wrangler 进程。 */\n cancelLogin(): CloudflareStatus {\n const child = this.login\n this.login = null\n this.authorizeUrl = ''\n this.checkedAt = 0\n try {\n child?.kill()\n } catch {\n // 忽略\n }\n return this.getStatus()\n }\n\n startDeploy(): CloudflareStatus {\n if (this.deploy) return this.getStatus()\n this.error = ''\n const cli = wranglerCommand()\n this.log = `wrangler: ${cli.label}\\n`\n try {\n // 显式指定 config:当前目录往上可能有别人的 wrangler.jsonc,会被误用\n const child = spawn(\n cli.command,\n [...cli.baseArgs, 'deploy', '--config', join(packageRoot(), 'wrangler.toml')],\n { cwd: packageRoot(), env: cli.env ? { ...process.env, ...cli.env } : process.env },\n )\n this.deploy = child\n const onData = (chunk: Buffer): void => {\n const text = stripAnsi(chunk.toString())\n this.log = `${this.log}${text}`\n const match = WORKER_URL_PATTERN.exec(text)\n if (match) {\n this.stored = { ...this.stored, workerUrl: match[0].replace(/\\/$/, '') }\n writeStored(this.stored)\n }\n }\n child.stdout.on('data', onData)\n child.stderr.on('data', onData)\n child.on('exit', (code) => {\n this.deploy = null\n if (code !== 0 && this.stored.workerUrl === '') this.error = '部署失败,看看 wrangler 输出的日志'\n })\n } catch (error) {\n this.error = error instanceof Error ? error.message : String(error)\n }\n return this.getStatus()\n }\n\n logout(): CloudflareStatus {\n try {\n const cli = wranglerCommand()\n spawnSync(cli.command, [...cli.baseArgs, 'logout'], {\n encoding: 'utf8',\n timeout: 10_000,\n env: cli.env ? { ...process.env, ...cli.env } : process.env,\n })\n } catch {\n // 忽略\n }\n this.authorized = false\n this.checkedAt = 0\n this.stored = { ...this.stored, account: '' }\n writeStored(this.stored)\n return this.getStatus()\n }\n\n dispose(): void {\n this.login?.kill()\n this.deploy?.kill()\n this.login = null\n this.deploy = null\n }\n}\n\n","import { request as httpRequest, createServer, type IncomingMessage, type ServerResponse } from 'node:http'\nimport net from 'node:net'\n\n/**\n * DSH 的 Web 服务只监听 127.0.0.1,同 Wi-Fi 的朋友连不上。\n * 这里起一个只转发 `/holdem/*`(HTTP + WS)的小代理,绑到 0.0.0.0,\n * 用它的端口生成「局域网邀请链接」。\n */\nexport interface LanProxy {\n port: number\n dispose(): void\n}\n\nexport interface LanProxyOptions {\n targetPort: number\n prefix: string\n /** 期望端口;被占用就交给系统分配。 */\n port?: number\n log?: (message: string) => void\n}\n\nexport function startLanProxy(options: LanProxyOptions): Promise<LanProxy | null> {\n const prefix = options.prefix\n const target = { host: '127.0.0.1', port: options.targetPort }\n const log = options.log\n\n const forward = (req: IncomingMessage, res: ServerResponse): void => {\n const upstream = httpRequest(\n {\n host: target.host,\n port: target.port,\n method: req.method,\n path: req.url,\n headers: req.headers,\n },\n (up) => {\n res.writeHead(up.statusCode ?? 502, up.headers)\n up.pipe(res)\n },\n )\n upstream.on('error', () => {\n if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' })\n res.end('upstream unavailable')\n })\n req.pipe(upstream)\n }\n\n const server = createServer((req, res) => {\n const path = (req.url ?? '/').split('?')[0] ?? '/'\n if (path !== prefix && !path.startsWith(`${prefix}/`)) {\n res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })\n res.end('not found')\n return\n }\n forward(req, res)\n })\n\n // WebSocket:把 upgrade 也原样转到本地端口\n server.on('upgrade', (req, socket, head) => {\n const path = (req.url ?? '/').split('?')[0] ?? '/'\n if (path !== prefix && !path.startsWith(`${prefix}/`)) {\n socket.destroy()\n return\n }\n const upstream = net.connect(target.port, target.host, () => {\n const lines = [`GET ${req.url} HTTP/1.1`]\n const headers = { ...req.headers }\n for (const [key, value] of Object.entries(headers)) {\n if (Array.isArray(value)) for (const item of value) lines.push(`${key}: ${item}`)\n else if (value !== undefined) lines.push(`${key}: ${value}`)\n }\n upstream.write(`${lines.join('\\r\\n')}\\r\\n\\r\\n`)\n if (head && head.length > 0) upstream.write(head)\n upstream.pipe(socket)\n socket.pipe(upstream)\n })\n upstream.on('error', () => socket.destroy())\n socket.on('error', () => upstream.destroy())\n })\n\n return new Promise((resolve) => {\n const fallback = (): void => {\n // 指定端口被占用时让系统挑一个\n if (options.port !== undefined) {\n const retry = createRetry()\n retry()\n return\n }\n resolve(null)\n }\n const createRetry = () => () => {\n server.removeAllListeners('error')\n server.once('error', () => resolve(null))\n server.listen(0, '0.0.0.0', () => {\n const address = server.address()\n const port = typeof address === 'object' && address ? address.port : 0\n if (port <= 0) {\n resolve(null)\n return\n }\n log?.(`局域网分享已开启:http://0.0.0.0:${port}${prefix}`)\n resolve({ port, dispose: () => server.close() })\n })\n }\n server.once('error', fallback)\n server.listen(options.port ?? 0, '0.0.0.0', () => {\n const address = server.address()\n const port = typeof address === 'object' && address ? address.port : 0\n if (port <= 0) {\n resolve(null)\n return\n }\n log?.(`局域网分享已开启:端口 ${port}${prefix}`)\n resolve({ port, dispose: () => server.close() })\n })\n })\n}\n","import { networkInterfaces } from 'node:os'\n\n/** 找本机一个可用的局域网 IPv4(同 Wi-Fi 的朋友能直接访问)。 */\nexport function firstLanIPv4(): string {\n const interfaces = networkInterfaces()\n const candidates: string[] = []\n for (const entries of Object.values(interfaces)) {\n for (const entry of entries ?? []) {\n if (entry.family !== 'IPv4' || entry.internal) continue\n candidates.push(entry.address)\n }\n }\n // 常见家用/办公网段优先\n const preferred = candidates.find((ip) => ip.startsWith('192.168.'))\n ?? candidates.find((ip) => ip.startsWith('10.'))\n ?? candidates.find((ip) => /^172\\.(1[6-9]|2\\d|3[01])\\./.test(ip))\n ?? candidates[0]\n return preferred ?? ''\n}\n","import type { HandResultView, SessionSummary } from '../types.ts'\n\nexport interface HandRecord extends HandResultView {\n roomCode: string\n title: string\n recordedAt: string\n}\n\nexport interface HoldemStore {\n appendHand(record: HandRecord): void\n appendSession(summary: SessionSummary): void\n}\n\n/** 无持久化时的空实现(也用于 Cloudflare Worker,那边没有文件系统)。 */\nexport const noopStore: HoldemStore = {\n appendHand() {},\n appendSession() {},\n}\n","import { appendFile, mkdir, writeFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport { noopStore, type HandRecord, type HoldemStore } from './noop.ts'\n\nexport { noopStore }\nexport type { HandRecord, HoldemStore }\n\nexport function storeRoot(): string {\n const home = process.env.DSH_HOME ?? join(homedir(), '.dsh')\n return join(home, 'holdem')\n}\n\n/** 只追加的 JSONL 审计日志;任何失败都不影响牌局。 */\nexport function createFileStore(root = storeRoot()): HoldemStore {\n let ready: Promise<void> | null = null\n const ensure = (): Promise<void> => {\n ready ??= mkdir(root, { recursive: true }).then(() => undefined)\n return ready\n }\n const append = (file: string, line: string): void => {\n void ensure()\n .then(() => appendFile(join(root, file), `${line}\\n`, 'utf8'))\n .catch(() => undefined)\n }\n return {\n appendHand(record) {\n append('hands.jsonl', JSON.stringify(record))\n },\n appendSession(summary) {\n append('sessions.jsonl', JSON.stringify(summary))\n void ensure()\n .then(() => writeFile(join(root, 'last-session.json'), JSON.stringify(summary, null, 2), 'utf8'))\n .catch(() => undefined)\n },\n }\n}\n","/** 纯 JS 的 SHA-256:Node 和 Cloudflare Workers 都能同步调用。 */\nconst K = new Uint32Array([\n 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,\n 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,\n 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,\n 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,\n 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,\n 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,\n 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,\n 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,\n])\n\nfunction rotr(value: number, bits: number): number {\n return (value >>> bits) | (value << (32 - bits))\n}\n\nexport function sha256Hex(text: string): string {\n const bytes = new TextEncoder().encode(text)\n const bitLength = bytes.length * 8\n const withPadding = new Uint8Array((((bytes.length + 9) >> 6) + 1) << 6)\n withPadding.set(bytes)\n withPadding[bytes.length] = 0x80\n const view = new DataView(withPadding.buffer)\n view.setUint32(withPadding.length - 4, bitLength >>> 0, false)\n view.setUint32(withPadding.length - 8, Math.floor(bitLength / 0x100000000), false)\n\n const hash = new Uint32Array([\n 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,\n ])\n const w = new Uint32Array(64)\n for (let offset = 0; offset < withPadding.length; offset += 64) {\n for (let index = 0; index < 16; index += 1) w[index] = view.getUint32(offset + index * 4, false)\n for (let index = 16; index < 64; index += 1) {\n const s0 = rotr(w[index - 15]!, 7) ^ rotr(w[index - 15]!, 18) ^ (w[index - 15]! >>> 3)\n const s1 = rotr(w[index - 2]!, 17) ^ rotr(w[index - 2]!, 19) ^ (w[index - 2]! >>> 10)\n w[index] = (w[index - 16]! + s0 + w[index - 7]! + s1) >>> 0\n }\n let [a, b, c, d, e, f, g, h] = hash\n for (let index = 0; index < 64; index += 1) {\n const s1 = rotr(e!, 6) ^ rotr(e!, 11) ^ rotr(e!, 25)\n const ch = (e! & f!) ^ (~e! & g!)\n const temp1 = (h! + s1 + ch + K[index]! + w[index]!) >>> 0\n const s0 = rotr(a!, 2) ^ rotr(a!, 13) ^ rotr(a!, 22)\n const maj = (a! & b!) ^ (a! & c!) ^ (b! & c!)\n const temp2 = (s0 + maj) >>> 0\n h = g\n g = f\n f = e\n e = (d! + temp1) >>> 0\n d = c\n c = b\n b = a\n a = (temp1 + temp2) >>> 0\n }\n hash[0] = (hash[0]! + a!) >>> 0\n hash[1] = (hash[1]! + b!) >>> 0\n hash[2] = (hash[2]! + c!) >>> 0\n hash[3] = (hash[3]! + d!) >>> 0\n hash[4] = (hash[4]! + e!) >>> 0\n hash[5] = (hash[5]! + f!) >>> 0\n hash[6] = (hash[6]! + g!) >>> 0\n hash[7] = (hash[7]! + h!) >>> 0\n }\n return [...hash].map((word) => word.toString(16).padStart(8, '0')).join('')\n}\n\ninterface RandomSource {\n getRandomValues<T extends ArrayBufferView>(array: T): T\n}\n\nfunction webcrypto(): RandomSource {\n return (globalThis as unknown as { crypto: RandomSource }).crypto\n}\n\nfunction randomBytes(length: number): Uint8Array {\n const bytes = new Uint8Array(length)\n webcrypto().getRandomValues(bytes)\n return bytes\n}\n\nfunction toBase64Url(bytes: Uint8Array): string {\n let binary = ''\n for (const byte of bytes) binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\n/** [0, maxExclusive) 的无偏随机整数(拒绝采样,Node 与 Workers 通用)。 */\nexport function randomBelow(maxExclusive: number): number {\n if (maxExclusive <= 0) throw new Error('maxExclusive must be positive')\n const limit = Math.floor(0x100000000 / maxExclusive) * maxExclusive\n const buffer = new Uint32Array(1)\n for (;;) {\n webcrypto().getRandomValues(buffer)\n const value = buffer[0]!\n if (value < limit) return value % maxExclusive\n }\n}\n\nexport function newId(prefix: string): string {\n return `${prefix}_${toBase64Url(randomBytes(9))}`\n}\n\n/** 6 位房号,CSPRNG 且无取模偏置。 */\nexport function newRoomCode(): string {\n return String(randomBelow(1_000_000)).padStart(6, '0')\n}\n\nexport function newToken(): string {\n return toBase64Url(randomBytes(24))\n}\n\nexport function newSecret(): string {\n return toBase64Url(randomBytes(16))\n}\n\nexport function hashInvite(secret: string, kind: 'sit' | 'watch', token: string): string {\n return sha256Hex(`${secret}|${kind}|${token}`)\n}\n\nexport function hashEquals(expectedHex: string, actualHex: string): boolean {\n if (expectedHex.length !== actualHex.length) return false\n let diff = 0\n for (let index = 0; index < expectedHex.length; index += 1) {\n diff |= expectedHex.charCodeAt(index) ^ actualHex.charCodeAt(index)\n }\n return diff === 0\n}\n\nexport function sanitizeDisplayName(value: unknown): string {\n const text = String(value ?? '').replace(/[\\u0000-\\u001f\\u007f]/g, '').trim()\n return text.slice(0, 16) || '玩家'\n}\n\nexport function sanitizeChat(value: unknown): string | null {\n const text = String(value ?? '')\n .replace(/[\\u0000-\\u001f\\u007f]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n if (text.length === 0) return null\n return text.slice(0, 80)\n}\n\nexport function sanitizeTitle(value: unknown): string {\n const text = String(value ?? '').replace(/[\\u0000-\\u001f\\u007f]/g, '').trim()\n return text.slice(0, 24) || '好友牌桌'\n}\n","import type { Card } from '../types.ts'\n\nexport const RANK_CHARS = ['2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A'] as const\nexport const SUIT_CHARS = ['s', 'h', 'd', 'c'] as const\nexport type Suit = (typeof SUIT_CHARS)[number]\n\nexport const SUIT_SYMBOL: Record<Suit, string> = { s: '♠', h: '♥', d: '♦', c: '♣' }\nexport const SUIT_NAME_ZH: Record<Suit, string> = { s: '黑桃', h: '红心', d: '方块', c: '梅花' }\nexport const SUIT_IS_RED: Record<Suit, boolean> = { s: false, h: true, d: true, c: false }\n\nconst CARD_RE = /^[2-9TJQKA][shdc]$/\n\nexport function isCard(value: unknown): value is Card {\n return typeof value === 'string' && CARD_RE.test(value)\n}\n\nexport function asCard(value: string): Card {\n if (!isCard(value)) throw new Error(`invalid card: ${value}`)\n return value\n}\n\nexport function rankOf(card: Card): number {\n const index = RANK_CHARS.indexOf(card[0] as (typeof RANK_CHARS)[number])\n return index + 2\n}\n\nexport function suitOf(card: Card): Suit {\n return card[1] as Suit\n}\n\nexport function makeCard(rank: number, suit: Suit): Card {\n const char = RANK_CHARS[rank - 2]\n if (!char) throw new Error(`invalid rank: ${rank}`)\n return `${char}${suit}` as Card\n}\n\nexport function cardLabel(card: Card): string {\n return `${card[0]}${SUIT_SYMBOL[suitOf(card)]}`\n}\n\nexport function sameCard(a: Card, b: Card): boolean {\n return a === b\n}\n\n/** 稳定的 0..51 牌序号,用于排序与去重断言。 */\nexport function cardIndex(card: Card): number {\n const rank = rankOf(card) - 2\n const suit = SUIT_CHARS.indexOf(suitOf(card))\n return rank * 4 + suit\n}\n\nexport function sortCards(cards: readonly Card[]): Card[] {\n return [...cards].sort((a, b) => cardIndex(a) - cardIndex(b))\n}\n","import { randomBelow } from '../room/ids.ts'\nimport type { Card } from '../types.ts'\nimport { RANK_CHARS, SUIT_CHARS, makeCard } from './cards.ts'\n\nexport type RandomBelow = (maxExclusive: number) => number\n\n/** 密码学无偏随机:拒绝采样,避免取模偏置。 */\nexport const cryptoBelow: RandomBelow = (maxExclusive) => randomBelow(maxExclusive)\n\nexport function makeDeck(): Card[] {\n const deck: Card[] = []\n for (let rank = 2; rank <= 14; rank += 1) {\n for (const suit of SUIT_CHARS) deck.push(makeCard(rank, suit))\n }\n return deck\n}\n\n/** Fisher–Yates;RNG 可注入以便测试复现。 */\nexport function shuffle<T>(items: readonly T[], random: RandomBelow = cryptoBelow): T[] {\n const out = [...items]\n for (let i = out.length - 1; i > 0; i -= 1) {\n const j = random(i + 1)\n const tmp = out[i]!\n out[i] = out[j]!\n out[j] = tmp\n }\n return out\n}\n\nexport function shuffledDeck(random: RandomBelow = cryptoBelow): Card[] {\n return shuffle(makeDeck(), random)\n}\n\n/** mulberry32:仅测试使用,可复现。 */\nexport function seededRandom(seed: number): RandomBelow {\n let state = seed >>> 0\n return (maxExclusive: number) => {\n state = (state + 0x6d2b79f5) >>> 0\n let t = state\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n const value = ((t ^ (t >>> 14)) >>> 0) / 4294967296\n return Math.floor(value * maxExclusive)\n }\n}\n\nexport const RANKS_IN_DECK = RANK_CHARS.length * SUIT_CHARS.length\n","import type { LegalActions } from '../types.ts'\n\nexport interface BetPlayer {\n seat: number\n stack: number\n streetCommitted: number\n folded: boolean\n allIn: boolean\n /** 上一次行动时的 raiseEpoch;-1 表示本街还未行动。 */\n actedEpoch: number\n}\n\nexport interface BetRound {\n currentBet: number\n /** 最近一次「满额加注」的加注量,用于计算最小加注。 */\n lastFullRaise: number\n /** 每发生一次满额加注 +1;短筹码全下不递增。 */\n raiseEpoch: number\n bigBlind: number\n}\n\nexport type BetAction =\n | { type: 'fold' }\n | { type: 'check' }\n | { type: 'call' }\n | { type: 'raise'; to: number }\n | { type: 'allin' }\n\nexport class BettingError extends Error {\n readonly code: 'illegal' | 'not-your-turn'\n constructor(code: 'illegal' | 'not-your-turn', reason: string) {\n super(reason)\n this.code = code\n this.name = 'BettingError'\n }\n}\n\nexport function toCall(player: BetPlayer, round: BetRound): number {\n return Math.max(0, round.currentBet - player.streetCommitted)\n}\n\nexport function maxTo(player: BetPlayer): number {\n return player.streetCommitted + player.stack\n}\n\n/** 最小加注到的总额:无人下注时为 BB,否则为当前注 + 上次满额加注量。 */\nexport function minRaiseTo(round: BetRound): number {\n return round.currentBet === 0 ? round.bigBlind : round.currentBet + round.lastFullRaise\n}\n\nexport function isActing(player: BetPlayer): boolean {\n return !player.folded && !player.allIn\n}\n\nexport function legalFor(player: BetPlayer, round: BetRound): LegalActions {\n const empty: LegalActions = {\n canFold: false,\n canCheck: false,\n canCall: false,\n callAmount: 0,\n canRaise: false,\n minRaiseTo: 0,\n maxRaiseTo: 0,\n canAllIn: false,\n allInTo: 0,\n }\n if (!isActing(player)) return empty\n\n const owed = toCall(player, round)\n const ceiling = maxTo(player)\n const canRaise = player.actedEpoch !== round.raiseEpoch && ceiling > round.currentBet && player.stack > 0\n const minTo = canRaise ? Math.min(minRaiseTo(round), ceiling) : 0\n return {\n canFold: true,\n canCheck: owed === 0,\n canCall: owed > 0,\n callAmount: Math.min(owed, player.stack),\n canRaise,\n minRaiseTo: minTo,\n maxRaiseTo: canRaise ? ceiling : 0,\n canAllIn: player.stack > 0,\n allInTo: ceiling,\n }\n}\n\nexport interface AppliedAction {\n action: BetAction['type']\n paid: number\n fullRaise: boolean\n label: string\n}\n\n/** 就地更新 player 与 round;非法动作抛 BettingError。 */\nexport function applyAction(player: BetPlayer, round: BetRound, action: BetAction): AppliedAction {\n if (!isActing(player)) throw new BettingError('illegal', '该玩家已弃牌或已全下')\n\n if (action.type === 'fold') {\n player.folded = true\n player.actedEpoch = round.raiseEpoch\n return { action: 'fold', paid: 0, fullRaise: false, label: '弃牌' }\n }\n\n if (action.type === 'check') {\n if (toCall(player, round) > 0) throw new BettingError('illegal', '有人下注时不能过牌')\n player.actedEpoch = round.raiseEpoch\n return { action: 'check', paid: 0, fullRaise: false, label: '过牌' }\n }\n\n if (action.type === 'call') {\n const owed = toCall(player, round)\n if (owed === 0) {\n player.actedEpoch = round.raiseEpoch\n return { action: 'check', paid: 0, fullRaise: false, label: '过牌' }\n }\n const paid = commit(player, Math.min(owed, player.stack))\n player.actedEpoch = round.raiseEpoch\n const short = paid < owed\n return {\n action: 'call',\n paid,\n fullRaise: false,\n label: short ? `全下跟注 ${paid}` : `跟注 ${paid}`,\n }\n }\n\n const target = action.type === 'allin' ? maxTo(player) : action.to\n const wasOpeningBet = round.currentBet === 0\n if (!Number.isInteger(target)) throw new BettingError('illegal', '加注额必须是整数')\n if (target <= round.currentBet) {\n if (action.type === 'allin') {\n const paid = commit(player, Math.min(toCall(player, round), player.stack))\n player.actedEpoch = round.raiseEpoch\n return { action: 'call', paid, fullRaise: false, label: `全下跟注 ${paid}` }\n }\n throw new BettingError('illegal', '加注额必须大于当前注')\n }\n const ceiling = maxTo(player)\n if (target > ceiling) throw new BettingError('illegal', '加注额超过自己的筹码')\n if (player.actedEpoch === round.raiseEpoch) {\n throw new BettingError('illegal', '本轮已行动,短筹码全下后不能再加注')\n }\n const paid = commit(player, target - player.streetCommitted)\n const raiseBy = target - round.currentBet\n const allIn = player.stack === 0\n const fullRaise = raiseBy >= round.lastFullRaise\n if (!fullRaise && !allIn) {\n throw new BettingError('illegal', `最小加注到 ${minRaiseTo(round)}`)\n }\n if (fullRaise) {\n round.lastFullRaise = raiseBy\n round.raiseEpoch += 1\n }\n round.currentBet = Math.max(round.currentBet, target)\n player.actedEpoch = round.raiseEpoch\n return {\n action: allIn ? 'allin' : 'raise',\n paid,\n fullRaise,\n label: allIn ? `全下 ${target}` : wasOpeningBet ? `下注 ${target}` : `加注到 ${target}`,\n }\n}\n\nfunction commit(player: BetPlayer, amount: number): number {\n const paid = Math.max(0, Math.min(amount, player.stack))\n player.stack -= paid\n player.streetCommitted += paid\n if (player.stack === 0) player.allIn = true\n return paid\n}\n\nexport function roundComplete(\n players: readonly BetPlayer[],\n round: BetRound,\n): boolean {\n const live = players.filter((p) => !p.folded)\n if (live.length <= 1) return true\n return live.every(\n (p) => p.allIn || (p.actedEpoch === round.raiseEpoch && p.streetCommitted === round.currentBet),\n )\n}\n\nexport function canActCount(players: readonly BetPlayer[]): number {\n return players.filter(isActing).length\n}\n","export interface PotPlayer {\n seat: number\n /** 本手累计投入(含本街)。 */\n committed: number\n folded: boolean\n}\n\nexport interface Pot {\n amount: number\n eligible: number[]\n}\n\nexport interface PotBuild {\n pots: Pot[]\n /** 未被跟注、应原路退回的筹码。 */\n returned: { seat: number; amount: number }[]\n}\n\n/**\n * 按投入额分层构造主池与边池。\n * 弃牌者的投入留在池里,但没有资格;只有一名玩家投入的最高层视为未被跟注,退回。\n */\nexport function buildPots(players: readonly PotPlayer[]): PotBuild {\n const contributors = players.filter((p) => p.committed > 0)\n if (contributors.length === 0) return { pots: [], returned: [] }\n\n const levels = [...new Set(contributors.map((p) => p.committed))].sort((a, b) => a - b)\n const pots: Pot[] = []\n let previous = 0\n for (const level of levels) {\n const inLevel = contributors.filter((p) => p.committed >= level)\n const amount = inLevel.reduce((sum, p) => sum + (Math.min(p.committed, level) - previous), 0)\n if (amount > 0) {\n pots.push({ amount, eligible: inLevel.filter((p) => !p.folded).map((p) => p.seat) })\n }\n previous = level\n }\n\n const returned: { seat: number; amount: number }[] = []\n const top = pots[pots.length - 1]\n if (top && top.eligible.length === 1) {\n const level = levels[levels.length - 1]!\n const contributorsAtTop = contributors.filter((p) => p.committed >= level)\n if (contributorsAtTop.length === 1) {\n // 整层只由一名玩家投入:这就是未被跟注的部分。\n const seat = contributorsAtTop[0]!.seat\n returned.push({ seat, amount: top.amount })\n pots.pop()\n }\n }\n return { pots, returned }\n}\n\nexport interface PotAward {\n amount: number\n eligible: number[]\n winners: number[]\n}\n\nexport interface Distribution {\n awards: Map<number, number>\n pots: PotAward[]\n}\n\n/**\n * 逐池分配。`scores` 为 seat → 牌力分;奇数筹码按「按钮左手第一个赢家」起逐个 +1。\n */\nexport function distributePots(\n pots: readonly Pot[],\n scores: ReadonlyMap<number, number>,\n button: number,\n seatCount = 6,\n): Distribution {\n const awards = new Map<number, number>()\n const results: PotAward[] = []\n for (const pot of pots) {\n const eligible = pot.eligible.filter((seat) => scores.has(seat))\n if (eligible.length === 0) {\n results.push({ amount: pot.amount, eligible: pot.eligible, winners: [] })\n continue\n }\n let best = -Infinity\n for (const seat of eligible) best = Math.max(best, scores.get(seat) ?? -1)\n const winners = eligible.filter((seat) => (scores.get(seat) ?? -1) === best)\n const share = Math.floor(pot.amount / winners.length)\n let remainder = pot.amount - share * winners.length\n const ordered = [...winners].sort(\n (a, b) => clockwiseDistance(button, a, seatCount) - clockwiseDistance(button, b, seatCount),\n )\n for (const seat of winners) addAward(awards, seat, share)\n for (const seat of ordered) {\n if (remainder <= 0) break\n addAward(awards, seat, 1)\n remainder -= 1\n }\n results.push({ amount: pot.amount, eligible, winners })\n }\n return { awards, pots: results }\n}\n\nfunction addAward(awards: Map<number, number>, seat: number, amount: number): void {\n awards.set(seat, (awards.get(seat) ?? 0) + amount)\n}\n\n/** 从 button 顺时针数到 seat 的步数(1 表示紧邻左手)。 */\nexport function clockwiseDistance(button: number, seat: number, seatCount = 6): number {\n return (seat - button - 1 + seatCount * 2) % seatCount\n}\n\nexport function potTotal(pots: readonly Pot[]): number {\n return pots.reduce((sum, pot) => sum + pot.amount, 0)\n}\n","import type { Card } from '../types.ts'\nimport { RANK_CHARS, rankOf, suitOf } from './cards.ts'\n\nexport const CAT_HIGH_CARD = 0\nexport const CAT_PAIR = 1\nexport const CAT_TWO_PAIR = 2\nexport const CAT_TRIPS = 3\nexport const CAT_STRAIGHT = 4\nexport const CAT_FLUSH = 5\nexport const CAT_FULL_HOUSE = 6\nexport const CAT_QUADS = 7\nexport const CAT_STRAIGHT_FLUSH = 8\n\nexport const CATEGORY_NAMES = [\n '高牌',\n '一对',\n '两对',\n '三条',\n '顺子',\n '同花',\n '葫芦',\n '四条',\n '同花顺',\n] as const\n\nexport interface HandValue {\n /** 0..8;-1 表示不足 5 张。 */\n cat: number\n kickers: number[]\n score: number\n name: string\n}\n\nexport function rankText(rank: number): string {\n return RANK_CHARS[rank - 2] ?? '?'\n}\n\nfunction makeValue(cat: number, kickers: number[]): HandValue {\n const padded = [...kickers]\n while (padded.length < 5) padded.push(0)\n let score = cat\n for (let i = 0; i < 5; i += 1) score = score * 16 + (padded[i] ?? 0)\n return { cat, kickers, score, name: handName(cat, kickers[0] ?? 0) }\n}\n\nexport function handName(cat: number, high: number): string {\n if (cat === CAT_STRAIGHT_FLUSH && high === 14) return '皇家同花顺'\n return CATEGORY_NAMES[cat] ?? '未知'\n}\n\n/** 一手 5 张牌的牌力。 */\nexport function evaluate5(cards: readonly Card[]): HandValue {\n if (cards.length !== 5) throw new Error(`evaluate5 expects 5 cards, got ${cards.length}`)\n const ranks = cards.map(rankOf).sort((a, b) => b - a)\n const suits = cards.map(suitOf)\n const flush = suits.every((suit) => suit === suits[0])\n\n const counts = new Map<number, number>()\n for (const rank of ranks) counts.set(rank, (counts.get(rank) ?? 0) + 1)\n\n const groups = [...counts.entries()].sort((a, b) => (b[1] - a[1]) || (b[0] - a[0]))\n const unique = [...counts.keys()].sort((a, b) => b - a)\n const straightHigh = straightHighOf(unique)\n\n if (straightHigh > 0 && flush) return makeValue(CAT_STRAIGHT_FLUSH, [straightHigh])\n if ((groups[0]?.[1] ?? 0) === 4) {\n const quad = groups[0]![0]\n const kicker = groups[1]![0]\n return makeValue(CAT_QUADS, [quad, kicker])\n }\n if ((groups[0]?.[1] ?? 0) === 3 && (groups[1]?.[1] ?? 0) >= 2) {\n return makeValue(CAT_FULL_HOUSE, [groups[0]![0], groups[1]![0]])\n }\n if (flush) return makeValue(CAT_FLUSH, ranks)\n if (straightHigh > 0) return makeValue(CAT_STRAIGHT, [straightHigh])\n if ((groups[0]?.[1] ?? 0) === 3) {\n const rest = unique.filter((rank) => rank !== groups[0]![0]).slice(0, 2)\n return makeValue(CAT_TRIPS, [groups[0]![0], ...rest])\n }\n if ((groups[0]?.[1] ?? 0) === 2 && (groups[1]?.[1] ?? 0) === 2) {\n const highPair = Math.max(groups[0]![0], groups[1]![0])\n const lowPair = Math.min(groups[0]![0], groups[1]![0])\n const kicker = groups[2]![0]\n return makeValue(CAT_TWO_PAIR, [highPair, lowPair, kicker])\n }\n if ((groups[0]?.[1] ?? 0) === 2) {\n const rest = unique.filter((rank) => rank !== groups[0]![0]).slice(0, 3)\n return makeValue(CAT_PAIR, [groups[0]![0], ...rest])\n }\n return makeValue(CAT_HIGH_CARD, ranks)\n}\n\n/** 5..7 张里最好的 5 张;不足 5 张返回无效值。 */\nexport function evaluateBest(cards: readonly Card[]): HandValue {\n if (cards.length < 5) return { cat: -1, kickers: [], score: -1, name: '未成牌' }\n if (cards.length === 5) return evaluate5(cards)\n let best: HandValue | null = null\n const n = cards.length\n for (let a = 0; a < n - 4; a += 1) {\n for (let b = a + 1; b < n - 3; b += 1) {\n for (let c = b + 1; c < n - 2; c += 1) {\n for (let d = c + 1; d < n - 1; d += 1) {\n for (let e = d + 1; e < n; e += 1) {\n const value = evaluate5([cards[a]!, cards[b]!, cards[c]!, cards[d]!, cards[e]!])\n if (!best || value.score > best.score) best = value\n }\n }\n }\n }\n }\n return best ?? { cat: -1, kickers: [], score: -1, name: '未成牌' }\n}\n\nexport function compareHandValues(a: HandValue, b: HandValue): number {\n if (a.score !== b.score) return a.score - b.score\n if (a.cat !== b.cat) return a.cat - b.cat\n for (let i = 0; i < Math.max(a.kickers.length, b.kickers.length); i += 1) {\n const diff = (a.kickers[i] ?? 0) - (b.kickers[i] ?? 0)\n if (diff !== 0) return diff\n }\n return 0\n}\n\n/** 返回顺子的最大牌点;A2345 记为 5;不成顺返回 0。 */\nfunction straightHighOf(descUnique: number[]): number {\n if (descUnique.length < 5) return 0\n const isWheel =\n descUnique.includes(14) &&\n descUnique.includes(5) &&\n descUnique.includes(4) &&\n descUnique.includes(3) &&\n descUnique.includes(2)\n if (isWheel) return 5\n for (let i = 0; i + 4 < descUnique.length; i += 1) {\n const high = descUnique[i]!\n const low = descUnique[i + 4]!\n if (high - low === 4) return high\n }\n return 0\n}\n\n/** 牌型名 + 关键牌点,如「一对 K」「同花 A」。 */\nexport function describeHand(value: HandValue): string {\n if (value.cat < 0) return '未成牌'\n if (value.cat === CAT_STRAIGHT_FLUSH && value.kickers[0] === 14) return '皇家同花顺'\n const high = value.kickers[0]\n if (high === undefined) return value.name\n return `${value.name} ${rankText(high)}`\n}\n","import type {\n Card,\n HandResultView,\n HandView,\n LegalActions,\n PlayerId,\n Seat,\n ShownHandView,\n Street,\n} from '../types.ts'\nimport { nowIso } from '../types.ts'\nimport { ALL_IN_RUNOUT_MS } from '../invariant.ts'\nimport { shuffledDeck, type RandomBelow } from './deck.ts'\nimport {\n applyAction,\n BettingError,\n legalFor,\n roundComplete,\n type BetAction,\n type BetPlayer,\n type BetRound,\n} from './betting.ts'\nimport { buildPots, distributePots, potTotal } from './pots.ts'\nimport { describeHand, evaluateBest, type HandValue } from './rank.ts'\n\nexport interface HandPlayer extends BetPlayer {\n playerId: PlayerId\n displayName: string\n buyIn: number\n /** 本手累计投入(跨街),边池按此分层。 */\n committed: number\n hole: Card[]\n lastAction: string | null\n}\n\nexport interface HandSeat {\n seat: Seat\n playerId: PlayerId\n displayName: string\n}\n\nexport interface CreateHandOptions {\n handNo: number\n /** 本手按钮位(由房间按上一手轮转后传入)。 */\n button: Seat\n seats: HandSeat[]\n sb: number\n bb: number\n buyIn: number\n turnTimeoutMs: number\n now: number\n random?: RandomBelow\n /** 测试用:直接给定牌堆(从末尾取牌)。 */\n deck?: Card[]\n}\n\nexport interface HandState {\n handNo: number\n button: Seat\n sbSeat: Seat\n bbSeat: Seat\n street: Street\n deck: Card[]\n board: Card[]\n players: HandPlayer[]\n round: BetRound\n toActSeat: Seat | null\n deadlineAt: number | null\n startedAt: number\n runoutDueAt: number | null\n completed: boolean\n result: HandResultView | null\n revealed: boolean\n turnTimeoutMs: number\n}\n\nexport class HandError extends Error {\n readonly code: 'illegal' | 'not-your-turn' | 'phase'\n constructor(code: 'illegal' | 'not-your-turn' | 'phase', reason: string) {\n super(reason)\n this.code = code\n this.name = 'HandError'\n }\n}\n\nconst STREET_ORDER: Street[] = ['preflop', 'flop', 'turn', 'river', 'showdown']\n\nexport function createHand(options: CreateHandOptions): HandState {\n const seats = [...options.seats].sort((a, b) => a.seat - b.seat)\n if (seats.length < 2) throw new HandError('phase', '至少需要两名玩家')\n if (!seats.some((entry) => entry.seat === options.button)) {\n throw new HandError('phase', '按钮位不在活跃座位上')\n }\n\n const players: HandPlayer[] = seats.map((entry) => ({\n seat: entry.seat,\n playerId: entry.playerId,\n displayName: entry.displayName,\n buyIn: options.buyIn,\n stack: options.buyIn,\n streetCommitted: 0,\n committed: 0,\n folded: false,\n allIn: false,\n actedEpoch: -1,\n hole: [],\n lastAction: null,\n }))\n\n const headsUp = players.length === 2\n const button = options.button\n const sbSeat = headsUp ? button : nextSeatIn(players, button)\n const bbSeat = nextSeatIn(players, sbSeat)\n\n const state: HandState = {\n handNo: options.handNo,\n button,\n sbSeat,\n bbSeat,\n street: 'preflop',\n deck: options.deck ? [...options.deck] : shuffledDeck(options.random),\n board: [],\n players,\n round: { currentBet: 0, lastFullRaise: options.bb, raiseEpoch: 0, bigBlind: options.bb },\n toActSeat: null,\n deadlineAt: null,\n startedAt: options.now,\n runoutDueAt: null,\n completed: false,\n result: null,\n revealed: false,\n turnTimeoutMs: options.turnTimeoutMs,\n }\n\n // 两轮发底牌:从 SB 起顺时针,每人一张,再来一圈。\n let cursor = sbSeat\n for (let pass = 0; pass < 2; pass += 1) {\n for (let i = 0; i < players.length; i += 1) {\n playerAt(state, cursor)?.hole.push(draw(state))\n cursor = nextSeatIn(players, cursor)\n }\n }\n\n postBlind(state, sbSeat, options.sb)\n postBlind(state, bbSeat, options.bb)\n state.round.currentBet = Math.max(\n playerAt(state, sbSeat)?.streetCommitted ?? 0,\n playerAt(state, bbSeat)?.streetCommitted ?? 0,\n )\n state.toActSeat = headsUp ? sbSeat : nextActingSeat(state, bbSeat)\n armDeadline(state, options.now)\n if (state.toActSeat === null) progress(state, options.now, null)\n return state\n}\n\nexport function playerAt(state: HandState, seat: Seat): HandPlayer | undefined {\n return state.players.find((player) => player.seat === seat)\n}\n\nexport function livePlayers(state: HandState): HandPlayer[] {\n return state.players.filter((player) => !player.folded)\n}\n\nexport function actingPlayers(state: HandState): HandPlayer[] {\n return state.players.filter((player) => !player.folded && !player.allIn)\n}\n\nexport function legalForSeat(state: HandState, seat: Seat): LegalActions | null {\n if (state.completed || state.runoutDueAt !== null) return null\n if (state.toActSeat !== seat) return null\n const player = playerAt(state, seat)\n if (!player) return null\n return legalFor(player, state.round)\n}\n\nexport interface ActOutcome {\n seat: Seat\n label: string\n streetChanged: boolean\n completed: boolean\n}\n\nexport function act(state: HandState, seat: Seat, action: BetAction, now: number): ActOutcome {\n if (state.completed) throw new HandError('phase', '本手已结束')\n if (state.runoutDueAt !== null) throw new HandError('phase', '全下跑马中,等待发完公共牌')\n if (state.toActSeat !== seat) throw new HandError('not-your-turn', '还没轮到你行动')\n const player = playerAt(state, seat)\n if (!player) throw new HandError('illegal', '座位不存在')\n\n const before = state.street\n let applied\n try {\n applied = applyAction(player, state.round, action)\n } catch (error) {\n if (error instanceof BettingError) throw new HandError(error.code, error.message)\n throw error\n }\n player.committed += applied.paid\n player.lastAction = applied.label\n state.deadlineAt = null\n state.toActSeat = null\n progress(state, now, seat)\n return {\n seat,\n label: applied.label,\n streetChanged: state.street !== before,\n completed: state.completed,\n }\n}\n\n/** 超时自动行动:能过牌就过牌,否则弃牌。 */\nexport function forceTimeout(state: HandState, now: number): ActOutcome | null {\n if (state.completed || state.toActSeat === null) return null\n const seat = state.toActSeat\n const player = playerAt(state, seat)\n if (!player) return null\n const legal = legalFor(player, state.round)\n const action: BetAction = legal.canCheck ? { type: 'check' } : { type: 'fold' }\n return act(state, seat, action, now)\n}\n\n/** 玩家在非自己回合离开/断线:直接弃牌;只在自己回合或只剩一人时推进。 */\nexport function foldSeat(state: HandState, seat: Seat, now: number): boolean {\n if (state.completed) return false\n const player = playerAt(state, seat)\n if (!player || player.folded) return false\n const wasToAct = state.toActSeat === seat\n player.folded = true\n player.actedEpoch = state.round.raiseEpoch\n player.lastAction = '弃牌'\n if (!wasToAct) {\n if (livePlayers(state).length <= 1) {\n state.toActSeat = null\n state.deadlineAt = null\n progress(state, now, seat)\n }\n return true\n }\n state.toActSeat = null\n state.deadlineAt = null\n if (state.runoutDueAt !== null) return true\n progress(state, now, seat)\n return true\n}\n\n/** 驱动全下跑马:到期逐街发牌,最后摊牌。返回状态是否变化。 */\nexport function tick(state: HandState, now: number): boolean {\n if (state.completed || state.runoutDueAt === null) return false\n if (now < state.runoutDueAt) return false\n if (state.street === 'river' || state.street === 'showdown') {\n showdown(state, now)\n return true\n }\n dealNextStreet(state)\n state.runoutDueAt = now + ALL_IN_RUNOUT_MS\n return true\n}\n\nexport function isRunout(state: HandState): boolean {\n return state.runoutDueAt !== null && !state.completed\n}\n\nfunction progress(state: HandState, now: number, fromSeat: Seat | null): void {\n if (livePlayers(state).length <= 1) {\n finishFoldWin(state, now)\n return\n }\n if (!roundComplete(state.players, state.round)) {\n const from = fromSeat ?? state.players.find((p) => !p.folded)?.seat ?? state.button\n state.toActSeat = nextActingSeat(state, from)\n if (state.toActSeat === null) {\n beginRunout(state, now)\n return\n }\n armDeadline(state, now)\n return\n }\n if (actingPlayers(state).length <= 1) {\n beginRunout(state, now)\n return\n }\n advanceStreet(state, now)\n}\n\nfunction beginRunout(state: HandState, now: number): void {\n state.toActSeat = null\n state.deadlineAt = null\n if (state.street === 'river' || state.street === 'showdown') {\n showdown(state, now)\n return\n }\n state.runoutDueAt = now + ALL_IN_RUNOUT_MS\n}\n\nfunction advanceStreet(state: HandState, now: number): void {\n if (state.street === 'river' || state.street === 'showdown') {\n showdown(state, now)\n return\n }\n dealNextStreet(state)\n state.runoutDueAt = null\n state.toActSeat = nextActingSeat(state, state.button)\n armDeadline(state, now)\n if (actingPlayers(state).length <= 1) beginRunout(state, now)\n}\n\nfunction dealNextStreet(state: HandState): void {\n const index = STREET_ORDER.indexOf(state.street)\n const next = STREET_ORDER[Math.min(index + 1, STREET_ORDER.length - 1)]!\n burn(state)\n if (next === 'flop') {\n for (let i = 0; i < 3; i += 1) state.board.push(draw(state))\n } else if (next === 'turn' || next === 'river') {\n state.board.push(draw(state))\n }\n state.street = next\n state.round = {\n currentBet: 0,\n lastFullRaise: state.round.bigBlind,\n raiseEpoch: state.round.raiseEpoch + 1,\n bigBlind: state.round.bigBlind,\n }\n for (const player of state.players) {\n player.streetCommitted = 0\n if (!player.folded && !player.allIn) player.actedEpoch = -1\n }\n}\n\nfunction armDeadline(state: HandState, now: number): void {\n state.deadlineAt = state.completed || state.toActSeat === null ? null : now + state.turnTimeoutMs\n}\n\nfunction showdown(state: HandState, _now: number): void {\n if (state.completed) return\n while (state.board.length < 5) {\n if (state.board.length === 0) {\n burn(state)\n for (let i = 0; i < 3; i += 1) state.board.push(draw(state))\n } else {\n burn(state)\n state.board.push(draw(state))\n }\n }\n state.street = 'showdown'\n state.revealed = true\n state.toActSeat = null\n state.deadlineAt = null\n state.runoutDueAt = null\n\n const live = livePlayers(state)\n const values = new Map<number, HandValue>()\n const shown: ShownHandView[] = []\n for (const player of live) {\n const value = evaluateBest([...player.hole, ...state.board])\n values.set(player.seat, value)\n shown.push({\n seat: player.seat,\n cards: [...player.hole],\n name: describeHand(value),\n score: value.score,\n })\n }\n\n const built = buildPots(state.players)\n const scoreMap = new Map<number, number>([...values].map(([seat, value]) => [seat, value.score]))\n const distribution = distributePots(built.pots, scoreMap, state.button)\n for (const entry of built.returned) addStack(state, entry.seat, entry.amount)\n for (const [seat, amount] of distribution.awards) addStack(state, seat, amount)\n\n const winners = [...new Set(distribution.pots.flatMap((pot) => pot.winners))]\n finishHand(state, {\n foldedWin: false,\n pots: built.pots,\n winners,\n shownHands: shown,\n })\n}\n\nfunction finishFoldWin(state: HandState, now: number): void {\n const winner = livePlayers(state)[0]\n if (!winner) return\n const built = buildPots(state.players)\n for (const entry of built.returned) addStack(state, entry.seat, entry.amount)\n const winnings = built.pots.reduce((sum, pot) => sum + pot.amount, 0)\n addStack(state, winner.seat, winnings)\n winner.lastAction = winner.lastAction ?? '获胜'\n state.street = 'showdown'\n state.revealed = true\n state.toActSeat = null\n state.deadlineAt = null\n state.runoutDueAt = null\n finishHand(state, {\n foldedWin: true,\n pots: built.pots,\n winners: [winner.seat],\n shownHands: [],\n })\n void now\n}\n\nfunction finishHand(\n state: HandState,\n payload: {\n foldedWin: boolean\n pots: { amount: number; eligible: Seat[] }[]\n winners: Seat[]\n shownHands: ShownHandView[]\n },\n): void {\n state.completed = true\n state.result = {\n handNo: state.handNo,\n board: [...state.board],\n pots: payload.pots,\n winners: payload.winners,\n shownHands: payload.shownHands,\n deltas: state.players.map((player) => ({\n seat: player.seat,\n playerId: player.playerId,\n displayName: player.displayName,\n net: player.stack - player.buyIn,\n })),\n foldedWin: payload.foldedWin,\n endedAt: nowIso(),\n }\n}\n\nexport function handView(state: HandState): HandView {\n const built = buildPots(state.players)\n const revealed = state.revealed && state.result !== null && !state.result.foldedWin\n return {\n handNo: state.handNo,\n street: state.street,\n board: [...state.board],\n button: state.button,\n sbSeat: state.sbSeat,\n bbSeat: state.bbSeat,\n toActSeat: state.toActSeat,\n currentBet: state.round.currentBet,\n minRaiseTo: state.round.currentBet === 0 ? state.round.bigBlind : state.round.currentBet + state.round.lastFullRaise,\n potTotal: potTotal(built.pots),\n pots: built.pots.map((pot) => ({ amount: pot.amount, eligible: [...pot.eligible] })),\n deadlineAt: state.deadlineAt === null ? null : new Date(state.deadlineAt).toISOString(),\n shownHands: revealed\n ? livePlayers(state).map((player) => {\n const value = evaluateBest([...player.hole, ...state.board])\n return {\n seat: player.seat,\n cards: [...player.hole],\n name: describeHand(value),\n score: value.score,\n }\n })\n : [],\n winners: state.result?.winners ?? [],\n }\n}\n\nfunction postBlind(state: HandState, seat: Seat, amount: number): void {\n const player = playerAt(state, seat)\n if (!player) return\n const paid = Math.min(amount, player.stack)\n player.stack -= paid\n player.streetCommitted += paid\n player.committed += paid\n if (player.stack === 0) player.allIn = true\n player.lastAction = amount === state.round.bigBlind ? `大盲 ${paid}` : `小盲 ${paid}`\n}\n\nfunction addStack(state: HandState, seat: Seat, amount: number): void {\n const player = playerAt(state, seat)\n if (player) player.stack += amount\n}\n\nfunction burn(state: HandState): void {\n draw(state)\n}\n\nfunction draw(state: HandState): Card {\n const card = state.deck.pop()\n if (!card) throw new HandError('phase', '牌堆已空')\n return card\n}\n\nfunction nextSeatIn(players: readonly { seat: Seat }[], from: Seat): Seat {\n const order = players.map((player) => player.seat).sort((a, b) => a - b)\n for (let step = 1; step <= 12; step += 1) {\n const candidate = ((from + step) % 12) as Seat\n if (order.includes(candidate)) return candidate\n }\n return order[0]!\n}\n\nfunction nextActingSeat(state: HandState, from: Seat): Seat | null {\n const seats = state.players\n .filter((player) => !player.folded && !player.allIn)\n .map((player) => player.seat)\n .sort((a, b) => a - b)\n if (seats.length === 0) return null\n for (let step = 1; step <= 12; step += 1) {\n const candidate = ((from + step) % 12) as Seat\n if (seats.includes(candidate)) return candidate\n }\n return seats[0]!\n}\n","import type { ResolvedConfig } from '../config.ts'\nimport type {\n ChatLine,\n ClientCommand,\n HandResultView,\n LeaderboardEntry,\n PlayerId,\n PlayerView,\n RoomId,\n RoomPhase,\n RoomPreview,\n RoomSettingsView,\n RoomView,\n SeatView,\n ServerEvent,\n SessionSummary,\n TableAction,\n} from '../types.ts'\nimport { CommandError, asPlayerId, asRoomId, asSessionId, nowIso } from '../types.ts'\nimport {\n MAX_CHAT_LINES,\n MAX_HISTORY_HANDS,\n MAX_SEATS,\n MAX_SPECTATORS,\n CHAT_COOLDOWN_MS,\n HAND_OVER_MIN_MS,\n TICK_MS,\n SESSION_TTL_MS,\n blindsFor,\n buyInFor,\n} from '../invariant.ts'\nimport { act, createHand, foldSeat, forceTimeout, handView, legalForSeat, tick as tickHand, type HandState } from '../engine/hand.ts'\nimport { HandError } from '../engine/hand.ts'\nimport type { HoldemStore } from '../persist/noop.ts'\nimport { noopStore } from '../persist/noop.ts'\nimport {\n hashEquals,\n hashInvite,\n newId,\n newRoomCode,\n newSecret,\n newToken,\n sanitizeChat,\n sanitizeDisplayName,\n sanitizeTitle,\n} from './ids.ts'\n\nexport interface Session {\n token: string\n playerId: PlayerId\n roomId: RoomId\n kind: 'seat' | 'watch' | 'host'\n createdAt: number\n expiresAt: number\n}\n\ninterface LivePlayer {\n playerId: PlayerId\n displayName: string\n seat: number | null\n ready: boolean\n connected: boolean\n sittingOut: boolean\n autoFold: boolean\n timeoutStreak: number\n browserId: string\n joinedAt: number\n leaving: boolean\n}\n\ninterface Stats {\n displayName: string\n seat: number | null\n handsPlayed: number\n handsWon: number\n netChips: number\n biggestWin: number\n showdownsWon: number\n showdownsPlayed: number\n}\n\ninterface LiveRoom {\n roomId: RoomId\n roomCode: string\n title: string\n hostPlayerId: PlayerId\n phase: RoomPhase\n settings: RoomSettingsView\n players: Map<PlayerId, LivePlayer>\n stats: Map<PlayerId, Stats>\n hand: HandState | null\n handNo: number\n lastHand: HandResultView | null\n lastButton: number | null\n history: HandResultView[]\n chat: ChatLine[]\n seq: number\n createdAt: number\n inviteExpiresAt: number\n secret: string\n sitToken: string\n watchToken: string\n sitHash: string\n watchHash: string\n handOverAt: number | null\n summary: SessionSummary | null\n closedAt: number | null\n hostLastSeen: number\n lastChatAt: Map<PlayerId, number>\n browsers: Map<string, PlayerId>\n actionCache: Map<string, number>\n}\n\nexport interface RoomManagerOptions {\n config: ResolvedConfig\n origin: () => string\n now?: () => number\n store?: HoldemStore\n log?: (message: string) => void\n /** 局域网 IPv4(Node 下用 os.networkInterfaces,Worker 下为空)。 */\n lanIp?: () => string\n}\n\nexport interface CreateRoomInput {\n title?: string | undefined\n blindLevel?: string | undefined\n displayName?: string | undefined\n browserId?: string | undefined\n}\n\nexport interface PeekInput {\n roomCode: string\n invite?: string | undefined\n role?: 'sit' | 'watch'\n local: boolean\n browserId?: string | undefined\n /** 浏览器带来的所有座位 token(每个房间一个),按新到旧排列。 */\n cookies?: string[] | undefined\n}\n\nexport interface JoinInput extends PeekInput {\n displayName: string\n role: 'sit' | 'watch'\n}\n\nexport interface CreateRoomResult {\n roomId: RoomId\n roomCode: string\n hostPlayerId: PlayerId\n joinUrl: string\n wsTicket: string\n sessionToken: string\n view: PlayerView\n}\n\nexport interface JoinResult {\n playerId: PlayerId\n roomId: RoomId\n seat: number | null\n wsTicket: string\n sessionToken: string\n view: PlayerView\n}\n\ninterface Ticket {\n playerId: PlayerId\n roomId: RoomId\n expiresAt: number\n}\n\nexport class StaleCommandError extends CommandError {\n readonly view: PlayerView\n constructor(reason: string, view: PlayerView) {\n super('stale', reason)\n this.view = view\n }\n}\n\nconst CLOSED_RETENTION_MS = 10 * 60 * 1000\n\nexport class RoomManager {\n readonly counters = { roomsCreated: 0, roomsClosed: 0, handsPlayed: 0, wsConnected: 0, commands: 0 }\n\n private config: ResolvedConfig\n private readonly origin: () => string\n private readonly now: () => number\n private readonly store: HoldemStore\n\n private readonly rooms = new Map<string, LiveRoom>()\n private readonly roomsByCode = new Map<string, LiveRoom>()\n private readonly sessions = new Map<string, Session>()\n private readonly tickets = new Map<string, Ticket>()\n private readonly listeners = new Set<(playerId: PlayerId, event: ServerEvent) => void>()\n\n constructor(options: RoomManagerOptions) {\n this.config = options.config\n this.origin = options.origin\n this.now = options.now ?? (() => Date.now())\n this.store = options.store ?? noopStore\n this.lanIp = options.lanIp ?? (() => '')\n void options.log\n }\n\n getConfig(): ResolvedConfig {\n return this.config\n }\n\n /** 局域网小代理的端口(0 表示没起来)。 */\n private lanPort = 0\n private readonly lanIp: () => string\n\n setLanPort(port: number): void {\n this.lanPort = port > 0 ? port : 0\n }\n\n replaceConfig(config: ResolvedConfig): void {\n this.config = config\n }\n\n start(): () => void {\n const timer = setInterval(() => this.tick(), TICK_MS)\n // Node 下别让定时器拖住进程;Workers 里 setInterval 返回数字,没有 unref\n const handle = timer as unknown as { unref?: () => void }\n if (typeof handle === 'object' && typeof handle.unref === 'function') handle.unref()\n return () => clearInterval(timer)\n }\n\n async dispose(): Promise<void> {\n this.rooms.clear()\n this.roomsByCode.clear()\n this.sessions.clear()\n this.tickets.clear()\n this.listeners.clear()\n }\n\n onEvent(listener: (playerId: PlayerId, event: ServerEvent) => void): () => void {\n this.listeners.add(listener)\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n // ---------------------------------------------------------------- 房间/会话\n\n createRoom(input: CreateRoomInput): CreateRoomResult {\n const now = this.now()\n const openRooms = [...this.rooms.values()].filter((room) => room.phase !== 'closed').length\n if (openRooms >= this.config.maxRooms) throw new CommandError('room-full', '房间数量已达上限')\n\n const blinds = blindsFor(input.blindLevel ?? this.config.blindLevel)\n const code = this.uniqueCode()\n const secret = newSecret()\n const sitToken = newToken()\n const watchToken = newToken()\n const roomId = asRoomId(newId('room'))\n const room: LiveRoom = {\n roomId,\n roomCode: code,\n title: sanitizeTitle(input.title),\n hostPlayerId: asPlayerId(''),\n phase: 'lobby',\n settings: {\n blindLevel: blinds.id,\n sb: blinds.sb,\n bb: blinds.bb,\n buyIn: buyInFor(blinds.bb),\n turnTimeoutMs: this.config.turnTimeoutMs,\n autoAdvanceMs: this.config.autoAdvanceMs,\n allowSpectators: this.config.allowSpectators,\n chatEnabled: this.config.chatEnabled,\n },\n players: new Map(),\n stats: new Map(),\n hand: null,\n handNo: 0,\n lastHand: null,\n lastButton: null,\n history: [],\n chat: [],\n seq: 0,\n createdAt: now,\n inviteExpiresAt: now + 24 * 60 * 60 * 1000,\n secret,\n sitToken,\n watchToken,\n sitHash: hashInvite(secret, 'sit', sitToken),\n watchHash: hashInvite(secret, 'watch', watchToken),\n handOverAt: null,\n summary: null,\n closedAt: null,\n hostLastSeen: now,\n lastChatAt: new Map(),\n browsers: new Map(),\n actionCache: new Map(),\n }\n\n const host = this.addPlayer(room, sanitizeDisplayName(input.displayName), 0, input.browserId ?? '')\n host.ready = true\n room.hostPlayerId = host.playerId\n this.rooms.set(roomId, room)\n this.roomsByCode.set(code, room)\n this.counters.roomsCreated += 1\n\n const session = this.newSession(room, host.playerId, 'host')\n const joinUrl = this.joinUrlFor(room, sitToken)\n return {\n roomId,\n roomCode: code,\n hostPlayerId: host.playerId,\n joinUrl,\n wsTicket: this.issueTicket(host.playerId, roomId),\n sessionToken: session.token,\n view: this.project(room, host.playerId),\n }\n }\n\n peek(input: PeekInput): RoomPreview {\n const room = this.roomByCode(input.roomCode)\n const inviteOk = this.checkInvite(room, input, input.role ?? 'sit', input.local)\n if (!inviteOk.allowed) throw new CommandError('expired', '邀请已失效,请向房主要一个新链接')\n\n const seated = [...room.players.values()].filter((player) => player.seat !== null)\n const seats = Array.from({ length: MAX_SEATS }, (_, seat) => {\n const occupant = seated.find((player) => player.seat === seat)\n return {\n seat,\n displayName: occupant?.displayName ?? null,\n ready: occupant?.ready ?? false,\n host: occupant?.playerId === room.hostPlayerId,\n connected: occupant?.connected ?? false,\n }\n })\n const existing = this.existingPlayer(room, input)\n return {\n roomCode: room.roomCode,\n title: room.title,\n hostDisplayName: room.players.get(room.hostPlayerId)?.displayName ?? '房主',\n phase: room.phase,\n settings: room.settings,\n seated: seated.length,\n spectators: [...room.players.values()].filter((player) => player.seat === null).length,\n seats,\n canSit: room.phase === 'lobby' && seated.length < MAX_SEATS,\n alreadyInRoom: existing !== null,\n inviteExpiresAt: new Date(room.inviteExpiresAt).toISOString(),\n }\n }\n\n join(input: JoinInput): JoinResult {\n const room = this.roomByCode(input.roomCode)\n const wanted = input.role === 'watch' ? 'watch' : 'sit'\n const invite = this.checkInvite(room, input, wanted, input.local)\n if (!invite.allowed) throw new CommandError('expired', '邀请已失效,请向房主要一个新链接')\n\n const cookieSession = this.sessionFromCookies(input.cookies, room.roomId)\n if (cookieSession) {\n const player = room.players.get(cookieSession.playerId)\n if (player && !player.leaving) {\n player.connected = true\n player.browserId = input.browserId ?? player.browserId\n return {\n playerId: player.playerId,\n roomId: room.roomId,\n seat: player.seat,\n wsTicket: this.issueTicket(player.playerId, room.roomId),\n sessionToken: cookieSession.token,\n view: this.project(room, player.playerId),\n }\n }\n }\n\n const existing = this.existingPlayer(room, input)\n if (existing) throw new CommandError('already-in-room', '这个浏览器已经在这个房间里了')\n\n const seatedCount = [...room.players.values()].filter((player) => player.seat !== null).length\n const canSit = wanted === 'sit' && invite.sit && room.phase === 'lobby' && seatedCount < MAX_SEATS\n if (!canSit) {\n if (!room.settings.allowSpectators) throw new CommandError('room-full', '房间已满且不允许观战')\n const spectators = [...room.players.values()].filter((player) => player.seat === null)\n if (spectators.length >= MAX_SPECTATORS) throw new CommandError('room-full', '观战席已满')\n if (wanted === 'sit' && invite.sit === false) throw new CommandError('auth', '没有入座权限')\n }\n\n const seat = canSit ? this.firstFreeSeat(room) : null\n const player = this.addPlayer(room, sanitizeDisplayName(input.displayName), seat, input.browserId ?? '')\n player.ready = seat !== null\n const session = this.newSession(room, player.playerId, seat === null ? 'watch' : 'seat')\n if (seat === null && room.phase === 'hand') {\n this.pushChat(room, null, `${player.displayName} 进入观战`)\n }\n this.publish(room)\n return {\n playerId: player.playerId,\n roomId: room.roomId,\n seat: player.seat,\n wsTicket: this.issueTicket(player.playerId, room.roomId),\n sessionToken: session.token,\n view: this.project(room, player.playerId),\n }\n }\n\n leave(playerId: PlayerId): void {\n const room = this.roomOf(playerId)\n if (!room) return\n if (room.phase === 'closed') {\n // 结算页点「返回大厅」:把座位与会话清掉,别让旧房间一直挂着\n this.removePlayer(room, playerId)\n this.dropSessionsOf(room, playerId)\n return\n }\n const isHost = room.hostPlayerId === playerId\n const player = room.players.get(playerId)\n\n // 手牌进行中:不管是谁(包括房主)都先把这手打完,只弃掉自己的牌\n if (player && room.phase === 'hand' && room.hand && !room.hand.completed && player.seat !== null) {\n foldSeat(room.hand, player.seat, this.now())\n player.leaving = true\n player.connected = false\n this.dropSessionsOf(room, playerId)\n if (room.hand.completed) this.finalizeHand(room, this.now())\n else this.publish(room)\n return\n }\n\n if (isHost) {\n // 大厅/两手之间离开:没有手牌要保,直接结算\n this.dissolve(room, 'host-dissolved')\n return\n }\n if (!player) return\n this.removePlayer(room, playerId)\n this.publish(room)\n }\n\n sessionByToken(token: string | undefined): Session | undefined {\n if (!token) return undefined\n const session = this.sessions.get(token)\n if (!session) return undefined\n if (session.expiresAt <= this.now()) {\n this.sessions.delete(token)\n return undefined\n }\n return session\n }\n\n /** 把浏览器带来的若干 token 解析成会话,新的在前。 */\n sessionsForTokens(tokens: readonly string[]): Session[] {\n const out: Session[] = []\n for (const token of tokens) {\n const session = this.sessionByToken(token)\n if (session && !out.some((entry) => entry.token === session.token)) out.push(session)\n }\n out.sort((a, b) => b.createdAt - a.createdAt)\n return out\n }\n\n private sessionFromCookies(tokens: readonly string[] | undefined, roomId: RoomId): Session | undefined {\n if (!tokens || tokens.length === 0) return undefined\n for (const token of tokens) {\n const session = this.sessionByToken(token)\n if (session && session.roomId === roomId) return session\n }\n return undefined\n }\n\n view(roomId: RoomId, playerId: PlayerId): PlayerView {\n const room = this.roomOf(playerId)\n if (!room || room.roomId !== roomId) throw new CommandError('auth', '不在这个房间里')\n if (room.hostPlayerId === playerId) room.hostLastSeen = this.now()\n return this.project(room, playerId)\n }\n\n // ---------------------------------------------------------------- 命令\n\n command(\n roomId: RoomId,\n playerId: PlayerId,\n command: ClientCommand,\n baseSeq?: number,\n actionId?: string,\n ): { seq: number; view: PlayerView; duplicate?: boolean } {\n const room = this.roomOf(playerId)\n if (!room || room.roomId !== roomId) throw new CommandError('auth', '不在这个房间里')\n const player = room.players.get(playerId)\n if (!player || player.leaving) throw new CommandError('auth', '不在这个房间里')\n if (room.hostPlayerId === playerId) room.hostLastSeen = this.now()\n\n if (command.type === 'ping') {\n return { seq: room.seq, view: this.project(room, playerId) }\n }\n if (room.phase === 'closed') throw new CommandError('phase', '房间已解散')\n if (baseSeq !== undefined && baseSeq !== room.seq) {\n throw new StaleCommandError('状态已更新,请重试', this.project(room, playerId))\n }\n const cacheKey = actionId ? `${playerId}:${actionId}` : ''\n if (cacheKey && room.actionCache.has(cacheKey)) {\n return { seq: room.seq, view: this.project(room, playerId), duplicate: true }\n }\n this.counters.commands += 1\n\n switch (command.type) {\n case 'sit': {\n if (room.phase !== 'lobby') throw new CommandError('phase', '对局进行中,暂时不能入座')\n if (player.seat !== null) throw new CommandError('illegal', '你已经在座位上了')\n const requested = command.seat\n const seat = requested !== undefined && Number.isInteger(requested) && requested >= 0 && requested < MAX_SEATS\n ? (this.seatTaken(room, requested) ? -1 : requested)\n : this.firstFreeSeat(room)\n if (seat < 0) throw new CommandError('room-full', '没有空座')\n player.seat = seat\n player.ready = true\n player.sittingOut = false\n player.autoFold = false\n player.timeoutStreak = 0\n if (command.displayName) player.displayName = sanitizeDisplayName(command.displayName)\n this.publish(room)\n break\n }\n case 'stand': {\n if (player.seat === null) throw new CommandError('illegal', '你本来就在观战')\n if (room.phase === 'hand' && room.hand && !room.hand.completed) foldSeat(room.hand, player.seat, this.now())\n player.seat = null\n player.ready = false\n if (room.hand?.completed) this.finalizeHand(room, this.now())\n else this.publish(room)\n break\n }\n case 'ready': {\n if (player.seat === null) throw new CommandError('not-seated', '先入座再准备')\n player.ready = command.ready\n if (command.ready) {\n player.sittingOut = false\n player.autoFold = false\n player.timeoutStreak = 0\n }\n this.publish(room)\n break\n }\n case 'start': {\n this.requireHost(room, playerId)\n if (room.phase !== 'lobby') throw new CommandError('phase', '当前阶段不能开局')\n this.startHand(room, this.now())\n break\n }\n case 'action': {\n if (room.phase !== 'hand' || !room.hand || room.hand.completed) throw new CommandError('phase', '现在不能行动')\n if (player.seat === null) throw new CommandError('not-seated', '你在观战')\n player.timeoutStreak = 0\n player.autoFold = false\n player.sittingOut = false\n this.applyTableAction(room, player.seat, command.action)\n break\n }\n case 'chat': {\n if (!room.settings.chatEnabled) throw new CommandError('illegal', '本桌已关闭聊天')\n const text = sanitizeChat(command.text)\n if (!text) throw new CommandError('illegal', '消息为空')\n const last = room.lastChatAt.get(playerId) ?? 0\n const now = this.now()\n if (now - last < CHAT_COOLDOWN_MS) throw new CommandError('rate-limited', '发得太快了')\n room.lastChatAt.set(playerId, now)\n this.pushChat(room, player, text)\n this.publish(room)\n break\n }\n case 'hostPause': {\n this.requireHost(room, playerId)\n if (room.phase === 'hand') throw new CommandError('phase', '一手牌进行中不能暂停')\n room.phase = 'lobby'\n room.hand = null\n room.handOverAt = null\n for (const seated of room.players.values()) if (seated.seat !== null) seated.ready = false\n this.publish(room)\n break\n }\n case 'hostKick': {\n this.requireHost(room, playerId)\n const target = command.playerId\n if (target === room.hostPlayerId) throw new CommandError('illegal', '不能踢房主')\n const targetPlayer = room.players.get(target)\n if (!targetPlayer) throw new CommandError('illegal', '玩家不在房间里')\n // 座位上的牌友只有在大厅能踢;观战者随时可以清走\n if (targetPlayer.seat !== null && room.phase !== 'lobby') {\n throw new CommandError('phase', '对局中不能踢座位上的人')\n }\n this.removePlayer(room, target)\n this.dropSessionsOf(room, target)\n this.sendTo(target, { type: 'kicked', seq: room.seq, reason: '你被房主请出了房间' })\n this.publish(room)\n break\n }\n case 'hostDissolve': {\n this.requireHost(room, playerId)\n this.dissolve(room, 'host-dissolved')\n break\n }\n default:\n throw new CommandError('illegal', '未知命令')\n }\n\n if (cacheKey) {\n room.actionCache.set(cacheKey, room.seq)\n while (room.actionCache.size > 64) {\n const oldest = room.actionCache.keys().next().value\n if (oldest === undefined) break\n room.actionCache.delete(oldest)\n }\n }\n return { seq: room.seq, view: this.project(room, playerId) }\n }\n\n /** 结算导出:房主与在场玩家都能取到本次会话汇总。 */\n exportSummary(roomId: RoomId, playerId: PlayerId): SessionSummary | null {\n const room = this.roomOf(playerId)\n if (!room || room.roomId !== roomId) throw new CommandError('auth', '不在这个房间里')\n return room.summary\n }\n\n // ---------------------------------------------------------------- WS 票据\n\n issueTicket(playerId: PlayerId, roomId: RoomId): string {\n const ticket = newToken()\n this.tickets.set(ticket, { playerId, roomId, expiresAt: this.now() + 60_000 })\n return ticket\n }\n\n consumeTicket(ticket: string): { playerId: PlayerId; roomId: RoomId } | null {\n const entry = this.tickets.get(ticket)\n if (!entry) return null\n this.tickets.delete(ticket)\n if (entry.expiresAt <= this.now()) return null\n return { playerId: entry.playerId, roomId: entry.roomId }\n }\n\n markConnected(playerId: PlayerId, roomId: RoomId, connected: boolean): void {\n const room = this.rooms.get(roomId)\n const player = room?.players.get(playerId)\n if (!room || !player) return\n if (room.hostPlayerId === playerId && connected) room.hostLastSeen = this.now()\n if (player.connected === connected) return\n player.connected = connected\n this.publish(room)\n }\n\n health(): Record<string, unknown> {\n return {\n rooms: this.rooms.size,\n open: [...this.rooms.values()].filter((room) => room.phase !== 'closed').length,\n sessions: this.sessions.size,\n ...this.counters,\n }\n }\n\n // ---------------------------------------------------------------- 内部:阶段\n\n private startHand(room: LiveRoom, now: number): void {\n const eligible = [...room.players.values()].filter(\n (player) => player.seat !== null && player.ready && !player.sittingOut && !player.autoFold && !player.leaving,\n )\n const seated = [...room.players.values()].filter((player) => player.seat !== null && !player.leaving)\n const notReady = seated.filter(\n (player) => player.playerId !== room.hostPlayerId && !player.ready && !player.sittingOut && !player.autoFold,\n )\n if (notReady.length > 0) throw new CommandError('phase', `还有 ${notReady.length} 人没准备`)\n if (eligible.length < 2) throw new CommandError('phase', '至少需要 2 人准备才能开局')\n\n const seats = eligible.map((player) => ({\n seat: player.seat as number,\n playerId: player.playerId,\n displayName: player.displayName,\n }))\n const button = this.nextButton(room, seats.map((entry) => entry.seat))\n room.hand = createHand({\n handNo: room.handNo + 1,\n button,\n seats,\n sb: room.settings.sb,\n bb: room.settings.bb,\n buyIn: room.settings.buyIn,\n turnTimeoutMs: room.settings.turnTimeoutMs,\n now,\n })\n room.handNo += 1\n room.phase = 'hand'\n room.handOverAt = null\n this.counters.handsPlayed += 1\n this.publish(room)\n }\n\n private applyTableAction(room: LiveRoom, seat: number, action: TableAction): void {\n const hand = room.hand\n if (!hand) throw new CommandError('phase', '没有进行中的手牌')\n try {\n act(hand, seat, action, this.now())\n } catch (error) {\n if (error instanceof HandError) throw new CommandError(error.code === 'not-your-turn' ? 'not-your-turn' : error.code, error.message)\n throw error\n }\n if (hand.completed) this.finalizeHand(room, this.now())\n else this.publish(room)\n }\n\n private finalizeHand(room: LiveRoom, now: number): void {\n const hand = room.hand\n const result = hand?.result\n if (!hand || !result) return\n\n for (const delta of result.deltas) {\n const stats = this.ensureStats(room, delta.playerId, delta.displayName, delta.seat)\n stats.handsPlayed += 1\n stats.netChips += delta.net\n stats.biggestWin = Math.max(stats.biggestWin, delta.net)\n stats.displayName = delta.displayName\n stats.seat = delta.seat\n if (result.winners.includes(delta.seat)) stats.handsWon += 1\n if (!result.foldedWin && result.shownHands.some((shown) => shown.seat === delta.seat)) {\n stats.showdownsPlayed += 1\n if (result.winners.includes(delta.seat)) stats.showdownsWon += 1\n }\n }\n\n room.lastHand = result\n room.lastButton = hand.button\n room.history.push(result)\n if (room.history.length > MAX_HISTORY_HANDS) room.history.splice(0, room.history.length - MAX_HISTORY_HANDS)\n room.phase = 'handOver'\n room.handOverAt = now + Math.max(room.settings.autoAdvanceMs, HAND_OVER_MIN_MS)\n\n for (const player of [...room.players.values()]) {\n if (player.leaving) this.removePlayer(room, player.playerId)\n }\n\n this.store.appendHand({\n ...result,\n roomCode: room.roomCode,\n title: room.title,\n recordedAt: nowIso(),\n })\n this.publish(room)\n }\n\n private advance(room: LiveRoom, now: number): void {\n const host = room.players.get(room.hostPlayerId)\n if (!host || host.leaving) {\n // 房主走了:这手照常算完,但不再开新局\n this.dissolve(room, 'host-dissolved')\n return\n }\n const eligible = [...room.players.values()].filter(\n (player) => player.seat !== null && !player.sittingOut && !player.autoFold && !player.leaving,\n )\n if (eligible.length >= 2) {\n try {\n this.startHand(room, now)\n return\n } catch {\n // 落到大厅\n }\n }\n room.phase = 'lobby'\n room.hand = null\n room.handOverAt = null\n for (const player of room.players.values()) if (player.seat !== null) player.ready = false\n this.publish(room)\n }\n\n private dissolve(room: LiveRoom, reason: 'host-dissolved' | 'host-idle'): void {\n if (room.phase === 'closed' || room.phase === 'settling') return\n room.hand = null\n room.handOverAt = null\n room.closedAt = this.now()\n const summary = this.buildSummary(room, reason)\n room.summary = summary\n room.phase = 'closed'\n this.counters.roomsClosed += 1\n this.store.appendSession(summary)\n this.publish(room)\n for (const player of room.players.values()) {\n this.sendTo(player.playerId, { type: 'settled', seq: room.seq, summary })\n }\n }\n\n private buildSummary(room: LiveRoom, reason: 'host-dissolved' | 'host-idle'): SessionSummary {\n const entries = [...room.stats.entries()]\n .map(([playerId, stats]) => ({ playerId, ...stats }))\n .filter((entry) => entry.handsPlayed > 0)\n entries.sort((a, b) => (b.netChips - a.netChips) || (b.handsWon - a.handsWon) || (a.displayName.localeCompare(b.displayName)))\n const endedAt = this.now()\n return {\n sessionId: asSessionId(`sess_${room.roomCode}_${room.createdAt}`),\n roomCode: room.roomCode,\n title: room.title,\n startedAt: new Date(room.createdAt).toISOString(),\n endedAt: new Date(endedAt).toISOString(),\n durationMs: endedAt - room.createdAt,\n handsPlayed: room.handNo,\n players: entries.map((entry, index) => ({\n rank: index + 1,\n playerId: entry.playerId,\n displayName: entry.displayName,\n handsPlayed: entry.handsPlayed,\n handsWon: entry.handsWon,\n netChips: entry.netChips,\n biggestWin: entry.biggestWin,\n showdownsWon: entry.showdownsWon,\n showdownsPlayed: entry.showdownsPlayed,\n })),\n history: [...room.history],\n reason,\n }\n }\n\n // ---------------------------------------------------------------- 内部:tick\n\n tick(): void {\n const now = this.now()\n for (const room of [...this.rooms.values()]) {\n if (room.phase === 'closed') {\n if (room.closedAt !== null && now - room.closedAt > CLOSED_RETENTION_MS) this.dropRoom(room)\n continue\n }\n if (room.hostPlayerId && now - room.hostLastSeen > this.config.hostIdleDissolveMs) {\n this.dissolve(room, 'host-idle')\n continue\n }\n if (room.phase === 'hand' && room.hand) {\n this.tickHand(room, now)\n }\n if (room.phase === 'handOver' && room.handOverAt !== null && now >= room.handOverAt) {\n this.advance(room, now)\n }\n for (const player of [...room.players.values()]) {\n if (player.leaving && room.phase !== 'hand') this.removePlayer(room, player.playerId)\n }\n }\n this.cleanupSessions(now)\n }\n\n private tickHand(room: LiveRoom, now: number): void {\n const hand = room.hand\n if (!hand || hand.completed) return\n if (hand.runoutDueAt !== null) {\n if (tickHand(hand, now)) {\n if (hand.completed) this.finalizeHand(room, now)\n else this.publish(room)\n }\n return\n }\n if (hand.deadlineAt === null || now < hand.deadlineAt) return\n\n const seat = hand.toActSeat\n if (seat !== null) {\n const player = [...room.players.values()].find((entry) => entry.seat === seat)\n if (player) {\n player.timeoutStreak += 1\n if (player.timeoutStreak >= 3) {\n player.autoFold = true\n player.sittingOut = true\n }\n }\n }\n const outcome = forceTimeout(hand, now)\n if (!outcome) return\n if (hand.completed) this.finalizeHand(room, now)\n else this.publish(room)\n }\n\n /** 本机地址兜底(拿不到局域网 IP 时用)。 */\n private joinUrlFor(room: LiveRoom, token: string): string {\n return `${this.origin()}${this.config.routePrefix}/play?code=${room.roomCode}&invite=${encodeURIComponent(token)}&role=auto`\n }\n\n /** 同一局域网可直连的邀请链接;需要局域网代理在跑、而且拿得到网卡地址。 */\n private lanJoinUrl(room: LiveRoom): string {\n if (this.lanPort <= 0) return ''\n const ip = this.lanIp()\n if (!ip) return ''\n return `http://${ip}:${this.lanPort}${this.config.routePrefix}/play?code=${room.roomCode}&invite=${encodeURIComponent(room.sitToken)}&role=auto`\n }\n\n // ---------------------------------------------------------------- 内部:视图\n\n private project(room: LiveRoom, playerId: PlayerId): PlayerView {\n const you = room.players.get(playerId)\n if (!you) throw new CommandError('auth', '不在这个房间里')\n const hand = room.hand\n const seats: SeatView[] = []\n for (let seat = 0; seat < MAX_SEATS; seat += 1) {\n const handPlayer = hand?.players.find((entry) => entry.seat === seat)\n const live = [...room.players.values()].find((entry) => entry.seat === seat)\n if (!handPlayer && !live) {\n seats.push({\n seat,\n playerId: null,\n displayName: null,\n ready: false,\n connected: false,\n sittingOut: false,\n autoFold: false,\n host: false,\n stack: 0,\n committed: 0,\n handNet: 0,\n folded: false,\n allIn: false,\n lastAction: null,\n holeCount: 0,\n })\n continue\n }\n const holder = handPlayer?.playerId ?? live!.playerId\n const delta = room.lastHand?.deltas.find((entry) => entry.seat === seat)\n seats.push({\n seat,\n playerId: holder,\n displayName: handPlayer?.displayName ?? live!.displayName,\n ready: live?.ready ?? false,\n connected: live?.connected ?? handPlayer !== undefined,\n sittingOut: live?.sittingOut ?? false,\n autoFold: live?.autoFold ?? false,\n host: holder === room.hostPlayerId,\n stack: handPlayer ? handPlayer.stack : room.settings.buyIn,\n committed: handPlayer ? handPlayer.streetCommitted : 0,\n handNet: delta?.net ?? 0,\n folded: handPlayer?.folded ?? false,\n allIn: handPlayer?.allIn ?? false,\n lastAction: handPlayer?.lastAction ?? null,\n holeCount: handPlayer ? handPlayer.hole.length : 0,\n })\n }\n\n const stats = room.stats.get(playerId)\n const ownHandPlayer = hand?.players.find((entry) => entry.playerId === playerId)\n const legal = hand && !hand.completed && you.seat !== null ? legalForSeat(hand, you.seat) : null\n\n const roomView: RoomView = {\n roomId: room.roomId,\n roomCode: room.roomCode,\n title: room.title,\n hostPlayerId: room.hostPlayerId,\n hostDisplayName: room.players.get(room.hostPlayerId)?.displayName ?? '房主',\n phase: room.phase,\n settings: room.settings,\n seats,\n handNo: room.handNo,\n createdAt: new Date(room.createdAt).toISOString(),\n nextHandAt: room.handOverAt === null ? null : new Date(room.handOverAt).toISOString(),\n seq: room.seq,\n joinUrl: this.lanJoinUrl(room) || this.joinUrlFor(room, room.sitToken),\n lanUrl: this.lanJoinUrl(room),\n }\n\n const leaderboard: LeaderboardEntry[] = [...room.stats.entries()]\n .map(([entryId, entry]) => ({\n playerId: entryId,\n displayName: entry.displayName,\n seat: room.players.get(entryId)?.seat ?? entry.seat,\n handsPlayed: entry.handsPlayed,\n handsWon: entry.handsWon,\n netChips: entry.netChips,\n biggestWin: entry.biggestWin,\n showdownsWon: entry.showdownsWon,\n showdownsPlayed: entry.showdownsPlayed,\n }))\n .sort((a, b) => (b.netChips - a.netChips) || (b.handsWon - a.handsWon))\n\n return {\n room: roomView,\n you: {\n playerId,\n displayName: you.displayName,\n seat: you.seat,\n spectator: you.seat === null,\n ready: you.ready,\n sittingOut: you.sittingOut,\n holeCards: ownHandPlayer ? [...ownHandPlayer.hole] : [],\n legal,\n canAct: legal !== null,\n netChips: stats?.netChips ?? 0,\n handsPlayed: stats?.handsPlayed ?? 0,\n },\n hand: hand ? handView(hand) : null,\n lastHand: room.lastHand,\n leaderboard,\n settlement: room.summary,\n chat: room.chat.slice(-40),\n spectators: [...room.players.values()]\n .filter((player) => player.seat === null)\n .map((player) => ({ playerId: player.playerId, displayName: player.displayName })),\n serverNow: nowIso(),\n }\n }\n\n // ---------------------------------------------------------------- 内部:玩家\n\n private addPlayer(room: LiveRoom, displayName: string, seat: number | null, browserId: string): LivePlayer {\n const player: LivePlayer = {\n playerId: asPlayerId(newId('p')),\n displayName,\n seat,\n ready: false,\n connected: true,\n sittingOut: false,\n autoFold: false,\n timeoutStreak: 0,\n browserId,\n joinedAt: this.now(),\n leaving: false,\n }\n room.players.set(player.playerId, player)\n if (browserId) room.browsers.set(browserId, player.playerId)\n this.ensureStats(room, player.playerId, displayName, seat)\n return player\n }\n\n private removePlayer(room: LiveRoom, playerId: PlayerId): void {\n const player = room.players.get(playerId)\n if (!player) return\n room.players.delete(playerId)\n if (player.browserId && room.browsers.get(player.browserId) === playerId) room.browsers.delete(player.browserId)\n this.dropSessionsOf(room, playerId)\n }\n\n private ensureStats(room: LiveRoom, playerId: PlayerId, displayName: string, seat: number | null): Stats {\n let stats = room.stats.get(playerId)\n if (!stats) {\n stats = {\n displayName,\n seat,\n handsPlayed: 0,\n handsWon: 0,\n netChips: 0,\n biggestWin: 0,\n showdownsWon: 0,\n showdownsPlayed: 0,\n }\n room.stats.set(playerId, stats)\n }\n return stats\n }\n\n private pushChat(room: LiveRoom, player: LivePlayer | null, text: string): void {\n room.chat.push({\n playerId: player?.playerId ?? asPlayerId('system'),\n displayName: player?.displayName ?? '桌子',\n text,\n ts: nowIso(),\n })\n if (room.chat.length > MAX_CHAT_LINES) room.chat.splice(0, room.chat.length - MAX_CHAT_LINES)\n }\n\n private newSession(room: LiveRoom, playerId: PlayerId, kind: Session['kind']): Session {\n const session: Session = {\n token: newToken(),\n playerId,\n roomId: room.roomId,\n kind,\n createdAt: this.now(),\n expiresAt: this.now() + SESSION_TTL_MS,\n }\n this.sessions.set(session.token, session)\n return session\n }\n\n private dropSessionsOf(room: LiveRoom, playerId: PlayerId): void {\n for (const [token, session] of this.sessions) {\n if (session.playerId === playerId && session.roomId === room.roomId) this.sessions.delete(token)\n }\n }\n\n private cleanupSessions(now: number): void {\n for (const [token, session] of this.sessions) {\n if (session.expiresAt <= now) this.sessions.delete(token)\n }\n for (const [ticket, entry] of this.tickets) {\n if (entry.expiresAt <= now) this.tickets.delete(ticket)\n }\n }\n\n private existingPlayer(room: LiveRoom, input: PeekInput): LivePlayer | null {\n const session = this.sessionFromCookies(input.cookies, room.roomId)\n if (session) {\n const player = room.players.get(session.playerId)\n if (player && !player.leaving) return player\n }\n if (input.browserId) {\n const playerId = room.browsers.get(input.browserId)\n if (playerId) {\n const player = room.players.get(playerId)\n if (player && !player.leaving) return player\n }\n }\n return null\n }\n\n private checkInvite(\n room: LiveRoom,\n input: PeekInput,\n role: 'sit' | 'watch',\n local: boolean,\n ): { allowed: boolean; sit: boolean } {\n if (local) return { allowed: true, sit: true }\n const token = input.invite ?? ''\n if (!token) return { allowed: false, sit: false }\n const sit = hashEquals(room.sitHash, hashInvite(room.secret, 'sit', token))\n const watch = hashEquals(room.watchHash, hashInvite(room.secret, 'watch', token))\n if (role === 'sit') return { allowed: sit || watch, sit }\n return { allowed: watch || sit, sit }\n }\n\n private requireHost(room: LiveRoom, playerId: PlayerId): void {\n if (room.hostPlayerId !== playerId) throw new CommandError('host-only', '只有房主能这么做')\n }\n\n private roomByCode(code: string): LiveRoom {\n const room = this.roomsByCode.get(String(code ?? '').trim())\n if (!room) throw new CommandError('room-not-found', '房号不存在或房间已关闭')\n if (room.phase === 'closed') throw new CommandError('expired', '房间已解散')\n return room\n }\n\n private roomOf(playerId: PlayerId): LiveRoom | undefined {\n for (const room of this.rooms.values()) {\n if (room.players.has(playerId)) return room\n }\n return undefined\n }\n\n private uniqueCode(): string {\n for (let attempt = 0; attempt < 20; attempt += 1) {\n const code = newRoomCode()\n if (!this.roomsByCode.has(code)) return code\n }\n throw new CommandError('room-full', '暂时没有可用房号,请稍后再试')\n }\n\n private firstFreeSeat(room: LiveRoom): number {\n for (let seat = 0; seat < MAX_SEATS; seat += 1) {\n if (!this.seatTaken(room, seat)) return seat\n }\n return -1\n }\n\n private seatTaken(room: LiveRoom, seat: number): boolean {\n return [...room.players.values()].some((player) => player.seat === seat && !player.leaving)\n }\n\n private nextButton(room: LiveRoom, seats: number[]): number {\n const ordered = [...seats].sort((a, b) => a - b)\n const previous = room.lastButton\n if (previous === null) return ordered[0]!\n for (const seat of ordered) if (seat > previous) return seat\n return ordered[0]!\n }\n\n private sendTo(playerId: PlayerId, event: ServerEvent): void {\n for (const listener of this.listeners) listener(playerId, event)\n }\n\n private publish(room: LiveRoom, extra?: (seq: number) => ServerEvent): void {\n room.seq += 1\n for (const player of room.players.values()) {\n const event: ServerEvent = { type: 'snapshot', seq: room.seq, view: this.project(room, player.playerId) }\n this.sendTo(player.playerId, event)\n }\n if (extra) {\n const event = extra(room.seq)\n for (const player of room.players.values()) this.sendTo(player.playerId, event)\n }\n }\n\n private dropRoom(room: LiveRoom): void {\n this.rooms.delete(room.roomId)\n this.roomsByCode.delete(room.roomCode)\n }\n}\n","import type { Context } from '@deepseek-ai/cordis'\nimport { Config, resolveConfig, type PluginConfig } from './config.ts'\nimport { PACKAGE_NAME } from './invariant.ts'\nimport { registerHoldemHttp } from './net/routes.ts'\nimport { registerHoldemSocket } from './net/ws.ts'\nimport { CloudflareDeployer } from './net/cloudflare.ts'\nimport { startLanProxy, type LanProxy } from './net/lan-proxy.ts'\nimport { firstLanIPv4 } from './net/network.ts'\nimport { createFileStore } from './persist/store.ts'\nimport { RoomManager } from './room/RoomManager.ts'\n\nexport const name = PACKAGE_NAME\nexport const inject = ['webServer']\nexport { Config }\n\nexport function apply(ctx: Context, config: PluginConfig): void {\n const resolved = resolveConfig(config)\n if (!resolved.enabled) return\n\n const manager = new RoomManager({\n config: resolved,\n origin: () => {\n try {\n return ctx.webServer.getOrigin()\n } catch {\n return `http://127.0.0.1:${ctx.webServer.port}`\n }\n },\n store: createFileStore(),\n lanIp: firstLanIPv4,\n })\n\n const cloudflare = new CloudflareDeployer()\n\n ctx.effect(() => {\n let lanProxy: LanProxy | null = null\n void startLanProxy({\n targetPort: ctx.webServer.port,\n prefix: resolved.routePrefix,\n log: (message) => ctx.logger?.info?.(message),\n }).then((started) => {\n lanProxy = started\n if (started) manager.setLanPort(started.port)\n })\n\n const stopTick = manager.start()\n const disposeHttp = registerHoldemHttp(ctx.webServer, manager, cloudflare)\n const disposeWs = registerHoldemSocket(ctx.webServer, manager)\n return () => {\n stopTick()\n disposeHttp()\n disposeWs()\n lanProxy?.dispose()\n cloudflare.dispose()\n void manager.dispose()\n }\n }, 'holdem: room routes')\n\n ctx.inject(['systemPrompt'], (promptCtx) => {\n promptCtx.systemPrompt?.section({\n name: 'plugin:@zhuweiyou/dsh-holdem',\n order: 200,\n text: [\n '本机已安装 @zhuweiyou/dsh-holdem(德州扑克)。',\n '开房与打牌都在侧栏的「德州扑克」面板里;牌桌可以跑在本机(局域网分享),也可以跑在 Cloudflare Worker 上(公网分享)。',\n '结算是本地娱乐筹码,不是 DeepSeek 余额;加入只需要房主发来的邀请链接。',\n ].join(''),\n })\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["level","name","out: Record<string, string>","chunks: Buffer[]","resolve","reject","escapeHtml","name","headers: Record<string, string | string[]>","send","payload: Record<string, unknown>","state: SocketState","parsed: { type?: string } | null","name","candidates: WranglerCommand[]","resolvedCommand: WranglerCommand | null","httpRequest","resolve","candidates: string[]","noopStore: HoldemStore","ready: Promise<void> | null","cryptoBelow: RandomBelow","deck: Card[]","empty: LegalActions","paid","pots: Pot[]","returned: { seat: number; amount: number }[]","results: PotAward[]","best: HandValue | null","STREET_ORDER: Street[]","players: HandPlayer[]","state: HandState","shown: ShownHandView[]","handle","room","room: LiveRoom","player","out: Session[]","tickHand","seats: SeatView[]","roomView: RoomView","leaderboard: LeaderboardEntry[]","player: LivePlayer","session: Session","event: ServerEvent","lanProxy: LanProxy | null"],"sources":["../src/invariant.ts","../src/config.ts","../src/types.ts","../src/net/auth.ts","../src/net/json.ts","../src/net/join-page.ts","../src/net/play-page.ts","../src/net/routes.ts","../src/net/ws.ts","../src/net/which.ts","../src/net/cloudflare.ts","../src/net/lan-proxy.ts","../src/net/network.ts","../src/persist/noop.ts","../src/persist/store.ts","../src/room/ids.ts","../src/engine/cards.ts","../src/engine/deck.ts","../src/engine/betting.ts","../src/engine/pots.ts","../src/engine/rank.ts","../src/engine/hand.ts","../src/room/RoomManager.ts","../src/index.ts"],"sourcesContent":["/** 全局常量:协议、上限、时间与筹码档位。 */\n\nexport const PACKAGE_NAME = '@zhuweiyou/dsh-holdem'\nexport const CLIENT_ID = '@zhuweiyou/dsh-holdem'\nexport const PANEL_KEY = 'holdem'\nexport const SUBPROTOCOL = 'holdem.v1'\nexport const ROUTE_PREFIX = '/holdem'\n\nexport const MAX_SEATS = 6\n/** 观战席位默认值;实际值由设置项 maxSpectators 决定。 */\nexport const DEFAULT_MAX_SPECTATORS = 16\n/** 设置项 maxSpectators 能配到的最大值。 */\nexport const SPECTATORS_LIMIT = 64\nexport const MAX_ROOMS = 8\nexport const MAX_TITLE = 24\nexport const MAX_DISPLAY_NAME = 16\nexport const MAX_CHAT = 80\nexport const MAX_CHAT_LINES = 80\nexport const MAX_HISTORY_HANDS = 50\nexport const MAX_CACHED_ACTIONS = 64\nexport const MAX_BODY_BYTES = 64 * 1024\nexport const MAX_FRAME_BYTES = 64 * 1024\n\nexport const TICK_MS = 250\nexport const WS_TICKET_TTL_MS = 60_000\nexport const SESSION_TTL_MS = 24 * 60 * 60 * 1000\nexport const HEARTBEAT_MS = 15_000\nexport const DISCONNECT_AFTER_MS = 45_000\n/** 没有实时通道时,HTTP 请求也算「人在」:这么久没动静才标成离线。 */\nexport const PRESENCE_TTL_MS = 20_000\nexport const LOBBY_KICK_IDLE_MS = 10 * 60 * 1000\nexport const CHAT_COOLDOWN_MS = 1200\nexport const ACTION_RATE_WINDOW_MS = 10_000\nexport const ACTION_RATE_MAX = 30\n\nexport const BUY_IN_BB = 100\nexport const ALL_IN_RUNOUT_MS = 1200\nexport const HAND_OVER_MIN_MS = 2500\n\nexport const BLIND_LEVELS = [\n { id: '10/20', sb: 10, bb: 20 },\n { id: '25/50', sb: 25, bb: 50 },\n { id: '100/200', sb: 100, bb: 200 },\n] as const\n\nexport type BlindLevelId = (typeof BLIND_LEVELS)[number]['id']\n\nexport function blindsFor(id: string): { id: BlindLevelId; sb: number; bb: number } {\n const found = BLIND_LEVELS.find((level) => level.id === id)\n const level = found ?? BLIND_LEVELS[1]!\n return { id: level.id, sb: level.sb, bb: level.bb }\n}\n\nexport function buyInFor(bb: number): number {\n return bb * BUY_IN_BB\n}\n","import z from '@deepseek-ai/schemastery'\nimport {\n BLIND_LEVELS,\n DEFAULT_MAX_SPECTATORS,\n MAX_ROOMS,\n MAX_SEATS,\n ROUTE_PREFIX,\n SPECTATORS_LIMIT,\n type BlindLevelId,\n blindsFor,\n buyInFor,\n} from './invariant.ts'\n\nexport interface PluginConfig {\n enabled?: boolean\n defaultBlindLevel?: string\n /** 每个行动时限,秒。 */\n turnTimeoutSec?: number\n /** 一手结束后自动开下一手的等待,秒。 */\n autoAdvanceSec?: number\n allowSpectators?: boolean\n /** 每个房间的观战席位上限;0 等于关闭观战。 */\n maxSpectators?: number\n chatEnabled?: boolean\n maxRooms?: number\n /** 房主失联多久后自动解散并结算,分钟。 */\n hostIdleDissolveMin?: number\n routePrefix?: string\n}\n\nexport const Config = z.object({\n enabled: z.boolean().default(true),\n defaultBlindLevel: z.string().default('25/50'),\n turnTimeoutSec: z.number().default(30).min(15).max(90),\n autoAdvanceSec: z.number().default(6).min(2).max(30),\n allowSpectators: z.boolean().default(true),\n maxSpectators: z.number().default(DEFAULT_MAX_SPECTATORS).min(0).max(SPECTATORS_LIMIT),\n chatEnabled: z.boolean().default(true),\n maxRooms: z.number().default(MAX_ROOMS).min(1).max(64),\n hostIdleDissolveMin: z.number().default(10).min(1).max(120),\n routePrefix: z.string().default(ROUTE_PREFIX),\n})\n\nexport interface ResolvedConfig {\n enabled: boolean\n blindLevel: BlindLevelId\n sb: number\n bb: number\n buyIn: number\n turnTimeoutMs: number\n autoAdvanceMs: number\n allowSpectators: boolean\n /** 观战席位上限;0 表示这桌不接观战。 */\n maxSpectators: number\n chatEnabled: boolean\n maxRooms: number\n hostIdleDissolveMs: number\n routePrefix: string\n}\n\nexport function normalizePrefix(prefix: string): string {\n const trimmed = prefix.trim()\n if (trimmed === '') return ROUTE_PREFIX\n const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`\n return withSlash.replace(/\\/+$/, '') || ROUTE_PREFIX\n}\n\nexport function resolveConfig(config: PluginConfig): ResolvedConfig {\n const blindId = (config.defaultBlindLevel ?? '25/50') as BlindLevelId\n const known = BLIND_LEVELS.some((level) => level.id === blindId)\n const blinds = blindsFor(known ? blindId : '25/50')\n const turnTimeoutSec = clamp(config.turnTimeoutSec ?? 30, 15, 90)\n const autoAdvanceSec = clamp(config.autoAdvanceSec ?? 6, 2, 30)\n return {\n enabled: config.enabled !== false,\n blindLevel: blinds.id,\n sb: blinds.sb,\n bb: blinds.bb,\n buyIn: buyInFor(blinds.bb),\n turnTimeoutMs: turnTimeoutSec * 1000,\n autoAdvanceMs: autoAdvanceSec * 1000,\n allowSpectators: config.allowSpectators !== false,\n maxSpectators: clamp(Math.trunc(config.maxSpectators ?? DEFAULT_MAX_SPECTATORS), 0, SPECTATORS_LIMIT),\n chatEnabled: config.chatEnabled !== false,\n maxRooms: clamp(Math.trunc(config.maxRooms ?? MAX_ROOMS), 1, 64),\n hostIdleDissolveMs: clamp(config.hostIdleDissolveMin ?? 10, 1, 120) * 60_000,\n routePrefix: normalizePrefix(config.routePrefix ?? ROUTE_PREFIX),\n }\n}\n\nexport const MAX_TABLE_SEATS = MAX_SEATS\n\nfunction clamp(value: number, min: number, max: number): number {\n if (!Number.isFinite(value)) return min\n return Math.min(max, Math.max(min, value))\n}\n","import type { BlindLevelId } from './invariant.ts'\n\nexport type RoomId = string & { readonly __brand: 'RoomId' }\nexport type PlayerId = string & { readonly __brand: 'PlayerId' }\nexport type HandId = string & { readonly __brand: 'HandId' }\nexport type SessionId = string & { readonly __brand: 'SessionId' }\n/** 牌面:'As' / 'Td' / '9h'。 */\nexport type Card = string & { readonly __brand: 'Card' }\nexport type Seat = number\n\nexport type RoomPhase = 'lobby' | 'hand' | 'handOver' | 'settling' | 'closed'\nexport type Street = 'preflop' | 'flop' | 'turn' | 'river' | 'showdown'\n\nexport interface RoomSettingsView {\n blindLevel: BlindLevelId\n sb: number\n bb: number\n buyIn: number\n turnTimeoutMs: number\n autoAdvanceMs: number\n allowSpectators: boolean\n /** 观战席位上限;0 表示这桌不接观战。 */\n maxSpectators: number\n chatEnabled: boolean\n}\n\nexport interface SeatView {\n seat: Seat\n playerId: PlayerId | null\n displayName: string | null\n ready: boolean\n connected: boolean\n sittingOut: boolean\n autoFold: boolean\n host: boolean\n stack: number\n committed: number\n handNet: number\n folded: boolean\n allIn: boolean\n lastAction: string | null\n holeCount: number\n}\n\nexport interface PotView {\n amount: number\n eligible: Seat[]\n}\n\nexport interface ShownHandView {\n seat: Seat\n cards: Card[]\n name: string\n score: number\n}\n\nexport interface HandView {\n handNo: number\n street: Street\n board: Card[]\n button: Seat\n sbSeat: Seat\n bbSeat: Seat\n toActSeat: Seat | null\n currentBet: number\n minRaiseTo: number\n potTotal: number\n pots: PotView[]\n deadlineAt: string | null\n shownHands: ShownHandView[]\n winners: Seat[]\n}\n\nexport interface HandDelta {\n seat: Seat\n playerId: PlayerId\n displayName: string\n net: number\n}\n\nexport interface HandResultView {\n handNo: number\n board: Card[]\n pots: PotView[]\n winners: Seat[]\n shownHands: ShownHandView[]\n deltas: HandDelta[]\n foldedWin: boolean\n endedAt: string\n}\n\nexport interface LeaderboardEntry {\n playerId: PlayerId\n displayName: string\n seat: Seat | null\n handsPlayed: number\n handsWon: number\n netChips: number\n biggestWin: number\n showdownsWon: number\n showdownsPlayed: number\n}\n\nexport interface ChatLine {\n playerId: PlayerId\n displayName: string\n text: string\n ts: string\n}\n\nexport interface LegalActions {\n canFold: boolean\n canCheck: boolean\n canCall: boolean\n callAmount: number\n canRaise: boolean\n minRaiseTo: number\n maxRaiseTo: number\n canAllIn: boolean\n allInTo: number\n}\n\nexport interface RoomView {\n roomId: RoomId\n roomCode: string\n title: string\n hostPlayerId: PlayerId\n hostDisplayName: string\n phase: RoomPhase\n settings: RoomSettingsView\n seats: SeatView[]\n handNo: number\n createdAt: string\n /** handOver 阶段自动开下一手的时间点;其它阶段为 null。 */\n nextHandAt: string | null\n seq: number\n /** 一条链接搞定:有空位就入座,没空位自动转观战。 */\n joinUrl: string\n /** 同一个局域网里可以直接用的地址(没有可用网卡时为空)。 */\n lanUrl: string\n}\n\nexport interface YouView {\n playerId: PlayerId\n displayName: string\n seat: Seat | null\n spectator: boolean\n ready: boolean\n sittingOut: boolean\n holeCards: Card[]\n legal: LegalActions | null\n canAct: boolean\n netChips: number\n handsPlayed: number\n}\n\nexport interface SpectatorView {\n playerId: PlayerId\n displayName: string\n}\n\nexport interface SessionPlayerView {\n rank: number\n playerId: PlayerId\n displayName: string\n handsPlayed: number\n handsWon: number\n netChips: number\n biggestWin: number\n showdownsWon: number\n showdownsPlayed: number\n}\n\nexport interface SessionSummary {\n sessionId: SessionId\n roomCode: string\n title: string\n startedAt: string\n endedAt: string\n durationMs: number\n handsPlayed: number\n players: SessionPlayerView[]\n history: HandResultView[]\n reason: 'host-dissolved' | 'host-idle'\n}\n\nexport interface PlayerView {\n room: RoomView\n you: YouView\n hand: HandView | null\n lastHand: HandResultView | null\n leaderboard: LeaderboardEntry[]\n settlement: SessionSummary | null\n chat: ChatLine[]\n spectators: SpectatorView[]\n serverNow: string\n}\n\nexport type TableAction =\n | { type: 'fold' }\n | { type: 'check' }\n | { type: 'call' }\n | { type: 'raise'; to: number }\n | { type: 'allin' }\n\nexport type ClientCommand =\n | { type: 'sit'; seat?: Seat; displayName?: string }\n | { type: 'stand' }\n | { type: 'ready'; ready: boolean }\n | { type: 'start' }\n | { type: 'action'; action: TableAction }\n | { type: 'chat'; text: string }\n | { type: 'hostPause' }\n | { type: 'hostKick'; playerId: PlayerId }\n | { type: 'hostDissolve' }\n | { type: 'ping' }\n\nexport type RejectCode =\n | 'auth'\n | 'illegal'\n | 'phase'\n | 'not-your-turn'\n | 'stale'\n | 'duplicate-nonce'\n | 'room-full'\n | 'room-not-found'\n | 'expired'\n | 'already-in-room'\n | 'not-seated'\n | 'host-only'\n | 'rate-limited'\n\nexport type ServerEvent =\n | { type: 'snapshot'; seq: number; view: PlayerView }\n | { type: 'reject'; code: RejectCode; reason: string }\n | { type: 'settled'; seq: number; summary: SessionSummary }\n | { type: 'left'; seq: number; reason: string }\n | { type: 'kicked'; seq: number; reason: string }\n | { type: 'pong'; ts: number }\n\nexport class CommandError extends Error {\n readonly code: RejectCode\n constructor(code: RejectCode, reason: string) {\n super(reason)\n this.code = code\n this.name = 'CommandError'\n }\n}\n\nexport interface RoomPreviewSeat {\n seat: Seat\n displayName: string | null\n ready: boolean\n host: boolean\n connected: boolean\n}\n\nexport interface RoomPreview {\n roomCode: string\n title: string\n hostDisplayName: string\n phase: RoomPhase\n settings: RoomSettingsView\n seated: number\n spectators: number\n seats: RoomPreviewSeat[]\n canSit: boolean\n alreadyInRoom: boolean\n inviteExpiresAt: string\n}\n\nexport function asRoomId(value: string): RoomId {\n return value as RoomId\n}\n\nexport function asPlayerId(value: string): PlayerId {\n return value as PlayerId\n}\n\nexport function asCard(value: string): Card {\n return value as Card\n}\n\nexport function asSessionId(value: string): SessionId {\n return value as SessionId\n}\n\nexport function nowIso(): string {\n return new Date().toISOString()\n}\n\nexport function isSeat(value: unknown): value is Seat {\n return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value < 6\n}\n","import type { IncomingMessage } from 'node:http'\n\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]'])\nconst LOOPBACK_REMOTES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1'])\n\nexport function header(req: IncomingMessage, name: string): string | undefined {\n const value = req.headers[name]\n return Array.isArray(value) ? value[0] : value\n}\n\nexport function isLoopbackHost(hostHeader: string | undefined, remoteAddress: string | undefined): boolean {\n const host = (hostHeader ?? '').split(':')[0]?.replace(/^\\[|\\]$/g, '').toLowerCase()\n const remote = (remoteAddress ?? '').replace(/^::ffff:/, '')\n return LOOPBACK_HOSTS.has(host ?? '') || LOOPBACK_REMOTES.has(remote)\n}\n\n/** 私有网段(局域网直连)也放行:同 Wi-Fi 的朋友用 http://192.168.x.x:端口 进来。 */\nexport function isPrivateOrigin(origin: string): boolean {\n try {\n const host = new URL(origin).hostname\n return host.startsWith('10.')\n || host.startsWith('192.168.')\n || /^172\\.(1[6-9]|2\\d|3[01])\\./.test(host)\n } catch {\n return false\n }\n}\n\nexport function isLoopbackOrigin(origin: string): boolean {\n try {\n const url = new URL(origin)\n return url.hostname === '127.0.0.1' || url.hostname === 'localhost' || url.hostname === '::1' || url.hostname === '[::1]'\n } catch {\n return false\n }\n}\n\nexport function requestOrigin(req: IncomingMessage): string | null {\n const origin = header(req, 'origin')\n if (origin) {\n try {\n return new URL(origin).origin\n } catch {\n return null\n }\n }\n const referer = header(req, 'referer')\n if (referer) {\n try {\n return new URL(referer).origin\n } catch {\n return null\n }\n }\n return null\n}\n\nexport function originAllowed(origin: string | null, browser: boolean): boolean {\n if (!origin) return !browser\n return isLoopbackOrigin(origin) || isPrivateOrigin(origin)\n}\n\nexport function isBrowserRequest(req: IncomingMessage): boolean {\n return Boolean(header(req, 'origin') || header(req, 'sec-fetch-site'))\n}\n\nexport function assertOrigin(req: IncomingMessage): void {\n if (header(req, 'sec-fetch-site') === 'cross-site') {\n throw Object.assign(new Error('cross-site forbidden'), { status: 403, code: 'auth' })\n }\n if (!originAllowed(requestOrigin(req), isBrowserRequest(req))) {\n throw Object.assign(new Error('origin not allowed'), { status: 403, code: 'auth' })\n }\n}\n\n/** 变更请求必须带 JSON 与自定义请求头(跨源页面设不了后者)。 */\nexport function assertMutatingHeaders(req: IncomingMessage): void {\n if (header(req, 'x-requested-with') === '@zhuweiyou/dsh-holdem') return\n const contentType = header(req, 'content-type') ?? ''\n if (contentType.includes('application/json') && header(req, 'x-requested-with')) return\n throw Object.assign(new Error('missing csrf header'), { status: 403, code: 'auth' })\n}\n\nexport function parseCookies(req: IncomingMessage): Record<string, string> {\n const raw = header(req, 'cookie') ?? ''\n const out: Record<string, string> = {}\n for (const part of raw.split(';')) {\n const index = part.indexOf('=')\n if (index < 0) continue\n const key = part.slice(0, index).trim()\n const value = part.slice(index + 1).trim()\n if (key) {\n try {\n out[key] = decodeURIComponent(value)\n } catch {\n out[key] = value\n }\n }\n }\n return out\n}\n\nexport function cookieHeader(\n name: string,\n value: string,\n options: { secure: boolean; sameSite: 'Lax' | 'Strict'; maxAge?: number; path: string },\n): string {\n const parts = [\n `${name}=${encodeURIComponent(value)}`,\n `Path=${options.path}`,\n 'HttpOnly',\n `SameSite=${options.sameSite}`,\n `Max-Age=${options.maxAge ?? 86400}`,\n ]\n if (options.secure) parts.push('Secure')\n return parts.join('; ')\n}\n","import type { IncomingMessage } from 'node:http'\nimport { MAX_BODY_BYTES } from '../invariant.ts'\n\nexport function encodeJson(body: unknown): string {\n return JSON.stringify(body)\n}\n\nexport async function readJson<T>(req: IncomingMessage): Promise<T> {\n const chunks: Buffer[] = []\n let size = 0\n await new Promise<void>((resolve, reject) => {\n req.on('data', (chunk: Buffer | string) => {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n size += buffer.length\n if (size > MAX_BODY_BYTES) {\n reject(Object.assign(new Error('payload too large'), { status: 413, code: 'illegal' }))\n return\n }\n chunks.push(buffer)\n })\n req.on('end', () => resolve())\n req.on('error', (error: Error) => reject(error))\n })\n if (chunks.length === 0) return {} as T\n try {\n return JSON.parse(Buffer.concat(chunks).toString('utf8')) as T\n } catch {\n throw Object.assign(new Error('invalid json'), { status: 400, code: 'illegal' })\n }\n}\n","export interface JoinPageOptions {\n prefix: string\n code: string\n invite: string\n role: 'auto' | 'sit' | 'watch'\n}\n\n/**\n * 邀请落地页:只做「这是哪一桌 / 要不要现在进去」。\n * 真正的牌桌在 /play —— 那个页面自带完整客户端,对方不用装任何东西。\n */\nexport function joinPageHtml(options: JoinPageOptions): string {\n const play = `${options.prefix}/play?code=${encodeURIComponent(options.code)}&invite=${encodeURIComponent(options.invite)}&role=${options.role}`\n const roleText = options.role === 'watch' ? '观战' : '入座(满员自动观战)'\n return `<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>加入德州扑克 · ${escapeHtml(options.code)}</title>\n<style>\n :root { color-scheme: dark; }\n * { box-sizing: border-box; }\n body {\n margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px;\n font: 14px/1.6 -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n background: #101418; color: #e9ecf2;\n background-image: radial-gradient(circle at 50% 0%, #17352c 0%, #101418 60%);\n }\n .card { width: min(460px, 100%); background: #181c22; border: 1px solid #2a313a; border-radius: 16px; padding: 24px; box-shadow: 0 24px 60px rgba(0,0,0,.45); }\n h1 { font-size: 19px; margin: 0 0 4px; }\n .meta { color: #93a0b1; font-size: 13px; margin-bottom: 16px; }\n .code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 26px; letter-spacing: 4px; color: #e8c15a; margin: 8px 0 16px; }\n .info { border: 1px solid #2a313a; border-radius: 10px; padding: 12px; background: #141a20; margin-bottom: 16px; }\n .info .row { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; }\n .info .row + .row { margin-top: 6px; }\n .info .k { color: #93a0b1; }\n .hint { color: #93a0b1; font-size: 12px; margin: 0 0 12px; }\n .error { color: #e06c75; font-size: 12px; margin: 0 0 12px; }\n button { font: inherit; border-radius: 10px; border: 1px solid transparent; padding: 11px 16px; cursor: pointer; width: 100%; }\n .primary { background: #2f9c76; color: #06130f; font-weight: 700; }\n .primary:disabled { opacity: .55; cursor: progress; }\n footer { margin-top: 14px; font-size: 11.5px; color: #7b899a; word-break: break-all; }\n</style>\n</head>\n<body>\n<div class=\"card\">\n <h1>加入德州扑克牌桌</h1>\n <div class=\"meta\">娱乐筹码 · 邀请链接直接进桌</div>\n <div class=\"code\">${escapeHtml(options.code)}</div>\n <div class=\"info\">\n <div class=\"row\"><span class=\"k\">房间</span><span id=\"title\">读取中…</span></div>\n <div class=\"row\"><span class=\"k\">盲注</span><span id=\"blinds\">—</span></div>\n <div class=\"row\"><span class=\"k\">座位</span><span id=\"seats\">—</span></div>\n <div class=\"row\"><span class=\"k\">身份</span><span>${roleText}</span></div>\n </div>\n <p class=\"hint\" id=\"hint\">点下面的按钮进入牌桌,浏览器里直接就能玩,不用安装任何东西。</p>\n <p class=\"error\" id=\"error\" hidden></p>\n <button class=\"primary\" id=\"enter\" type=\"button\">进入牌桌</button>\n <footer>牌桌地址:${escapeHtml(play)}</footer>\n</div>\n<script>\n(function () {\n var prefix = ${JSON.stringify(options.prefix)};\n var code = ${JSON.stringify(options.code)};\n var invite = ${JSON.stringify(options.invite)};\n var role = ${JSON.stringify(options.role)};\n var target = ${JSON.stringify(play)};\n var enter = document.getElementById('enter');\n var error = document.getElementById('error');\n var hint = document.getElementById('hint');\n\n fetch(prefix + '/api/preview?code=' + encodeURIComponent(code) + '&invite=' + encodeURIComponent(invite) + '&role=' + encodeURIComponent(role), { credentials: 'same-origin' })\n .then(function (response) { return response.ok ? response.json() : null; })\n .then(function (room) {\n if (!room) {\n error.hidden = false;\n error.textContent = '房间可能已解散,请向房主要一个新链接。';\n return;\n }\n document.getElementById('title').textContent = room.title;\n document.getElementById('blinds').textContent = room.settings.sb + '/' + room.settings.bb + ' · 带入 ' + room.settings.buyIn;\n document.getElementById('seats').textContent = room.seated + '/' + room.seats.length + ' 人在座';\n hint.textContent = room.alreadyInRoom\n ? '你之前已经进过这一桌,直接继续就行。'\n : '点下面的按钮进入牌桌,浏览器里直接就能玩,不用安装任何东西。';\n })\n .catch(function () {\n error.hidden = false;\n error.textContent = '连不上房主的进程,确认链接没有过期。';\n });\n\n enter.addEventListener('click', function () {\n enter.disabled = true;\n enter.textContent = '正在进入…';\n location.replace(target);\n });\n})();\n</script>\n</body>\n</html>`\n}\n\nfunction escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => ({\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n })[char] ?? char)\n}\n","/**\n * 给「不在同一台机器上」的朋友用的独立牌桌页面。\n *\n * 邀请链接指向的就是这里:宿主进程直接把这个页面发给对方浏览器,不需要对方装 DSH、\n * 也不需要 DSH Web UI 的访问令牌——页面自己调 /heldem 的 HTTP 接口(轮询)。\n */\n\nexport interface PlayPageOptions {\n prefix: string\n code: string\n invite: string\n role: 'auto' | 'sit' | 'watch'\n /** 邀请链接里带了昵称就直接进,不再弹昵称框。 */\n name?: string\n}\n\nexport function playPageHtml(options: PlayPageOptions): string {\n const role = options.role\n return `<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>德州扑克 · ${escapeHtml(options.code)}</title>\n<style>\n :root { color-scheme: dark; }\n * { box-sizing: border-box; }\n body {\n margin: 0; min-height: 100vh; display: flex; flex-direction: column;\n font: 13px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n background: #0f1512; color: #e9ecf2;\n }\n header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 10px 16px; border-bottom: 1px solid #1f2a24; }\n header h1 { margin: 0; font-size: 15px; }\n .pill { border: 1px solid #2a3a32; border-radius: 999px; padding: 1px 9px; font-size: 11px; color: #9db0a6; }\n .grow { flex: 1; }\n main { display: flex; flex: 1; min-height: 0; }\n #feltWrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; padding: 10px 12px 12px; }\n #felt {\n position: relative; flex: 1; min-height: 300px; border-radius: 24px; overflow: hidden;\n background:\n radial-gradient(ellipse at 50% 38%, rgba(255,255,255,.12), transparent 58%),\n radial-gradient(ellipse at 50% 120%, rgba(0,0,0,.5), transparent 62%),\n linear-gradient(165deg, #1f6a4f 0%, #17553f 55%, #123f30 100%);\n border: 1px solid rgba(0,0,0,.32);\n box-shadow: inset 0 0 70px rgba(0,0,0,.42);\n }\n .seat {\n position: absolute; width: 158px; padding: 7px 9px; border-radius: 12px;\n background: rgba(247,247,248,.94); color: #14181c; display: flex; flex-direction: column; gap: 4px;\n border: 1px solid rgba(0,0,0,.18);\n }\n .seat.you { border-color: #e8c15a; box-shadow: 0 0 0 1px #e8c15a; }\n .seat.actor { border-color: #e8c15a; box-shadow: 0 0 0 1px #e8c15a, 0 0 18px rgba(232,193,90,.45); }\n .seat.folded { opacity: .5; }\n .seat.empty { background: rgba(255,255,255,.06); color: rgba(255,255,255,.6); border: 1px dashed rgba(255,255,255,.28); align-items: center; justify-content: center; min-height: 64px; }\n .seatTop { display: flex; align-items: center; gap: 6px; }\n .avatar { width: 28px; height: 28px; border-radius: 50%; flex: none; display: grid; place-items: center; font-weight: 700; font-size: 12px; color: #fff; background: linear-gradient(150deg,#3f7f66,#245040); }\n .who { min-width: 0; flex: 1; }\n .name { font-weight: 600; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .stack { font-size: 11.5px; color: #4b5b52; font-variant-numeric: tabular-nums; }\n .badges { display: flex; gap: 4px; flex-wrap: wrap; font-size: 9px; color: #6c7a70; }\n .badges span { border: 1px solid currentColor; border-radius: 4px; padding: 0 3px; }\n .lastAction { font-size: 10px; color: #6c7a70; }\n .cards { display: flex; gap: 3px; justify-content: center; min-height: 46px; align-items: center; }\n .p0 { left: 50%; bottom: 8px; transform: translateX(-50%); }\n .p1 { left: 2%; bottom: 96px; }\n .p2 { left: 1%; top: 62px; }\n .p3 { left: 50%; top: 6px; transform: translateX(-50%); }\n .p4 { right: 1%; top: 62px; }\n .p5 { right: 2%; bottom: 96px; }\n #center { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); text-align: center; display: flex; flex-direction: column; gap: 6px; align-items: center; }\n #street { font-size: 10px; letter-spacing: .18em; text-transform: uppercase; color: rgba(255,255,255,.62); }\n #board { display: flex; gap: 6px; }\n #pot { font-size: 12.5px; font-weight: 700; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,.4); }\n #banner { background: #fff; color: #14181c; border-radius: 10px; padding: 7px 12px; font-size: 12px; box-shadow: 0 10px 24px rgba(0,0,0,.3); }\n #banner b { display: block; margin-bottom: 2px; }\n /* 方案 A:左上角点数 + 中央大花色 */\n .card {\n position: relative; display: block; flex: none; overflow: hidden;\n width: 50px; height: 70px; border-radius: 8px;\n background: #fcfbf7; color: #16171a; border: 1px solid #e2e0d7;\n box-shadow: 0 1px 2px rgba(0,0,0,.22), 0 3px 8px rgba(0,0,0,.12);\n font-family: ui-sans-serif, system-ui, -apple-system, \"Helvetica Neue\", sans-serif;\n font-weight: 750; font-variant-numeric: tabular-nums; line-height: 1;\n --rank: 16px; --suit: 27px;\n }\n .card .rank { position: absolute; left: 5px; top: 4px; font-size: var(--rank); line-height: 1; }\n .card .suit { position: absolute; left: 50%; top: 60%; transform: translate(-50%,-50%); font-size: var(--suit); line-height: 1; }\n .card.red { color: #d81b3c; }\n .card.sm { width: 38px; height: 54px; border-radius: 6px; --rank: 13px; --suit: 19px; }\n .card.back {\n background: radial-gradient(circle at 50% 50%, #2f6f5c 0 2.5px, transparent 2.6px) 0 0 / 9px 9px, #f6f3ea;\n border-color: #d9d6cb; box-shadow: inset 0 0 0 3px #f6f3ea, 0 1px 2px rgba(0,0,0,.22);\n }\n .card.slot { background: transparent; border: 1px dashed rgba(255,255,255,.32); box-shadow: none; }\n #me { display: flex; align-items: center; gap: 12px; border: 1px solid #22302a; background: #141c18; border-radius: 12px; padding: 8px 10px; flex: none; }\n #myCards { display: flex; gap: 6px; }\n #myMeta { display: flex; flex-direction: column; gap: 1px; min-width: 88px; }\n #myName { font-weight: 600; }\n #myStack { font-size: 11.5px; color: #9db0a6; }\n #actions { flex: 1; display: flex; flex-direction: column; gap: 6px; min-width: 0; }\n .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }\n button { font: inherit; border-radius: 8px; border: 1px solid #2f4038; background: #1b2621; color: #e9ecf2; padding: 7px 12px; cursor: pointer; }\n button:hover:not(:disabled) { border-color: #47604f; }\n button:disabled { opacity: .45; cursor: default; }\n button.primary { background: #2f9c76; border-color: transparent; color: #06130f; font-weight: 700; }\n button.warn { border-color: #d64545; color: #d64545; background: transparent; }\n button.ghost { background: transparent; }\n input[type=range] { flex: 1; min-width: 120px; accent-color: #2f9c76; }\n input[type=text] { font: inherit; border-radius: 8px; border: 1px solid #33443b; background: #101714; color: #e9ecf2; padding: 7px 10px; }\n #raiseValue { font-variant-numeric: tabular-nums; font-weight: 700; min-width: 70px; text-align: right; }\n .hint { color: #8fa197; font-size: 11.5px; }\n aside { width: 260px; flex: none; border-left: 1px solid #1f2a24; padding: 10px 12px; display: flex; flex-direction: column; gap: 12px; overflow-y: auto; }\n aside h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .1em; color: #8fa197; margin: 0; }\n .list { display: flex; flex-direction: column; gap: 3px; font-size: 11.5px; }\n .leaderRow { display: grid; grid-template-columns: 16px 1fr auto; gap: 6px; }\n .chat { display: flex; flex-direction: column; gap: 4px; font-size: 11.5px; max-height: 180px; overflow-y: auto; }\n .chat b { color: #6fd0a8; font-weight: 600; }\n #toast { position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%); background: #1b2621; border: 1px solid #2f4038; border-radius: 999px; padding: 8px 16px; display: none; z-index: 40; }\n #overlay { position: fixed; inset: 0; background: rgba(6,10,8,.72); display: none; place-items: center; padding: 20px; z-index: 30; }\n #overlayCard { width: min(680px, 100%); max-height: 100%; overflow-y: auto; background: #131a16; border: 1px solid #2a3a32; border-radius: 14px; padding: 18px; }\n #overlayCard h2 { margin: 0 0 6px; font-size: 18px; }\n table { width: 100%; border-collapse: collapse; font-size: 12.5px; margin-top: 10px; }\n th, td { text-align: left; padding: 7px 8px; border-top: 1px solid #1f2a24; }\n th { color: #8fa197; font-weight: 500; font-size: 11px; }\n .up { color: #4cc38a; }\n .down { color: #e06c75; }\n #join { position: fixed; inset: 0; display: grid; place-items: center; background: rgba(6,10,8,.78); z-index: 50; }\n #joinCard { width: min(380px, 92vw); background: #131a16; border: 1px solid #2a3a32; border-radius: 14px; padding: 18px; display: flex; flex-direction: column; gap: 10px; }\n @media (max-width: 980px) {\n aside { display: none; }\n .seat { width: 132px; }\n .p1, .p2, .p4, .p5 { top: auto; bottom: auto; }\n }\n</style>\n</head>\n<body data-prefix=\"${escapeHtml(options.prefix)}\" data-code=\"${escapeHtml(options.code)}\" data-invite=\"${escapeHtml(options.invite)}\" data-role=\"${role}\" data-name=\"${escapeHtml(options.name ?? '')}\">\n<header>\n <h1 id=\"roomTitle\">德州扑克</h1>\n <span class=\"pill\" id=\"roomCode\">房号 ${escapeHtml(options.code)}</span>\n <span class=\"pill\" id=\"phase\">连接中…</span>\n <span class=\"pill\" id=\"blinds\"></span>\n <span class=\"grow\"></span>\n <span class=\"pill\" id=\"conn\">○</span>\n <button class=\"ghost\" id=\"leave\" type=\"button\">离开</button>\n</header>\n<main>\n <div id=\"feltWrap\">\n <div id=\"felt\">\n <div id=\"seats\"></div>\n <div id=\"center\">\n <div id=\"street\"></div>\n <div id=\"board\"></div>\n <div id=\"pot\"></div>\n <div id=\"banner\" style=\"display:none\"></div>\n </div>\n </div>\n <div id=\"me\">\n <div id=\"myCards\"></div>\n <div id=\"myMeta\">\n <span id=\"myName\">—</span>\n <span id=\"myStack\"></span>\n </div>\n <div id=\"actions\"><span class=\"hint\" id=\"actionHint\">等待其他人…</span></div>\n </div>\n </div>\n <aside>\n <div><h2>排行榜</h2><div class=\"list\" id=\"leaderboard\"></div></div>\n <div><h2>桌边聊天</h2><div class=\"chat\" id=\"chat\"></div><div class=\"row\" style=\"margin-top:6px\"><input type=\"text\" id=\"chatInput\" placeholder=\"说点什么…\" style=\"flex:1\" /><button id=\"chatSend\" type=\"button\">发送</button></div></div>\n </aside>\n</main>\n<div id=\"toast\"></div>\n<div id=\"overlay\"><div id=\"overlayCard\"></div></div>\n<div id=\"join\" style=\"display:none\">\n <div id=\"joinCard\">\n <h2 style=\"margin:0;font-size:16px\">加入牌桌</h2>\n <p class=\"hint\" id=\"joinHint\" style=\"margin:0\"></p>\n <input type=\"text\" id=\"nameInput\" placeholder=\"你的昵称\" maxlength=\"16\" />\n <div class=\"row\">\n <button class=\"primary\" id=\"joinSit\" type=\"button\">进入牌桌</button>\n <button id=\"joinWatch\" type=\"button\">只看不打</button>\n </div>\n </div>\n</div>\n<script>\n(function () {\n var body = document.body\n var prefix = body.dataset.prefix || '/holdem'\n var code = body.dataset.code || ''\n var invite = body.dataset.invite || ''\n var preferWatch = body.dataset.role === 'watch'\n var presetName = body.dataset.name || ''\n var storeKey = 'holdem-guest:' + code\n var state = { token: '', roomId: '', view: null, busy: false, sig: '', ended: '', poll: 0, timer: 0 }\n var els = {\n title: document.getElementById('roomTitle'),\n code: document.getElementById('roomCode'),\n phase: document.getElementById('phase'),\n blinds: document.getElementById('blinds'),\n conn: document.getElementById('conn'),\n seats: document.getElementById('seats'),\n street: document.getElementById('street'),\n board: document.getElementById('board'),\n pot: document.getElementById('pot'),\n banner: document.getElementById('banner'),\n myCards: document.getElementById('myCards'),\n myName: document.getElementById('myName'),\n myStack: document.getElementById('myStack'),\n actions: document.getElementById('actions'),\n leaderboard: document.getElementById('leaderboard'),\n chat: document.getElementById('chat'),\n chatInput: document.getElementById('chatInput'),\n toast: document.getElementById('toast'),\n overlay: document.getElementById('overlay'),\n overlayCard: document.getElementById('overlayCard'),\n join: document.getElementById('join'),\n joinHint: document.getElementById('joinHint'),\n nameInput: document.getElementById('nameInput'),\n leave: document.getElementById('leave')\n }\n\n var SUIT = { s: '♠', h: '♥', d: '♦', c: '♣' }\n var RANK = { T: '10' }\n\n function cardHtml(card, size) {\n var rank = card.charAt(0)\n var suit = card.charAt(1)\n var red = suit === 'h' || suit === 'd'\n var cls = 'card' + (size === 'sm' ? ' sm' : '') + (red ? ' red' : '')\n return '<span class=\"' + cls + '\"><span class=\"rank\">' + (RANK[rank] || rank) + '</span><span class=\"suit\">' + (SUIT[suit] || '') + '</span></span>'\n }\n\n function backHtml(size) {\n return '<span class=\"card back' + (size === 'sm' ? ' sm' : '') + '\"></span>'\n }\n\n function slotHtml() {\n return '<span class=\"card slot\"></span>'\n }\n\n function chips(value) {\n return Math.round(value).toLocaleString('en-US')\n }\n\n function signed(value) {\n if (!value) return '0'\n return (value > 0 ? '+' : '−') + chips(Math.abs(value))\n }\n\n function toast(text, isError) {\n els.toast.textContent = text\n els.toast.style.display = 'block'\n els.toast.style.borderColor = isError ? '#d64545' : '#2f4038'\n clearTimeout(toast.timer)\n toast.timer = setTimeout(function () { els.toast.style.display = 'none' }, isError ? 4200 : 2400)\n }\n\n function headers(extra) {\n var out = { 'content-type': 'application/json', 'x-requested-with': '@zhuweiyou/dsh-holdem' }\n if (state.token) out['x-holdem-seat'] = state.token\n if (extra) for (var key in extra) out[key] = extra[key]\n return out\n }\n\n function api(path, options) {\n var init = options || {}\n return fetch(prefix + path, {\n method: init.method || 'GET',\n headers: headers(),\n credentials: 'same-origin',\n body: init.body === undefined ? undefined : JSON.stringify(init.body)\n }).then(function (response) {\n return response.text().then(function (text) {\n var payload = text ? JSON.parse(text) : {}\n if (!response.ok) {\n var error = new Error(payload.message || ('HTTP ' + response.status))\n error.status = response.status\n error.code = payload.error\n error.view = payload.view\n throw error\n }\n return payload\n })\n })\n }\n\n function actionId() {\n if (window.crypto && window.crypto.randomUUID) return window.crypto.randomUUID()\n return 'g' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8)\n }\n\n // ---------------------------------------------------------------- 加入\n function storedName() {\n try { return localStorage.getItem('holdem-name') || '' } catch (e) { return '' }\n }\n\n function saveName(name) {\n try { localStorage.setItem('holdem-name', name) } catch (e) {}\n }\n\n function askJoin() {\n els.join.style.display = 'grid'\n els.nameInput.value = storedName()\n els.joinHint.textContent = preferWatch ? '以观战身份进入房间 ' + code : '房间 ' + code + ':有空位就直接入座,满员自动观战'\n setTimeout(function () { els.nameInput.focus() }, 50)\n }\n\n function join(role) {\n var name = (els.nameInput.value || '').trim()\n if (!name) { toast('先填个昵称', true); return }\n saveName(name)\n els.join.style.display = 'none'\n api('/api/join', { method: 'POST', body: { roomCode: code, invite: invite, displayName: name, role: role } }).then(function (payload) {\n state.token = payload.sessionToken || ''\n state.roomId = payload.roomId || ''\n try { sessionStorage.setItem(storeKey, state.token) } catch (e) {}\n applyView(payload.view)\n startPolling()\n toast(role === 'watch' ? '已进入观战席' : '已入座')\n }).catch(function (error) {\n els.join.style.display = 'grid'\n els.joinHint.textContent = error.message || '加入失败'\n })\n }\n\n els.nameInput.addEventListener('keydown', function (event) { if (event.key === 'Enter') join(preferWatch ? 'watch' : 'sit') })\n document.getElementById('joinSit').addEventListener('click', function () { join('sit') })\n document.getElementById('joinWatch').addEventListener('click', function () { join('watch') })\n\n // ---------------------------------------------------------------- 视图\n function applyView(view) {\n if (!view) return\n state.view = view\n state.roomId = view.room.roomId\n if (view.room.phase === 'closed') stopPolling()\n render()\n }\n\n function startPolling() {\n if (state.poll) return\n state.poll = setInterval(tick, 1200)\n tick()\n }\n\n function stopPolling() {\n if (state.poll) clearInterval(state.poll)\n state.poll = 0\n }\n\n function tick() {\n if (!state.roomId || state.busy) return\n api('/api/rooms/' + encodeURIComponent(state.roomId) + '/state').then(function (payload) {\n els.conn.textContent = '●'\n els.conn.style.color = '#4cc38a'\n applyView(payload.view)\n }).catch(function (error) {\n if (error.status === 403 || error.status === 404) {\n els.conn.textContent = '○'\n els.conn.style.color = '#e06c75'\n if (state.ended !== 'gone') {\n state.ended = 'gone'\n stopPolling()\n toast('房间已解散', true)\n }\n return\n }\n els.conn.textContent = '◐'\n els.conn.style.color = '#c58a00'\n })\n }\n\n function command(cmd) {\n if (!state.view) return Promise.resolve()\n state.busy = true\n var baseSeq = state.view.room.seq\n return api('/api/rooms/' + encodeURIComponent(state.roomId) + '/action', {\n method: 'POST',\n body: { actionId: actionId(), baseSeq: baseSeq, command: cmd }\n }).then(function (payload) {\n applyView(payload.view)\n }).catch(function (error) {\n if (error.view) applyView(error.view)\n if (error.code === 'stale') toast('状态已更新,请重试', true)\n else if (error.code !== 'not-your-turn') toast(error.message || '操作失败', true)\n }).then(function () {\n state.busy = false\n tick()\n })\n }\n\n // ---------------------------------------------------------------- 渲染\n function phaseText(phase) {\n return phase === 'hand' ? '正在打牌' : phase === 'handOver' ? '本手结束' : phase === 'closed' ? '已结束' : '等待开局'\n }\n\n function render() {\n var view = state.view\n if (!view) return\n var room = view.room\n els.title.textContent = room.title\n els.code.textContent = '房号 ' + room.roomCode\n els.phase.textContent = phaseText(room.phase)\n els.blinds.textContent = room.settings.sb + '/' + room.settings.bb + ' · 带入 ' + chips(room.settings.buyIn)\n els.conn.textContent = '●'\n renderSeats(view)\n renderCenter(view)\n renderMe(view)\n renderRail(view)\n if (room.phase === 'closed' && view.settlement) renderSettlement(view)\n else els.overlay.style.display = 'none'\n }\n\n function renderSeats(view) {\n var room = view.room\n var hand = view.hand\n var base = view.you.seat === null ? 0 : view.you.seat\n var html = ''\n for (var i = 0; i < room.seats.length; i += 1) {\n var seat = room.seats[i]\n var rel = (seat.seat - base + 6) % 6\n var cls = 'seat p' + rel\n if (seat.seat === view.you.seat) cls += ' you'\n if (hand && hand.toActSeat === seat.seat && room.phase === 'hand') cls += ' actor'\n if (seat.folded) cls += ' folded'\n if (!seat.playerId) {\n html += '<div class=\"seat empty p' + rel + '\">入座</div>'\n continue\n }\n var shown = null\n if (hand) for (var j = 0; j < hand.shownHands.length; j += 1) if (hand.shownHands[j].seat === seat.seat) shown = hand.shownHands[j]\n html += '<div class=\"' + cls + '\">'\n html += '<div class=\"seatTop\"><div class=\"avatar\">' + escape((seat.displayName || '?').slice(0, 1)) + '</div>'\n html += '<div class=\"who\"><div class=\"name\">' + escape(seat.displayName || '—') + '</div><div class=\"stack\">' + chips(seat.stack) + '</div></div></div>'\n html += '<div class=\"badges\">'\n if (seat.host) html += '<span>房主</span>'\n if (hand && hand.button === seat.seat) html += '<span>D</span>'\n if (hand && hand.sbSeat === seat.seat) html += '<span>SB</span>'\n if (hand && hand.bbSeat === seat.seat) html += '<span>BB</span>'\n if (seat.allIn) html += '<span>全下</span>'\n html += '</div>'\n html += '<div class=\"cards\">'\n if (shown) { for (var k = 0; k < shown.cards.length; k += 1) html += cardHtml(shown.cards[k], 'sm') }\n else { for (var b = 0; b < seat.holeCount; b += 1) html += backHtml('sm') }\n html += '</div>'\n if (seat.lastAction) html += '<div class=\"lastAction\">' + escape(seat.lastAction) + '</div>'\n if (seat.committed > 0) html += '<div class=\"lastAction\">本轮 ' + chips(seat.committed) + '</div>'\n html += '</div>'\n }\n els.seats.innerHTML = html\n }\n\n function renderCenter(view) {\n var hand = view.hand\n var room = view.room\n if (hand) {\n els.street.textContent = ({ preflop: '翻牌前', flop: '翻牌', turn: '转牌', river: '河牌', showdown: '摊牌' })[hand.street] || ''\n var board = ''\n for (var i = 0; i < 5; i += 1) board += hand.board[i] ? cardHtml(hand.board[i]) : slotHtml()\n els.board.innerHTML = board\n els.pot.textContent = '底池 ' + chips(hand.potTotal)\n } else {\n els.street.textContent = ''\n els.board.innerHTML = ''\n els.pot.textContent = ''\n }\n var last = view.lastHand\n if (room.phase === 'handOver' && last) {\n var winners = last.winners.map(function (seat) {\n var found = room.seats.filter(function (entry) { return entry.seat === seat })[0]\n return found && found.displayName ? found.displayName : '座位 ' + seat\n }).join(' · ')\n var gains = last.deltas.filter(function (delta) { return delta.net !== 0 }).map(function (delta) {\n return escape(delta.displayName) + ' ' + signed(delta.net)\n }).join(' ')\n els.banner.style.display = 'block'\n els.banner.innerHTML = '<b>' + (last.foldedWin ? '无人跟注,直接收池' : escape(winners) + ' 赢得 ' + chips(last.pots.reduce(function (sum, pot) { return sum + pot.amount }, 0))) + '</b>' + gains\n } else {\n els.banner.style.display = 'none'\n }\n }\n\n function renderMe(view) {\n var you = view.you\n var room = view.room\n var hand = view.hand\n var html = ''\n if (you.holeCards.length > 0) {\n for (var i = 0; i < you.holeCards.length; i += 1) html += cardHtml(you.holeCards[i])\n } else if (room.phase === 'hand' && !you.spectator) {\n html += backHtml() + backHtml()\n }\n els.myCards.innerHTML = html\n els.myName.textContent = you.displayName\n els.myStack.textContent = you.spectator ? '观战中' : chips((room.seats.filter(function (seat) { return seat.seat === you.seat })[0] || {}).stack || 0) + ' · 净赢 ' + signed(you.netChips)\n renderActions(view)\n }\n\n function renderActions(view) {\n var you = view.you\n var room = view.room\n var hand = view.hand\n var legal = you.legal\n var canAct = you.canAct && legal && room.phase === 'hand'\n var sig = canAct\n ? ['act', hand ? hand.handNo : 0, hand ? hand.toActSeat : -1, legal.minRaiseTo, legal.maxRaiseTo, legal.canCheck, legal.canCall, legal.canRaise, legal.callAmount].join(':')\n : ['idle', room.phase, you.ready, you.spectator, you.seat, room.hostPlayerId === you.playerId]\n if (sig === state.sig) { updateTimer(); return }\n state.sig = sig\n if (canAct) {\n var isAllIn = legal.canRaise && legal.minRaiseTo >= legal.maxRaiseTo\n var raiseValue = legal.minRaiseTo\n var step = Math.max(1, Math.round(room.settings.bb / 2))\n els.actions.innerHTML =\n '<div class=\"row\"><span class=\"hint\" style=\"color:#e8c15a;font-weight:700\">轮到你</span><span class=\"hint\" id=\"timer\"></span></div>' +\n '<div class=\"row\">' +\n '<button class=\"ghost\" data-act=\"fold\"' + (legal.canFold ? '' : ' disabled') + '>弃牌</button>' +\n (legal.canCheck\n ? '<button data-act=\"check\">过牌</button>'\n : '<button data-act=\"call\"' + (legal.canCall ? '' : ' disabled') + '>跟注 ' + chips(legal.callAmount) + '</button>') +\n (legal.canRaise\n ? '<span id=\"raiseValue\">' + chips(raiseValue) + '</span>' +\n '<input type=\"range\" id=\"raiseRange\" min=\"' + legal.minRaiseTo + '\" max=\"' + legal.maxRaiseTo + '\" step=\"' + step + '\" value=\"' + raiseValue + '\" />' +\n '<button data-range=\"0.5\" class=\"ghost\">1/2 池</button>' +\n '<button data-range=\"0.66\" class=\"ghost\">2/3 池</button>' +\n '<button data-range=\"1\" class=\"ghost\">一池</button>' +\n '<button data-range=\"max\" class=\"ghost\">最大</button>' +\n '<button class=\"primary\" data-act=\"raise\">' + (isAllIn ? '全下' : '加注') + '</button>'\n : '') +\n '</div>'\n var range = document.getElementById('raiseRange')\n var value = document.getElementById('raiseValue')\n if (range) {\n range.addEventListener('input', function () { value.textContent = chips(Number(range.value)) })\n }\n els.actions.querySelectorAll('[data-range]').forEach(function (button) {\n button.addEventListener('click', function () {\n var ratio = button.dataset.range\n var next = ratio === 'max' ? legal.maxRaiseTo : Math.min(legal.maxRaiseTo, Math.max(legal.minRaiseTo, Math.round((hand ? hand.potTotal : 0) * Number(ratio))))\n range.value = String(next)\n value.textContent = chips(next)\n })\n })\n els.actions.querySelectorAll('[data-act]').forEach(function (button) {\n button.addEventListener('click', function () {\n var act = button.dataset.act\n if (act === 'fold') command({ type: 'action', action: { type: 'fold' } })\n else if (act === 'check') command({ type: 'action', action: { type: 'check' } })\n else if (act === 'call') command({ type: 'action', action: { type: 'call' } })\n else if (act === 'raise') command({ type: 'action', action: { type: 'raise', to: Number(range.value) } })\n })\n })\n updateTimer()\n return\n }\n\n // 不能行动时:按阶段给出等待/准备/开始/入座\n var parts = []\n if (you.spectator) {\n parts.push('<span class=\"hint\">你在观战席' + (room.phase === 'lobby' ? ',等待阶段可以入座' : '') + '</span>')\n if (room.phase === 'lobby') parts.push('<button class=\"primary\" data-cmd=\"sit\">入座</button>')\n } else if (room.phase === 'lobby') {\n parts.push('<span class=\"hint\">' + (you.ready ? '已准备,等房主开局' : '尚未准备') + '</span>')\n parts.push('<button data-cmd=\"' + (you.ready ? 'unready' : 'ready') + '\">' + (you.ready ? '取消准备' : '准备') + '</button>')\n if (room.hostPlayerId === you.playerId) parts.push('<button class=\"primary\" data-cmd=\"start\">开始</button>')\n parts.push('<button class=\"ghost\" data-cmd=\"stand\">离座</button>')\n } else if (room.phase === 'hand') {\n var actorName = ''\n if (hand && hand.toActSeat !== null) {\n var actorSeat = room.seats.filter(function (seat) { return seat.seat === hand.toActSeat })[0]\n actorName = actorSeat && actorSeat.displayName ? actorSeat.displayName : ''\n }\n parts.push('<span class=\"hint\">' + (actorName ? '等待 ' + escape(actorName) + ' 行动' : '等待中') + '</span>')\n } else if (room.phase === 'handOver') {\n parts.push('<span class=\"hint\">' + (room.nextHandAt ? '马上开下一手…' : '本手结束') + '</span>')\n } else {\n parts.push('<span class=\"hint\">房间已结束</span>')\n }\n els.actions.innerHTML = '<div class=\"row\">' + parts.join('') + '</div>'\n els.actions.querySelectorAll('[data-cmd]').forEach(function (button) {\n button.addEventListener('click', function () {\n var cmd = button.dataset.cmd\n if (cmd === 'ready') command({ type: 'ready', ready: true })\n else if (cmd === 'unready') command({ type: 'ready', ready: false })\n else if (cmd === 'start') command({ type: 'start' })\n else if (cmd === 'sit') command({ type: 'sit' })\n else if (cmd === 'stand') command({ type: 'stand' })\n })\n })\n }\n\n function updateTimer() {\n var view = state.view\n var node = document.getElementById('timer')\n if (!node || !view || !view.hand || !view.hand.deadlineAt) return\n var left = Math.max(0, Math.ceil((Date.parse(view.hand.deadlineAt) - Date.now()) / 1000))\n node.textContent = left + ' 秒'\n node.style.color = left <= 5 ? '#e06c75' : '#9db0a6'\n }\n\n function renderRail(view) {\n var board = ''\n for (var i = 0; i < view.leaderboard.length; i += 1) {\n var entry = view.leaderboard[i]\n board += '<div class=\"leaderRow\"><span>' + (i + 1) + '</span><span>' + escape(entry.displayName) + '</span><span class=\"' + (entry.netChips >= 0 ? 'up' : 'down') + '\">' + signed(entry.netChips) + '</span></div>'\n }\n els.leaderboard.innerHTML = board || '<span class=\"hint\">还没有记录</span>'\n var chat = ''\n for (var j = 0; j < view.chat.length; j += 1) {\n chat += '<div><b>' + escape(view.chat[j].displayName) + '</b> ' + escape(view.chat[j].text) + '</div>'\n }\n els.chat.innerHTML = chat || '<span class=\"hint\">还没有记录</span>'\n els.chatInput.disabled = !view.room.settings.chatEnabled\n }\n\n function renderSettlement(view) {\n var summary = view.settlement\n if (!summary) return\n var rows = summary.players.map(function (player) {\n return '<tr><td>' + player.rank + '</td><td>' + escape(player.displayName) + '</td><td>' + player.handsPlayed + '</td><td>' + player.handsWon + '</td><td class=\"' + (player.netChips >= 0 ? 'up' : 'down') + '\">' + signed(player.netChips) + '</td></tr>'\n }).join('')\n els.overlayCard.innerHTML =\n '<h2>本场结算</h2>' +\n '<p class=\"hint\" style=\"margin:0\">' + escape(summary.title) + ' · 房号 ' + escape(summary.roomCode) + ' · 共 ' + summary.handsPlayed + ' 手 · ' + (summary.reason === 'host-idle' ? '房主失联自动解散' : '房主解散房间') + '</p>' +\n '<table><thead><tr><th>名次</th><th>玩家</th><th>手数</th><th>胜局</th><th>净赢</th></tr></thead><tbody>' + rows + '</tbody></table>' +\n '<p class=\"hint\" style=\"margin-top:12px\">这局已经结束,房间不会重开。想再打请向房主要新链接。</p>'\n els.overlay.style.display = 'grid'\n }\n\n function escape(text) {\n return String(text).replace(/[&<>\"']/g, function (char) {\n return ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' })[char]\n })\n }\n\n // ---------------------------------------------------------------- 聊天 / 离开\n document.getElementById('chatSend').addEventListener('click', function () {\n var text = (els.chatInput.value || '').trim()\n if (!text) return\n els.chatInput.value = ''\n command({ type: 'chat', text: text.slice(0, 80) })\n })\n els.chatInput.addEventListener('keydown', function (event) {\n if (event.key === 'Enter') document.getElementById('chatSend').click()\n })\n\n els.leave.addEventListener('click', function () {\n api('/api/leave', { method: 'POST', body: { roomId: state.roomId } }).catch(function () {}).then(function () {\n try { sessionStorage.removeItem(storeKey) } catch (e) {}\n location.reload()\n })\n })\n\n // 键盘:F 弃牌 / C 过牌-跟注 / R 加注\n window.addEventListener('keydown', function (event) {\n var target = event.target\n if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) return\n var view = state.view\n if (!view || !view.you.legal || !view.you.canAct) return\n var legal = view.you.legal\n var key = event.key.toLowerCase()\n if (key === 'f' && legal.canFold) command({ type: 'action', action: { type: 'fold' } })\n else if (key === 'c') command({ type: 'action', action: legal.canCheck ? { type: 'check' } : { type: 'call' } })\n else if (key === 'r' && legal.canRaise) {\n var range = document.getElementById('raiseRange')\n command({ type: 'action', action: { type: 'raise', to: Number(range ? range.value : legal.minRaiseTo) } })\n }\n })\n\n // ---------------------------------------------------------------- 启动\n state.timer = setInterval(updateTimer, 250)\n var saved = ''\n try { saved = sessionStorage.getItem(storeKey) || '' } catch (e) {}\n if (presetName) {\n els.nameInput.value = presetName\n join(preferWatch ? 'watch' : 'sit')\n state.timer = setInterval(updateTimer, 250)\n return\n }\n if (saved) {\n state.token = saved\n api('/api/session?roomId=' + encodeURIComponent(''))\n .then(function (payload) {\n if (payload.inRoom && payload.view && payload.view.room.roomCode === code) {\n applyView(payload.view)\n startPolling()\n return\n }\n state.token = ''\n try { sessionStorage.removeItem(storeKey) } catch (e) {}\n askJoin()\n })\n .catch(function () { askJoin() })\n } else {\n askJoin()\n }\n})();\n</script>\n</body>\n</html>`\n}\n\nfunction escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => ({\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n })[char] ?? char)\n}\n","import { appendFileSync, mkdirSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { WebServer } from '@deepseek-ai/cordis'\nimport { type ClientCommand } from '../types.ts'\nimport { asRoomId } from '../types.ts'\nimport type { RoomManager } from '../room/RoomManager.ts'\nimport {\n assertMutatingHeaders,\n assertOrigin,\n cookieHeader,\n header,\n isLoopbackHost,\n isLoopbackOrigin,\n isPrivateOrigin,\n parseCookies,\n} from './auth.ts'\nimport { encodeJson, readJson } from './json.ts'\nimport { joinPageHtml } from './join-page.ts'\nimport { playPageHtml } from './play-page.ts'\n\n/** 每个房间一个座位 cookie:同一个浏览器可以同时待在不同房间的多个标签页里。 */\nconst SEAT_COOKIE_PREFIX = 'hdm_seat'\n/** 客户端同时用请求头带 token:不依赖 cookie 是否被浏览器/宿主保留。 */\nconst SEAT_HEADER = 'x-holdem-seat'\n\nfunction seatCookieName(roomId: string): string {\n return `${SEAT_COOKIE_PREFIX}_${roomId}`\n}\n\nfunction seatTokens(req: IncomingMessage, cookies: Record<string, string>): string[] {\n const fromHeader = (header(req, SEAT_HEADER) ?? '')\n .split(',')\n .map((value) => value.trim())\n .filter((value) => value !== '')\n const fromCookies = Object.entries(cookies)\n .filter(([name]) => name.startsWith(SEAT_COOKIE_PREFIX))\n .map(([, value]) => value)\n .filter((value) => value !== '')\n return [...new Set([...fromHeader, ...fromCookies])]\n}\n\nfunction sessionOf(manager: RoomManager, tokens: string[], roomId?: string) {\n const sessions = manager.sessionsForTokens(tokens)\n if (roomId) return sessions.find((session) => session.roomId === roomId)\n return sessions[0]\n}\n\n/** 临时诊断日志:把每个请求的结果写到 ~/.dsh/holdem/debug.jsonl */\nfunction trace(req: IncomingMessage, res: ServerResponse): void {\n const started = Date.now()\n res.on('finish', () => {\n try {\n const cookies = Object.keys(parseCookies(req))\n const line = JSON.stringify({\n at: new Date().toISOString(),\n ms: Date.now() - started,\n method: req.method,\n url: req.url,\n status: res.statusCode,\n origin: req.headers.origin ?? null,\n host: req.headers.host ?? null,\n fetchSite: req.headers['sec-fetch-site'] ?? null,\n cookieNames: cookies,\n })\n const dir = join(homedir(), '.dsh', 'holdem')\n mkdirSync(dir, { recursive: true })\n appendFileSync(join(dir, 'debug.jsonl'), line + '\\n')\n } catch {\n // 诊断日志失败不影响主流程\n }\n })\n}\n\nexport interface CloudflareLike {\n getStatus(): unknown\n startLogin(): unknown\n startDeploy(): unknown\n cancelLogin(): unknown\n logout(): unknown\n}\n\n/** role=auto 是默认:有空位就入座,满员自动转观战。 */\nfunction playRole(value: string | null): 'auto' | 'sit' | 'watch' {\n return value === 'watch' ? 'watch' : value === 'sit' ? 'sit' : 'auto'\n}\n\nexport function registerHoldemHttp(\n server: WebServer,\n manager: RoomManager,\n cloudflare?: CloudflareLike,\n): () => void {\n const prefix = manager.getConfig().routePrefix\n return server.register({\n kind: 'prefix',\n path: prefix,\n handler: (req, res) => {\n trace(req, res)\n // 面板可以从本机或局域网另一个 origin 调过来(例如加入云端/别人机器的房间)\n const origin = header(req, 'origin')\n const allow = origin !== undefined && (isLoopbackOrigin(origin) || isPrivateOrigin(origin))\n if (allow) {\n res.setHeader('access-control-allow-origin', origin)\n res.setHeader('access-control-allow-methods', 'GET,POST,OPTIONS')\n res.setHeader('access-control-allow-headers', 'content-type,x-requested-with,x-holdem-seat')\n res.setHeader('access-control-max-age', '600')\n res.setHeader('vary', 'Origin')\n }\n if (req.method === 'OPTIONS') {\n res.writeHead(allow ? 204 : 403)\n res.end()\n return\n }\n void handle(req, res, manager, prefix, cloudflare)\n },\n })\n}\n\nasync function handle(\n req: IncomingMessage,\n res: ServerResponse,\n manager: RoomManager,\n prefix: string,\n cloudflare?: CloudflareLike,\n): Promise<void> {\n try {\n const url = new URL(req.url ?? '/', 'http://127.0.0.1')\n const path = url.pathname\n const cookies = parseCookies(req)\n\n if (req.method === 'GET' && path === `${prefix}/play`) {\n html(res, playPageHtml({\n prefix,\n code: String(url.searchParams.get('code') ?? ''),\n invite: String(url.searchParams.get('invite') ?? ''),\n role: playRole(url.searchParams.get('role')),\n name: String(url.searchParams.get('name') ?? ''),\n }))\n return\n }\n\n if (req.method === 'GET' && (path === prefix || path === `${prefix}/` || path === `${prefix}/join`)) {\n const role = playRole(url.searchParams.get('role'))\n html(res, joinPageHtml({\n prefix,\n code: String(url.searchParams.get('code') ?? ''),\n invite: String(url.searchParams.get('invite') ?? ''),\n role,\n }))\n return\n }\n\n if (req.method === 'GET' && path === `${prefix}/api/ready`) {\n json(res, 200, {\n ok: true,\n plugin: '@zhuweiyou/dsh-holdem',\n turnTimeoutMs: manager.getConfig().turnTimeoutMs,\n })\n return\n }\n\n if (req.method === 'GET' && path === `${prefix}/api/health`) {\n if (!isLoopbackHost(header(req, 'host'), req.socket.remoteAddress)) throw auth('仅本机可查看健康状态')\n json(res, 200, manager.health())\n return\n }\n\n if (path === `${prefix}/api/cloudflare`) {\n if (!cloudflare) {\n json(res, 200, { authorized: false, account: '', workerUrl: '', pending: false, deploying: false, authorizeUrl: '', error: 'not-supported', log: '' })\n return\n }\n if (req.method === 'POST') {\n assertOrigin(req)\n assertMutatingHeaders(req)\n if (!isLoopbackHost(header(req, 'host'), req.socket.remoteAddress)) throw auth('只能在本机操作 Cloudflare 授权')\n const action = url.searchParams.get('action') ?? ''\n if (action === 'login') json(res, 200, cloudflare.startLogin())\n else if (action === 'deploy') json(res, 200, cloudflare.startDeploy())\n else if (action === 'cancel') json(res, 200, cloudflare.cancelLogin())\n else if (action === 'logout') json(res, 200, cloudflare.logout())\n else json(res, 400, { error: 'illegal', message: '未知操作' })\n return\n }\n json(res, 200, cloudflare.getStatus())\n return\n }\n\n if (req.method === 'GET' && path === `${prefix}/api/session`) {\n const wanted = url.searchParams.get('roomId') ?? ''\n const tokens = seatTokens(req, cookies)\n const session = (wanted ? sessionOf(manager, tokens, wanted) : undefined) ?? sessionOf(manager, tokens)\n const seatCookieCount = tokens.length\n if (!session) {\n json(res, 200, { inRoom: false, seatCookies: seatCookieCount })\n return\n }\n try {\n json(res, 200, { inRoom: true, roomId: session.roomId, view: manager.view(session.roomId, session.playerId) })\n } catch {\n json(res, 200, { inRoom: false, seatCookies: seatCookieCount })\n }\n return\n }\n\n if (req.method === 'POST' && path === `${prefix}/api/rooms`) {\n assertOrigin(req)\n assertMutatingHeaders(req)\n const local = isLoopbackHost(header(req, 'host'), req.socket.remoteAddress)\n if (!local) throw auth('只能在运行 Harness 的本机开房')\n const body = await readJson<{ title?: string; blindLevel?: string; displayName?: string; browserId?: string }>(req)\n const created = manager.createRoom({\n title: body.title,\n blindLevel: body.blindLevel,\n displayName: body.displayName,\n browserId: body.browserId,\n })\n json(res, 200, {\n roomId: created.roomId,\n roomCode: created.roomCode,\n hostPlayerId: created.hostPlayerId,\n joinUrl: created.joinUrl,\n wsTicket: created.wsTicket,\n sessionToken: created.sessionToken,\n view: created.view,\n }, [seatCookie(manager, created.sessionToken, created.roomId)])\n return\n }\n\n if (req.method === 'GET' && path === `${prefix}/api/preview`) {\n const preview = manager.peek({\n roomCode: String(url.searchParams.get('code') ?? ''),\n invite: String(url.searchParams.get('invite') ?? ''),\n role: url.searchParams.get('role') === 'watch' ? 'watch' : 'sit',\n local: isLoopbackHost(header(req, 'host'), req.socket.remoteAddress),\n browserId: url.searchParams.get('browser') ?? undefined,\n cookies: seatTokens(req, cookies),\n })\n json(res, 200, preview)\n return\n }\n\n if (req.method === 'POST' && path === `${prefix}/api/join`) {\n assertOrigin(req)\n assertMutatingHeaders(req)\n const body = await readJson<{\n roomCode?: string\n invite?: string\n displayName?: string\n role?: 'sit' | 'watch'\n browserId?: string\n }>(req)\n const joined = manager.join({\n roomCode: String(body.roomCode ?? ''),\n invite: String(body.invite ?? ''),\n role: body.role === 'watch' ? 'watch' : 'sit',\n displayName: String(body.displayName ?? ''),\n local: isLoopbackHost(header(req, 'host'), req.socket.remoteAddress),\n browserId: body.browserId,\n cookies: seatTokens(req, cookies),\n })\n json(res, 200, {\n playerId: joined.playerId,\n roomId: joined.roomId,\n seat: joined.seat,\n wsTicket: joined.wsTicket,\n sessionToken: joined.sessionToken,\n view: joined.view,\n }, [seatCookie(manager, joined.sessionToken, joined.roomId)])\n return\n }\n\n if (req.method === 'POST' && path === `${prefix}/api/leave`) {\n assertOrigin(req)\n assertMutatingHeaders(req)\n const body = await readJson<{ roomId?: string }>(req).catch(() => ({}) as { roomId?: string })\n const tokens = seatTokens(req, cookies)\n const session = sessionOf(manager, tokens, body.roomId) ?? sessionOf(manager, tokens)\n if (session) {\n manager.leave(session.playerId)\n json(res, 200, { ok: true }, [clearSeatCookie(manager, session.roomId)])\n return\n }\n json(res, 200, { ok: true })\n return\n }\n\n if (req.method === 'POST' && path === `${prefix}/api/ws-ticket`) {\n assertOrigin(req)\n assertMutatingHeaders(req)\n const body = await readJson<{ roomId?: string }>(req).catch(() => ({}) as { roomId?: string })\n const tokens = seatTokens(req, cookies)\n const session = sessionOf(manager, tokens, body.roomId) ?? sessionOf(manager, tokens)\n if (!session) throw auth('还没进房间')\n json(res, 200, { wsTicket: manager.issueTicket(session.playerId, session.roomId) })\n return\n }\n\n const roomMatch = /^\\/api\\/rooms\\/([^/]+)(?:\\/(state|action|history))?$/.exec(path.slice(prefix.length))\n if (roomMatch) {\n const action = roomMatch[2]\n const roomId = asRoomId(decodeURIComponent(roomMatch[1] ?? ''))\n const session = sessionOf(manager, seatTokens(req, cookies), roomId)\n if (!session) throw auth('不在这个房间里')\n if (req.method === 'GET' && (!action || action === 'state')) {\n const view = manager.view(roomId, session.playerId)\n json(res, 200, { seq: view.room.seq, view })\n return\n }\n if (req.method === 'GET' && action === 'history') {\n json(res, 200, { summary: manager.exportSummary(roomId, session.playerId) })\n return\n }\n if (req.method === 'POST' && action === 'action') {\n assertOrigin(req)\n assertMutatingHeaders(req)\n const body = await readJson<{\n actionId?: string\n baseSeq?: number\n command?: ClientCommand\n }>(req)\n if (!body.command || typeof body.command !== 'object') throw Object.assign(new Error('缺少 command'), { status: 400, code: 'illegal' })\n const result = manager.command(\n roomId,\n session.playerId,\n body.command,\n Number.isFinite(body.baseSeq) ? Number(body.baseSeq) : undefined,\n typeof body.actionId === 'string' ? body.actionId : undefined,\n )\n json(res, 200, { seq: result.seq, view: result.view, duplicate: result.duplicate === true })\n return\n }\n send(res, 404, encodeJson({ error: 'expired', message: 'not found' }), jsonHeaders())\n return\n }\n\n send(res, 404, encodeJson({ error: 'expired', message: 'not found' }), jsonHeaders())\n } catch (error) {\n writeError(res, error)\n }\n}\n\nfunction seatCookie(manager: RoomManager, token: string, roomId: string): string {\n return cookieHeader(seatCookieName(roomId), token, {\n secure: false,\n sameSite: 'Lax',\n maxAge: 86400,\n path: manager.getConfig().routePrefix,\n })\n}\n\nfunction clearSeatCookie(manager: RoomManager, roomId: string): string {\n return cookieHeader(seatCookieName(roomId), '', {\n secure: false,\n sameSite: 'Lax',\n maxAge: 0,\n path: manager.getConfig().routePrefix,\n })\n}\n\nfunction auth(message: string): Error {\n return Object.assign(new Error(message), { status: 403, code: 'auth' })\n}\n\nfunction json(res: ServerResponse, status: number, body: unknown, cookies?: string[]): void {\n const headers: Record<string, string | string[]> = jsonHeaders()\n if (cookies && cookies.length > 0) headers['set-cookie'] = cookies\n send(res, status, encodeJson(body), headers)\n}\n\nfunction html(res: ServerResponse, body: string): void {\n send(res, 200, body, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' })\n}\n\nfunction jsonHeaders(): Record<string, string> {\n return { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }\n}\n\nfunction send(res: ServerResponse, status: number, body: string, headers: Record<string, string | string[]>): void {\n if (res.headersSent) return\n res.writeHead(status, headers)\n res.end(body)\n}\n\nfunction writeError(res: ServerResponse, error: unknown): void {\n const coded = error as { code?: string; status?: number; view?: unknown }\n const code = coded.code ?? 'illegal'\n const status = coded.status ?? statusOf(code)\n const payload: Record<string, unknown> = {\n error: code,\n message: error instanceof Error ? error.message : 'error',\n }\n if (code === 'stale' && coded.view) payload.view = coded.view\n send(res, status, encodeJson(payload), jsonHeaders())\n}\n\nfunction statusOf(code: string): number {\n if (code === 'auth') return 403\n if (code === 'room-not-found' || code === 'expired') return 404\n if (code === 'rate-limited') return 429\n if (code === 'room-full' || code === 'phase' || code === 'stale' || code === 'already-in-room' || code === 'duplicate-nonce') return 409\n return 400\n}\n","import { appendFileSync, mkdirSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport { WebSocket, WebSocketServer } from 'ws'\nimport type { WebServer } from '@deepseek-ai/cordis'\nimport { DISCONNECT_AFTER_MS, MAX_FRAME_BYTES, SUBPROTOCOL } from '../invariant.ts'\nimport type { RoomManager } from '../room/RoomManager.ts'\nimport type { PlayerId, RoomId, ServerEvent } from '../types.ts'\nimport { assertOrigin } from './auth.ts'\nimport { encodeJson } from './json.ts'\n\ninterface SocketState {\n playerId: PlayerId\n roomId: RoomId\n lastSeen: number\n}\n\n/** 临时诊断日志:WS 升级成功/失败都记一条。 */\nfunction traceWs(line: Record<string, unknown>): void {\n try {\n const dir = join(homedir(), '.dsh', 'holdem')\n mkdirSync(dir, { recursive: true })\n appendFileSync(join(dir, 'debug.jsonl'), JSON.stringify({ at: new Date().toISOString(), ws: true, ...line }) + '\\n')\n } catch {\n // ignore\n }\n}\n\nexport function registerHoldemSocket(server: WebServer, manager: RoomManager): () => void {\n const wss = new WebSocketServer({\n noServer: true,\n // 客户端现在不请求子协议(Worker 那边没法回 Sec-WebSocket-Protocol);\n // 老客户端请求了就照给,没请求/不认识就回空串=不设这个头,但别掐握手。\n handleProtocols: (protocols) => (protocols.has(SUBPROTOCOL) ? SUBPROTOCOL : ''),\n })\n const sockets = new Map<WebSocket, SocketState>()\n\n const off = manager.onEvent((playerId, event) => {\n for (const [socket, state] of sockets) {\n if (state.playerId === playerId && socket.readyState === WebSocket.OPEN) send(socket, event)\n }\n })\n\n const heartbeat = setInterval(() => {\n const now = Date.now()\n const limit = DISCONNECT_AFTER_MS\n for (const [socket, state] of sockets) {\n if (now - state.lastSeen > limit) {\n socket.terminate()\n continue\n }\n if (socket.readyState === WebSocket.OPEN) socket.ping()\n }\n }, 15_000)\n if (typeof heartbeat === 'object' && 'unref' in heartbeat) heartbeat.unref()\n\n const disposeRoute = server.registerUpgrade({\n path: `${manager.getConfig().routePrefix}/ws`,\n handler: (req, socket, head) => {\n try {\n assertOrigin(req as IncomingMessage)\n } catch (error) {\n traceWs({ outcome: 'reject-origin', origin: req.headers.origin ?? null, host: req.headers.host ?? null, message: String(error) })\n reject(socket, '403 Forbidden')\n return\n }\n const url = new URL(req.url ?? '/', 'http://127.0.0.1')\n const ticket = url.searchParams.get('ticket') ?? ''\n const consumed = manager.consumeTicket(ticket)\n if (!consumed) {\n traceWs({ outcome: 'reject-ticket', origin: req.headers.origin ?? null, host: req.headers.host ?? null, hasTicket: ticket !== '' })\n reject(socket, '401 Unauthorized')\n return\n }\n traceWs({ outcome: 'open', origin: req.headers.origin ?? null, host: req.headers.host ?? null })\n wss.handleUpgrade(req as IncomingMessage, socket as Duplex, head, (ws) => {\n const state: SocketState = { playerId: consumed.playerId, roomId: consumed.roomId, lastSeen: Date.now() }\n sockets.set(ws, state)\n manager.counters.wsConnected += 1\n manager.markConnected(state.playerId, state.roomId, true)\n try {\n send(ws, { type: 'snapshot', seq: 0, view: manager.view(state.roomId, state.playerId) })\n } catch {\n ws.close()\n return\n }\n ws.on('pong', () => {\n state.lastSeen = Date.now()\n manager.markConnected(state.playerId, state.roomId, true)\n })\n ws.on('message', (data) => {\n state.lastSeen = Date.now()\n if (Buffer.byteLength(data.toString()) > MAX_FRAME_BYTES) {\n ws.close()\n return\n }\n let parsed: { type?: string } | null = null\n try {\n parsed = JSON.parse(data.toString()) as { type?: string }\n } catch {\n return\n }\n if (parsed?.type === 'ping') send(ws, { type: 'pong', ts: Date.now() })\n })\n ws.on('close', () => {\n sockets.delete(ws)\n manager.counters.wsConnected = Math.max(0, manager.counters.wsConnected - 1)\n const stillOpen = [...sockets.values()].some(\n (entry) => entry.playerId === state.playerId && entry.roomId === state.roomId,\n )\n if (!stillOpen) manager.markConnected(state.playerId, state.roomId, false)\n })\n ws.on('error', () => {\n ws.terminate()\n })\n })\n },\n })\n\n return () => {\n clearInterval(heartbeat)\n off()\n disposeRoute()\n for (const socket of sockets.keys()) socket.terminate()\n wss.close()\n }\n}\n\nfunction send(ws: WebSocket, event: ServerEvent): void {\n if (ws.readyState === WebSocket.OPEN) ws.send(encodeJson(event))\n}\n\nfunction reject(socket: Duplex, status: string): void {\n socket.write(`HTTP/1.1 ${status}\\r\\nConnection: close\\r\\n\\r\\n`)\n socket.destroy()\n}\n","import { existsSync } from 'node:fs'\nimport { delimiter, join } from 'node:path'\n\n/** 在 PATH 里找一个可执行文件(Node 与打包后的插件通用)。 */\nexport function whichSync(name: string): string | undefined {\n const exts = process.platform === 'win32' ? (process.env.PATHEXT ?? '.EXE').split(';') : ['']\n for (const dir of (process.env.PATH ?? '').split(delimiter)) {\n if (!dir) continue\n for (const ext of exts) {\n const candidate = join(dir, name + ext)\n if (existsSync(candidate)) return candidate\n }\n }\n return undefined\n}\n","import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process'\nimport { createRequire } from 'node:module'\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, join, resolve } from 'node:path'\nimport { whichSync } from './which.ts'\n\n/**\n * Cloudflare 授权 + 部署:直接驱动 wrangler(官方 CLI)。\n *\n * - 授权:spawn `wrangler login`,它会在本机起回调服务并打开浏览器;\n * 把 OAuth 链接截出来交给界面,浏览器没自动打开时可以复制。\n * - 部署:spawn `wrangler deploy`,从输出里抓 workers.dev 地址并记住。\n */\n\nexport interface CloudflareStatus {\n authorized: boolean\n account: string\n workerUrl: string\n pending: boolean\n deploying: boolean\n authorizeUrl: string\n error: string\n log: string\n}\n\ninterface StoredState {\n workerUrl: string\n account: string\n}\n\nconst OAUTH_PATTERN = /https:\\/\\/dash\\.cloudflare\\.com\\/oauth2\\/auth\\?[^\\s\"'`]+/\nconst WORKER_URL_PATTERN = /https:\\/\\/[a-z0-9-]+\\.[a-z0-9-]+\\.workers\\.dev/i\n\nfunction stateFile(): string {\n const home = process.env.DSH_HOME ?? join(homedir(), '.dsh')\n return join(home, 'holdem', 'cloudflare.json')\n}\n\nfunction readStored(): StoredState {\n try {\n const parsed = JSON.parse(readFileSync(stateFile(), 'utf8')) as Partial<StoredState>\n return { workerUrl: parsed.workerUrl ?? '', account: parsed.account ?? '' }\n } catch {\n return { workerUrl: '', account: '' }\n }\n}\n\nfunction writeStored(next: StoredState): void {\n const file = stateFile()\n mkdirSync(dirname(file), { recursive: true })\n writeFileSync(file, JSON.stringify(next, null, 2))\n}\n\n/**\n * 插件根目录:从当前模块位置往上找 package.json(打包后是 lib/index.js,\n * 源码里是 src/net/cloudflare.ts,层级不一样,不能写死 .. 的层数)。\n */\nfunction packageRoot(): string {\n let dir = dirname(new URL(import.meta.url).pathname)\n for (let depth = 0; depth < 5; depth += 1) {\n if (existsSync(join(dir, 'wrangler.toml'))) return dir\n const parent = resolve(dir, '..')\n if (parent === dir) break\n dir = parent\n }\n return resolve(dirname(new URL(import.meta.url).pathname), '..')\n}\n\nfunction wranglerCredentialFiles(): string[] {\n const home = homedir()\n return [\n join(home, 'Library', 'Preferences', '.wrangler', 'config', 'default.toml'),\n join(home, '.config', '.wrangler', 'config', 'default.toml'),\n join(home, '.wrangler', 'config', 'default.toml'),\n ]\n}\n\nfunction hasWranglerCredentials(): boolean {\n return wranglerCredentialFiles().some((file) => existsSync(file))\n}\n\ninterface WranglerCommand {\n command: string\n baseArgs: string[]\n env?: Record<string, string>\n label: string\n}\n\n/**\n * 找一套能跑起来的 wrangler 调用方式,并做一次 `--version` 探活:\n * 1. HOLDEM_WRANGLER 显式指定;\n * 2. PATH 里的 node + 依赖里的 wrangler(正常情况,不依赖全局 wrangler);\n * 3. Electron 自带 Node 模式(用户机器上没装 Node 时):\n * ELECTRON_RUN_AS_NODE=1 <Electron 可执行文件> wrangler/bin/wrangler.js;\n * 4. 插件目录的 .bin/wrangler 软链;\n * 5. PATH 里的全局 wrangler。\n */\nfunction wranglerCandidates(): WranglerCommand[] {\n const candidates: WranglerCommand[] = []\n const custom = process.env.HOLDEM_WRANGLER\n if (custom) candidates.push({ command: custom, baseArgs: [], label: `HOLDEM_WRANGLER=${custom}` })\n\n let cli = ''\n try {\n const require = createRequire(import.meta.url)\n const pkg = require.resolve('wrangler/package.json')\n const candidate = join(dirname(pkg), 'bin', 'wrangler.js')\n if (existsSync(candidate)) cli = candidate\n } catch {\n // 依赖里没有 wrangler\n }\n\n if (cli) {\n const node = whichSync('node')\n if (node) candidates.push({ command: node, baseArgs: [cli], label: `${node} ${cli}` })\n if (process.versions.electron) {\n candidates.push({\n command: process.execPath,\n baseArgs: [cli],\n env: { ELECTRON_RUN_AS_NODE: '1' },\n label: `ELECTRON_RUN_AS_NODE ${cli}`,\n })\n }\n }\n\n const shim = join(packageRoot(), 'node_modules', '.bin', 'wrangler')\n if (existsSync(shim)) candidates.push({ command: shim, baseArgs: [], label: shim })\n const onPath = whichSync('wrangler')\n if (onPath) candidates.push({ command: onPath, baseArgs: [], label: onPath })\n return candidates\n}\n\nlet resolvedCommand: WranglerCommand | null = null\n\nfunction wranglerCommand(): WranglerCommand {\n if (resolvedCommand) return resolvedCommand\n for (const candidate of wranglerCandidates()) {\n try {\n const probe = spawnSync(candidate.command, [...candidate.baseArgs, '--version'], {\n encoding: 'utf8',\n timeout: 15_000,\n env: candidate.env ? { ...process.env, ...candidate.env } : process.env,\n })\n if (probe.status === 0) {\n resolvedCommand = candidate\n return candidate\n }\n } catch {\n // 下一个候选\n }\n }\n return { command: 'wrangler', baseArgs: [], label: 'wrangler' }\n}\n\nfunction stripAnsi(text: string): string {\n return text.replace(/[\\u001b\\u009b][[()#;?]*(?:\\d{1,4}(?:;\\d{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '')\n}\n\nexport class CloudflareDeployer {\n private stored = readStored()\n private login: ChildProcessWithoutNullStreams | null = null\n private deploy: ChildProcessWithoutNullStreams | null = null\n private authorizeUrl = ''\n private log = ''\n private error = ''\n private checkedAt = 0\n private authorized = false\n\n getStatus(): CloudflareStatus {\n this.refreshAuth()\n return {\n authorized: this.authorized,\n account: this.stored.account,\n workerUrl: this.stored.workerUrl,\n pending: this.login !== null,\n deploying: this.deploy !== null,\n authorizeUrl: this.authorizeUrl,\n error: this.error,\n log: this.log.slice(-1500),\n }\n }\n\n /** 已经授权过就不再反复跑 whoami(60 秒缓存)。 */\n private refreshAuth(): void {\n if (this.login || this.deploy) return\n const now = Date.now()\n if (now - this.checkedAt < 60_000) return\n this.checkedAt = now\n try {\n const cli = wranglerCommand()\n const result = spawnSync(cli.command, [...cli.baseArgs, 'whoami'], {\n encoding: 'utf8',\n timeout: 15_000,\n env: cli.env ? { ...process.env, ...cli.env } : process.env,\n })\n const output = stripAnsi(`${result.stdout ?? ''}\\n${result.stderr ?? ''}`)\n const notLoggedIn = /not logged in|You are not authenticated/i.test(output)\n if (notLoggedIn) {\n this.authorized = false\n return\n }\n const email = /associated with the email\\s+([^\\s.]+(?:\\.[^\\s.]+)*)/i.exec(output)?.[1]\n if (email) {\n this.authorized = true\n this.stored = { ...this.stored, account: email }\n return\n }\n // whoami 超时或输出不认识:凭据文件在就当已授权,别让界面误报\n this.authorized = result.status === 0 || hasWranglerCredentials()\n } catch {\n this.authorized = hasWranglerCredentials()\n }\n }\n\n startLogin(): CloudflareStatus {\n if (this.login) return this.getStatus()\n this.error = ''\n this.authorizeUrl = ''\n const cli = wranglerCommand()\n this.log = `wrangler: ${cli.label}\\n`\n try {\n const child = spawn(cli.command, [...cli.baseArgs, 'login'], {\n cwd: packageRoot(),\n env: cli.env ? { ...process.env, ...cli.env } : process.env,\n })\n this.login = child\n const onData = (chunk: Buffer): void => {\n const text = stripAnsi(chunk.toString())\n this.log = `${this.log}${text}`\n const match = OAUTH_PATTERN.exec(text)\n if (match && this.authorizeUrl === '') {\n // wrangler login 自己会拉起浏览器;这里只把链接暴露给界面,\n // 万一浏览器没弹出来,用户可以复制这条链接手动打开。\n this.authorizeUrl = match[0]\n }\n }\n child.stdout.on('data', onData)\n child.stderr.on('data', onData)\n child.on('exit', (code) => {\n this.login = null\n this.checkedAt = 0\n if (code !== 0) this.error = 'Cloudflare 授权没有完成'\n })\n } catch (error) {\n this.error = error instanceof Error ? error.message : String(error)\n }\n return this.getStatus()\n }\n\n /** 等待浏览器回调时用户点了取消:直接杀掉 wrangler 进程。 */\n cancelLogin(): CloudflareStatus {\n const child = this.login\n this.login = null\n this.authorizeUrl = ''\n this.checkedAt = 0\n try {\n child?.kill()\n } catch {\n // 忽略\n }\n return this.getStatus()\n }\n\n startDeploy(): CloudflareStatus {\n if (this.deploy) return this.getStatus()\n this.error = ''\n const cli = wranglerCommand()\n this.log = `wrangler: ${cli.label}\\n`\n try {\n // 显式指定 config:当前目录往上可能有别人的 wrangler.jsonc,会被误用\n const child = spawn(\n cli.command,\n [...cli.baseArgs, 'deploy', '--config', join(packageRoot(), 'wrangler.toml')],\n { cwd: packageRoot(), env: cli.env ? { ...process.env, ...cli.env } : process.env },\n )\n this.deploy = child\n const onData = (chunk: Buffer): void => {\n const text = stripAnsi(chunk.toString())\n this.log = `${this.log}${text}`\n const match = WORKER_URL_PATTERN.exec(text)\n if (match) {\n this.stored = { ...this.stored, workerUrl: match[0].replace(/\\/$/, '') }\n writeStored(this.stored)\n }\n }\n child.stdout.on('data', onData)\n child.stderr.on('data', onData)\n child.on('exit', (code) => {\n this.deploy = null\n if (code !== 0 && this.stored.workerUrl === '') this.error = '部署失败,看看 wrangler 输出的日志'\n })\n } catch (error) {\n this.error = error instanceof Error ? error.message : String(error)\n }\n return this.getStatus()\n }\n\n logout(): CloudflareStatus {\n try {\n const cli = wranglerCommand()\n spawnSync(cli.command, [...cli.baseArgs, 'logout'], {\n encoding: 'utf8',\n timeout: 10_000,\n env: cli.env ? { ...process.env, ...cli.env } : process.env,\n })\n } catch {\n // 忽略\n }\n this.authorized = false\n this.checkedAt = 0\n this.stored = { ...this.stored, account: '' }\n writeStored(this.stored)\n return this.getStatus()\n }\n\n dispose(): void {\n this.login?.kill()\n this.deploy?.kill()\n this.login = null\n this.deploy = null\n }\n}\n\n","import { request as httpRequest, createServer, type IncomingMessage, type ServerResponse } from 'node:http'\nimport net from 'node:net'\n\n/**\n * DSH 的 Web 服务只监听 127.0.0.1,同 Wi-Fi 的朋友连不上。\n * 这里起一个只转发 `/holdem/*`(HTTP + WS)的小代理,绑到 0.0.0.0,\n * 用它的端口生成「局域网邀请链接」。\n */\nexport interface LanProxy {\n port: number\n dispose(): void\n}\n\nexport interface LanProxyOptions {\n targetPort: number\n prefix: string\n /** 期望端口;被占用就交给系统分配。 */\n port?: number\n log?: (message: string) => void\n}\n\nexport function startLanProxy(options: LanProxyOptions): Promise<LanProxy | null> {\n const prefix = options.prefix\n const target = { host: '127.0.0.1', port: options.targetPort }\n const log = options.log\n\n const forward = (req: IncomingMessage, res: ServerResponse): void => {\n const upstream = httpRequest(\n {\n host: target.host,\n port: target.port,\n method: req.method,\n path: req.url,\n headers: req.headers,\n },\n (up) => {\n res.writeHead(up.statusCode ?? 502, up.headers)\n up.pipe(res)\n },\n )\n upstream.on('error', () => {\n if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' })\n res.end('upstream unavailable')\n })\n req.pipe(upstream)\n }\n\n const server = createServer((req, res) => {\n const path = (req.url ?? '/').split('?')[0] ?? '/'\n if (path !== prefix && !path.startsWith(`${prefix}/`)) {\n res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })\n res.end('not found')\n return\n }\n forward(req, res)\n })\n\n // WebSocket:把 upgrade 也原样转到本地端口\n server.on('upgrade', (req, socket, head) => {\n const path = (req.url ?? '/').split('?')[0] ?? '/'\n if (path !== prefix && !path.startsWith(`${prefix}/`)) {\n socket.destroy()\n return\n }\n const upstream = net.connect(target.port, target.host, () => {\n const lines = [`GET ${req.url} HTTP/1.1`]\n const headers = { ...req.headers }\n for (const [key, value] of Object.entries(headers)) {\n if (Array.isArray(value)) for (const item of value) lines.push(`${key}: ${item}`)\n else if (value !== undefined) lines.push(`${key}: ${value}`)\n }\n upstream.write(`${lines.join('\\r\\n')}\\r\\n\\r\\n`)\n if (head && head.length > 0) upstream.write(head)\n upstream.pipe(socket)\n socket.pipe(upstream)\n })\n upstream.on('error', () => socket.destroy())\n socket.on('error', () => upstream.destroy())\n })\n\n return new Promise((resolve) => {\n const fallback = (): void => {\n // 指定端口被占用时让系统挑一个\n if (options.port !== undefined) {\n const retry = createRetry()\n retry()\n return\n }\n resolve(null)\n }\n const createRetry = () => () => {\n server.removeAllListeners('error')\n server.once('error', () => resolve(null))\n server.listen(0, '0.0.0.0', () => {\n const address = server.address()\n const port = typeof address === 'object' && address ? address.port : 0\n if (port <= 0) {\n resolve(null)\n return\n }\n log?.(`局域网分享已开启:http://0.0.0.0:${port}${prefix}`)\n resolve({ port, dispose: () => server.close() })\n })\n }\n server.once('error', fallback)\n server.listen(options.port ?? 0, '0.0.0.0', () => {\n const address = server.address()\n const port = typeof address === 'object' && address ? address.port : 0\n if (port <= 0) {\n resolve(null)\n return\n }\n log?.(`局域网分享已开启:端口 ${port}${prefix}`)\n resolve({ port, dispose: () => server.close() })\n })\n })\n}\n","import { networkInterfaces } from 'node:os'\n\n/** 找本机一个可用的局域网 IPv4(同 Wi-Fi 的朋友能直接访问)。 */\nexport function firstLanIPv4(): string {\n const interfaces = networkInterfaces()\n const candidates: string[] = []\n for (const entries of Object.values(interfaces)) {\n for (const entry of entries ?? []) {\n if (entry.family !== 'IPv4' || entry.internal) continue\n candidates.push(entry.address)\n }\n }\n // 常见家用/办公网段优先\n const preferred = candidates.find((ip) => ip.startsWith('192.168.'))\n ?? candidates.find((ip) => ip.startsWith('10.'))\n ?? candidates.find((ip) => /^172\\.(1[6-9]|2\\d|3[01])\\./.test(ip))\n ?? candidates[0]\n return preferred ?? ''\n}\n","import type { HandResultView, SessionSummary } from '../types.ts'\n\nexport interface HandRecord extends HandResultView {\n roomCode: string\n title: string\n recordedAt: string\n}\n\nexport interface HoldemStore {\n appendHand(record: HandRecord): void\n appendSession(summary: SessionSummary): void\n}\n\n/** 无持久化时的空实现(也用于 Cloudflare Worker,那边没有文件系统)。 */\nexport const noopStore: HoldemStore = {\n appendHand() {},\n appendSession() {},\n}\n","import { appendFile, mkdir, writeFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport { noopStore, type HandRecord, type HoldemStore } from './noop.ts'\n\nexport { noopStore }\nexport type { HandRecord, HoldemStore }\n\nexport function storeRoot(): string {\n const home = process.env.DSH_HOME ?? join(homedir(), '.dsh')\n return join(home, 'holdem')\n}\n\n/** 只追加的 JSONL 审计日志;任何失败都不影响牌局。 */\nexport function createFileStore(root = storeRoot()): HoldemStore {\n let ready: Promise<void> | null = null\n const ensure = (): Promise<void> => {\n ready ??= mkdir(root, { recursive: true }).then(() => undefined)\n return ready\n }\n const append = (file: string, line: string): void => {\n void ensure()\n .then(() => appendFile(join(root, file), `${line}\\n`, 'utf8'))\n .catch(() => undefined)\n }\n return {\n appendHand(record) {\n append('hands.jsonl', JSON.stringify(record))\n },\n appendSession(summary) {\n append('sessions.jsonl', JSON.stringify(summary))\n void ensure()\n .then(() => writeFile(join(root, 'last-session.json'), JSON.stringify(summary, null, 2), 'utf8'))\n .catch(() => undefined)\n },\n }\n}\n","/** 纯 JS 的 SHA-256:Node 和 Cloudflare Workers 都能同步调用。 */\nconst K = new Uint32Array([\n 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,\n 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,\n 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,\n 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,\n 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,\n 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,\n 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,\n 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,\n])\n\nfunction rotr(value: number, bits: number): number {\n return (value >>> bits) | (value << (32 - bits))\n}\n\nexport function sha256Hex(text: string): string {\n const bytes = new TextEncoder().encode(text)\n const bitLength = bytes.length * 8\n const withPadding = new Uint8Array((((bytes.length + 9) >> 6) + 1) << 6)\n withPadding.set(bytes)\n withPadding[bytes.length] = 0x80\n const view = new DataView(withPadding.buffer)\n view.setUint32(withPadding.length - 4, bitLength >>> 0, false)\n view.setUint32(withPadding.length - 8, Math.floor(bitLength / 0x100000000), false)\n\n const hash = new Uint32Array([\n 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,\n ])\n const w = new Uint32Array(64)\n for (let offset = 0; offset < withPadding.length; offset += 64) {\n for (let index = 0; index < 16; index += 1) w[index] = view.getUint32(offset + index * 4, false)\n for (let index = 16; index < 64; index += 1) {\n const s0 = rotr(w[index - 15]!, 7) ^ rotr(w[index - 15]!, 18) ^ (w[index - 15]! >>> 3)\n const s1 = rotr(w[index - 2]!, 17) ^ rotr(w[index - 2]!, 19) ^ (w[index - 2]! >>> 10)\n w[index] = (w[index - 16]! + s0 + w[index - 7]! + s1) >>> 0\n }\n let [a, b, c, d, e, f, g, h] = hash\n for (let index = 0; index < 64; index += 1) {\n const s1 = rotr(e!, 6) ^ rotr(e!, 11) ^ rotr(e!, 25)\n const ch = (e! & f!) ^ (~e! & g!)\n const temp1 = (h! + s1 + ch + K[index]! + w[index]!) >>> 0\n const s0 = rotr(a!, 2) ^ rotr(a!, 13) ^ rotr(a!, 22)\n const maj = (a! & b!) ^ (a! & c!) ^ (b! & c!)\n const temp2 = (s0 + maj) >>> 0\n h = g\n g = f\n f = e\n e = (d! + temp1) >>> 0\n d = c\n c = b\n b = a\n a = (temp1 + temp2) >>> 0\n }\n hash[0] = (hash[0]! + a!) >>> 0\n hash[1] = (hash[1]! + b!) >>> 0\n hash[2] = (hash[2]! + c!) >>> 0\n hash[3] = (hash[3]! + d!) >>> 0\n hash[4] = (hash[4]! + e!) >>> 0\n hash[5] = (hash[5]! + f!) >>> 0\n hash[6] = (hash[6]! + g!) >>> 0\n hash[7] = (hash[7]! + h!) >>> 0\n }\n return [...hash].map((word) => word.toString(16).padStart(8, '0')).join('')\n}\n\ninterface RandomSource {\n getRandomValues<T extends ArrayBufferView>(array: T): T\n}\n\nfunction webcrypto(): RandomSource {\n return (globalThis as unknown as { crypto: RandomSource }).crypto\n}\n\nfunction randomBytes(length: number): Uint8Array {\n const bytes = new Uint8Array(length)\n webcrypto().getRandomValues(bytes)\n return bytes\n}\n\nfunction toBase64Url(bytes: Uint8Array): string {\n let binary = ''\n for (const byte of bytes) binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\n/** [0, maxExclusive) 的无偏随机整数(拒绝采样,Node 与 Workers 通用)。 */\nexport function randomBelow(maxExclusive: number): number {\n if (maxExclusive <= 0) throw new Error('maxExclusive must be positive')\n const limit = Math.floor(0x100000000 / maxExclusive) * maxExclusive\n const buffer = new Uint32Array(1)\n for (;;) {\n webcrypto().getRandomValues(buffer)\n const value = buffer[0]!\n if (value < limit) return value % maxExclusive\n }\n}\n\nexport function newId(prefix: string): string {\n return `${prefix}_${toBase64Url(randomBytes(9))}`\n}\n\n/** 6 位房号,CSPRNG 且无取模偏置。 */\nexport function newRoomCode(): string {\n return String(randomBelow(1_000_000)).padStart(6, '0')\n}\n\nexport function newToken(): string {\n return toBase64Url(randomBytes(24))\n}\n\nexport function newSecret(): string {\n return toBase64Url(randomBytes(16))\n}\n\nexport function hashInvite(secret: string, kind: 'sit' | 'watch', token: string): string {\n return sha256Hex(`${secret}|${kind}|${token}`)\n}\n\nexport function hashEquals(expectedHex: string, actualHex: string): boolean {\n if (expectedHex.length !== actualHex.length) return false\n let diff = 0\n for (let index = 0; index < expectedHex.length; index += 1) {\n diff |= expectedHex.charCodeAt(index) ^ actualHex.charCodeAt(index)\n }\n return diff === 0\n}\n\nexport function sanitizeDisplayName(value: unknown): string {\n const text = String(value ?? '').replace(/[\\u0000-\\u001f\\u007f]/g, '').trim()\n return text.slice(0, 16) || '玩家'\n}\n\nexport function sanitizeChat(value: unknown): string | null {\n const text = String(value ?? '')\n .replace(/[\\u0000-\\u001f\\u007f]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n if (text.length === 0) return null\n return text.slice(0, 80)\n}\n\nexport function sanitizeTitle(value: unknown): string {\n const text = String(value ?? '').replace(/[\\u0000-\\u001f\\u007f]/g, '').trim()\n return text.slice(0, 24) || '好友牌桌'\n}\n","import type { Card } from '../types.ts'\n\nexport const RANK_CHARS = ['2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A'] as const\nexport const SUIT_CHARS = ['s', 'h', 'd', 'c'] as const\nexport type Suit = (typeof SUIT_CHARS)[number]\n\nexport const SUIT_SYMBOL: Record<Suit, string> = { s: '♠', h: '♥', d: '♦', c: '♣' }\nexport const SUIT_NAME_ZH: Record<Suit, string> = { s: '黑桃', h: '红心', d: '方块', c: '梅花' }\nexport const SUIT_IS_RED: Record<Suit, boolean> = { s: false, h: true, d: true, c: false }\n\nconst CARD_RE = /^[2-9TJQKA][shdc]$/\n\nexport function isCard(value: unknown): value is Card {\n return typeof value === 'string' && CARD_RE.test(value)\n}\n\nexport function asCard(value: string): Card {\n if (!isCard(value)) throw new Error(`invalid card: ${value}`)\n return value\n}\n\nexport function rankOf(card: Card): number {\n const index = RANK_CHARS.indexOf(card[0] as (typeof RANK_CHARS)[number])\n return index + 2\n}\n\nexport function suitOf(card: Card): Suit {\n return card[1] as Suit\n}\n\nexport function makeCard(rank: number, suit: Suit): Card {\n const char = RANK_CHARS[rank - 2]\n if (!char) throw new Error(`invalid rank: ${rank}`)\n return `${char}${suit}` as Card\n}\n\nexport function cardLabel(card: Card): string {\n return `${card[0]}${SUIT_SYMBOL[suitOf(card)]}`\n}\n\nexport function sameCard(a: Card, b: Card): boolean {\n return a === b\n}\n\n/** 稳定的 0..51 牌序号,用于排序与去重断言。 */\nexport function cardIndex(card: Card): number {\n const rank = rankOf(card) - 2\n const suit = SUIT_CHARS.indexOf(suitOf(card))\n return rank * 4 + suit\n}\n\nexport function sortCards(cards: readonly Card[]): Card[] {\n return [...cards].sort((a, b) => cardIndex(a) - cardIndex(b))\n}\n","import { randomBelow } from '../room/ids.ts'\nimport type { Card } from '../types.ts'\nimport { RANK_CHARS, SUIT_CHARS, makeCard } from './cards.ts'\n\nexport type RandomBelow = (maxExclusive: number) => number\n\n/** 密码学无偏随机:拒绝采样,避免取模偏置。 */\nexport const cryptoBelow: RandomBelow = (maxExclusive) => randomBelow(maxExclusive)\n\nexport function makeDeck(): Card[] {\n const deck: Card[] = []\n for (let rank = 2; rank <= 14; rank += 1) {\n for (const suit of SUIT_CHARS) deck.push(makeCard(rank, suit))\n }\n return deck\n}\n\n/** Fisher–Yates;RNG 可注入以便测试复现。 */\nexport function shuffle<T>(items: readonly T[], random: RandomBelow = cryptoBelow): T[] {\n const out = [...items]\n for (let i = out.length - 1; i > 0; i -= 1) {\n const j = random(i + 1)\n const tmp = out[i]!\n out[i] = out[j]!\n out[j] = tmp\n }\n return out\n}\n\nexport function shuffledDeck(random: RandomBelow = cryptoBelow): Card[] {\n return shuffle(makeDeck(), random)\n}\n\n/** mulberry32:仅测试使用,可复现。 */\nexport function seededRandom(seed: number): RandomBelow {\n let state = seed >>> 0\n return (maxExclusive: number) => {\n state = (state + 0x6d2b79f5) >>> 0\n let t = state\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n const value = ((t ^ (t >>> 14)) >>> 0) / 4294967296\n return Math.floor(value * maxExclusive)\n }\n}\n\nexport const RANKS_IN_DECK = RANK_CHARS.length * SUIT_CHARS.length\n","import type { LegalActions } from '../types.ts'\n\nexport interface BetPlayer {\n seat: number\n stack: number\n streetCommitted: number\n folded: boolean\n allIn: boolean\n /** 上一次行动时的 raiseEpoch;-1 表示本街还未行动。 */\n actedEpoch: number\n}\n\nexport interface BetRound {\n currentBet: number\n /** 最近一次「满额加注」的加注量,用于计算最小加注。 */\n lastFullRaise: number\n /** 每发生一次满额加注 +1;短筹码全下不递增。 */\n raiseEpoch: number\n bigBlind: number\n}\n\nexport type BetAction =\n | { type: 'fold' }\n | { type: 'check' }\n | { type: 'call' }\n | { type: 'raise'; to: number }\n | { type: 'allin' }\n\nexport class BettingError extends Error {\n readonly code: 'illegal' | 'not-your-turn'\n constructor(code: 'illegal' | 'not-your-turn', reason: string) {\n super(reason)\n this.code = code\n this.name = 'BettingError'\n }\n}\n\nexport function toCall(player: BetPlayer, round: BetRound): number {\n return Math.max(0, round.currentBet - player.streetCommitted)\n}\n\nexport function maxTo(player: BetPlayer): number {\n return player.streetCommitted + player.stack\n}\n\n/** 最小加注到的总额:无人下注时为 BB,否则为当前注 + 上次满额加注量。 */\nexport function minRaiseTo(round: BetRound): number {\n return round.currentBet === 0 ? round.bigBlind : round.currentBet + round.lastFullRaise\n}\n\nexport function isActing(player: BetPlayer): boolean {\n return !player.folded && !player.allIn\n}\n\nexport function legalFor(player: BetPlayer, round: BetRound): LegalActions {\n const empty: LegalActions = {\n canFold: false,\n canCheck: false,\n canCall: false,\n callAmount: 0,\n canRaise: false,\n minRaiseTo: 0,\n maxRaiseTo: 0,\n canAllIn: false,\n allInTo: 0,\n }\n if (!isActing(player)) return empty\n\n const owed = toCall(player, round)\n const ceiling = maxTo(player)\n const canRaise = player.actedEpoch !== round.raiseEpoch && ceiling > round.currentBet && player.stack > 0\n const minTo = canRaise ? Math.min(minRaiseTo(round), ceiling) : 0\n return {\n canFold: true,\n canCheck: owed === 0,\n canCall: owed > 0,\n callAmount: Math.min(owed, player.stack),\n canRaise,\n minRaiseTo: minTo,\n maxRaiseTo: canRaise ? ceiling : 0,\n canAllIn: player.stack > 0,\n allInTo: ceiling,\n }\n}\n\nexport interface AppliedAction {\n action: BetAction['type']\n paid: number\n fullRaise: boolean\n label: string\n}\n\n/** 就地更新 player 与 round;非法动作抛 BettingError。 */\nexport function applyAction(player: BetPlayer, round: BetRound, action: BetAction): AppliedAction {\n if (!isActing(player)) throw new BettingError('illegal', '该玩家已弃牌或已全下')\n\n if (action.type === 'fold') {\n player.folded = true\n player.actedEpoch = round.raiseEpoch\n return { action: 'fold', paid: 0, fullRaise: false, label: '弃牌' }\n }\n\n if (action.type === 'check') {\n if (toCall(player, round) > 0) throw new BettingError('illegal', '有人下注时不能过牌')\n player.actedEpoch = round.raiseEpoch\n return { action: 'check', paid: 0, fullRaise: false, label: '过牌' }\n }\n\n if (action.type === 'call') {\n const owed = toCall(player, round)\n if (owed === 0) {\n player.actedEpoch = round.raiseEpoch\n return { action: 'check', paid: 0, fullRaise: false, label: '过牌' }\n }\n const paid = commit(player, Math.min(owed, player.stack))\n player.actedEpoch = round.raiseEpoch\n const short = paid < owed\n return {\n action: 'call',\n paid,\n fullRaise: false,\n label: short ? `全下跟注 ${paid}` : `跟注 ${paid}`,\n }\n }\n\n const target = action.type === 'allin' ? maxTo(player) : action.to\n const wasOpeningBet = round.currentBet === 0\n if (!Number.isInteger(target)) throw new BettingError('illegal', '加注额必须是整数')\n if (target <= round.currentBet) {\n if (action.type === 'allin') {\n const paid = commit(player, Math.min(toCall(player, round), player.stack))\n player.actedEpoch = round.raiseEpoch\n return { action: 'call', paid, fullRaise: false, label: `全下跟注 ${paid}` }\n }\n throw new BettingError('illegal', '加注额必须大于当前注')\n }\n const ceiling = maxTo(player)\n if (target > ceiling) throw new BettingError('illegal', '加注额超过自己的筹码')\n if (player.actedEpoch === round.raiseEpoch) {\n throw new BettingError('illegal', '本轮已行动,短筹码全下后不能再加注')\n }\n const paid = commit(player, target - player.streetCommitted)\n const raiseBy = target - round.currentBet\n const allIn = player.stack === 0\n const fullRaise = raiseBy >= round.lastFullRaise\n if (!fullRaise && !allIn) {\n throw new BettingError('illegal', `最小加注到 ${minRaiseTo(round)}`)\n }\n if (fullRaise) {\n round.lastFullRaise = raiseBy\n round.raiseEpoch += 1\n }\n round.currentBet = Math.max(round.currentBet, target)\n player.actedEpoch = round.raiseEpoch\n return {\n action: allIn ? 'allin' : 'raise',\n paid,\n fullRaise,\n label: allIn ? `全下 ${target}` : wasOpeningBet ? `下注 ${target}` : `加注到 ${target}`,\n }\n}\n\nfunction commit(player: BetPlayer, amount: number): number {\n const paid = Math.max(0, Math.min(amount, player.stack))\n player.stack -= paid\n player.streetCommitted += paid\n if (player.stack === 0) player.allIn = true\n return paid\n}\n\nexport function roundComplete(\n players: readonly BetPlayer[],\n round: BetRound,\n): boolean {\n const live = players.filter((p) => !p.folded)\n if (live.length <= 1) return true\n return live.every(\n (p) => p.allIn || (p.actedEpoch === round.raiseEpoch && p.streetCommitted === round.currentBet),\n )\n}\n\nexport function canActCount(players: readonly BetPlayer[]): number {\n return players.filter(isActing).length\n}\n","export interface PotPlayer {\n seat: number\n /** 本手累计投入(含本街)。 */\n committed: number\n folded: boolean\n}\n\nexport interface Pot {\n amount: number\n eligible: number[]\n}\n\nexport interface PotBuild {\n pots: Pot[]\n /** 未被跟注、应原路退回的筹码。 */\n returned: { seat: number; amount: number }[]\n}\n\n/**\n * 按投入额分层构造主池与边池。\n * 弃牌者的投入留在池里,但没有资格;只有一名玩家投入的最高层视为未被跟注,退回。\n */\nexport function buildPots(players: readonly PotPlayer[]): PotBuild {\n const contributors = players.filter((p) => p.committed > 0)\n if (contributors.length === 0) return { pots: [], returned: [] }\n\n const levels = [...new Set(contributors.map((p) => p.committed))].sort((a, b) => a - b)\n const pots: Pot[] = []\n let previous = 0\n for (const level of levels) {\n const inLevel = contributors.filter((p) => p.committed >= level)\n const amount = inLevel.reduce((sum, p) => sum + (Math.min(p.committed, level) - previous), 0)\n if (amount > 0) {\n pots.push({ amount, eligible: inLevel.filter((p) => !p.folded).map((p) => p.seat) })\n }\n previous = level\n }\n\n const returned: { seat: number; amount: number }[] = []\n const top = pots[pots.length - 1]\n if (top && top.eligible.length === 1) {\n const level = levels[levels.length - 1]!\n const contributorsAtTop = contributors.filter((p) => p.committed >= level)\n if (contributorsAtTop.length === 1) {\n // 整层只由一名玩家投入:这就是未被跟注的部分。\n const seat = contributorsAtTop[0]!.seat\n returned.push({ seat, amount: top.amount })\n pots.pop()\n }\n }\n return { pots, returned }\n}\n\nexport interface PotAward {\n amount: number\n eligible: number[]\n winners: number[]\n}\n\nexport interface Distribution {\n awards: Map<number, number>\n pots: PotAward[]\n}\n\n/**\n * 逐池分配。`scores` 为 seat → 牌力分;奇数筹码按「按钮左手第一个赢家」起逐个 +1。\n */\nexport function distributePots(\n pots: readonly Pot[],\n scores: ReadonlyMap<number, number>,\n button: number,\n seatCount = 6,\n): Distribution {\n const awards = new Map<number, number>()\n const results: PotAward[] = []\n for (const pot of pots) {\n const eligible = pot.eligible.filter((seat) => scores.has(seat))\n if (eligible.length === 0) {\n results.push({ amount: pot.amount, eligible: pot.eligible, winners: [] })\n continue\n }\n let best = -Infinity\n for (const seat of eligible) best = Math.max(best, scores.get(seat) ?? -1)\n const winners = eligible.filter((seat) => (scores.get(seat) ?? -1) === best)\n const share = Math.floor(pot.amount / winners.length)\n let remainder = pot.amount - share * winners.length\n const ordered = [...winners].sort(\n (a, b) => clockwiseDistance(button, a, seatCount) - clockwiseDistance(button, b, seatCount),\n )\n for (const seat of winners) addAward(awards, seat, share)\n for (const seat of ordered) {\n if (remainder <= 0) break\n addAward(awards, seat, 1)\n remainder -= 1\n }\n results.push({ amount: pot.amount, eligible, winners })\n }\n return { awards, pots: results }\n}\n\nfunction addAward(awards: Map<number, number>, seat: number, amount: number): void {\n awards.set(seat, (awards.get(seat) ?? 0) + amount)\n}\n\n/** 从 button 顺时针数到 seat 的步数(1 表示紧邻左手)。 */\nexport function clockwiseDistance(button: number, seat: number, seatCount = 6): number {\n return (seat - button - 1 + seatCount * 2) % seatCount\n}\n\nexport function potTotal(pots: readonly Pot[]): number {\n return pots.reduce((sum, pot) => sum + pot.amount, 0)\n}\n","import type { Card } from '../types.ts'\nimport { RANK_CHARS, rankOf, suitOf } from './cards.ts'\n\nexport const CAT_HIGH_CARD = 0\nexport const CAT_PAIR = 1\nexport const CAT_TWO_PAIR = 2\nexport const CAT_TRIPS = 3\nexport const CAT_STRAIGHT = 4\nexport const CAT_FLUSH = 5\nexport const CAT_FULL_HOUSE = 6\nexport const CAT_QUADS = 7\nexport const CAT_STRAIGHT_FLUSH = 8\n\nexport const CATEGORY_NAMES = [\n '高牌',\n '一对',\n '两对',\n '三条',\n '顺子',\n '同花',\n '葫芦',\n '四条',\n '同花顺',\n] as const\n\nexport interface HandValue {\n /** 0..8;-1 表示不足 5 张。 */\n cat: number\n kickers: number[]\n score: number\n name: string\n}\n\nexport function rankText(rank: number): string {\n return RANK_CHARS[rank - 2] ?? '?'\n}\n\nfunction makeValue(cat: number, kickers: number[]): HandValue {\n const padded = [...kickers]\n while (padded.length < 5) padded.push(0)\n let score = cat\n for (let i = 0; i < 5; i += 1) score = score * 16 + (padded[i] ?? 0)\n return { cat, kickers, score, name: handName(cat, kickers[0] ?? 0) }\n}\n\nexport function handName(cat: number, high: number): string {\n if (cat === CAT_STRAIGHT_FLUSH && high === 14) return '皇家同花顺'\n return CATEGORY_NAMES[cat] ?? '未知'\n}\n\n/** 一手 5 张牌的牌力。 */\nexport function evaluate5(cards: readonly Card[]): HandValue {\n if (cards.length !== 5) throw new Error(`evaluate5 expects 5 cards, got ${cards.length}`)\n const ranks = cards.map(rankOf).sort((a, b) => b - a)\n const suits = cards.map(suitOf)\n const flush = suits.every((suit) => suit === suits[0])\n\n const counts = new Map<number, number>()\n for (const rank of ranks) counts.set(rank, (counts.get(rank) ?? 0) + 1)\n\n const groups = [...counts.entries()].sort((a, b) => (b[1] - a[1]) || (b[0] - a[0]))\n const unique = [...counts.keys()].sort((a, b) => b - a)\n const straightHigh = straightHighOf(unique)\n\n if (straightHigh > 0 && flush) return makeValue(CAT_STRAIGHT_FLUSH, [straightHigh])\n if ((groups[0]?.[1] ?? 0) === 4) {\n const quad = groups[0]![0]\n const kicker = groups[1]![0]\n return makeValue(CAT_QUADS, [quad, kicker])\n }\n if ((groups[0]?.[1] ?? 0) === 3 && (groups[1]?.[1] ?? 0) >= 2) {\n return makeValue(CAT_FULL_HOUSE, [groups[0]![0], groups[1]![0]])\n }\n if (flush) return makeValue(CAT_FLUSH, ranks)\n if (straightHigh > 0) return makeValue(CAT_STRAIGHT, [straightHigh])\n if ((groups[0]?.[1] ?? 0) === 3) {\n const rest = unique.filter((rank) => rank !== groups[0]![0]).slice(0, 2)\n return makeValue(CAT_TRIPS, [groups[0]![0], ...rest])\n }\n if ((groups[0]?.[1] ?? 0) === 2 && (groups[1]?.[1] ?? 0) === 2) {\n const highPair = Math.max(groups[0]![0], groups[1]![0])\n const lowPair = Math.min(groups[0]![0], groups[1]![0])\n const kicker = groups[2]![0]\n return makeValue(CAT_TWO_PAIR, [highPair, lowPair, kicker])\n }\n if ((groups[0]?.[1] ?? 0) === 2) {\n const rest = unique.filter((rank) => rank !== groups[0]![0]).slice(0, 3)\n return makeValue(CAT_PAIR, [groups[0]![0], ...rest])\n }\n return makeValue(CAT_HIGH_CARD, ranks)\n}\n\n/** 5..7 张里最好的 5 张;不足 5 张返回无效值。 */\nexport function evaluateBest(cards: readonly Card[]): HandValue {\n if (cards.length < 5) return { cat: -1, kickers: [], score: -1, name: '未成牌' }\n if (cards.length === 5) return evaluate5(cards)\n let best: HandValue | null = null\n const n = cards.length\n for (let a = 0; a < n - 4; a += 1) {\n for (let b = a + 1; b < n - 3; b += 1) {\n for (let c = b + 1; c < n - 2; c += 1) {\n for (let d = c + 1; d < n - 1; d += 1) {\n for (let e = d + 1; e < n; e += 1) {\n const value = evaluate5([cards[a]!, cards[b]!, cards[c]!, cards[d]!, cards[e]!])\n if (!best || value.score > best.score) best = value\n }\n }\n }\n }\n }\n return best ?? { cat: -1, kickers: [], score: -1, name: '未成牌' }\n}\n\nexport function compareHandValues(a: HandValue, b: HandValue): number {\n if (a.score !== b.score) return a.score - b.score\n if (a.cat !== b.cat) return a.cat - b.cat\n for (let i = 0; i < Math.max(a.kickers.length, b.kickers.length); i += 1) {\n const diff = (a.kickers[i] ?? 0) - (b.kickers[i] ?? 0)\n if (diff !== 0) return diff\n }\n return 0\n}\n\n/** 返回顺子的最大牌点;A2345 记为 5;不成顺返回 0。 */\nfunction straightHighOf(descUnique: number[]): number {\n if (descUnique.length < 5) return 0\n const isWheel =\n descUnique.includes(14) &&\n descUnique.includes(5) &&\n descUnique.includes(4) &&\n descUnique.includes(3) &&\n descUnique.includes(2)\n if (isWheel) return 5\n for (let i = 0; i + 4 < descUnique.length; i += 1) {\n const high = descUnique[i]!\n const low = descUnique[i + 4]!\n if (high - low === 4) return high\n }\n return 0\n}\n\n/** 牌型名 + 关键牌点,如「一对 K」「同花 A」。 */\nexport function describeHand(value: HandValue): string {\n if (value.cat < 0) return '未成牌'\n if (value.cat === CAT_STRAIGHT_FLUSH && value.kickers[0] === 14) return '皇家同花顺'\n const high = value.kickers[0]\n if (high === undefined) return value.name\n return `${value.name} ${rankText(high)}`\n}\n","import type {\n Card,\n HandResultView,\n HandView,\n LegalActions,\n PlayerId,\n Seat,\n ShownHandView,\n Street,\n} from '../types.ts'\nimport { nowIso } from '../types.ts'\nimport { ALL_IN_RUNOUT_MS } from '../invariant.ts'\nimport { shuffledDeck, type RandomBelow } from './deck.ts'\nimport {\n applyAction,\n BettingError,\n legalFor,\n roundComplete,\n type BetAction,\n type BetPlayer,\n type BetRound,\n} from './betting.ts'\nimport { buildPots, distributePots, potTotal } from './pots.ts'\nimport { describeHand, evaluateBest, type HandValue } from './rank.ts'\n\nexport interface HandPlayer extends BetPlayer {\n playerId: PlayerId\n displayName: string\n buyIn: number\n /** 本手累计投入(跨街),边池按此分层。 */\n committed: number\n hole: Card[]\n lastAction: string | null\n}\n\nexport interface HandSeat {\n seat: Seat\n playerId: PlayerId\n displayName: string\n}\n\nexport interface CreateHandOptions {\n handNo: number\n /** 本手按钮位(由房间按上一手轮转后传入)。 */\n button: Seat\n seats: HandSeat[]\n sb: number\n bb: number\n buyIn: number\n turnTimeoutMs: number\n now: number\n random?: RandomBelow\n /** 测试用:直接给定牌堆(从末尾取牌)。 */\n deck?: Card[]\n}\n\nexport interface HandState {\n handNo: number\n button: Seat\n sbSeat: Seat\n bbSeat: Seat\n street: Street\n deck: Card[]\n board: Card[]\n players: HandPlayer[]\n round: BetRound\n toActSeat: Seat | null\n deadlineAt: number | null\n startedAt: number\n runoutDueAt: number | null\n completed: boolean\n result: HandResultView | null\n revealed: boolean\n turnTimeoutMs: number\n}\n\nexport class HandError extends Error {\n readonly code: 'illegal' | 'not-your-turn' | 'phase'\n constructor(code: 'illegal' | 'not-your-turn' | 'phase', reason: string) {\n super(reason)\n this.code = code\n this.name = 'HandError'\n }\n}\n\nconst STREET_ORDER: Street[] = ['preflop', 'flop', 'turn', 'river', 'showdown']\n\nexport function createHand(options: CreateHandOptions): HandState {\n const seats = [...options.seats].sort((a, b) => a.seat - b.seat)\n if (seats.length < 2) throw new HandError('phase', '至少需要两名玩家')\n if (!seats.some((entry) => entry.seat === options.button)) {\n throw new HandError('phase', '按钮位不在活跃座位上')\n }\n\n const players: HandPlayer[] = seats.map((entry) => ({\n seat: entry.seat,\n playerId: entry.playerId,\n displayName: entry.displayName,\n buyIn: options.buyIn,\n stack: options.buyIn,\n streetCommitted: 0,\n committed: 0,\n folded: false,\n allIn: false,\n actedEpoch: -1,\n hole: [],\n lastAction: null,\n }))\n\n const headsUp = players.length === 2\n const button = options.button\n const sbSeat = headsUp ? button : nextSeatIn(players, button)\n const bbSeat = nextSeatIn(players, sbSeat)\n\n const state: HandState = {\n handNo: options.handNo,\n button,\n sbSeat,\n bbSeat,\n street: 'preflop',\n deck: options.deck ? [...options.deck] : shuffledDeck(options.random),\n board: [],\n players,\n round: { currentBet: 0, lastFullRaise: options.bb, raiseEpoch: 0, bigBlind: options.bb },\n toActSeat: null,\n deadlineAt: null,\n startedAt: options.now,\n runoutDueAt: null,\n completed: false,\n result: null,\n revealed: false,\n turnTimeoutMs: options.turnTimeoutMs,\n }\n\n // 两轮发底牌:从 SB 起顺时针,每人一张,再来一圈。\n let cursor = sbSeat\n for (let pass = 0; pass < 2; pass += 1) {\n for (let i = 0; i < players.length; i += 1) {\n playerAt(state, cursor)?.hole.push(draw(state))\n cursor = nextSeatIn(players, cursor)\n }\n }\n\n postBlind(state, sbSeat, options.sb)\n postBlind(state, bbSeat, options.bb)\n state.round.currentBet = Math.max(\n playerAt(state, sbSeat)?.streetCommitted ?? 0,\n playerAt(state, bbSeat)?.streetCommitted ?? 0,\n )\n state.toActSeat = headsUp ? sbSeat : nextActingSeat(state, bbSeat)\n armDeadline(state, options.now)\n if (state.toActSeat === null) progress(state, options.now, null)\n return state\n}\n\nexport function playerAt(state: HandState, seat: Seat): HandPlayer | undefined {\n return state.players.find((player) => player.seat === seat)\n}\n\nexport function livePlayers(state: HandState): HandPlayer[] {\n return state.players.filter((player) => !player.folded)\n}\n\nexport function actingPlayers(state: HandState): HandPlayer[] {\n return state.players.filter((player) => !player.folded && !player.allIn)\n}\n\nexport function legalForSeat(state: HandState, seat: Seat): LegalActions | null {\n if (state.completed || state.runoutDueAt !== null) return null\n if (state.toActSeat !== seat) return null\n const player = playerAt(state, seat)\n if (!player) return null\n return legalFor(player, state.round)\n}\n\nexport interface ActOutcome {\n seat: Seat\n label: string\n streetChanged: boolean\n completed: boolean\n}\n\nexport function act(state: HandState, seat: Seat, action: BetAction, now: number): ActOutcome {\n if (state.completed) throw new HandError('phase', '本手已结束')\n if (state.runoutDueAt !== null) throw new HandError('phase', '全下跑马中,等待发完公共牌')\n if (state.toActSeat !== seat) throw new HandError('not-your-turn', '还没轮到你行动')\n const player = playerAt(state, seat)\n if (!player) throw new HandError('illegal', '座位不存在')\n\n const before = state.street\n let applied\n try {\n applied = applyAction(player, state.round, action)\n } catch (error) {\n if (error instanceof BettingError) throw new HandError(error.code, error.message)\n throw error\n }\n player.committed += applied.paid\n player.lastAction = applied.label\n state.deadlineAt = null\n state.toActSeat = null\n progress(state, now, seat)\n return {\n seat,\n label: applied.label,\n streetChanged: state.street !== before,\n completed: state.completed,\n }\n}\n\n/** 超时自动行动:能过牌就过牌,否则弃牌。 */\nexport function forceTimeout(state: HandState, now: number): ActOutcome | null {\n if (state.completed || state.toActSeat === null) return null\n const seat = state.toActSeat\n const player = playerAt(state, seat)\n if (!player) return null\n const legal = legalFor(player, state.round)\n const action: BetAction = legal.canCheck ? { type: 'check' } : { type: 'fold' }\n return act(state, seat, action, now)\n}\n\n/** 玩家在非自己回合离开/断线:直接弃牌;只在自己回合或只剩一人时推进。 */\nexport function foldSeat(state: HandState, seat: Seat, now: number): boolean {\n if (state.completed) return false\n const player = playerAt(state, seat)\n if (!player || player.folded) return false\n const wasToAct = state.toActSeat === seat\n player.folded = true\n player.actedEpoch = state.round.raiseEpoch\n player.lastAction = '弃牌'\n if (!wasToAct) {\n if (livePlayers(state).length <= 1) {\n state.toActSeat = null\n state.deadlineAt = null\n progress(state, now, seat)\n }\n return true\n }\n state.toActSeat = null\n state.deadlineAt = null\n if (state.runoutDueAt !== null) return true\n progress(state, now, seat)\n return true\n}\n\n/** 驱动全下跑马:到期逐街发牌,最后摊牌。返回状态是否变化。 */\nexport function tick(state: HandState, now: number): boolean {\n if (state.completed || state.runoutDueAt === null) return false\n if (now < state.runoutDueAt) return false\n if (state.street === 'river' || state.street === 'showdown') {\n showdown(state, now)\n return true\n }\n dealNextStreet(state)\n state.runoutDueAt = now + ALL_IN_RUNOUT_MS\n return true\n}\n\nexport function isRunout(state: HandState): boolean {\n return state.runoutDueAt !== null && !state.completed\n}\n\nfunction progress(state: HandState, now: number, fromSeat: Seat | null): void {\n if (livePlayers(state).length <= 1) {\n finishFoldWin(state, now)\n return\n }\n if (!roundComplete(state.players, state.round)) {\n const from = fromSeat ?? state.players.find((p) => !p.folded)?.seat ?? state.button\n state.toActSeat = nextActingSeat(state, from)\n if (state.toActSeat === null) {\n beginRunout(state, now)\n return\n }\n armDeadline(state, now)\n return\n }\n if (actingPlayers(state).length <= 1) {\n beginRunout(state, now)\n return\n }\n advanceStreet(state, now)\n}\n\nfunction beginRunout(state: HandState, now: number): void {\n state.toActSeat = null\n state.deadlineAt = null\n if (state.street === 'river' || state.street === 'showdown') {\n showdown(state, now)\n return\n }\n state.runoutDueAt = now + ALL_IN_RUNOUT_MS\n}\n\nfunction advanceStreet(state: HandState, now: number): void {\n if (state.street === 'river' || state.street === 'showdown') {\n showdown(state, now)\n return\n }\n dealNextStreet(state)\n state.runoutDueAt = null\n state.toActSeat = nextActingSeat(state, state.button)\n armDeadline(state, now)\n if (actingPlayers(state).length <= 1) beginRunout(state, now)\n}\n\nfunction dealNextStreet(state: HandState): void {\n const index = STREET_ORDER.indexOf(state.street)\n const next = STREET_ORDER[Math.min(index + 1, STREET_ORDER.length - 1)]!\n burn(state)\n if (next === 'flop') {\n for (let i = 0; i < 3; i += 1) state.board.push(draw(state))\n } else if (next === 'turn' || next === 'river') {\n state.board.push(draw(state))\n }\n state.street = next\n state.round = {\n currentBet: 0,\n lastFullRaise: state.round.bigBlind,\n raiseEpoch: state.round.raiseEpoch + 1,\n bigBlind: state.round.bigBlind,\n }\n for (const player of state.players) {\n player.streetCommitted = 0\n if (!player.folded && !player.allIn) player.actedEpoch = -1\n }\n}\n\nfunction armDeadline(state: HandState, now: number): void {\n state.deadlineAt = state.completed || state.toActSeat === null ? null : now + state.turnTimeoutMs\n}\n\nfunction showdown(state: HandState, _now: number): void {\n if (state.completed) return\n while (state.board.length < 5) {\n if (state.board.length === 0) {\n burn(state)\n for (let i = 0; i < 3; i += 1) state.board.push(draw(state))\n } else {\n burn(state)\n state.board.push(draw(state))\n }\n }\n state.street = 'showdown'\n state.revealed = true\n state.toActSeat = null\n state.deadlineAt = null\n state.runoutDueAt = null\n\n const live = livePlayers(state)\n const values = new Map<number, HandValue>()\n const shown: ShownHandView[] = []\n for (const player of live) {\n const value = evaluateBest([...player.hole, ...state.board])\n values.set(player.seat, value)\n shown.push({\n seat: player.seat,\n cards: [...player.hole],\n name: describeHand(value),\n score: value.score,\n })\n }\n\n const built = buildPots(state.players)\n const scoreMap = new Map<number, number>([...values].map(([seat, value]) => [seat, value.score]))\n const distribution = distributePots(built.pots, scoreMap, state.button)\n for (const entry of built.returned) addStack(state, entry.seat, entry.amount)\n for (const [seat, amount] of distribution.awards) addStack(state, seat, amount)\n\n const winners = [...new Set(distribution.pots.flatMap((pot) => pot.winners))]\n finishHand(state, {\n foldedWin: false,\n pots: built.pots,\n winners,\n shownHands: shown,\n })\n}\n\nfunction finishFoldWin(state: HandState, now: number): void {\n const winner = livePlayers(state)[0]\n if (!winner) return\n const built = buildPots(state.players)\n for (const entry of built.returned) addStack(state, entry.seat, entry.amount)\n const winnings = built.pots.reduce((sum, pot) => sum + pot.amount, 0)\n addStack(state, winner.seat, winnings)\n winner.lastAction = winner.lastAction ?? '获胜'\n state.street = 'showdown'\n state.revealed = true\n state.toActSeat = null\n state.deadlineAt = null\n state.runoutDueAt = null\n finishHand(state, {\n foldedWin: true,\n pots: built.pots,\n winners: [winner.seat],\n shownHands: [],\n })\n void now\n}\n\nfunction finishHand(\n state: HandState,\n payload: {\n foldedWin: boolean\n pots: { amount: number; eligible: Seat[] }[]\n winners: Seat[]\n shownHands: ShownHandView[]\n },\n): void {\n state.completed = true\n state.result = {\n handNo: state.handNo,\n board: [...state.board],\n pots: payload.pots,\n winners: payload.winners,\n shownHands: payload.shownHands,\n deltas: state.players.map((player) => ({\n seat: player.seat,\n playerId: player.playerId,\n displayName: player.displayName,\n net: player.stack - player.buyIn,\n })),\n foldedWin: payload.foldedWin,\n endedAt: nowIso(),\n }\n}\n\nexport function handView(state: HandState): HandView {\n const built = buildPots(state.players)\n const revealed = state.revealed && state.result !== null && !state.result.foldedWin\n return {\n handNo: state.handNo,\n street: state.street,\n board: [...state.board],\n button: state.button,\n sbSeat: state.sbSeat,\n bbSeat: state.bbSeat,\n toActSeat: state.toActSeat,\n currentBet: state.round.currentBet,\n minRaiseTo: state.round.currentBet === 0 ? state.round.bigBlind : state.round.currentBet + state.round.lastFullRaise,\n potTotal: potTotal(built.pots),\n pots: built.pots.map((pot) => ({ amount: pot.amount, eligible: [...pot.eligible] })),\n deadlineAt: state.deadlineAt === null ? null : new Date(state.deadlineAt).toISOString(),\n shownHands: revealed\n ? livePlayers(state).map((player) => {\n const value = evaluateBest([...player.hole, ...state.board])\n return {\n seat: player.seat,\n cards: [...player.hole],\n name: describeHand(value),\n score: value.score,\n }\n })\n : [],\n winners: state.result?.winners ?? [],\n }\n}\n\nfunction postBlind(state: HandState, seat: Seat, amount: number): void {\n const player = playerAt(state, seat)\n if (!player) return\n const paid = Math.min(amount, player.stack)\n player.stack -= paid\n player.streetCommitted += paid\n player.committed += paid\n if (player.stack === 0) player.allIn = true\n player.lastAction = amount === state.round.bigBlind ? `大盲 ${paid}` : `小盲 ${paid}`\n}\n\nfunction addStack(state: HandState, seat: Seat, amount: number): void {\n const player = playerAt(state, seat)\n if (player) player.stack += amount\n}\n\nfunction burn(state: HandState): void {\n draw(state)\n}\n\nfunction draw(state: HandState): Card {\n const card = state.deck.pop()\n if (!card) throw new HandError('phase', '牌堆已空')\n return card\n}\n\nfunction nextSeatIn(players: readonly { seat: Seat }[], from: Seat): Seat {\n const order = players.map((player) => player.seat).sort((a, b) => a - b)\n for (let step = 1; step <= 12; step += 1) {\n const candidate = ((from + step) % 12) as Seat\n if (order.includes(candidate)) return candidate\n }\n return order[0]!\n}\n\nfunction nextActingSeat(state: HandState, from: Seat): Seat | null {\n const seats = state.players\n .filter((player) => !player.folded && !player.allIn)\n .map((player) => player.seat)\n .sort((a, b) => a - b)\n if (seats.length === 0) return null\n for (let step = 1; step <= 12; step += 1) {\n const candidate = ((from + step) % 12) as Seat\n if (seats.includes(candidate)) return candidate\n }\n return seats[0]!\n}\n","import type { ResolvedConfig } from '../config.ts'\nimport type {\n ChatLine,\n ClientCommand,\n HandResultView,\n LeaderboardEntry,\n PlayerId,\n PlayerView,\n RoomId,\n RoomPhase,\n RoomPreview,\n RoomSettingsView,\n RoomView,\n SeatView,\n ServerEvent,\n SessionSummary,\n TableAction,\n} from '../types.ts'\nimport { CommandError, asPlayerId, asRoomId, asSessionId, nowIso } from '../types.ts'\nimport {\n MAX_CHAT_LINES,\n MAX_HISTORY_HANDS,\n MAX_SEATS,\n CHAT_COOLDOWN_MS,\n HAND_OVER_MIN_MS,\n PRESENCE_TTL_MS,\n TICK_MS,\n SESSION_TTL_MS,\n blindsFor,\n buyInFor,\n} from '../invariant.ts'\nimport { act, createHand, foldSeat, forceTimeout, handView, legalForSeat, tick as tickHand, type HandState } from '../engine/hand.ts'\nimport { HandError } from '../engine/hand.ts'\nimport type { HoldemStore } from '../persist/noop.ts'\nimport { noopStore } from '../persist/noop.ts'\nimport {\n hashEquals,\n hashInvite,\n newId,\n newRoomCode,\n newSecret,\n newToken,\n sanitizeChat,\n sanitizeDisplayName,\n sanitizeTitle,\n} from './ids.ts'\n\nexport interface Session {\n token: string\n playerId: PlayerId\n roomId: RoomId\n kind: 'seat' | 'watch' | 'host'\n createdAt: number\n expiresAt: number\n}\n\ninterface LivePlayer {\n playerId: PlayerId\n displayName: string\n seat: number | null\n ready: boolean\n connected: boolean\n /** 当前有一条打开的实时通道(HTTP 轮询不算)。 */\n socketOpen: boolean\n /** 最近一次被观察到「人在」的时间:实时通道或 HTTP 请求都算。 */\n lastSeenAt: number\n sittingOut: boolean\n autoFold: boolean\n timeoutStreak: number\n browserId: string\n joinedAt: number\n leaving: boolean\n}\n\ninterface Stats {\n displayName: string\n seat: number | null\n handsPlayed: number\n handsWon: number\n netChips: number\n biggestWin: number\n showdownsWon: number\n showdownsPlayed: number\n}\n\ninterface LiveRoom {\n roomId: RoomId\n roomCode: string\n title: string\n hostPlayerId: PlayerId\n phase: RoomPhase\n settings: RoomSettingsView\n players: Map<PlayerId, LivePlayer>\n stats: Map<PlayerId, Stats>\n hand: HandState | null\n handNo: number\n lastHand: HandResultView | null\n lastButton: number | null\n history: HandResultView[]\n chat: ChatLine[]\n seq: number\n createdAt: number\n inviteExpiresAt: number\n secret: string\n sitToken: string\n watchToken: string\n sitHash: string\n watchHash: string\n handOverAt: number | null\n summary: SessionSummary | null\n closedAt: number | null\n hostLastSeen: number\n lastChatAt: Map<PlayerId, number>\n browsers: Map<string, PlayerId>\n actionCache: Map<string, number>\n}\n\nexport interface RoomManagerOptions {\n config: ResolvedConfig\n origin: () => string\n now?: () => number\n store?: HoldemStore\n log?: (message: string) => void\n /** 局域网 IPv4(Node 下用 os.networkInterfaces,Worker 下为空)。 */\n lanIp?: () => string\n}\n\nexport interface CreateRoomInput {\n title?: string | undefined\n blindLevel?: string | undefined\n displayName?: string | undefined\n browserId?: string | undefined\n}\n\nexport interface PeekInput {\n roomCode: string\n invite?: string | undefined\n role?: 'sit' | 'watch'\n local: boolean\n browserId?: string | undefined\n /** 浏览器带来的所有座位 token(每个房间一个),按新到旧排列。 */\n cookies?: string[] | undefined\n}\n\nexport interface JoinInput extends PeekInput {\n displayName: string\n role: 'sit' | 'watch'\n}\n\nexport interface CreateRoomResult {\n roomId: RoomId\n roomCode: string\n hostPlayerId: PlayerId\n joinUrl: string\n wsTicket: string\n sessionToken: string\n view: PlayerView\n}\n\nexport interface JoinResult {\n playerId: PlayerId\n roomId: RoomId\n seat: number | null\n wsTicket: string\n sessionToken: string\n view: PlayerView\n}\n\ninterface Ticket {\n playerId: PlayerId\n roomId: RoomId\n expiresAt: number\n}\n\nexport class StaleCommandError extends CommandError {\n readonly view: PlayerView\n constructor(reason: string, view: PlayerView) {\n super('stale', reason)\n this.view = view\n }\n}\n\nconst CLOSED_RETENTION_MS = 10 * 60 * 1000\n\nexport class RoomManager {\n readonly counters = { roomsCreated: 0, roomsClosed: 0, handsPlayed: 0, wsConnected: 0, commands: 0 }\n\n private config: ResolvedConfig\n private readonly origin: () => string\n private readonly now: () => number\n private readonly store: HoldemStore\n\n private readonly rooms = new Map<string, LiveRoom>()\n private readonly roomsByCode = new Map<string, LiveRoom>()\n private readonly sessions = new Map<string, Session>()\n private readonly tickets = new Map<string, Ticket>()\n private readonly listeners = new Set<(playerId: PlayerId, event: ServerEvent) => void>()\n\n constructor(options: RoomManagerOptions) {\n this.config = options.config\n this.origin = options.origin\n this.now = options.now ?? (() => Date.now())\n this.store = options.store ?? noopStore\n this.lanIp = options.lanIp ?? (() => '')\n void options.log\n }\n\n getConfig(): ResolvedConfig {\n return this.config\n }\n\n /** 局域网小代理的端口(0 表示没起来)。 */\n private lanPort = 0\n private readonly lanIp: () => string\n\n setLanPort(port: number): void {\n this.lanPort = port > 0 ? port : 0\n }\n\n replaceConfig(config: ResolvedConfig): void {\n this.config = config\n }\n\n start(): () => void {\n const timer = setInterval(() => this.tick(), TICK_MS)\n // Node 下别让定时器拖住进程;Workers 里 setInterval 返回数字,没有 unref\n const handle = timer as unknown as { unref?: () => void }\n if (typeof handle === 'object' && typeof handle.unref === 'function') handle.unref()\n return () => clearInterval(timer)\n }\n\n async dispose(): Promise<void> {\n this.rooms.clear()\n this.roomsByCode.clear()\n this.sessions.clear()\n this.tickets.clear()\n this.listeners.clear()\n }\n\n onEvent(listener: (playerId: PlayerId, event: ServerEvent) => void): () => void {\n this.listeners.add(listener)\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n // ---------------------------------------------------------------- 房间/会话\n\n createRoom(input: CreateRoomInput): CreateRoomResult {\n const now = this.now()\n const openRooms = [...this.rooms.values()].filter((room) => room.phase !== 'closed').length\n if (openRooms >= this.config.maxRooms) throw new CommandError('room-full', '房间数量已达上限')\n\n const blinds = blindsFor(input.blindLevel ?? this.config.blindLevel)\n const code = this.uniqueCode()\n const secret = newSecret()\n const sitToken = newToken()\n const watchToken = newToken()\n const roomId = asRoomId(newId('room'))\n const room: LiveRoom = {\n roomId,\n roomCode: code,\n title: sanitizeTitle(input.title),\n hostPlayerId: asPlayerId(''),\n phase: 'lobby',\n settings: {\n blindLevel: blinds.id,\n sb: blinds.sb,\n bb: blinds.bb,\n buyIn: buyInFor(blinds.bb),\n turnTimeoutMs: this.config.turnTimeoutMs,\n autoAdvanceMs: this.config.autoAdvanceMs,\n allowSpectators: this.config.allowSpectators,\n maxSpectators: this.config.maxSpectators,\n chatEnabled: this.config.chatEnabled,\n },\n players: new Map(),\n stats: new Map(),\n hand: null,\n handNo: 0,\n lastHand: null,\n lastButton: null,\n history: [],\n chat: [],\n seq: 0,\n createdAt: now,\n inviteExpiresAt: now + 24 * 60 * 60 * 1000,\n secret,\n sitToken,\n watchToken,\n sitHash: hashInvite(secret, 'sit', sitToken),\n watchHash: hashInvite(secret, 'watch', watchToken),\n handOverAt: null,\n summary: null,\n closedAt: null,\n hostLastSeen: now,\n lastChatAt: new Map(),\n browsers: new Map(),\n actionCache: new Map(),\n }\n\n const host = this.addPlayer(room, sanitizeDisplayName(input.displayName), 0, input.browserId ?? '')\n host.ready = true\n room.hostPlayerId = host.playerId\n this.rooms.set(roomId, room)\n this.roomsByCode.set(code, room)\n this.counters.roomsCreated += 1\n\n const session = this.newSession(room, host.playerId, 'host')\n const joinUrl = this.joinUrlFor(room, sitToken)\n return {\n roomId,\n roomCode: code,\n hostPlayerId: host.playerId,\n joinUrl,\n wsTicket: this.issueTicket(host.playerId, roomId),\n sessionToken: session.token,\n view: this.project(room, host.playerId),\n }\n }\n\n peek(input: PeekInput): RoomPreview {\n const room = this.roomByCode(input.roomCode)\n const inviteOk = this.checkInvite(room, input, input.role ?? 'sit', input.local)\n if (!inviteOk.allowed) throw new CommandError('expired', '邀请已失效,请向房主要一个新链接')\n\n const seated = [...room.players.values()].filter((player) => player.seat !== null)\n const seats = Array.from({ length: MAX_SEATS }, (_, seat) => {\n const occupant = seated.find((player) => player.seat === seat)\n return {\n seat,\n displayName: occupant?.displayName ?? null,\n ready: occupant?.ready ?? false,\n host: occupant?.playerId === room.hostPlayerId,\n connected: occupant?.connected ?? false,\n }\n })\n const existing = this.existingPlayer(room, input)\n return {\n roomCode: room.roomCode,\n title: room.title,\n hostDisplayName: room.players.get(room.hostPlayerId)?.displayName ?? '房主',\n phase: room.phase,\n settings: room.settings,\n seated: seated.length,\n spectators: [...room.players.values()].filter((player) => player.seat === null).length,\n seats,\n canSit: room.phase === 'lobby' && seated.length < MAX_SEATS,\n alreadyInRoom: existing !== null,\n inviteExpiresAt: new Date(room.inviteExpiresAt).toISOString(),\n }\n }\n\n join(input: JoinInput): JoinResult {\n const room = this.roomByCode(input.roomCode)\n const wanted = input.role === 'watch' ? 'watch' : 'sit'\n const invite = this.checkInvite(room, input, wanted, input.local)\n if (!invite.allowed) throw new CommandError('expired', '邀请已失效,请向房主要一个新链接')\n\n const cookieSession = this.sessionFromCookies(input.cookies, room.roomId)\n if (cookieSession) {\n const player = room.players.get(cookieSession.playerId)\n if (player && !player.leaving) {\n player.browserId = input.browserId ?? player.browserId\n this.touch(room, player)\n return {\n playerId: player.playerId,\n roomId: room.roomId,\n seat: player.seat,\n wsTicket: this.issueTicket(player.playerId, room.roomId),\n sessionToken: cookieSession.token,\n view: this.project(room, player.playerId),\n }\n }\n }\n\n const existing = this.existingPlayer(room, input)\n if (existing) throw new CommandError('already-in-room', '这个浏览器已经在这个房间里了')\n\n const seatedCount = [...room.players.values()].filter((player) => player.seat !== null).length\n const canSit = wanted === 'sit' && invite.sit && room.phase === 'lobby' && seatedCount < MAX_SEATS\n if (!canSit) {\n if (!room.settings.allowSpectators || room.settings.maxSpectators <= 0) {\n throw new CommandError('room-full', '房间已满且不允许观战')\n }\n const spectators = [...room.players.values()].filter((player) => player.seat === null)\n if (spectators.length >= room.settings.maxSpectators) throw new CommandError('room-full', '观战席已满')\n if (wanted === 'sit' && invite.sit === false) throw new CommandError('auth', '没有入座权限')\n }\n\n const seat = canSit ? this.firstFreeSeat(room) : null\n const player = this.addPlayer(room, sanitizeDisplayName(input.displayName), seat, input.browserId ?? '')\n player.ready = seat !== null\n const session = this.newSession(room, player.playerId, seat === null ? 'watch' : 'seat')\n if (seat === null && room.phase === 'hand') {\n this.pushChat(room, null, `${player.displayName} 进入观战`)\n }\n this.publish(room)\n return {\n playerId: player.playerId,\n roomId: room.roomId,\n seat: player.seat,\n wsTicket: this.issueTicket(player.playerId, room.roomId),\n sessionToken: session.token,\n view: this.project(room, player.playerId),\n }\n }\n\n leave(playerId: PlayerId): void {\n const room = this.roomOf(playerId)\n if (!room) return\n if (room.phase === 'closed') {\n // 结算页点「返回大厅」:把座位与会话清掉,别让旧房间一直挂着\n this.removePlayer(room, playerId)\n this.dropSessionsOf(room, playerId)\n return\n }\n const isHost = room.hostPlayerId === playerId\n const player = room.players.get(playerId)\n\n // 手牌进行中:不管是谁(包括房主)都先把这手打完,只弃掉自己的牌\n if (player && room.phase === 'hand' && room.hand && !room.hand.completed && player.seat !== null) {\n foldSeat(room.hand, player.seat, this.now())\n player.leaving = true\n player.connected = false\n this.dropSessionsOf(room, playerId)\n if (room.hand.completed) this.finalizeHand(room, this.now())\n else this.publish(room)\n return\n }\n\n if (isHost) {\n // 大厅/两手之间离开:没有手牌要保,直接结算\n this.dissolve(room, 'host-dissolved')\n return\n }\n if (!player) return\n this.removePlayer(room, playerId)\n this.publish(room)\n }\n\n sessionByToken(token: string | undefined): Session | undefined {\n if (!token) return undefined\n const session = this.sessions.get(token)\n if (!session) return undefined\n if (session.expiresAt <= this.now()) {\n this.sessions.delete(token)\n return undefined\n }\n return session\n }\n\n /** 把浏览器带来的若干 token 解析成会话,新的在前。 */\n sessionsForTokens(tokens: readonly string[]): Session[] {\n const out: Session[] = []\n for (const token of tokens) {\n const session = this.sessionByToken(token)\n if (session && !out.some((entry) => entry.token === session.token)) out.push(session)\n }\n out.sort((a, b) => b.createdAt - a.createdAt)\n return out\n }\n\n private sessionFromCookies(tokens: readonly string[] | undefined, roomId: RoomId): Session | undefined {\n if (!tokens || tokens.length === 0) return undefined\n for (const token of tokens) {\n const session = this.sessionByToken(token)\n if (session && session.roomId === roomId) return session\n }\n return undefined\n }\n\n view(roomId: RoomId, playerId: PlayerId): PlayerView {\n const room = this.roomOf(playerId)\n if (!room || room.roomId !== roomId) throw new CommandError('auth', '不在这个房间里')\n // 能取到状态就说明人在:轮询(实时通道断掉时的退路)不该显示成离线\n const player = room.players.get(playerId)\n if (player) this.touch(room, player)\n return this.project(room, playerId)\n }\n\n // ---------------------------------------------------------------- 命令\n\n command(\n roomId: RoomId,\n playerId: PlayerId,\n command: ClientCommand,\n baseSeq?: number,\n actionId?: string,\n ): { seq: number; view: PlayerView; duplicate?: boolean } {\n const room = this.roomOf(playerId)\n if (!room || room.roomId !== roomId) throw new CommandError('auth', '不在这个房间里')\n const player = room.players.get(playerId)\n if (!player || player.leaving) throw new CommandError('auth', '不在这个房间里')\n this.touch(room, player)\n\n if (command.type === 'ping') {\n return { seq: room.seq, view: this.project(room, playerId) }\n }\n if (room.phase === 'closed') throw new CommandError('phase', '房间已解散')\n if (baseSeq !== undefined && baseSeq !== room.seq) {\n throw new StaleCommandError('状态已更新,请重试', this.project(room, playerId))\n }\n const cacheKey = actionId ? `${playerId}:${actionId}` : ''\n if (cacheKey && room.actionCache.has(cacheKey)) {\n return { seq: room.seq, view: this.project(room, playerId), duplicate: true }\n }\n this.counters.commands += 1\n\n switch (command.type) {\n case 'sit': {\n if (room.phase !== 'lobby') throw new CommandError('phase', '对局进行中,暂时不能入座')\n if (player.seat !== null) throw new CommandError('illegal', '你已经在座位上了')\n const requested = command.seat\n const seat = requested !== undefined && Number.isInteger(requested) && requested >= 0 && requested < MAX_SEATS\n ? (this.seatTaken(room, requested) ? -1 : requested)\n : this.firstFreeSeat(room)\n if (seat < 0) throw new CommandError('room-full', '没有空座')\n player.seat = seat\n player.ready = true\n player.sittingOut = false\n player.autoFold = false\n player.timeoutStreak = 0\n if (command.displayName) player.displayName = sanitizeDisplayName(command.displayName)\n this.publish(room)\n break\n }\n case 'stand': {\n if (player.seat === null) throw new CommandError('illegal', '你本来就在观战')\n if (room.phase === 'hand' && room.hand && !room.hand.completed) foldSeat(room.hand, player.seat, this.now())\n player.seat = null\n player.ready = false\n if (room.hand?.completed) this.finalizeHand(room, this.now())\n else this.publish(room)\n break\n }\n case 'ready': {\n if (player.seat === null) throw new CommandError('not-seated', '先入座再准备')\n player.ready = command.ready\n if (command.ready) {\n player.sittingOut = false\n player.autoFold = false\n player.timeoutStreak = 0\n }\n this.publish(room)\n break\n }\n case 'start': {\n this.requireHost(room, playerId)\n if (room.phase !== 'lobby') throw new CommandError('phase', '当前阶段不能开局')\n this.startHand(room, this.now())\n break\n }\n case 'action': {\n if (room.phase !== 'hand' || !room.hand || room.hand.completed) throw new CommandError('phase', '现在不能行动')\n if (player.seat === null) throw new CommandError('not-seated', '你在观战')\n player.timeoutStreak = 0\n player.autoFold = false\n player.sittingOut = false\n this.applyTableAction(room, player.seat, command.action)\n break\n }\n case 'chat': {\n if (!room.settings.chatEnabled) throw new CommandError('illegal', '本桌已关闭聊天')\n const text = sanitizeChat(command.text)\n if (!text) throw new CommandError('illegal', '消息为空')\n const last = room.lastChatAt.get(playerId) ?? 0\n const now = this.now()\n if (now - last < CHAT_COOLDOWN_MS) throw new CommandError('rate-limited', '发得太快了')\n room.lastChatAt.set(playerId, now)\n this.pushChat(room, player, text)\n this.publish(room)\n break\n }\n case 'hostPause': {\n this.requireHost(room, playerId)\n if (room.phase === 'hand') throw new CommandError('phase', '一手牌进行中不能暂停')\n room.phase = 'lobby'\n room.hand = null\n room.handOverAt = null\n for (const seated of room.players.values()) if (seated.seat !== null) seated.ready = false\n this.publish(room)\n break\n }\n case 'hostKick': {\n this.requireHost(room, playerId)\n const target = command.playerId\n if (target === room.hostPlayerId) throw new CommandError('illegal', '不能踢房主')\n const targetPlayer = room.players.get(target)\n if (!targetPlayer) throw new CommandError('illegal', '玩家不在房间里')\n // 座位上的牌友只有在大厅能踢;观战者随时可以清走\n if (targetPlayer.seat !== null && room.phase !== 'lobby') {\n throw new CommandError('phase', '对局中不能踢座位上的人')\n }\n this.removePlayer(room, target)\n this.dropSessionsOf(room, target)\n this.sendTo(target, { type: 'kicked', seq: room.seq, reason: '你被房主请出了房间' })\n this.publish(room)\n break\n }\n case 'hostDissolve': {\n this.requireHost(room, playerId)\n this.dissolve(room, 'host-dissolved')\n break\n }\n default:\n throw new CommandError('illegal', '未知命令')\n }\n\n if (cacheKey) {\n room.actionCache.set(cacheKey, room.seq)\n while (room.actionCache.size > 64) {\n const oldest = room.actionCache.keys().next().value\n if (oldest === undefined) break\n room.actionCache.delete(oldest)\n }\n }\n return { seq: room.seq, view: this.project(room, playerId) }\n }\n\n /** 结算导出:房主与在场玩家都能取到本次会话汇总。 */\n exportSummary(roomId: RoomId, playerId: PlayerId): SessionSummary | null {\n const room = this.roomOf(playerId)\n if (!room || room.roomId !== roomId) throw new CommandError('auth', '不在这个房间里')\n return room.summary\n }\n\n // ---------------------------------------------------------------- WS 票据\n\n issueTicket(playerId: PlayerId, roomId: RoomId): string {\n const ticket = newToken()\n this.tickets.set(ticket, { playerId, roomId, expiresAt: this.now() + 60_000 })\n return ticket\n }\n\n consumeTicket(ticket: string): { playerId: PlayerId; roomId: RoomId } | null {\n const entry = this.tickets.get(ticket)\n if (!entry) return null\n this.tickets.delete(ticket)\n if (entry.expiresAt <= this.now()) return null\n return { playerId: entry.playerId, roomId: entry.roomId }\n }\n\n /**\n * 实时通道打开/关闭。\n *\n * 关掉 socket 不等于立刻离线:后台轮询(HTTP)还活着的话人还在。真正判离线交给\n * `tick` 按 `PRESENCE_TTL_MS` 扫一遍,两边都没有动静才标成离线。\n */\n markConnected(playerId: PlayerId, roomId: RoomId, connected: boolean): void {\n const room = this.rooms.get(roomId)\n const player = room?.players.get(playerId)\n if (!room || !player) return\n player.socketOpen = connected\n if (connected) player.lastSeenAt = this.now()\n this.refreshPresence(room, player)\n }\n\n /** HTTP 请求也算心跳:轮询模式下不能把自己和别人都显示成离线。 */\n private touch(room: LiveRoom, player: LivePlayer): void {\n if (player.leaving) return\n player.lastSeenAt = this.now()\n if (room.hostPlayerId === player.playerId) room.hostLastSeen = player.lastSeenAt\n this.refreshPresence(room, player)\n }\n\n /** 有实时通道,或者最近有 HTTP 动静,就算在线。 */\n private refreshPresence(room: LiveRoom, player: LivePlayer): void {\n const online = !player.leaving && (player.socketOpen || this.now() - player.lastSeenAt < PRESENCE_TTL_MS)\n if (player.connected === online) return\n player.connected = online\n this.publish(room)\n }\n\n health(): Record<string, unknown> {\n return {\n rooms: this.rooms.size,\n open: [...this.rooms.values()].filter((room) => room.phase !== 'closed').length,\n sessions: this.sessions.size,\n ...this.counters,\n }\n }\n\n // ---------------------------------------------------------------- 内部:阶段\n\n private startHand(room: LiveRoom, now: number): void {\n const eligible = [...room.players.values()].filter(\n (player) => player.seat !== null && player.ready && !player.sittingOut && !player.autoFold && !player.leaving,\n )\n const seated = [...room.players.values()].filter((player) => player.seat !== null && !player.leaving)\n const notReady = seated.filter(\n (player) => player.playerId !== room.hostPlayerId && !player.ready && !player.sittingOut && !player.autoFold,\n )\n if (notReady.length > 0) throw new CommandError('phase', `还有 ${notReady.length} 人没准备`)\n if (eligible.length < 2) throw new CommandError('phase', '至少需要 2 人准备才能开局')\n\n const seats = eligible.map((player) => ({\n seat: player.seat as number,\n playerId: player.playerId,\n displayName: player.displayName,\n }))\n const button = this.nextButton(room, seats.map((entry) => entry.seat))\n room.hand = createHand({\n handNo: room.handNo + 1,\n button,\n seats,\n sb: room.settings.sb,\n bb: room.settings.bb,\n buyIn: room.settings.buyIn,\n turnTimeoutMs: room.settings.turnTimeoutMs,\n now,\n })\n room.handNo += 1\n room.phase = 'hand'\n room.handOverAt = null\n this.counters.handsPlayed += 1\n this.publish(room)\n }\n\n private applyTableAction(room: LiveRoom, seat: number, action: TableAction): void {\n const hand = room.hand\n if (!hand) throw new CommandError('phase', '没有进行中的手牌')\n try {\n act(hand, seat, action, this.now())\n } catch (error) {\n if (error instanceof HandError) throw new CommandError(error.code === 'not-your-turn' ? 'not-your-turn' : error.code, error.message)\n throw error\n }\n if (hand.completed) this.finalizeHand(room, this.now())\n else this.publish(room)\n }\n\n private finalizeHand(room: LiveRoom, now: number): void {\n const hand = room.hand\n const result = hand?.result\n if (!hand || !result) return\n\n for (const delta of result.deltas) {\n const stats = this.ensureStats(room, delta.playerId, delta.displayName, delta.seat)\n stats.handsPlayed += 1\n stats.netChips += delta.net\n stats.biggestWin = Math.max(stats.biggestWin, delta.net)\n stats.displayName = delta.displayName\n stats.seat = delta.seat\n if (result.winners.includes(delta.seat)) stats.handsWon += 1\n if (!result.foldedWin && result.shownHands.some((shown) => shown.seat === delta.seat)) {\n stats.showdownsPlayed += 1\n if (result.winners.includes(delta.seat)) stats.showdownsWon += 1\n }\n }\n\n room.lastHand = result\n room.lastButton = hand.button\n room.history.push(result)\n if (room.history.length > MAX_HISTORY_HANDS) room.history.splice(0, room.history.length - MAX_HISTORY_HANDS)\n room.phase = 'handOver'\n room.handOverAt = now + Math.max(room.settings.autoAdvanceMs, HAND_OVER_MIN_MS)\n\n for (const player of [...room.players.values()]) {\n if (player.leaving) this.removePlayer(room, player.playerId)\n }\n\n this.store.appendHand({\n ...result,\n roomCode: room.roomCode,\n title: room.title,\n recordedAt: nowIso(),\n })\n this.publish(room)\n }\n\n private advance(room: LiveRoom, now: number): void {\n const host = room.players.get(room.hostPlayerId)\n if (!host || host.leaving) {\n // 房主走了:这手照常算完,但不再开新局\n this.dissolve(room, 'host-dissolved')\n return\n }\n const eligible = [...room.players.values()].filter(\n (player) => player.seat !== null && !player.sittingOut && !player.autoFold && !player.leaving,\n )\n if (eligible.length >= 2) {\n try {\n this.startHand(room, now)\n return\n } catch {\n // 落到大厅\n }\n }\n room.phase = 'lobby'\n room.hand = null\n room.handOverAt = null\n for (const player of room.players.values()) if (player.seat !== null) player.ready = false\n this.publish(room)\n }\n\n private dissolve(room: LiveRoom, reason: 'host-dissolved' | 'host-idle'): void {\n if (room.phase === 'closed' || room.phase === 'settling') return\n room.hand = null\n room.handOverAt = null\n room.closedAt = this.now()\n const summary = this.buildSummary(room, reason)\n room.summary = summary\n room.phase = 'closed'\n this.counters.roomsClosed += 1\n this.store.appendSession(summary)\n this.publish(room)\n for (const player of room.players.values()) {\n this.sendTo(player.playerId, { type: 'settled', seq: room.seq, summary })\n }\n }\n\n private buildSummary(room: LiveRoom, reason: 'host-dissolved' | 'host-idle'): SessionSummary {\n const entries = [...room.stats.entries()]\n .map(([playerId, stats]) => ({ playerId, ...stats }))\n .filter((entry) => entry.handsPlayed > 0)\n entries.sort((a, b) => (b.netChips - a.netChips) || (b.handsWon - a.handsWon) || (a.displayName.localeCompare(b.displayName)))\n const endedAt = this.now()\n return {\n sessionId: asSessionId(`sess_${room.roomCode}_${room.createdAt}`),\n roomCode: room.roomCode,\n title: room.title,\n startedAt: new Date(room.createdAt).toISOString(),\n endedAt: new Date(endedAt).toISOString(),\n durationMs: endedAt - room.createdAt,\n handsPlayed: room.handNo,\n players: entries.map((entry, index) => ({\n rank: index + 1,\n playerId: entry.playerId,\n displayName: entry.displayName,\n handsPlayed: entry.handsPlayed,\n handsWon: entry.handsWon,\n netChips: entry.netChips,\n biggestWin: entry.biggestWin,\n showdownsWon: entry.showdownsWon,\n showdownsPlayed: entry.showdownsPlayed,\n })),\n history: [...room.history],\n reason,\n }\n }\n\n // ---------------------------------------------------------------- 内部:tick\n\n tick(): void {\n const now = this.now()\n for (const room of [...this.rooms.values()]) {\n if (room.phase === 'closed') {\n if (room.closedAt !== null && now - room.closedAt > CLOSED_RETENTION_MS) this.dropRoom(room)\n continue\n }\n if (room.hostPlayerId && now - room.hostLastSeen > this.config.hostIdleDissolveMs) {\n this.dissolve(room, 'host-idle')\n continue\n }\n if (room.phase === 'hand' && room.hand) {\n this.tickHand(room, now)\n }\n if (room.phase === 'handOver' && room.handOverAt !== null && now >= room.handOverAt) {\n this.advance(room, now)\n }\n for (const player of [...room.players.values()]) {\n if (player.leaving && room.phase !== 'hand') this.removePlayer(room, player.playerId)\n else this.refreshPresence(room, player)\n }\n }\n this.cleanupSessions(now)\n }\n\n private tickHand(room: LiveRoom, now: number): void {\n const hand = room.hand\n if (!hand || hand.completed) return\n if (hand.runoutDueAt !== null) {\n if (tickHand(hand, now)) {\n if (hand.completed) this.finalizeHand(room, now)\n else this.publish(room)\n }\n return\n }\n if (hand.deadlineAt === null || now < hand.deadlineAt) return\n\n const seat = hand.toActSeat\n if (seat !== null) {\n const player = [...room.players.values()].find((entry) => entry.seat === seat)\n if (player) {\n player.timeoutStreak += 1\n if (player.timeoutStreak >= 3) {\n player.autoFold = true\n player.sittingOut = true\n }\n }\n }\n const outcome = forceTimeout(hand, now)\n if (!outcome) return\n if (hand.completed) this.finalizeHand(room, now)\n else this.publish(room)\n }\n\n /** 本机地址兜底(拿不到局域网 IP 时用)。 */\n private joinUrlFor(room: LiveRoom, token: string): string {\n return `${this.origin()}${this.config.routePrefix}/play?code=${room.roomCode}&invite=${encodeURIComponent(token)}&role=auto`\n }\n\n /** 同一局域网可直连的邀请链接;需要局域网代理在跑、而且拿得到网卡地址。 */\n private lanJoinUrl(room: LiveRoom): string {\n if (this.lanPort <= 0) return ''\n const ip = this.lanIp()\n if (!ip) return ''\n return `http://${ip}:${this.lanPort}${this.config.routePrefix}/play?code=${room.roomCode}&invite=${encodeURIComponent(room.sitToken)}&role=auto`\n }\n\n // ---------------------------------------------------------------- 内部:视图\n\n private project(room: LiveRoom, playerId: PlayerId): PlayerView {\n const you = room.players.get(playerId)\n if (!you) throw new CommandError('auth', '不在这个房间里')\n const hand = room.hand\n const seats: SeatView[] = []\n for (let seat = 0; seat < MAX_SEATS; seat += 1) {\n const handPlayer = hand?.players.find((entry) => entry.seat === seat)\n const live = [...room.players.values()].find((entry) => entry.seat === seat)\n if (!handPlayer && !live) {\n seats.push({\n seat,\n playerId: null,\n displayName: null,\n ready: false,\n connected: false,\n sittingOut: false,\n autoFold: false,\n host: false,\n stack: 0,\n committed: 0,\n handNet: 0,\n folded: false,\n allIn: false,\n lastAction: null,\n holeCount: 0,\n })\n continue\n }\n const holder = handPlayer?.playerId ?? live!.playerId\n const delta = room.lastHand?.deltas.find((entry) => entry.seat === seat)\n seats.push({\n seat,\n playerId: holder,\n displayName: handPlayer?.displayName ?? live!.displayName,\n ready: live?.ready ?? false,\n connected: live?.connected ?? handPlayer !== undefined,\n sittingOut: live?.sittingOut ?? false,\n autoFold: live?.autoFold ?? false,\n host: holder === room.hostPlayerId,\n stack: handPlayer ? handPlayer.stack : room.settings.buyIn,\n committed: handPlayer ? handPlayer.streetCommitted : 0,\n handNet: delta?.net ?? 0,\n folded: handPlayer?.folded ?? false,\n allIn: handPlayer?.allIn ?? false,\n lastAction: handPlayer?.lastAction ?? null,\n holeCount: handPlayer ? handPlayer.hole.length : 0,\n })\n }\n\n const stats = room.stats.get(playerId)\n const ownHandPlayer = hand?.players.find((entry) => entry.playerId === playerId)\n const legal = hand && !hand.completed && you.seat !== null ? legalForSeat(hand, you.seat) : null\n\n const roomView: RoomView = {\n roomId: room.roomId,\n roomCode: room.roomCode,\n title: room.title,\n hostPlayerId: room.hostPlayerId,\n hostDisplayName: room.players.get(room.hostPlayerId)?.displayName ?? '房主',\n phase: room.phase,\n settings: room.settings,\n seats,\n handNo: room.handNo,\n createdAt: new Date(room.createdAt).toISOString(),\n nextHandAt: room.handOverAt === null ? null : new Date(room.handOverAt).toISOString(),\n seq: room.seq,\n joinUrl: this.lanJoinUrl(room) || this.joinUrlFor(room, room.sitToken),\n lanUrl: this.lanJoinUrl(room),\n }\n\n const leaderboard: LeaderboardEntry[] = [...room.stats.entries()]\n .map(([entryId, entry]) => ({\n playerId: entryId,\n displayName: entry.displayName,\n seat: room.players.get(entryId)?.seat ?? entry.seat,\n handsPlayed: entry.handsPlayed,\n handsWon: entry.handsWon,\n netChips: entry.netChips,\n biggestWin: entry.biggestWin,\n showdownsWon: entry.showdownsWon,\n showdownsPlayed: entry.showdownsPlayed,\n }))\n .sort((a, b) => (b.netChips - a.netChips) || (b.handsWon - a.handsWon))\n\n return {\n room: roomView,\n you: {\n playerId,\n displayName: you.displayName,\n seat: you.seat,\n spectator: you.seat === null,\n ready: you.ready,\n sittingOut: you.sittingOut,\n holeCards: ownHandPlayer ? [...ownHandPlayer.hole] : [],\n legal,\n canAct: legal !== null,\n netChips: stats?.netChips ?? 0,\n handsPlayed: stats?.handsPlayed ?? 0,\n },\n hand: hand ? handView(hand) : null,\n lastHand: room.lastHand,\n leaderboard,\n settlement: room.summary,\n chat: room.chat.slice(-40),\n spectators: [...room.players.values()]\n .filter((player) => player.seat === null)\n .map((player) => ({ playerId: player.playerId, displayName: player.displayName })),\n serverNow: nowIso(),\n }\n }\n\n // ---------------------------------------------------------------- 内部:玩家\n\n private addPlayer(room: LiveRoom, displayName: string, seat: number | null, browserId: string): LivePlayer {\n const player: LivePlayer = {\n playerId: asPlayerId(newId('p')),\n displayName,\n seat,\n ready: false,\n connected: true,\n socketOpen: false,\n lastSeenAt: this.now(),\n sittingOut: false,\n autoFold: false,\n timeoutStreak: 0,\n browserId,\n joinedAt: this.now(),\n leaving: false,\n }\n room.players.set(player.playerId, player)\n if (browserId) room.browsers.set(browserId, player.playerId)\n this.ensureStats(room, player.playerId, displayName, seat)\n return player\n }\n\n private removePlayer(room: LiveRoom, playerId: PlayerId): void {\n const player = room.players.get(playerId)\n if (!player) return\n room.players.delete(playerId)\n if (player.browserId && room.browsers.get(player.browserId) === playerId) room.browsers.delete(player.browserId)\n this.dropSessionsOf(room, playerId)\n }\n\n private ensureStats(room: LiveRoom, playerId: PlayerId, displayName: string, seat: number | null): Stats {\n let stats = room.stats.get(playerId)\n if (!stats) {\n stats = {\n displayName,\n seat,\n handsPlayed: 0,\n handsWon: 0,\n netChips: 0,\n biggestWin: 0,\n showdownsWon: 0,\n showdownsPlayed: 0,\n }\n room.stats.set(playerId, stats)\n }\n return stats\n }\n\n private pushChat(room: LiveRoom, player: LivePlayer | null, text: string): void {\n room.chat.push({\n playerId: player?.playerId ?? asPlayerId('system'),\n displayName: player?.displayName ?? '桌子',\n text,\n ts: nowIso(),\n })\n if (room.chat.length > MAX_CHAT_LINES) room.chat.splice(0, room.chat.length - MAX_CHAT_LINES)\n }\n\n private newSession(room: LiveRoom, playerId: PlayerId, kind: Session['kind']): Session {\n const session: Session = {\n token: newToken(),\n playerId,\n roomId: room.roomId,\n kind,\n createdAt: this.now(),\n expiresAt: this.now() + SESSION_TTL_MS,\n }\n this.sessions.set(session.token, session)\n return session\n }\n\n private dropSessionsOf(room: LiveRoom, playerId: PlayerId): void {\n for (const [token, session] of this.sessions) {\n if (session.playerId === playerId && session.roomId === room.roomId) this.sessions.delete(token)\n }\n }\n\n private cleanupSessions(now: number): void {\n for (const [token, session] of this.sessions) {\n if (session.expiresAt <= now) this.sessions.delete(token)\n }\n for (const [ticket, entry] of this.tickets) {\n if (entry.expiresAt <= now) this.tickets.delete(ticket)\n }\n }\n\n private existingPlayer(room: LiveRoom, input: PeekInput): LivePlayer | null {\n const session = this.sessionFromCookies(input.cookies, room.roomId)\n if (session) {\n const player = room.players.get(session.playerId)\n if (player && !player.leaving) return player\n }\n if (input.browserId) {\n const playerId = room.browsers.get(input.browserId)\n if (playerId) {\n const player = room.players.get(playerId)\n if (player && !player.leaving) return player\n }\n }\n return null\n }\n\n private checkInvite(\n room: LiveRoom,\n input: PeekInput,\n role: 'sit' | 'watch',\n local: boolean,\n ): { allowed: boolean; sit: boolean } {\n if (local) return { allowed: true, sit: true }\n const token = input.invite ?? ''\n if (!token) return { allowed: false, sit: false }\n const sit = hashEquals(room.sitHash, hashInvite(room.secret, 'sit', token))\n const watch = hashEquals(room.watchHash, hashInvite(room.secret, 'watch', token))\n if (role === 'sit') return { allowed: sit || watch, sit }\n return { allowed: watch || sit, sit }\n }\n\n private requireHost(room: LiveRoom, playerId: PlayerId): void {\n if (room.hostPlayerId !== playerId) throw new CommandError('host-only', '只有房主能这么做')\n }\n\n private roomByCode(code: string): LiveRoom {\n const room = this.roomsByCode.get(String(code ?? '').trim())\n if (!room) throw new CommandError('room-not-found', '房号不存在或房间已关闭')\n if (room.phase === 'closed') throw new CommandError('expired', '房间已解散')\n return room\n }\n\n private roomOf(playerId: PlayerId): LiveRoom | undefined {\n for (const room of this.rooms.values()) {\n if (room.players.has(playerId)) return room\n }\n return undefined\n }\n\n private uniqueCode(): string {\n for (let attempt = 0; attempt < 20; attempt += 1) {\n const code = newRoomCode()\n if (!this.roomsByCode.has(code)) return code\n }\n throw new CommandError('room-full', '暂时没有可用房号,请稍后再试')\n }\n\n private firstFreeSeat(room: LiveRoom): number {\n for (let seat = 0; seat < MAX_SEATS; seat += 1) {\n if (!this.seatTaken(room, seat)) return seat\n }\n return -1\n }\n\n private seatTaken(room: LiveRoom, seat: number): boolean {\n return [...room.players.values()].some((player) => player.seat === seat && !player.leaving)\n }\n\n private nextButton(room: LiveRoom, seats: number[]): number {\n const ordered = [...seats].sort((a, b) => a - b)\n const previous = room.lastButton\n if (previous === null) return ordered[0]!\n for (const seat of ordered) if (seat > previous) return seat\n return ordered[0]!\n }\n\n private sendTo(playerId: PlayerId, event: ServerEvent): void {\n for (const listener of this.listeners) listener(playerId, event)\n }\n\n private publish(room: LiveRoom, extra?: (seq: number) => ServerEvent): void {\n room.seq += 1\n for (const player of room.players.values()) {\n const event: ServerEvent = { type: 'snapshot', seq: room.seq, view: this.project(room, player.playerId) }\n this.sendTo(player.playerId, event)\n }\n if (extra) {\n const event = extra(room.seq)\n for (const player of room.players.values()) this.sendTo(player.playerId, event)\n }\n }\n\n private dropRoom(room: LiveRoom): void {\n this.rooms.delete(room.roomId)\n this.roomsByCode.delete(room.roomCode)\n }\n}\n","import type { Context } from '@deepseek-ai/cordis'\nimport { Config, resolveConfig, type PluginConfig } from './config.ts'\nimport { PACKAGE_NAME } from './invariant.ts'\nimport { registerHoldemHttp } from './net/routes.ts'\nimport { registerHoldemSocket } from './net/ws.ts'\nimport { CloudflareDeployer } from './net/cloudflare.ts'\nimport { startLanProxy, type LanProxy } from './net/lan-proxy.ts'\nimport { firstLanIPv4 } from './net/network.ts'\nimport { createFileStore } from './persist/store.ts'\nimport { RoomManager } from './room/RoomManager.ts'\n\nexport const name = PACKAGE_NAME\nexport const inject = ['webServer']\nexport { Config }\n\nexport function apply(ctx: Context, config: PluginConfig): void {\n const resolved = resolveConfig(config)\n if (!resolved.enabled) return\n\n const manager = new RoomManager({\n config: resolved,\n origin: () => {\n try {\n return ctx.webServer.getOrigin()\n } catch {\n return `http://127.0.0.1:${ctx.webServer.port}`\n }\n },\n store: createFileStore(),\n lanIp: firstLanIPv4,\n })\n\n const cloudflare = new CloudflareDeployer()\n\n ctx.effect(() => {\n let lanProxy: LanProxy | null = null\n void startLanProxy({\n targetPort: ctx.webServer.port,\n prefix: resolved.routePrefix,\n log: (message) => ctx.logger?.info?.(message),\n }).then((started) => {\n lanProxy = started\n if (started) manager.setLanPort(started.port)\n })\n\n const stopTick = manager.start()\n const disposeHttp = registerHoldemHttp(ctx.webServer, manager, cloudflare)\n const disposeWs = registerHoldemSocket(ctx.webServer, manager)\n return () => {\n stopTick()\n disposeHttp()\n disposeWs()\n lanProxy?.dispose()\n cloudflare.dispose()\n void manager.dispose()\n }\n }, 'holdem: room routes')\n\n ctx.inject(['systemPrompt'], (promptCtx) => {\n promptCtx.systemPrompt?.section({\n name: 'plugin:@zhuweiyou/dsh-holdem',\n order: 200,\n text: [\n '本机已安装 @zhuweiyou/dsh-holdem(德州扑克)。',\n '开房与打牌都在侧栏的「德州扑克」面板里;牌桌可以跑在本机(局域网分享),也可以跑在 Cloudflare Worker 上(公网分享)。',\n '结算是本地娱乐筹码,不是 DeepSeek 余额;加入只需要房主发来的邀请链接。',\n ].join(''),\n })\n })\n}\n"],"mappings":"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"}
|