可插拔 13 款实时棋牌对战平台实践:房间容器、Plugin SDK 与幂等账本

说明 :本文介绍的是开源式技术实践 / 学习项目「Chess & Cards」。平台使用虚拟筹码记分,不提供充值、提现、兑换或任何资金闭环;文中涉及的牌类/棋类玩法均为规则与状态机演练,不构成博彩服务。

写在前面

多人实时棋牌系统常见的工程痛点很集中:

  1. 规则膨胀:每加一款游戏就改房间服,耦合越来越重;
  2. 状态不一致:客户端本地推演与服务端结果对不上;
  3. 账务重复入账:重连、重试、重复结算把余额打穿;
  4. 回归成本高:改一处规则,十几款玩法都要手工点一遍。

「Chess & Cards」用一套 大厅 + 房间容器 + 游戏插件 的结构把这几件事拆开:大厅只负责账号/任务/榜单/钱包,房间服只负责座位、快照、重连与结算上报,具体玩法全部下沉到独立插件 。当前内置 13 款 游戏(扑克、记分竞技、棋类),新增一款游戏通常只需要实现 GamePlugin、注册一行、补一个前端渲染组件。

图 0:整体架构------大厅前端通过 HTTP 访问 API,通过 WebSocket 进入房间容器;游戏规则以插件形式挂载。

一、Monorepo 怎么切

仓库用 pnpm-workspace 管理:

