# 08 · 本地局域网联机 ## 做什么 用户要求做联机对战,但**不是部署到云服务器**,而是**本地局域网**(用户机器上跑 server,同一 LAN 内的其他设备/浏览器通过 ws://[lan-ip]:8000 连接)。 目标: 1. Server 能跑起来(tsx 模式即可,不一定要 Docker) 2. Web 客户端有"本地" / "联机" 模式切换 3. 联机模式下输入 server URL + 房间 ID + 玩家凭证 4. 两台浏览器开同一房间,互相操作能实时同步 ## 遇到什么问题 ### 问题 1:指挥官方向错了 最初按 plan 走 P2.1"部署到公共域名",写了 Dockerfile + docker-compose.yml + 生产配置。但**用户没有公共域名的服务器访问权限给我**,也**不想在云上部署**。需要先在本地 LAN 跑起来验证。 **解决**:撤回云部署路径,专注本地开发。Dockerfile 作为 v2 部署的素材保留,但当前目标是: - server 用 `pnpm --filter @tts-like/server dev` 跑(tsx watch) - 客户端配置 `http://localhost:8000`(同一机器)或 `http://192.168.x.x:8000`(LAN 内其他机器) ### 问题 2:SocketIO URL 协议搞错 最初我用 `ws://localhost:8000`,浏览器报错 `ERR_NAME_NOT_RESOLVED` for `http://ws/socket.io/...`。 原因:SocketIO 接受 HTTP URL,内部自动升级到 WebSocket。`ws://` 是浏览器用来直接连 WebSocket 的协议,不是 SocketIO 期望的。 **解决**:改成 `http://localhost:8000`。 ### 问题 3:player 1 调 move 被拒 点击 收牌(player 1 操作)时报 `disallowed move: collect`。 原因:boardgame.io 默认 `currentPlayer = '0'`,SocketIO 传输层只允许 currentPlayer 调 move。War 没配置 turn,所以永远是 player 0 active。 **解决**:在 War 加 `turn: { activePlayers: ActivePlayers.ALL }`: ```ts import { ActivePlayers } from 'boardgame.io/core'; // ... turn: { activePlayers: ActivePlayers.ALL }, ``` > ⚠️ `boardgame.io`(主入口)导出的是 type `ActivePlayers`,常量对象要从 `boardgame.io/core` 导入。 ### 问题 4:ActivePlayers 类型/值混淆 `import { ActivePlayers } from 'boardgame.io'` 拿到的是 type alias(接口),不是 const 对象。编译报错:`'ActivePlayers' only refers to a type, but is being used as a value`。 **解决**:从 `boardgame.io/core` 导入: ```ts import { ActivePlayers } from 'boardgame.io/core'; ``` ### 问题 5:Docker 镜像跑不起来(与本地无关) 试过 `node packages/server/dist/index.js` 启动 Docker 镜像,报错: ``` ERR_UNSUPPORTED_DIR_IMPORT: Directory import '...boardgame.io/server' ``` 原因:ESM 模式下 Node 拒绝目录导入(即便 `package.json` 里有 `main` 字段)。 **解决**:暂时不用 Docker 镜像(撤回)。Task #8 标题改为"本地局域网联机"。Dockerfile 保留,v2 部署时再修。 > 📝 Docker 镜像运行的方案(留作未来): > - 方案 A:保持 ESM,在 server/package.json 加 `"main": "./dist/index.js"` + `babel-plugin-transform-modules-commonjs` 编译产物 > - 方案 B:改 server 为 CJS(移除 `"type": "module"`、tsconfig 改 `"module": "CommonJS"`、`moduleResolution: "Node16"`)——但 `boardgame.io/server` 是 CJS,会让 server 内部 import 反向(from ESM to CJS,里 ESM 调用 CJS 通常 OK,但 `import.meta` 之类用不了) ## 怎么解决 ### 步骤 1:让 server 跑起来 ```bash pnpm --filter @tts-like/server dev # tsx watch src/index.ts # 🎲 tts-like server listening on :8000 ``` 加 `/healthz` 端点 + 注册 3 个游戏 + 环境变量读 ALLOWED_ORIGINS。 ### 步骤 2:web App 加 Online 模式 顶部加"本地 / 联机"切换按钮。联机模式下显示一个 `OnlineConfigBar`: - Server URL(默认 `http://localhost:8000`) - Room ID(默认 `room1`) - Player(0 / 1) - Secret(默认 `p0` / `p1`) 用 `localStorage` 持久化。 ### 步骤 3:3 个游戏分别有 Online 视图 ```tsx import { SocketIO } from 'boardgame.io/multiplayer'; Client({ game, board: ..., multiplayer: SocketIO({ server: config.serverUrl }), debug: false, }); // 使用时 ``` ### 步骤 4:end-to-end 验证 用 Playwright 打开 2 个 tab: - Tab 0:player 0, secret p0 - Tab 1:player 1, secret p1 - 同一 room "room1" 操作: - Tab 0 点 翻牌 → 双方 deck 26→25,双方 pile 各 1 张 - Tab 1 点 收牌 → 7♣ 赢,p0-deck 25→27 - 两个 tab 状态完全同步 ## 成果 - Commit `585930c feat(multiplayer): LAN/local online mode via SocketIO` - 7 文件改动,+426/-92 行 - 服务器 tsx 跑通: - `pnpm --filter @tts-like/server dev` → 监听 :8000 - `/healthz` 返回 `{"status":"ok","port":8000,"uptime":3.57}` - 2 客户端 SocketIO 状态实时同步验证通过 ## 当前测试方式(你接下来怎么用) ### 同一台机器(最简单) ```bash # 终端 1 pnpm --filter @tts-like/server dev # 终端 2 pnpm --filter @tts-like/web dev # 浏览器开 2 个 tab (或 2 个隐身窗口) → 都进联机模式 # Tab 1: Player 0, Secret p0 # Tab 2: Player 1, Secret p1 # 同一 Room ID (默认 room1) ``` ### 跨设备(LAN) - 看 server 机器的局域网 IP(如 `ifconfig` 或 `ipconfig` 看 `192.168.x.x`) - 客户端的 Server URL 填 `http://192.168.x.x:8000` - 防火墙放行 8000 端口 ### 用户验证 - Open 2 browser tabs to same `http://localhost:5173` - Switch to War → 联机 → 默认配置(player 0 / room1 / p0) - 在 tab 1 改成 Player 1, Secret p1 - 在 tab 0 点 翻牌 → 两边都看到翻牌结果 - 在 tab 1 点 收牌 → 两边都看到收牌结果 ## 关联 - [03-monorepo-scaffold.md](./03-monorepo-scaffold.md) — 之前 query 错误方向,撤回 Docker 部署 - [lessons-learned.md §23](./lessons-learned.md) — 这一轮的踩坑(SocketIO URL protocol / ActivePlayers 导入 / disallowed move) - `docs/decisions.md` — 决策记录更新