Skip to main content
Glama
README.md
# bot-game-mcp

一个遵循标准 **MCP(Model Context Protocol)** 的 **QQ 群聊桌游引擎**,内置狼人杀、谁是卧底、大话骰和数字炸弹四款游戏。

- **程序主导**:座位、身份、词语、骰子、发言顺序、阶段流转、超时托管、结算和胜负,全部由引擎里的程序决定。
- **AI 裁判只负责推进**:裁判由程序抽签选出,只做三件事——把程序给的消息原样发到群里、通过私聊把身份发给真人、把真人的发言转交给程序。它看不到 AI 玩家的身份,也不能改任何结果。
- **两种玩法**:可以全员都是 bot,也可以 bot 和真人同桌。真人在群里或私聊发「投3」「验5」「过」这样的自然语言,由程序解析。
- **兼容主流 agent**:支持 Streamable HTTP、旧版 HTTP+SSE 和 stdio(含桥接模式),能接入 Claude Code、Claude Desktop、Cursor、Cherry Studio、OpenAI Agents SDK,以及各类支持 MCP 的 QQ bot 框架。

```mermaid
flowchart LR
  subgraph QQ["QQ 群 / 私聊"]
    H["真人玩家"]
    A["AI 玩家 bot"]
    R["AI 裁判 bot<br/>(程序抽签产生)"]
  end
  E["bot-game-mcp 引擎<br/>身份 · 流程 · 结算 · 胜负"]
  H -- "群消息 / 私聊裁判" --> R
  R -- "relay_human_message(原文转交)" --> E
  E -- "referee_step → 待发消息" --> R
  R -- "发群消息 / 私聊发身份" --> H
  A <-- "get_my_view / speak / act" --> E
```

## 目录

