Files
huajishe-tts/docs/dev-log/08-local-lan-multiplayer.md

164 lines
5.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 内其他机器)
### 问题 2SocketIO URL 协议搞错
最初我用 `ws://localhost:8000`,浏览器报错 `ERR_NAME_NOT_RESOLVED` for `http://ws/socket.io/...`
原因SocketIO 接受 HTTP URL内部自动升级到 WebSocket。`ws://` 是浏览器用来直接连 WebSocket 的协议,不是 SocketIO 期望的。
**解决**:改成 `http://localhost:8000`
### 问题 3player 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` 导入。
### 问题 4ActivePlayers 类型/值混淆
`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';
```
### 问题 5Docker 镜像跑不起来(与本地无关)
试过 `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。
### 步骤 2web App 加 Online 模式
顶部加"本地 / 联机"切换按钮。联机模式下显示一个 `OnlineConfigBar`
- Server URL默认 `http://localhost:8000`
- Room ID默认 `room1`
- Player0 / 1
- Secret默认 `p0` / `p1`
`localStorage` 持久化。
### 步骤 33 个游戏分别有 Online 视图
```tsx
import { SocketIO } from 'boardgame.io/multiplayer';
Client({
game,
board: ...,
multiplayer: SocketIO({ server: config.serverUrl }),
debug: false,
});
// 使用时
<OnlineClient
matchID={config.matchID}
playerID={config.playerID}
credentials={config.credentials}
/>
```
### 步骤 4end-to-end 验证
用 Playwright 打开 2 个 tab
- Tab 0player 0, secret p0
- Tab 1player 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` — 决策记录更新