在桌面游戏的世界里,规则的复杂性和玩家人数的限制常常成为阻碍体验的门槛。为了解决这一痛点,我们设计了 Imaginary Friend,旨在通过大语言模型的强大理解与推理能力,打造一个能够自动解析规则书、主持游戏并模拟对手的智能跨桌游通用 Agent。

本文将深入剖析项目的核心架构、技术亮点以及其带来的创新体验。

游戏开始界面


# 一、项目定位与核心理念

我曾在年初的文章(做了个桌游:时空拍卖行)的结尾这么写:

这个桌游会保持开源,如果任何人想玩的话,只需要打印出来,召集朋友(最难的一步)就能组起来一局。

现在,你就算没有朋友也能把这个游戏玩起来了。

Imaginary Friend 是一个基于 Python 和 FastAPI 构建的单人桌游 AI 对战系统。其核心设计理念可以概括为:"代码只做存储,GM 决定一切"。

传统的电子化桌游往往需要开发者为每一款游戏硬编码状态机、规则逻辑和卡牌效果。而 Imaginary Friend 采用了一种截然不同的文档驱动架构。用户只需上传一份 PDF、DOCX 或 Markdown 格式的规则书,系统便能自动解析规则,生成游戏定义,并由一个中央 GM Agent 作为 "大脑" 负责所有的流程推进和规则裁定。底层代码仅提供原子化的数据存储和操作工具,极大地提升了系统的通用性和扩展性。


# 二、技术模块选型设计

# 模块一:状态存储层

在最新的架构重构中,项目彻底摒弃了硬编码的强类型数据模型,转而采用基于 TinyDB 的文档型数据库。这种设计使得系统不再预设任何游戏特定的字段,而是通过四个核心表来管理所有状态。

表名作用说明
global存储全局变量,如当前回合、当前阶段、公共资源池等。通常只包含一个文档。
players存储玩家实体,包含其私有状态(如金币、手牌、得分等)。每个玩家一个文档。
zones存储游戏区域和对象容器(如牌库、弃牌堆、拍卖区等)。
logs存储不可变的结构化游戏日志,用于追溯和展示。

这种灵活的 JSON 文档型状态管理,使得系统能够轻松适配任意桌游的数据结构,真正实现了 "通用" 的目标。值得一提的是,DocStore 还实现了 snapshot_for_player 方法,在向前端广播状态时,会自动过滤其他玩家的 handprivate_ 前缀字段,保证了信息隔离。

def snapshot_for_player(self, player_id: str) -> dict:
    result = self.snapshot()
    filtered_players = []
    for p in result.get("players", []):
        if p.get("_id") == player_id:
            filtered_players.append(p)
        else:
            public = {k: v for k, v in p.items()
                      if not k.startswith("private_") and k != "hand"}
            filtered_players.append(public)
    result["players"] = filtered_players
    return result

# 模块二:规则提取层

规则提取是实现 "上传即玩" 的关键。系统支持 PDF、DOCX、Markdown 三种格式的规则书解析,分别使用 PyMuPDFpython-docx 和原生文件读取。

提取出的原始文本会经过 RuleCleaner 的两轮 LLM 处理流水线:

  1. 第一轮(文本清洗):修复换行断层,识别并格式化表格和列表,生成高保真的完整 Markdown 规则手册。
  2. 第二轮(元数据提取):从清洗后的规则书中提取极简元数据(游戏名称、玩家人数范围、一句话简介),用于游戏大厅展示。

清洗结果会以文件哈希为 key 缓存到本地,避免重复消耗 API Token。

游戏管理界面

上图展示了游戏管理页面,用户可以在此上传规则书文件,系统会自动解析并展示已有的游戏定义列表。

游戏详情编辑

点击游戏卡片后,可以查看和编辑游戏的元数据(名称、简介、玩家人数)以及完整的 Markdown 规则手册。


# 模块三:工具层

