# 14 · 双升(DoubleUp)游戏 + 单端口一键启动 / 部署 ## 做什么 1. **新增双升(两副牌升级 / 拖拉机 / 80 分)游戏**:完整规则引擎 + 牌桌 UI + 联机。 - 纯逻辑在 `packages/engine/src/games/double-up-core.ts`(与 boardgame.io 无关,便于单测)。 - 游戏定义在 `packages/engine/src/games/double-up.ts`(boardgame.io `Game` + `ui` 字段)。 - 牌桌 UI 在 `apps/web/src/DoubleUpBoard.tsx`(自定义 Board,非通用 ``)。 - 规则选型见 `docs/double-up/00-rules.md`:V1 含大小王(108) / V2 2 为常主 / V3 翻第四张兜底 / V4 甩牌罚分 / V6 抠底封顶 64 倍。 - E2E 实测脚本在 `apps/web/e2e/double-up-full-game.spec.ts`。 2. **新增 `start.sh`**:单端口(`PORT=5173`)一键启动——Koa 同时 serve 前端页面 + `/api` REST + `/socket.io` WS,浏览器直连 `:5173`,无需 Vite 代理。 3. **玩家名字 server 权威化**:join 时由 server 把登录用户名写入 `G.playerNames[playerID]`,不再依赖客户端 `setName` 是否触发。 4. **LobbyRoomList 支持固定人数**:双升固定 4 人,创建房间不再显示人数下拉(`fixedSeats` prop)。 ## 怎么解决(具体做法) ### 1. 双升引擎结构 - `double-up-core.ts`:所有确定性逻辑(主牌判定 `isTrump`、牌序 `compareCards`/`trumpWeight`、牌型解析 `parsePlay`、跟牌校验 `validateFollow`、甩牌全大 `isThrowAllBig`、赢墩 `trickWinnerIndex`、抠底倍数 `kouchudiMultiplier`、计分升级 `levelDelta`)。无随机、无 `Date`、无副作用,便于单测与 playerView 多端一致。 - `double-up.ts`:`Game` 定义 + 阶段机 `waiting → deal → bottom → play → score`。关键 move: - `startGame`:房主触发,原子 `dealAll` 一次发满 100 张(每家 25),剩 8 张作底;**这是修掉联机同步卡死的关键**——早期一张张 `dealOne` 发会产生大量连续 move,触发 boardgame.io 客户端 stateID 竞态,其他客户端收不到更新。 - `declareTrump` / `confirmTrump`:发牌阶段亮主/反主(优先级 单级牌<对级牌<对小王<对大王),四家全确认后 `resolveTrump` 定主花色 + 庄家 + 庄家收 8 底牌。 - `setBottom`:庄家扣 8 张隐藏底牌。 - `playCards`:单/对/拖拉机/甩牌;首出若 `mixed` 且同花色副牌则判为甩牌,调用 `isThrowAllBig` 校验,失败罚分并强制出最小牌。 - 一墩结算在 `applyPlay`:四家出齐(按人数计,不按牌数)判赢家,赢家收走全部桌面牌;`currentTrick` 为扁平每牌列表,`trickPlays` 为每玩家一手。 ### 2. 跟牌校验(甩牌修复点) `validateFollow` 对 `mixed`(甩牌领出,如 AAK 三张同花)单独分支:手里该类别有牌就只需出 ≥1 张同花色牌,**不强制对子/拖拉机,也不要求张数一致**;断门则任意出。该修复让甩牌后其他家可正常跟单张同花色牌(此前误落到 pair/tractor 逻辑被拒)。 ### 3. 单端口 `start.sh` 根目录 `start.sh`(已 `chmod +x`): ```sh ./start.sh ``` 行为: - 加载 nvm + pnpm,缺依赖自动 `pnpm install`。 - 若 `apps/web/dist` 不存在,自动先 `pnpm --filter @tts-like/web build`(单端口 serve 需要该产物)。 - 清掉残留的 5173/8000 占用。 - `PORT=5173 pnpm --filter @tts-like/server dev` 后台启动(tsx watch,开发热重载)。 - 等 `:5173/healthz` 就绪后打印访问地址;Ctrl-C 连同派生进程一起清理。 **为什么映射 5173 即可**:`packages/server/src/index.ts` 的 `PORT` 默认 8000,但显式支持 `PORT=5173`;且 `serveStatic` 在 `apps/web/dist` 存在时自动 serve 整个前端。所以 Koa 单一进程在 5173 上同时提供页面 + `/api` + `/socket.io`,浏览器同域直连,**无需 Vite 代理**。这契合 `CODEBUDDY.md`「统一端口」一节的设计。 ### 4. 玩家名字 server 权威 `packages/server/src/rooms.ts` 的 join 成功后,`dispatchMove(server, games, matchID, playerID, credentials, 'setName', [playerID, username])` 把登录用户名写进 `G.playerNames`。这样无论客户端是否触发 `setName` 都能看到真实名字,而非 `玩家0/1/2/3`。best-effort:失败仅影响显示。 ### 5. 固定人数大厅 `LobbyRoomList` 新增 `fixedSeats` prop:`App.tsx` 给 `double-up` 传 `fixedSeats: 4`,创建房间时不显示人数下拉,直接用 4。`onSelect` 回传的 `numPlayers` 也用该固定值。 ## 部署(公网暴露,不含 frp 配置) > frp 的具体配置由用户另行管理;这里只记录服务器侧与客户端侧的非 frp 部分。 **服务端**(生产单端口): ```sh pnpm --filter @tts-like/web build # 生成 apps/web/dist PORT=5173 ALLOWED_ORIGINS=https://你的域名 pnpm --filter @tts-like/server start # 或开发/调试:直接 ./start.sh(占 5173) ``` - 必须设 `ALLOWED_ORIGINS` 为浏览器将使用的公网 origin(留空走默认 LAN 段,公网会被 CORS 拒绝,Socket.IO 握手也失败)。 - TLS 终止在反代(Nginx/Caddy)或隧道层;整链 http 或整链 https 保持一致,避免 `wss://` 与 `ws://` 混用被浏览器拦。 - 数据库 `data/*.db`(根目录 SQLite)是 server 运行时数据,已在 `.gitignore` 的 `packages/server/data/*.db` 覆盖思路下管理——注意根目录 `data/` 当前未忽略,提交时需排除(见 commit 说明)。 **客户端(浏览器)**:零配置。打开 `http://<公网地址>:5173`(或隧道给的 https 域名)即可。`createLobbyClient('')` 走同域相对路径,`/socket.io` 同源。 **局域网联机**:同 dev 模式,`http://<本机内网IP>:5173/` 直接可用(origins 默认含 LAN 段)。 ## 成果 - 新增文件:`double-up-core.ts` / `double-up.ts` / `DoubleUpBoard.tsx` / `double-up.test.ts` / `double-up-sim.test.ts` / `e2e/double-up-full-game.spec.ts` / `start.sh` / `docs/double-up/*`。 - 修改:`App.tsx`(注册双升 + 待机 UI 简化)、`vite.config.ts`(allowedHosts 更新)、`server/index.ts`(注册 DoubleUp)、`rooms.ts`(setName 权威化)、`LobbyRoomList.tsx`(fixedSeats)。 - 测试:双升引擎单测 58 例通过;全引擎套件 174 例通过;`tsc --noEmit`(web + engine)干净。 ## 用户验证路径 ```bash ./start.sh # 单端口 :5173 # 浏览器开 http://localhost:5173 # 1. 选「双升」→ 创建房间(固定 4 人)→ 4 个客户端加入 # 2. 房主开始 → 发牌 → 亮主/确认 → 庄家扣底 → 出牌(单/对/拖拉机/甩) # 3. 整局打完自动结算 + 升级,进入下一局 ``` ## 关联 - [12-unified-port-and-player-names.md](./12-unified-port-and-player-names.md) — 统一端口基础(vite proxy + 静态 serve) - [13-holdem-game.md](./13-holdem-game.md) — 同类「引擎 + Board + 可变人数大厅」范式 - `docs/double-up/00-rules.md` — 双升规则拍板 - `CODEBUDDY.md`「统一端口联网」一节 — 单端口同源设计说明