Files
huajishe-tts/docs/impl.md
Claude c5e55150ca chore: scaffold monorepo skeleton
Initial scaffold for tts-like tabletop simulator.

- pnpm workspace: packages/{protocol,engine,ui,server} + apps/{web,desktop}
- engine: boardgame.io 0.50.2 re-exports + War (战争) game definition
- ui: placeholder React primitives (Board / Zone / Card / Token / ActionButton)
- server: Koa + boardgame.io Server relay (war game registered)
- web: Vite + React SPA wired to engine
- desktop: Tauri shell placeholder (awaiting tauri init)
- docs: implementation plan + UI primitives API spec (copied from boardgame.io/docs/new/)

Verified: pnpm install (312 deps), pnpm -r ts passes for all 4 packages + apps/web.
2026-08-16 23:25:03 +08:00

404 lines
17 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.
# 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/<name>/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 牌桌里
<Board game={game} state={G}>
<Zone id="dealer" position={[400, 50]} fan="horizontal" />
<Zone id="player-0" position={[200, 500]} fan="horizontal" hand />
<Zone id="discard" position={[700, 50]} pile faceDown />
{/* 引擎渲染每个实体 */}
<EntityRenderer state={G} />
{/* 自定义 UI按钮等 */}
<ActionButton move="hit" disabled={!canHit}></ActionButton>
<ActionButton move="stand"></ActionButton>
</Board>
```
内置组件清单v1 至少要实现):
| 组件 | 职责 |
| --- | --- |
| `<Board>` | 顶层容器,处理缩放/拖拽/坐标变换 |
| `<Card>` | 单张牌(正/背面、可拖动、可堆叠) |
| `<Token>` | 单个 token棋子、标记 |
| `<Zone>` | 容器(手牌、牌堆、弃牌堆、棋盘格) |
| `<Hand>` | 手牌区,自动扇形/网格布局 |
| `<Deck>` | 牌堆,支持翻牌/洗牌动画 |
| `<ActionButton>` | 触发 move自动根据 `ctx` 启用/禁用 |
| `<Dice>` | 投骰子(可选) |
> **关键决策**:渲染原语由"框架"提供,但**每个游戏的视觉布局**由游戏自己通过 `ui` 字段描述。这避免了"卡死到一个固定布局"的问题(也是 TTS 灵活性的来源)。
### 3.4 交互(拖拽、点击、右键)
- **拖拽**:原生 HTML5 drag-and-drop + 自定义 ghost 预览。
- **落点判定**:拖到某个 `<Zone>` 时触发 `zone.onDrop(card, zoneId)`,由游戏决定是否合法。
- **多选**Shift/Ctrl+点击,卡牌游戏中常用。
- **右键菜单**:每个 game 可注册 `contextMenu`,例如"查看牌面 / 弃牌 / 标记"。
- **撤销/回放**:复用 boardgame.io 的 log + undov1 可只做 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/<roomID>
```
最小实现(直接用 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 MVP2-3 周)
- [ ] 实现 `<Board> <Card> <Hand> <Deck> <Zone>` 基础组件
- [ ] 实现拖拽 + 落点判定
- [ ] 一个完整本地可玩的卡牌游戏(建议 **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/ # <Card> <Board> <Hand> 等 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`,开始写 `<Card>` 组件(先纯展示)。
3. **写第一个游戏**:从 War战争开始——规则简单双方翻牌比大小但能验证整套链路定义 → 引擎 → 渲染 → 交互 → 联机。
准备好之后,建议先跑 Phase 1 的 War2-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 替代