为了让 GM Agent 能够操作游戏状态,系统提供了一组精简而强大的 6 个固定工具。这些工具被定义为 Anthropic API 的 Tool Schema,GM Agent 只能通过这些工具与游戏世界交互:

工具名类别说明
db_find数据库操作查询 DocStore 中符合条件的文档。修改前必须先查询。
db_insert数据库操作插入新文档(初始化玩家、区域等)。
db_update数据库操作更新文档,支持 setset、inc、pushpush、pull 操作符。
db_delete数据库操作删除文档(谨慎使用)。
request_player_action交互工具挂起 GM 线程,向指定玩家请求行动输入。
broadcast_message交互工具向所有玩家广播文本消息。

GM Agent 必须严格遵循 "先查询再修改" 的原则,通过这些原子操作来管理整个游戏的生命周期。 db_update 支持 MongoDB 风格的操作符,例如 {"$inc": {"gold": -5}, "$push": {"hand": {"name": "新卡牌"}}} 可以在一次调用中同时扣除金币并向手牌添加卡牌。


# 模块四:Agent 逻辑层

系统的核心大脑是 GM Agent,由 Claude 大语言模型驱动。它负责接收用户输入,结合当前的系统状态和规则书,决定下一步的行动。

# Prompt Caching 优化

为了优化长文本规则书带来的 Token 成本和延迟,项目巧妙地利用了 Anthropic 的 Prompt Caching 机制。系统将 System Prompt 分为两层:

  1. 静态层:角色定义、工具使用规范(不变)
  2. 规则手册层:完整的 rules.md(不变,设置 cache_control 断点)

动态的游戏状态(TinyDB 快照)则作为每次请求的 User 消息附加,绝不混入静态缓存区。这种设计确保了规则书的 KV Cache 能够被 100% 复用,大幅降低了 API 成本。

def _build_system_messages(self) -> list[dict]:
    return [
        {"type": "text", "text": _GM_SYSTEM_INSTRUCTIONS},
        {
            "type": "text",
            "text": f"# 游戏规则手册\n\n{self.rules_md}",
            "cache_control": {"type": "ephemeral"},
        },
    ]

# AI 对手性格系统

系统还实现了 Player Agent,为 AI 对手赋予了不同的性格,使得单人对战体验更加丰富:

  • 策略家:深思熟虑,善于制定长期计划
  • 冒险家:高风险高回报,直觉敏锐
  • 外交家:圆滑得体,善于谈判
  • 收藏家:对高价值物品情有独钟
  • 搅局者:喜欢出其不意,破坏对手计划

# 三、Web 界面与交互体验

Imaginary Friend 提供了精心设计的三栏式 Web 游戏界面,通过 WebSocket 实时推送游戏状态和日志。

游戏进行中界面

上图展示了游戏进行中的主界面:左栏显示游戏总览(全局资源)、玩家状态和 API 统计;中栏是 GM 实时播报区,以 Markdown 渲染 GM 的叙述和 AI 玩家的对话;右栏是玩家的背包,展示手牌和持有的文物。

公共区域展示

底部的公共区域展示了所有游戏 Zone 的状态,包括文物牌库、事件牌库、拍卖区、弃牌堆等,前端根据 TinyDB 的 JSON 树动态渲染,无需硬编码任何字段。


# 四、总结

Imaginary Friend 展示了 LLM 在复杂逻辑推理和状态管理方面的巨大潜力。通过文档驱动的架构灵活的文档型数据库以及精巧的 Prompt Caching 策略,它成功地证明了静态的桌游可以被低成本地转化为动态的、可交互的游戏体验。

这个项目最核心的创新在于:它不是在 "教" 计算机如何玩某一款游戏,而是在 "教" 计算机如何理解任意一款游戏的规则,并自主地成为那款游戏的裁判和对手。你可以在这里获取到这个项目的源码,更多的玩法演示也会贴在这里。

项目地址:github.com/Tritium0041/imaginary-friend