# TTS-like 桌游模拟器实现规划 > 目标:一个可自定义玩法、轻视觉(2D 为主)、支持联机、可跨平台自编译分发的桌游模拟平台。 --- ## 0. 关键约束复盘 | 维度 | 约束 | | --- | --- | | 玩法定义 | 用户编写代码(类 TTS 的 Mod,但用 JS/TS) | | 联机 | 用户已有的公共域名服务器做中转 | | 平台 | 跨平台,可自编译分发(类 reVC 的"从源码构建"模型) | | 视觉 | 2D 简洁界面,重规则不重特效 | | 内容范围 | 牌类、棋类、token、区域等基本元素 | | 技术栈偏好 | Node / Web 可接受 | --- ## 1. 总体架构:四层分离 ``` ┌─────────────────────────────────────────────┐ │ UI Layer(渲染 + 交互) │ ← SVG / React ├─────────────────────────────────────────────┤ │ Game Definition(游戏定义,用户编写) │ ← TS/JS 模块,热重载 ├─────────────────────────────────────────────┤ │ Rules Engine(规则引擎,纯函数) │ ← Reducer + Flow + Plugins ├─────────────────────────────────────────────┤ │ Network Layer(联机中转) │ ← WebSocket 中继 └─────────────────────────────────────────────┘ ↑ ↑ 浏览器 / Tauri 你拥有的公共域名 ``` **为什么这么分?** - 规则引擎与 UI 解耦 → 可以独立测试、可以离线运行、可以换 UI 框架。 - 游戏定义是纯数据 + 纯函数 → 可以在浏览器/服务器任意一端执行,安全且可序列化。 - 网络层只做"转发 + 房间管理",不掺业务逻辑 → 部署、扩容、防作弊都简单。 --- ## 2. 推荐技术栈 | 层 | 选择 | 理由 | | --- | --- | --- | | 语言 | **TypeScript**(全栈) | 类型安全,前后端共享类型 | | 规则引擎 | **boardgame.io core**(本仓库已有的引擎) | 已实现 reducer / phases / stages / turn-order / plugins / log / MCTS,可直接复用,省下自写引擎的 1-2 个月 | | 前端框架 | **React 18+** | boardgame.io 已有 React 绑定,生态成熟 | | 渲染 | **SVG**(优先) + 必要时 Canvas | 卡牌天然矢量、可访问、可直接 DOM 操作;只在需要粒子/复杂动画时退化到 Canvas | | 网络 | **Socket.IO** | boardgame.io 已用,与降级、长连接、重连开箱即用 | | 中转服务器 | **Node.js + boardgame.io server** | 复用 Koa + socket.io,省下自己写 WebSocket 路由的工作 | | 桌面壳 | **Tauri**(Rust 内核) | 包小(~5 MB vs Electron ~100 MB)、安全、跨平台编译友好(macOS / Windows / Linux) | | 移动端 | PWA(先)+ Capacitor(后) | 浏览器优先,必要时可打包成原生 App | | 构建工具 | **Vite** | 启动快、HMR 稳定、产物干净 | > **关于 boardgame.io**:本仓库就是 boardgame.io 的源码。它的核心(reducer + flow + plugins + master)正好覆盖你需要的"规则引擎 + 联机中转",**不需要从零写**。建议把它作为底层依赖,而不是另起炉灶。 --- ## 3. 关键设计 ### 3.1 游戏定义格式("Game" 文件) 最自然的做法是直接用 boardgame.io 的 `Game` 定义,再加一层 UI 描述: ```typescript // games/blackjack/index.ts export const game = { name: 'Blackjack', minPlayers: 2, maxPlayers: 4, // 1. 初始状态 setup: () => ({ deck: shuffle(makeDeck()), hands: {}, // { [playerID]: Card[] } dealers: [], }), // 2. 玩法(纯函数 reducer) moves: { deal: (G, ctx) => { /* 发牌 */ return G; }, hit: (G, ctx) => { /* 要牌 */ return G; }, stand: (G, ctx) => { /* 停牌 */ return G; }, }, // 3. 阶段 / 回合(turn structure) phases: { betting: { /* 下注阶段 */ }, play: { /* 出牌阶段 */ }, reveal: { /* 摊牌 */ }, }, // 4. UI 描述(增量,自定义字段,boardgame.io 不消费它) ui: { background: '/games/blackjack/table.svg', zones: [ { id: 'hand', type: 'hand', owner: 'self' }, { id: 'dealer', type: 'zone', position: { x: 400, y: 50 } }, { id: 'discard', type: 'pile', position: { x: 700, y: 50 }, faceDown: true }, ], cardTemplate: { width: 63, height: 88, back: '/games/blackjack/card-back.svg', front: (card: Card) => renderSvgFace(card), // 返回 SVG 字符串或 DataURL }, }, // 5. 客户端钩子(可选,用于自定义按钮、动画) hooks: { onMoveEnd: (G, ctx, moveName) => { /* 例如出牌音效 */ }, }, }; ``` > `ui` 和 `hooks` 是 boardgame.io 之外的扩展,需要在 UI 层识别并消费,但不影响核心引擎。 ### 3.2 资产(Asset)协议 推荐 **SVG 内嵌 / DataURL** + **远程 URL 两种混合**: ```jsonc // games//assets.json { "cards": [ { "id": "AS", "name": "黑桃A", "image": "https://cdn.example.com/cards/AS.svg", "fallback": "data:image/svg+xml;base64,..." } ], "tokens": [ { "id": "red-meeple", "image": "data:image/svg+xml;base64,..." } ] } ``` - **SVG 优先**:缩放无锯齿、文件小(一张卡 1-5 KB),可直接 inline。 - **DataURL**:用于离线/单文件游戏(一个 `.json` 描述一个完整游戏)。 - **CDN/远程 URL**:用于大型素材库。 ### 3.3 渲染原语(UI 组件) 声明式高层组件,业务层只描述"是什么": ```tsx // 用法示例:在一个 Blackjack 牌桌里 {/* 引擎渲染每个实体 */} {/* 自定义 UI(按钮等) */} 要牌 停牌 ``` 内置组件清单(v1 至少要实现): | 组件 | 职责 | | --- | --- | | `` | 顶层容器,处理缩放/拖拽/坐标变换 | | `` | 单张牌(正/背面、可拖动、可堆叠) | | `` | 单个 token(棋子、标记) | | `` | 容器(手牌、牌堆、弃牌堆、棋盘格) | | `` | 手牌区,自动扇形/网格布局 | | `` | 牌堆,支持翻牌/洗牌动画 | | `` | 触发 move,自动根据 `ctx` 启用/禁用 | | `` | 投骰子(可选) | > **关键决策**:渲染原语由"框架"提供,但**每个游戏的视觉布局**由游戏自己通过 `ui` 字段描述。这避免了"卡死到一个固定布局"的问题(也是 TTS 灵活性的来源)。 ### 3.4 交互(拖拽、点击、右键) - **拖拽**:原生 HTML5 drag-and-drop + 自定义 ghost 预览。 - **落点判定**:拖到某个 `` 时触发 `zone.onDrop(card, zoneId)`,由游戏决定是否合法。 - **多选**:Shift/Ctrl+点击,卡牌游戏中常用。 - **右键菜单**:每个 game 可注册 `contextMenu`,例如"查看牌面 / 弃牌 / 标记"。 - **撤销/回放**:复用 boardgame.io 的 log + undo(v1 可只做 undo)。 ### 3.5 网络协议(CS 模式,服务器权威) ``` [Client A] [Relay Server] [Client B] │ │ │ │── MAKE_MOVE('hit', []) ─────────>│ │ │ │ apply reducer (服务器权威) │ │ │── SYNC(state) ─────────────────>│ │<── SYNC(state) ──────────────────│ │ │ │ │ │ │<── MAKE_MOVE('stand', []) ──────│ │<── SYNC(state) ──────────────────│── SYNC(state) ─────────────────>│ ``` **消息类型**(v1 最小集合): | 方向 | type | 含义 | | --- | --- | --- | | C → S | `JOIN` | 加入房间(带 roomId / playerID / credentials) | | C → S | `LEAVE` | 离开 | | C → S | `MAKE_MOVE` | 触发一个 move | | C → S | `GAME_EVENT` | 客户端事件(如聊天、准备) | | S → C | `SYNC` | 全量状态同步 | | S → C | `PATCH` | 增量状态(v2,性能优化时再加) | | S → C | `CHAT` | 聊天/系统消息 | | S → C | `ERROR` | 错误(如非法 move) | > 直接复用 boardgame.io master 已经定义的协议就行,**不要发明新协议**——master 已经处理了认证、状态快照、断开重连等。 ### 3.6 中转服务器 部署在你的公共域名上: ``` wss://play.example.com/ ``` 最小实现(直接用 boardgame.io server): ```typescript // server/index.ts import { Server, Origins } from 'boardgame.io/server'; import { MyGame } from './games/index.js'; const server = Server({ games: [MyGame /* ... */], origins: [Origins.WILDCARD], // 开发时,生产收紧 }); server.run(8000); ``` 部署: - **Docker**:`FROM node:24-alpine`,COPY 打包好的 dist,启动 `node dist/cjs/server.js`。 - **反向代理**:Nginx/Caddy 终止 TLS,转发到 Node。 - **持久化**:v1 用内存;v2 接 Redis(房间恢复)/ Postgres(历史回放)。 - **房间清理**:30 分钟无活动自动销毁(释放资源)。 > 你已有的服务器是公共域名的,意味着 TLS 证书应该已有(Let's Encrypt / Cloudflare)。Socket.IO 默认走 WSS。 ### 3.7 跨平台分发 ``` ┌──────────────────────────┐ │ Vite build → dist/web │ └────────────┬─────────────┘ │ ┌──────────────────┼──────────────────┐ │ │ │ ▼ ▼ ▼ 浏览器直接访问 Tauri 壳(macOS) Tauri 壳(Win/Linux) (https://app.xx) .dmg / .app .msi / .AppImage ``` **类 reVC 的"自编译"模型**: ```bash # 拉源码 git clone https://github.com/you/tts-like cd tts-like corepack enable pnpm install # 跑起来(开发) pnpm dev # 启动 vite dev server + 本地 socket.io server # 打包桌面 pnpm tauri:dev # 开发模式(桌面壳 + 热重载前端) pnpm tauri:build # 生产构建,输出原生安装包 ``` Tauri 的 `tauri:build` 会按当前平台产出对应安装包;用 GitHub Actions 做 CI matrix 就能产出 macOS / Windows / Linux 三平台产物。 --- ## 4. 实施阶段(建议时间线) ### Phase 0:选型与验证(1-2 天) - [ ] 在本仓库跑一个 boardgame.io 的简单示例(tic-tac-toe) - [ ] 跑通 `pnpm start`(react-web example),确认开发链路 OK - [ ] 决定 SVG vs Canvas(建议先全 SVG) - [ ] 决定 Tauri vs Electron(建议 Tauri) ### Phase 1:本地 2D MVP(2-3 周) - [ ] 实现 ` ` 基础组件 - [ ] 实现拖拽 + 落点判定 - [ ] 一个完整本地可玩的卡牌游戏(建议 **War / 战争** 或 **Snap**——规则极简,专注验证交互) - [ ] 一个本地棋类(Morris / Nine Men's Morris——简单,验证棋盘交互) - [ ] 截图、键盘可访问性测试 ### Phase 2:联机(1-2 周) - [ ] 部署 boardgame.io server 到你的公共域名 - [ ] 联调:两台电脑能互相看到对方操作 - [ ] 实现房间码(6 位短码)+ 二维码分享 - [ ] 断线重连(boardgame.io 自带,需测试边界) - [ ] 错误反馈(非法 move 提示) ### Phase 3:游戏生态(2-3 周) - [ ] 游戏定义文件**热重载**(开发模式改文件不用刷新) - [ ] 游戏选择 UI(左/右栏列出可用游戏) - [ ] 资产加载器(DataURL / 远程 URL 双模式) - [ ] 至少 3 个示例游戏(War / Blackjack / 一款棋类) - [ ] 写一篇 `docs/game-authoring.md`,告诉别人怎么写一个新游戏 ### Phase 4:跨平台打包(1 周) - [ ] Tauri 壳接入 - [ ] 自动更新(v2 再说) - [ ] GitHub Actions 三平台构建 ### Phase 5:可选打磨 - [ ] 回放系统(基于 boardgame.io log) - [ ] 撤销(`undo` move) - [ ] 观战模式 - [ ] 语音/视频(WebRTC,不在你的服务器上跑 SFU,用 LiveKit / Daily / 自建 mediasoup) - [ ] 游戏市场(用户上传游戏) --- ## 5. 项目结构(建议) 如果你要单仓多包(推荐),结构如下: ``` tts-like/ ├── packages/ │ ├── engine/ # boardgame.io core 的再封装 + UI 原语 │ ├── ui/ # 等 React 组件 │ ├── protocol/ # 共享类型(消息、State schema) │ └── server/ # 中转服务器(基于 boardgame.io) ├── games/ # 用户游戏(每个游戏一个目录) │ ├── war/ │ ├── blackjack/ │ └── chess/ ├── apps/ │ ├── web/ # Vite + React SPA │ └── desktop/ # Tauri 壳 ├── docs/ │ ├── game-authoring.md │ └── deployment.md ├── pnpm-workspace.yaml └── package.json ``` 如果你想**更激进地复用本仓库**,可以把它改成 monorepo 的根: - `src/` → `packages/engine` - 新增 `packages/ui`、`packages/server` - 保留 `examples/` 作为 `apps/` --- ## 6. 风险与权衡 | 风险 | 影响 | 缓解 | | --- | --- | --- | | 拖拽实时同步延迟 | 体验卡顿 | v1 不做"实时拖拽广播",只在落点时同步;v2 再加 CRDT | | 大量 SVG 资产 | 首屏慢 | 按需加载 + DataURL 内嵌小图、CDN 大图 | | 自定义规则的沙箱 | 用户代码搞坏服务 | v1 信任用户代码(游戏定义是可信方写的);v2 加 Web Worker / iframe 隔离 | | 服务器被刷 | 资源耗尽 | 限流(每 IP 每分钟最多创建 N 个房间)、房间 TTL | | 跨平台打包陷阱 | Tauri 在不同平台签名/公证麻烦 | v1 用 Tauri 默认(无签名),发"自己用"就好;正式分发再处理签名 | | boardgame.io 维护活跃度 | 引擎 bug 无人修 | 本仓库就是它,主分支活跃;自有问题可改 | --- ## 7. 决策记录(已锁定 ✅) 下列三项决策已确定(2026-08-16),后续不再讨论: 1. ✅ **基础引擎**:使用 boardgame.io(不另写 reducer 引擎) 2. ✅ **桌面壳**:Tauri(不选 Electron) 3. ✅ **游戏定义格式**:沿用 boardgame.io `Game` + 自定义 `ui` 字段(不另创 DSL) 锁定理由(保留备查): - boardgame.io 已经覆盖了 reducer + flow + plugins + server,省下大量造轮子; - Tauri 包小、自编译友好,符合"reVC 式"分发; - 沿用 boardgame.io Game 意味着示例代码可以直接借用现有 50+ 游戏样例。 --- ## 8. 下一步 立即可做的三件事: 1. **跑通示例**:`pnpm start`,玩一下 `examples/react-web` 的 tic-tac-toe / chess,确认开发链路 OK。 2. **新建包**:在 `packages/` 下加一个 `ui.ts`,开始写 `` 组件(先纯展示)。 3. **写第一个游戏**:从 War(战争)开始——规则简单(双方翻牌比大小),但能验证整套链路:定义 → 引擎 → 渲染 → 交互 → 联机。 准备好之后,建议先跑 Phase 1 的 War,2-3 周内拿到一个**本地能玩**的最小版本,再决定是否继续投入联机和打包。 --- ## 9. 参考资料 - **boardgame.io 文档**:本仓库 `docs/documentation/`,或 https://boardgame.io/documentation/ - **Tauri 跨平台打包**:https://tauri.app/ - **SVG vs Canvas 选型**:卡牌/UI 选 SVG;粒子/复杂效果选 Canvas(参考 https://css-tricks.com/when-to-use-svg-vs-canvas/) - **Socket.IO 协议**:复用 boardgame.io master 即可,无需自创 - **TTS 的 Modding 模型**:https://api.tabletopsimulator.com/ (Lua 脚本)—— 我们用 JS/TS 替代