text 复制代码
packages/protocol     # 前后端共享协议与类型(唯一来源)
packages/game-sdk     # GamePlugin 契约、牌型引擎
packages/auth         # HS256 JWT
apps/api-server       # 大厅 API(Fastify)
apps/lobby-web        # 大厅前端(React 18 + Vite 6)
services/room-server  # WebSocket 房间容器
services/games/*      # 13 个独立游戏插件
infra/                # systemd + nginx 片段 + deploy.sh
scripts/              # e2e-smoke / play-all-games
tools/gm-cli          # 本地 GM 运维工具

根 package.json 提供 dev:api / dev:room / dev:web / e2e。未配置数据库时 API 自动降级为内存仓储,本地可以零依赖跑通全流程。

图 1:大厅。左侧按「全部热门 / 记分竞技 / 经典扑克 / 智谋对弈」分类;中部为大卡入口;顶栏显示虚拟筹码余额与福利、榜单、助手入口。

选台页把「极速匹配」和「创建专属房间」拆开,空台状态也有明确引导:

图 2:斗地主选台。活跃台为 0 时提示可极速匹配或创建专属房间。

二、统一协议:客户端只发意图

协议集中在 @cc/protocol。WebSocket 消息统一信封:

ts 复制代码
export interface Envelope<T = unknown> {
  t: string;   // 消息类型
  d: T;        // 负载
  seq?: number;
  ts: number;
}

客户端消息类型包括 client:hello、client:join_room、client:ready、client:intent、client:reconnect、client:add_bot 等;服务端对应下发 server:room_view、server:snapshot、server:settlement、server:wallet、server:error。

关键约束是:客户端只上报意图(intent),不擅自改本地权威状态。合法性校验、发牌、比牌、结算全部在服务端完成;快照按观察者裁剪(例如只把自己的手牌发给自己)。

三、房间容器:与玩法无关的那一层

GameRoom 是通用容器:座位、准备、开局、200ms tick、断线宽限、快照脏标记、结算一次性上报。它不包含任何具体游戏规则。

插件通过注入的 RoomContext 做事,禁止直接碰存储:

ts 复制代码
export interface RoomContext {
  readonly config: RoomConfig;
  readonly players: readonly PlayerContext[];
  broadcast(t: string, d: unknown): void;
  sendTo(playerId: string, t: string, d: unknown): void;
  markDirty(): void;
  settle(input: SettlementInput): void;          // 每局只允许一次
  charge(playerId: string, amount: number): Promise<string>;
  refund(playerId: string, ref: string): Promise<void>;
  beginRound(): void;
}

对「21 点 / 骰宝」这类常驻记分玩法,容器还会维护平台台(persistent room):无人时也按时间轴开局,大厅列表可展示近期走势。

四、GamePlugin:加一款游戏要写什么

契约在 @cc/game-sdk:

ts 复制代码
export interface GamePlugin {
  readonly apiVersion: number;
  readonly meta: GameMeta;
  readonly commands: readonly string[];  // 指令白名单
  onRoomCreate(ctx: RoomContext): void;
  onRoundStart?(ctx: RoomContext): void;
  onPlayerJoin(ctx: RoomContext, player: PlayerContext): void;
  onIntent(ctx: RoomContext, playerId: string, intent: GameIntent): void | Promise<void>;
  getSnapshot(ctx: RoomContext, viewerId: string | null): GameSnapshot;
  getCurrentActor?(ctx: RoomContext): string | null;
  botIntent?(ctx: RoomContext, playerId: string): GameIntent | null;
}

BaseGamePlugin 提供空实现;容器加载时校验 apiVersion,不兼容直接拒绝,避免协议漂移。

注册表集中在一行一行挂载:

ts 复制代码
export function registerBuiltinGames(manager: RoomManager): void {
  manager.register(() => new GomokuPlugin());
  manager.register(() => new DoudizhuPlugin());
  manager.register(() => new XiangqiPlugin());
  // ... 其余插件
  for (const gameId of PERSISTENT_GAMES) {
    manager.ensurePlatformRooms(gameId, PLATFORM_ROOMS_INITIAL);
  }
}

以斗地主为例:onRoundStart 用房间 seed 洗牌发牌,onIntent 只接受 play / pass,牌型比较复用 SDK 的 parseCombo / canBeat,终局调用 ctx.settle。

图 3:斗地主对局。三人桌、手牌扇形排布,机器人陪玩入座;操作区提供提示与出牌。

棋类插件走同一套生命周期,只是快照变成棋盘矩阵与走子合法性:

图 4:中国象棋。棋盘按可用高度等比缩放;顶栏统一展示局数、在线人数与虚拟筹码。

记分竞技类(如 21 点)强调「下注阶段倒计时 + 发牌区 + 走势侧栏」三栏布局,用来验证常驻台的时间轴与即时扣款:

图 5:21 点常驻记分台。下注阶段倒计时、虚拟筹码档位、右侧历史局走势。

五、虚拟筹码账本:只追加、可幂等

筹码在本项目中只是记分单位(界面里也称「欢乐豆」),没有购买、提现或实物兑换路径。

账务设计要点:

  • 下注即时扣款 :插件调 charge,大厅侧原子校验余额并记账,返回幂等凭证;撤注走 refund(凭证)。
  • 结算净盈亏 :本金已扣时,插件上报的 delta 按净结果计算;容器把 settlementId 报到大厅。
  • 幂等键:任务奖励、助手救济、结算入账都带唯一业务键,重复请求不会多发。
  • 失败补偿:结算上报最多重试 3 次,仍失败记日志等待人工补偿(生产可换成 MQ + outbox)。

钱包客户端示意:

ts 复制代码
export function createWalletClient(config: RoomServerConfig): WalletClient {
  return async (input) => {
    const res = await fetch(`${config.apiBaseUrl}/internal/wallet`, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        'x-internal-key': config.internalKey,
      },
      body: JSON.stringify(input), // playerId / amount / action / bizId
    });
    // ... 解析 balance
  };
}

排行榜按虚拟筹码余额排序,前若干名挂演示称号(每日结算);任务中心用签到、完成局数、体验多款游戏等条件发记分奖励:

图 6:财富排行榜。按虚拟筹码余额排序,当前用户行高亮。

图 7:福利中心。每日签到 / 完成对局 / 救济金等任务,奖励写入幂等账本。

六、机器人与牌桌助手

机器人陪玩:房主可添加 bot,人满自动开局。决策链路是:

text 复制代码
回合到达 →(可选)大模型建议 → 房间统一合法性校验 → 失败则回退插件 botIntent

模型只输出 JSON 建议动作,非法动作会被丢弃;未配置模型密钥时纯用规则 AI,对局不会卡住。

牌桌助手 :大厅侧聊天问答,可解释规则、任务、排行榜。涉及「救济筹码」时,模型只能给意向标记,真正发放由服务端校验余额门槛、每日次数,并用幂等键入账------模型无权直接改余额。

图 8:牌桌助手。快捷问题 + 对话窗口;救济发放逻辑固定在服务端。

鉴权方面,@cc/auth 实现极简 HS256 JWT;注册/登录带图形验证码,并做 IP / 账号维度的尝试限流(细节见 apps/api-server/src/security.ts)。

七、测试与部署

测试分层:

脚本 作用
game-smoke.ts 不依赖网络,直接驱动插件规则
e2e-smoke.ts 注册 → 匹配 → 对局 → 结算 → 任务 → 榜单
play-all-games.ts 逐款建房、加机器人、验证开局

部署目标形态是 Ubuntu + Node 20 + PostgreSQL + nginx。infra/nginx-lobby.conf 是可 include 的片段:静态资源走 /lobby/,API 走 /lobby/api/,WebSocket 走 /lobby/ws,方便与同机其他站点共存。cc-api / cc-room 两个 systemd 单元由 deploy.sh 幂等构建并重启。日常运维可通过本地 GM工具(打包上传、服务状态、筹码补偿、公告),GM 接口单独鉴权,Web 端不暴露后台页。

八、新增一款游戏的最小路径

  1. 复制 services/games/gomoku 为新目录,实现 GamePlugin;
  2. 在 plugins.ts 注册,并在 room-server/package.json 加依赖;
  3. 前端 apps/lobby-web/src/games/ 增加渲染组件,在 Room.tsx 分发;
  4. 跑 game-smoke / play-all-games 冒烟后部署。

牌型解析、比较、洗牌种子等通用能力放在 packages/game-sdk/src/cards.ts,多数扑克类可直接复用。

小结

这套实现验证了几条可复用的工程选择:

  • 容器与规则分离:房间生命周期稳定,玩法可独立发版;
  • 意图 + 快照:服务端权威,客户端只渲染观察者视图;
  • 只追加账本 + 幂等键:重试与重连不破坏余额;
  • 规则 AI 兜底:机器人与 LLM 建议都不得绕过合法性校验;
  • 脚本化回归:13 款玩法靠自动化冒烟而不是纯手工点点点。

若你也在做实时对战或多玩法大厅,可以把「协议包 + 插件契约 + 幂等账本」这三块先立住,再往上堆 UI 与运营功能。项目定位始终是技术学习与架构演示,虚拟筹码仅用于记分。

相关推荐
FungLeo41 分钟前
成为全栈·Vue 管理后台篇·Vue3 + Vite + Naive UI:后台工程基座怎么搭
前端·vue.js·ui·typescript·vite·前端工程化·biome
FungLeo1 小时前
成为全栈·Vue管理后台篇·<scriptsetup>:把页面拆成可读、可测的 Vue 组件
前端·javascript·vue.js·typescript·前端组件化·composition api·vue sfc
SmalBox1 天前
09-03-生态-与GameFramework-xAsset等框架集成
unity3d·游戏开发
SmalBox2 天前
09-02-生态-小游戏平台适配
unity3d·游戏开发
ZzT2 天前
vibe coding 做小游戏·玩法重做篇:只留自走棋、去掉羁绊,用电脑自动对局调难度
ai编程·游戏开发
SmalBox2 天前
09-01-生态-YooAsset+HybridCLR热更新方案
unity3d·游戏开发
theMuseCatcher3 天前
Vue3导航菜单(Menu)
typescript·vue3·less
theMuseCatcher3 天前
Vue3评论(Comment)
typescript·vue3
房西杭4 天前
AttrMgr 属性管理器(传奇MMO Lua服务端)
游戏开发