图书馆闭馆夜。案件的关键证据是一张借书卡,卡上的签名能证明管理员当晚借出过那本被撕页的古籍。生成器把它随机放进了“阅览室三号柜”,而三号柜的钥匙,按同一套规则,被随机放进了“馆长办公室抽屉”。馆长办公室的门,需要一张借书卡才能刷开。三样东西各自合理,合在一起,玩家永远进不了那扇门。

把证据放置看成一张图

这类问题的可靠描述方式,不是一句“请保证线索能找到”,而是把游戏空间画成一张图:节点是地点、容器和物品,边是“打开”“拿取”“获得信息”这样的动作,边上可以带条件(需要钥匙、需要知道口令、需要先与某人交谈)。玩家从起点出发,能沿边走到的节点集合,就是当前可达集合。

关键证据是否能被找到,就变成一个朴素的问题:它所在的节点,是否属于玩家的可达闭包。上面那个死局,用图一画就露形了:借书卡与钥匙互相依赖,形成了一个从起点无法进入的环。

房间平面图上,钥匙、纸条等证据图标由紫色虚线连成证据链,橙红实线标出入口到关键线索的可达路径,被堵住的门画有叉号。
这张图说明:生成随机布局后,要验证关键线索始终在可达路径上,被堵住的门不能切断证据链。

三种失败模式,各有检测办法

失败模式表现 检测办法
线索缺失账本里的遗留物没有被放进场景逐条对照真相账本的“遗留”字段,检查每一项是否有对应的实体
被锁在不可达处 证据在图上,但从起点走不到,或依赖成环从起点做可达性搜索,检查关键节点是否在闭包内,并检查依赖环
指向错误结论 证据本身存在,却与账本矛盾,会把玩家引向无辜者 把每条证据映射回账本事件,检查它所支持的结论与真相是否一致

第一种最容易检查,第三种最容易被忽视。生成模型为了增加迷惑性,有时会添一件“像是凶手用过的”物件,却忘了给出足以排除它的证据,于是玩家有理由怀疑无辜者,而系统里没有任何办法让他洗清。与真相账本的映射,就是为了堵住这个口子。

冗余:每个关键推断至少两条独立的链

即便所有证据都可达,玩家也可能漏看。图书馆的借书卡再显眼,也有人根本不去翻抽屉。所以关键推断不能只靠一条证据链。这里的“独立”很重要:两条链不能共用同一个上游节点,否则一处断了,两条同时失效。

以“管理员当晚接触过古籍”这个推断为例,可以设计为:链A是借书卡的签名;链B是闭馆后门禁日志与保洁员的证词。两条链走的是不同的房间、不同的信息类型。检查时用简单的方法:对每条推断,依次去掉一条链的所有上游节点,看剩下的链是否仍然可达。

冗余也有代价。证据链越多,案件越容易被玩家“提前破掉”,悬念随之变薄。一个折中是让两条链的信息量不对等:一条只能证明“他有机会”,另一条才能证明“他确实做了”,玩家需要把两者合并起来才算破案,这样冗余既是保险,也成了推理的一部分。

放置证据建可达图检查双链模拟探索补兜底线索
线索放完以后,先把地图和证据当作一张图,再用模拟玩家走一遍。

随机放置的约束

完全随机是问题的来源,但也不必回到手工摆放。更可行的方式是“带约束的随机”:先随机,再用规则过滤。示例规则有:

  • 关键证据的前置条件深度不超过某个上限(示例:不超过3步),避免玩家被长链拖垮。
  • 关键证据与其解锁工具不得放在同一个上锁容器里,也不得互为前置。
  • 同一房间内的关键证据不超过示例上限2件,避免玩家一次搜完就一网打尽,也避免全局分布过散。
  • 每一条推断的两条证据链,至少有一条深度不超过2。

违反规则的布局直接丢弃重抽,抽样次数超过示例上限(比如50次)仍无解,则说明地图本身或案件结构有问题,需要回到上游调整,而不是继续硬抽。这一点与生成式谜题的验证思路相通:生成只负责提出候选,检查器负责放行。

漏看之后:兜底线索与NPC提示

再严密的验证,也拦不住玩家心不在焉。所以需要在运行时补一层。当模拟出的“玩家进度”与实际相比,关键推断长时间没有推进,系统可以启用两种兜底:一种是环境线索,比如让保洁员在走廊里主动嘀咕“那本书今晚怎么被人动过”;另一种是NPC的暗示,由知情但无恶意的角色提起相关话题。它们只降低发现门槛,不直接给出结论,避免把解谜变成领取答案。

自动验证:让模拟玩家先跑一遍

最后一道关是模拟探索。让一个脚本化的玩家从起点出发,按“尽可能翻开所有可交互物”的策略走一遍,记录每个时刻获得了哪些信息,然后检查两件事:一是它能否在有限步内获得足够的证据链,推出账本里的凶手;二是把每个关键节点分别屏蔽后,是否仍然可以推出。任何一项失败,都直接退回生成。

for each key_inference in case.inferences:
    chains = independent_chains(key_inference)
    assert len(chains) >= 2
    for chain in chains:
        assert reachable(start, chain.nodes, inventory=simulate())
assert solver.unique_culprit(player_info_after_full_search) == truth.culprit

需要坦白的是,模拟玩家永远比真实玩家更有耐心,它证明的是“可以解”,而不是“会觉得好解”。真实的难度还要靠测试和玩家状态的观察去调,这部分放在案件一致性的核心长文里和其他校验一起讨论。PA真人AI推理游戏其他文章,可以在推理游戏栏目找到,而嫌疑人如何在这些线索面前说话,见角色知识层一篇。