# 问题 2:删除房间 / 房主心跳 / 房主退出自动关房 / 踢人 > 通用问题:所有对局(War / Mill / Holdem / DragTest)都适用,因为房间管理在 **boardgame.io Lobby 层**,与具体游戏无关。 ## 现状(boardgame.io 0.50.2 的能力边界) - Lobby REST **只有** `create / join / leave / playAgain / update`,**没有 delete / heartbeat / kick**。 - `leave`:最后一个玩家离开时 `db.wipe(matchID)` 删房间(`server.js:2331`);否则仅清该玩家 name/credentials。 - **没有在线检测 / 心跳**:server 不知道谁“还连着”。客户端断开 socket 不会自动 leave,房间会一直挂着。 - **房主定义**:本项目约定 `playerID '0'`(创建者)即房主(`holdem.ts` 的 `host()` 取最低编号 seated 且有筹码者,联机下等价于 P0)。 ## 需求拆解 1. **删除房间**:房主有权删除;删除后所有客户端应被踢出 / 房间消失。 2. **房主退出自动关房**:房主无 heartbeat(断开)超过 10min,房间自动解除。 3. **心跳**:房主心跳周期长(10min);为支持“自动关房”,需要一个心跳端点。 4. **踢人**:房主有权把某个玩家踢出房间。 ## 拟采用方案(在 `packages/server` 加自定义 koa 路由) 利用 `Server()` 返回的 `server.app / server.db / server.auth`,在 `index.ts` 里挂一组 `/api/rooms/...` 路由。 房主鉴权:请求携带 `playerID=0` + `credentials`(与 lobby 一致),用 `server.auth.authenticateCredentials` 校验。 ### 2.1 内存在线表(心跳) ```ts // 房间 -> { pid: lastSeenEpochMs } const presence = new Map>(); const HOST_TIMEOUT_MS = 10 * 60 * 1000; // 房主 10min const PLAYER_TIMEOUT_MS = 3 * 60 * 1000; ``` - `POST /api/rooms/:id/heartbeat`:body `{ playerID, credentials }`,鉴权后 `presence[matchID][playerID] = Date.now()`。 所有玩家都建议定时 ping(房主 10min 一次,普通玩家可更频繁),至少房主必须 ping。 - 定时器(server 启动时 `setInterval`,如每 60s):扫描 presence, 若房主(`playerID '0'` 或当前 host)超时 → 执行“自动关房”(同删除逻辑)。 ### 2.2 删除房间 ``` POST /api/rooms/:id/delete body: { playerID, credentials } ``` 仅房主可删。逻辑: ```ts const { metadata } = await server.db.fetch(matchID, { metadata: true }); // 校验 host 凭证 await server.db.wipe(matchID); presence.delete(matchID); // 通知所有 socket 客户端(通过 pubSub / socket.io)房间已删 ``` 客户端在 `LobbyRoomList` 的房间列表轮询(已有 `refresh`)会自然发现房间消失; 另可在 boardgame.io 的 `onUpdate`/自定义事件里让已挂载的 Client 监听到“房间被删”并回退到列表。 ### 2.3 踢人 ``` POST /api/rooms/:id/kick body: { playerID: hostId, credentials, target: targetPid } ``` 仅房主可踢。逻辑: ```ts // 1) lobby metadata 清掉 target 的 name/credentials metadata.players[target].name = undefined; metadata.players[target].credentials = undefined; await server.db.setMetadata(matchID, metadata); // 2) 在游戏状态 G 里把该座位 seated=false(需发一个 game move 或走 master) // 简化:用 master 的 dispatch 调一个 engine move `kickMove(target)` // 3) presence 清掉 target ``` - **关键难点**:踢人既要改 lobby metadata(让座位空出来可被别人加入),又要改游戏内 G(让被踢者从牌桌消失)。 改 G 需经 boardgame.io 的 `Master` dispatch(不能直接改 db 状态,否则客户端状态漂移)。 方案:在**每个游戏**里加一个 `kick` move(通用约定),server 用 `Master` 触发它。 为减小改动,可定义一个**通用插件/约定**:所有游戏实现 `kick(targetPid)` move, 删房间时也对所有人调用。 ### 2.4 房主退出自动关房(合并到 2.1 定时器) 定时器发现房主超时 → 等同“删除房间”。 ## 客户端改动(ui / web) - `LobbyRoomList`: - 房主视角:每个房间 pill 增加“删除房间”按钮(调 `delete`); 房间内当前玩家列表增加“踢”按钮(调 `kick`)。 - 挂载后启动 `setInterval` 调 `heartbeat`(房主 10min;普通玩家按 PLAYER_TIMEOUT 的一半)。 - `App.tsx` / `HoldemBoard`:无需特殊处理,房间消失后列表刷新即可;可加“房间已被删除”提示。 ## 风险与决策点 - **Master dispatch 改 G**:需要拿到 `server` 的 master 实例。boardgame.io `Server()` 没直接暴露 `master`, 但可通过 `server.db` + `transport` 重新构造,或复用 `router` 内部的 `db`。 **备选(更简单)**:把“座位空出 / 踢人”完全放进 lobby metadata,游戏内 `playerView`/move 读取 metadata? 不行(G 拿不到 metadata)。因此**必须**经一个游戏内 move。 → 建议:每个游戏加 `kick` move(空实现也行,至少清 `seated`),server 用 `db` + 自定义 dispatch。 - **是否现在就给所有游戏加 kick move**:会影响 War/Mill/DragTest。可先做 Holdem + 通用骨架,其余游戏加最小 `kick`。 - **房主判定**:当前 `host()` 取“最低编号 seated 且有筹码者”,联机下创建者 = P0,但若 P0 破产/离开,host 会漂移。 建议把“房主”显式固化在 `metadata`(创建者 playerID),server 端以 metadata 的 `createdBy` 为准,engine `host()` 也改为读 G 里的 `hostId`(创建时写死)。 ## 验证 - 房主删除 → 列表消失、其他客户端刷新后无此房间、已挂载 Client 收到“已删除”。 - 房主断网 10min(测试用短超时)→ 房间被自动 wipe。 - 房主踢人 → 被踢者 metadata 清、G 里 seated=false、座位可被重新加入。 - 非房主调 delete/kick → 403。