# 12 · 统一端口 + 玩家命名系统 ## 做什么 用户要求两个改造(先给方案审核,通过后实施): 1. **统一端口**:不映射两个端口(之前 5173 页面 + 8000 API),改成页面和 API 都在同一个端口,如页面 `:5173`、API `:5173/api`。 2. **命名系统**:战局内可以修改自己的名字(大厅显示所有玩家名字)。 用户还问了一个关键问题:**加新游戏(如德州扑克)是不是有 Lua 之类的脚本语言让玩家直接写?** 答案记录在本文末尾。 ## 方案(审核通过) ### 方案 1:统一端口 **开发环境**:vite proxy 把 `/api` 和 `/socket.io` 转发到内部 `:8000`。 **生产环境**:server 直接 serve 静态文件 + mount `/api` 路由,单一进程。 ``` 浏览器 ──> :5173 (vite / server static) ├── / → index.html (SPA) ├── /api/* → boardgame.io lobby REST └── /socket.io/* → WebSocket ``` ### 方案 2:命名系统 - `joinMatch` 时传 `playerName` - 大厅显示所有玩家名字(替代 `2/2` 这种数字) - 游戏内改名字调 `POST /api/games/:name/:id/update` `{newName}` - 名字持久化到 localStorage ## 遇到什么问题 ### 问题 1:`koa-send` 装包被拒(权限) **修法**:用 Node 原生 `fs/promises` 的 `readFile` + `stat` 自己实现静态文件 serve(约 40 行),不依赖第三方包。 ### 问题 2:boardgame.io 的 `LobbyClient.request` 是 TS private,无法继承加前缀 **修法**:放弃继承,直接写一个 fetch 包装(`packages/engine/src/lobby-client.ts`),行为兼容 `LobbyClient` 但请求路径自动加 `/api` 前缀。 ### 问题 3:`ctx.URL` 是只读 getter,临时改 path 时报错 **修法**:只改 `ctx.path`(Koa 里这是 getter/setter 可写),不改 `ctx.URL`。 ### 问题 4:服务端测试 import index.ts 会触发 `server.run(8000)` 导致 EADDRINUSE **修法**:把 `mountApiPrefix` 抽到独立文件 `packages/server/src/mount-api-prefix.ts`,index.ts 和 test 都从那里 import。 ### 问题 5:vite proxy rewrite 和 server static serve 冲突 最初 vite proxy 把 `/api` 剥掉转发到 `:8000/games`,但 server 端 static serve 拦截了 `/games`(返回 index.html),导致 `/api/games` 返回 HTML。 **修法**:vite proxy **不 rewrite**,保留 `/api` 转发到 `:8000/api/games`,由 server 端的 `mountApiPrefix` 剥掉 `/api` 转给 boardgame.io router。 ### 问题 6:`pnpm dev` 在错误目录执行 之前 `cd` 到 `packages/server` 后跑 `pnpm dev` 启动的是 server(tsx watch),导致 EADDRINUSE。**教训**:跑命令前明确 `cd apps/web`。 ## 怎么解决(具体做法) ### 1. vite proxy(apps/web/vite.config.ts) ```ts server: { port: 5173, strictPort: true, host: '0.0.0.0', proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true }, // 不 rewrite '/socket.io': { target: 'http://localhost:8000', ws: true, changeOrigin: true }, }, }, ``` ### 2. server /api 挂载(packages/server/src/mount-api-prefix.ts) ```ts export function mountApiPrefix(app, router) { app.use(async (ctx, next) => { if (!ctx.path.startsWith('/api/')) return next(); const original = ctx.path; ctx.path = ctx.path.replace(/^\/api/, '') || '/'; try { await router.routes()(ctx, async () => {}); await router.allowedMethods()(ctx, async () => {}); } finally { ctx.path = original; } }); } ``` ### 3. server 静态文件 serve(packages/server/src/index.ts) 用 `fs/promises` 实现,非 `/api`、`/socket.io`、`/healthz` 的请求返回 `apps/web/dist/index.html`(SPA fallback)。 ### 4. 客户端 LobbyClient(packages/engine/src/lobby-client.ts) ```ts export function createLobbyClient(serverUrl: string, apiPrefix: string = '/api') { // fetch 包装,请求路径自动加 /api 前缀 // createLobbyClient('') → 同源 + /api(默认) } ``` ### 5. 命名系统(App.tsx + LobbyRoomList.tsx) - App header 加"你的名字"输入框,localStorage 持久化 - `LobbyRoomList` 显示 `formatPlayers()`("Alice · Bob") - 房间内"改名"按钮调 `updatePlayer` ## 成果 - Commit: 见 git log - 测试: - `pnpm ts` 6/6 - `pnpm -r test` engine 34 + server 9 - `pnpm test:e2e` 4/4 ## 用户验证路径 ```bash # 本机 + 局域网另一台 # 都开 http://192.168.5.11:5173/(同一个端口!) # 1. 输入名字 → 创建房间 → 顶部显示"在房间 X 作为 P0" # 2. 另一台输入名字 → 刷新列表 → 看到 "Alice" → 加入 P1 # 3. 大厅显示 "Alice · Bob" # 4. 点"改名" → 输入新名字 → 大厅同步 ``` ## 关于加新游戏(用户问的问题) **当前系统没有 Lua/脚本语言层。** 所有游戏都用 TypeScript 写(`packages/engine/src/games/*.ts`),编译进 server 进程,运行期不能替换。 加德州扑克需要(不是"调一下 Lua 文件"): 1. 写 `texas-holdem.ts`(reducer + 5 个 phase + 8 个 move + 隐藏信息,约 400 行 TS) 2. 写 `TexasHoldemBoard.tsx`(手牌 + 公共牌 + 筹码 + 行动按钮,约 500 行 TSX) 3. 手牌评估库(10 种牌型 + kicker,约 200 行) 4. 隐藏信息 plugin(每人只看自己手牌) 5. 注册到 server + web 6. 预估 3-4 周 如果要支持"玩家自己写游戏",需要单独做 **脚本化层**(Lua/JS 沙箱 + game-DSL + 热加载),那是另一个 1-2 个月的产品方向。详见 `docs/impl.md` Phase 3。 ## 关联 - [11-user-test-feedback-round-1.md](./11-user-test-feedback-round-1.md) — 上一轮用户反馈 - [10-cors-and-join-bugs.md](./10-cors-and-join-bugs.md) — CORS 修复(统一端口后不再需要) - lessons-learned §12.1-12.6 — 本轮的坑 - decisions D27-D28 — 统一端口、命名系统