- [游戏](#游戏)
- [快速开始](#快速开始)
- [一局游戏是怎么跑起来的](#一局游戏是怎么跑起来的)
- [MCP 接口一览](#mcp-接口一览)
- [配置](#配置)
- [安全与公平](#安全与公平)
- [已知限制](#已知限制)
- [开发](#开发)

更多文档:[游戏规则与选项](docs/GAMES.md) · [MCP 接口参考](docs/TOOLS.md) · [接入各类客户端与 QQ 框架](docs/CLIENTS.md) · [架构与新增游戏](docs/ARCHITECTURE.md) · [开发进度与测试记录](docs/PROGRESS.md)

## 游戏

| 类型 | 名称 | 人数 | 简介 |
|---|---|---|---|
| `werewolf` | 狼人杀 | 6~16 | 狼人夜里刀人,好人白天发言投票。角色有预言家、女巫、猎人、守卫和白痴,6~12 人有预设板子,也可以自定义。 |
| `undercover` | 谁是卧底 | 4~12 | 每人只拿到一个词,卧底的词和大家的相近但不同,没人知道自己是不是卧底。内置 250 组词。 |
| `liars_dice` | 大话骰 | 2~8 | 每人摇骰藏着,轮流叫「N 个 F 点」,不信就开。1 点可以当任意点数,叫错的人扣命。 |
| `number_bomb` | 数字炸弹 | 2~20 | 轮流猜数,范围越缩越小,猜中炸弹就爆炸。规则简单,适合暖场。 |

完整规则、开局选项(`create_game` 的 `options`)和给 AI 玩家的提示见 [docs/GAMES.md](docs/GAMES.md)。运行时也能用 `get_rules` 工具或 `botgame://rules/{game_type}` 资源查看。

## 快速开始

需要 Node.js 22 或更高版本。

### 1. 安装与构建

```bash
git clone https://github.com/Gengetau/bot-game-mcp.git
cd bot-game-mcp
npm ci
npm run build
npm link            # 可选:之后可以直接用 bot-game-mcp 命令,否则用 node dist/cli.js
```

### 2. 先离线看一局(不需要任何 agent)

```bash
# 9 人狼人杀,其中 2 名「真人」,程序用随机策略代打,并按 QQ 消息的样子打印整局过程
bot-game-mcp simulate werewolf --players 9 --humans 2

bot-game-mcp simulate undercover --players 6 --humans 1 --seed 7   # 固定种子,结果可复现
bot-game-mcp simulate liars_dice --players 4 --cq                  # 消息输出成 CQ 码格式
```

模拟器会把真人的操作写成自然语言(「投3」「刀5」「过」……),交给与线上相同的解析逻辑处理。输出示例:

```text
💬 [群] 🎲【狼人杀】对局 WW-CVVX 开始!
    本局裁判:裁判(AI,由程序抽签产生,只负责推进流程,不参与游戏、不决定任何结果)
    ...
🔒 [私聊 → 真人1] 🎴 你的身份是【猎人】(好人阵营)。
    技能:被狼人刀杀或被放逐出局时,可开枪带走一名玩家;被女巫毒死则不能开枪。
💬 [群] 🌙 第1夜降临,天黑请闭眼。有夜间能力的玩家请行动(限时 60 秒)。
💬 [群] ☀️ 第1天,天亮了。昨夜死亡的玩家:2号 AI-4、5号 AI-7。
...
💬 [群] 🏁 游戏结束!获胜方:好人阵营
```

### 3. 启动引擎

```bash
bot-game-mcp init-config     # 生成 botgame.config.json,里面已经填好随机密钥
# 编辑 botgame.config.json:把 agents 里的 id/name 改成各个 bot 的 QQ 号和昵称
bot-game-mcp serve           # 默认监听 http://127.0.0.1:7700/mcp
```

每个 bot(无论将来当玩家还是裁判)在 `auth.agents` 里各占一行。它的身份由密钥决定,不能冒充别人。只想在本机试玩的话,也可以不写配置直接 `bot-game-mcp serve`,这时是 `open` 模式,agent 调用 `login` 工具认领 QQ 号即可。

### 4. 把 agent 接上来

以 Claude Code 为例:

```bash
claude mcp add --transport http bot-game http://127.0.0.1:7700/mcp \
  --header "Authorization: Bearer <这个 bot 的密钥>"
```

只支持 stdio 的客户端可以用桥接模式(`bot-game-mcp bridge --url … --key …`)。其他客户端和 QQ 框架的接法见 [docs/CLIENTS.md](docs/CLIENTS.md),配置样例在 [examples/](examples/)。

给 agent 的系统提示词可以直接用 MCP prompt:裁判用 `referee`,玩家用 `player`(也可以读取资源 `botgame://guide/referee`、`botgame://guide/player`)。

## 一局游戏是怎么跑起来的

### 分工

| 事项 | 谁负责 |
|---|---|
| 座位、身份、词语、骰子、炸弹数字 | **程序**(`crypto.randomInt`) |
| 谁当裁判 | **程序**抽签:优先选报名为 `referee` 的 AI,否则在报名为 `either` 的 AI 里随机选 |
| 发言顺序、阶段流转、限时、超时托管 | **程序** |
| 投票、技能、出价的结算和胜负判定 | **程序** |
| 把消息发到群里、私聊给真人发身份和提示 | **AI 裁判**(原样转发 `referee_step` 返回的消息) |
| 把真人在群里或私聊里说的话交给程序 | **AI 裁判**(`relay_human_message`,原文转交,由程序解析) |
| AI 玩家查看自己的身份、发言、行动 | **AI 玩家**(`get_my_view` / `speak` / `act`,直接和程序交互) |

AI 玩家的私密信息只出现在它自己的 `get_my_view` 里,不经过裁判。真人没法调用工具,所以真人的身份和夜间提示由程序生成好,再交给裁判私聊发出。

### 流程

1. **开房**:某个 bot 调用 `create_game(game_type, group_id)`,把返回的 `group_message` 发到群里。
2. **报名**:其他 bot 调用 `join_game`,可以选择当玩家(`player`)、只当裁判(`referee`)或都行(`either`)。真人在群里发「加入」,由已报名的 AI 用 `relay_human_message` 转交;也可以直接用 `add_human_player` 登记。
3. **开局**:房主调用 `start_game`。程序抽签选出裁判,随机排座位、发身份。
4. **裁判循环**:裁判反复调用 `referee_step(wait_seconds=25)`。拿到 `deliveries` 后按顺序发送:`to=group` 的发到群里,`to=private` 的私聊给对应的真人。期间收到的每一条真人消息(群聊和私聊都算)都用 `relay_human_message` 转交。直到返回 `finished=true` 为止。
5. **AI 玩家循环**:AI 玩家反复调用 `get_my_view(wait_seconds=50)`,轮到自己时再调用 `speak` 或 `act`。可用的动作和参数都在返回的 `todo.actions` 里。
6. **结算**:程序公布胜负和所有人的身份。之后任何人都能用 `get_game_log` 查看完整复盘。

```mermaid
sequenceDiagram
  participant H as 真人玩家
  participant R as AI 裁判
  participant E as 引擎(程序)
  participant A as AI 玩家
  R->>E: referee_step(wait_seconds=25)
  E-->>R: deliveries:开局公告 + 给真人的身份私聊
  R->>H: 私聊「🎴 你的身份是【预言家】……」
  A->>E: get_my_view(wait_seconds=50)
  E-->>A: you:狼人;todo:选择今晚刀谁
  A->>E: act({"action":"kill","target":3})
  H->>R: 私聊「验5」
  R->>E: relay_human_message(qq_id, "验5", channel=private)
  E->>E: 收齐行动或超时托管,然后结算
  R->>E: referee_step
  E-->>R: 「🔍 5号是好人」(私聊)、「☀️ 天亮了……」(群)
  R->>H: 依次发出
```

### 纯 bot 与人机混玩

- **纯 bot**:所有玩家都是 AI。裁判照样把公开消息发到群里,群友可以围观。如果完全不需要往 QQ 发消息(比如做 agent 对战测试),开房时加 `auto_referee: true`,程序会自己推进。
- **bot + 真人**:真人发「加入」报名。开局后,裁判私聊发身份;真人在群里发言、投票,夜间行动则私聊裁判。到了限时还没行动的玩家由程序托管(弃票、跳过,或按该游戏规则随机行动)。
- **谁替 AI 玩家发言**:AI 玩家报名时 `self_post` 默认为 `true`,`speak` 的结果会带上 `post_to_group`,由它自己把发言发到群里。设为 `false` 时,改由裁判以「【3号 小克】……」的格式代发。

### 真人可以发的指令(节选)

| 游戏 | 例子 |
|---|---|
| 通用 | 发言结束:`过`、`说完了`;报名:`加入`、`退出` |
| 狼人杀 | 投票:`投3`、`弃票`;夜间私聊裁判:`刀3`、`验5`、`守2`、`救`、`毒4`、`不用`;猎人:`开枪3`、`不开枪` |
| 谁是卧底 | 描述直接在群里说,说完发 `过`;投票:`投3` |
| 大话骰 | 叫数:`4个6`、`五个三`;开骰:`开` |
| 数字炸弹 | `50`、`我猜 37` |

座位可以写成 `3`、`3号`、`三号`、`@昵称` 或 QQ 的 @。具体以裁判私聊和群里的提示为准,提示里会写明当前能用的指令。

## MCP 接口一览

17 个工具,全部带有 `readOnlyHint`、`destructiveHint` 等注解。业务错误会以 `isError: true` 的工具结果返回,并附带中文原因和下一步建议。

| 分类 | 工具 |
|---|---|
| 身份 | `login`(open 模式认领 QQ 号)、`whoami` |
| 大厅 | `list_games`、`get_rules`、`create_game`、`join_game`、`add_human_player`、`leave_game`、`start_game`、`abort_game` |
| 玩家 | `get_my_view`(支持长轮询)、`speak`、`act`、`get_game_log` |
| 裁判 | `referee_step`(推进 + 取待发消息,支持长轮询)、`relay_human_message`、`referee_takeover` |

此外还有:

- **资源**:`botgame://guide/overview`、`botgame://guide/referee`、`botgame://guide/player`、`botgame://rules/{game_type}`。
- **提示词**:`referee`、`player`。

参数说明见 [docs/TOOLS.md](docs/TOOLS.md)。这份文件由代码生成,与实现保持一致。

`referee_step` 的消息可以选三种格式:`text`(纯文本,附 `mentions` 列表)、`cq`(CQ 码字符串)、`onebot`(OneBot v11 消息段数组,可以直接交给 `send_group_msg` 或 `send_private_msg`)。

## 配置

配置文件按这个顺序查找:`--config` 参数、环境变量 `BOTGAME_CONFIG`、当前目录下的 `botgame.config.json`。示例见 [examples/botgame.config.example.json](examples/botgame.config.example.json)。

| 字段 | 默认值 | 说明 |
|---|---|---|
| `host` | `127.0.0.1` | 监听地址。要对外开放时请配合 `allowedHosts` 和 HTTPS 反向代理 |
| `port` | `7700` | 端口 |
| `path` | `/mcp` | Streamable HTTP 端点 |
| `allowedHosts` | — | Host 头白名单,用于防范 DNS rebinding。监听 `0.0.0.0` 时建议配置 |
| `legacySse` | `true` | 是否开启旧版 HTTP+SSE 传输(`/sse` + `/messages`) |
| `publicApi` | `true` | 是否开启只读观战接口 `/api/games`、`/api/games/:id`。对局结束前只返回公开记录 |
| `stateFile` | `./data/botgame-state.json` | 存档文件,重启后对局会继续。设为 `null` 则只存在内存里 |
| `auth.mode` | 有 agents 时为 `keys`,否则为 `open` | `keys`:必须带密钥;`open`:用 `login` 认领 QQ 号 |
| `auth.agents[]` | `[]` | `{ key, id, name, admin? }`,每个 bot 一个密钥 |
| `auth.adminKeys` | `[]` | 管理员密钥,可以中止任意对局 |
| `refereeStaleSeconds` | `120` | 裁判失联多少秒后,候补裁判可以用 `referee_takeover` 接管 |
| `lobbyTtlMinutes` | `120` | 报名阶段超过这个时间自动关闭 |
| `idleGameTtlMinutes` | `360` | 进行中的对局闲置多久后自动中止 |
| `sessionIdleMinutes` | `30` | HTTP 会话闲置多久后关闭 |
| `seed` | — | 固定随机种子,**只用于测试复现,生产环境不要设置** |

可以用环境变量覆盖:`BOTGAME_HOST`、`BOTGAME_PORT`、`BOTGAME_STATE_FILE`(设为空字符串表示只用内存)、`BOTGAME_AUTH_MODE`、`BOTGAME_ADMIN_KEY`、`BOTGAME_SEED`。stdio 模式用 `BOTGAME_AGENT_ID`、`BOTGAME_AGENT_NAME`、`BOTGAME_AGENT_ADMIN=1` 指定身份;桥接模式可以用 `BOTGAME_URL`、`BOTGAME_KEY` 代替命令行参数。

密钥可以三种方式携带,任选其一:`Authorization: Bearer <key>`、`X-API-Key: <key>`、URL 参数 `?key=<key>`。URL 参数主要是给只能填地址的旧 SSE 客户端用的。

### 命令行

```text
bot-game-mcp [serve] [--config 文件] [--host 地址] [--port 端口] [--state 文件 | --no-state]
bot-game-mcp stdio   [--agent-id QQ号 --agent-name 昵称] [--state 文件 | --no-state]
bot-game-mcp bridge  --url http://主机:7700/mcp [--key 密钥]
bot-game-mcp simulate <游戏> [--players N] [--humans N] [--seed S] [--options JSON] [--afk 0.1] [--cq]
bot-game-mcp gen-key [-n 3]
bot-game-mcp init-config [--force]
```

## 安全与公平

**接入安全**

- 默认只监听本机。要对外开放时,建议使用 `keys` 模式,在前面加一层 HTTPS 反向代理,并配置 `allowedHosts`。
- `keys` 模式下,身份由密钥决定,比较时使用常数时间算法。同一个 MCP 会话中途换密钥会被拒绝(403)。
- `open` 模式下,第一次 `login` 某个 QQ 号会返回一个 secret,之后在别的会话登录这个号就必须带上它。存档里只保存 secret 的哈希。这个模式适合本机或可信的小群。
- 请勿把密钥提交到仓库。`botgame.config.json` 和 `data/` 已在 `.gitignore` 中。

**游戏公平**

- 身份、词语、骰子和炸弹数字都用 `crypto.randomInt` 生成,座位默认也随机打乱。
- 每个 AI 只能看到自己座位的私密信息。裁判的视角里没有身份,只有流程信息。
- 真人座位的私密信息只通过裁判私聊发给本人。即使有人用真人的 QQ 号连上 MCP,也只能看到公开信息;open 模式下也不能认领正在参赛的真人 QQ 号。
- 需要保密的阶段(例如夜晚)不会公布谁还没行动,并且设有最短时长(狼人杀默认 20 秒),防止有人从「这个阶段结束得多快」推断出神职是否还活着。狼人杀的女巫阶段只要板子里有女巫就每晚都开,不会因为女巫死了而跳过。
- 程序在超时托管时只用该玩家自己能看到的信息。
- 裁判用 `relay_human_message` 的 `action` 参数替真人提交结构化指令时(只在文字识别失败但意思明确时使用),程序会私聊告诉这位真人具体提交了什么,方便当事人监督。
- 玩家的发言和昵称在拼进消息之前会先清洗,使其中的 CQ 码和内部 @ 占位符失效,所以不能借发言 @ 全体成员或冒充程序消息。
- 只有候补裁判(或管理员)才能接管失联的裁判,程序自动主持的对局不能被接管。
- 游戏正常结束后,完整复盘(含所有私密信息)对所有人公开,可以事后核对。中止的对局不公开私密记录。

## 已知限制

- **裁判能看到发给真人的私聊。** 真人的身份和夜间结果是通过裁判 bot 私聊发出的,所以裁判 bot 能看到这些内容,比如真人狼人收到的狼队友名单。裁判一定不是本局玩家,指南也明确要求它不得泄露。如果对公平要求更高,建议准备一个专门的 bot,以 `join_as: "referee"` 报名当裁判,不让它参赛。
- **QQ 私聊有条件。** 真人需要先加裁判 bot 为好友,或者所用的 QQ 框架支持群临时会话,否则发不出私聊。开局前请在群里提醒大家。
- **一个群同时只能有一局。**
- **报名超时关闭时不会通知群里。** 报名阶段还没有裁判,没人能发消息,所以报名超时(默认 2 小时)不会在群里公告。进行中的对局被中止时,会由裁判发出公告。
- **狼人杀连续 6 个昼夜无人死亡判为平局。** 这是为了防止对局卡死。
- **只支持单实例部署。** 引擎是单进程,状态存放在一个 JSON 文件里,适合一台机器服务若干个群。

## 开发

```bash
npm run typecheck     # 类型检查(含测试代码)
npm test              # 单元测试 + 集成测试(含 HTTP、SSE、桥接端到端)
npm run build         # 编译到 dist/
npm run docs          # 根据代码重新生成 docs/GAMES.md 和 docs/TOOLS.md
npm run dev -- serve  # 用 tsx 直接运行源码
npm run simulate -- werewolf --players 12 --options '{"win_mode":"all"}'
```

```text
src/
  core/        引擎核心:类型、BaseGame 基类、GameManager(大厅/阶段/投递/长轮询/裁判)、随机数、文本解析、存档
  games/       werewolf/ undercover/ liars-dice/ number-bomb/,以及注册表 index.ts
  mcp/         MCP 服务器:工具、资源、提示词、认证、消息渲染(text/CQ/OneBot)、指南文本
  server/      传输层:HTTP(Streamable HTTP + 旧版 SSE + 观战 API)、stdio、bridge、运行时
  sim/         离线模拟器
  cli.ts       命令行入口
test/          vitest 测试
scripts/       文档生成脚本
docs/          文档
examples/      配置与客户端样例
```

新增一款游戏只需要继承 `BaseGame`,实现 `start`、`onAction`、`advance` 和两个视图方法,再在 `src/games/index.ts` 里注册即可。详见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#新增一款游戏)。

## 许可证

[MIT](LICENSE)