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

17 KiB
Raw Permalink Blame History

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 路由的工作
桌面壳 TauriRust 内核) 包小(~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 描述:

// 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) => { /* 例如出牌音效 */ },
  },
};

uihooks 是 boardgame.io 之外的扩展,需要在 UI 层识别并消费,但不影响核心引擎。

3.2 资产Asset协议

推荐 SVG 内嵌 / DataURL + 远程 URL 两种混合

// 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 组件)

声明式高层组件,业务层只描述"是什么"

// 用法示例:在一个 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

// 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);

部署:

  • DockerFROM node:24-alpineCOPY 打包好的 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 的"自编译"模型

# 拉源码
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 startreact-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/uipackages/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. 参考资料