AI 不读代码也不看坐标,它和人类玩家拿到的东西完全一样——只有「按什么键」的权利。
把游戏世界在某一帧「拍照」:两个玩家的位置、速度、脚下平台,所有门与踏板的开合状态,移动平台的进度,水与熔岩的范围。观测是不可变的——AI 后续所有推理都基于这张快照,不会边想边改。
从 TMX 地图几何中提取「能站的地方」和「能走的路」,生成一张图:节点是平台段、踏板、门两侧、出口;边是走、跳、下落、爬台阶、乘平台。这一步把「物理世界」翻译成「数学世界」,是搜索的前提。
这是一个协作游戏,得先决定「我现在该干嘛」:是去踩踏板开门,还是在门边等队友,还是冲向出口。任务选择器读取当前机关状态推断目标——不写死任何关卡流程。
拿到起点和终点,让 BFS、DFS、A* 三个算法同时在图上跑,融合出一条最稳的路线;再由执行器把它翻译成一帧一帧的按键序列。跳不过去的沟?还有一个模型预测执行器专门负责「怎么跳」。
Action(left, right, jump, down) 四个布尔值——和人类键盘完全等价。它不能瞬移、不能改坐标、不能伪造输入。测试套件甚至用 AST 静态检查禁止 AI 代码给玩家的 pos / rect / velocity 赋值。
搜索算法不懂「平台」「熔岩」,它只懂节点和边。所以第一步是把物理世界翻译过去。
提取器扫描地图几何,把每一块站得上去的表面切成平台段,再加上语义节点:压力板 plate:0 门的两侧 door:0.left 台阶的每一级 stairs:2 出口 exit:1。每个节点带位置、类型和覆盖范围。
判定「玩家站在哪个节点」用的是脚下真实重叠:以脚底为中心的矩形与平台段求交,而不是拿中心点做几何猜测——中心点探测会在平台边缘产生「假到达」,让路径被错误快进。
两个节点之间有没有边,不是拍脑袋连的,而是用与真实游戏完全一致的物理参数验证出来的:跳跃力 720、重力 1800、单跳最高上升 144px、二段跳约 288px。
| 边类型 | 含义 | 物理约束举例 |
|---|---|---|
| walk | 平地直接走过去 | 两平台间逐像素探测有连续支撑 |
| jump | 跳上去 / 跳过沟 | 上升量 ≤ 单跳 144px 或二段跳 288px;水下起跳拒绝高弧线 |
| drop | 踩穿单向板落下去 | 只允许向下,落点必须真的接得住 |
| stairs | 按住跳爬台阶 | 限制在同列短距攀爬 |
| ride | 乘移动平台 | 只在正确停靠位置与方向可用,代价随进度实时更新 |
| door | 穿过已开启的门 | 带条件边:门没开?这条边在图里根本不可通行 |
条件边是协作玩法的数学基础:「门后的踏板」这个节点,只有当携带条件 open:door:0 被满足时才可达。AI 推理时把「哪些门已开」编码进搜索条件,一张静态地图就表达出了随协作状态变化的动态世界。
每次规划,BFS、DFS、A* 都在同一张图、同一组条件、同一份黑名单上完整跑一遍,然后融合。
一层一层向外扩散,天然找最少步数的路径。不知道地形好坏,但保证边数最少——适合当「保守备胎」。
一条道走到黑,撞墙才回头。找到的路径往往怪但出奇地有用——绕远路的方案有时恰好避开危险区。为了可解释性,邻居按目标方向排序。
带「方向感」的最优搜索,按代价找最低成本路径。是融合时的默认赢家,也是本文的主角——往下看。
| 优先级 | 规则 |
|---|---|
| ① A* 为基线 | 三家中 A* 的最低代价路径默认获胜 |
| ② 黑名单一票否决 | 如果 A* 路线的下一条边刚失败过(被拉黑),自动换成 BFS / DFS 中下一条边干净的那个 |
| ③ 风险加权平局裁决 | 代价打平时,按边类型安全度评分:walk / stairs < door / ride < drop / jump——能走就不跳 |
这带来一个很有意思的性质:AI 的「性格」会随环境流动。顺畅时它像 A* 一样精明;频繁失败时它自动带上 BFS 的保守或 DFS 的脑洞。面板上的 Hybrid/A* 就是当前融合赢家。
AI 每秒要做几十次完整规划,纯 Python 会成为瓶颈。三个算法被实现在一个 C++17 的 CSR(压缩稀疏行) 图结构上,通过稳定版本的 C ABI 用 ctypes 调用——没有 Python 头文件依赖,加载是严格的:缺二进制、ABI 不匹配、自检失败都直接报错,绝不静默降级。
精确语义被完整保留:BFS 在「生成节点时」判中,DFS 反转邻居入栈,A* 在「弹出节点时」判中——这些细节都被回归测试逐字节对齐。
这是整个 AI 的大脑。理解了这一节,你就理解了它为什么既快又准。
开放列表里的每个候选节点都带一个总分 f = g + h,A* 每次只展开总分最低的那个节点:
g 来自图的边代价:走过一条边就累加它的类型代价(走 1.2/px、跳跃 +5.0,完整表见下)——同样抵达终点,绕了一堆跳跃边的路线累计 g 自然更高。h 由几何直接算出:到终点的直线距离,乘以图里「每像素至少要花多少代价」的下界,作为剩余代价的乐观估计。两者相加,g 是历史,h 是直觉。
优先队列按 f 排序,每次弹出 f 最小的节点展开,弹出的是终点即刻收官;代价打平时按插入先后取(稳定平局序),同一张图、同一组状态,每次搜索都得到完全相同的路径——这是 44 项回归测试能逐字节对齐的前提。
场景:AI 在左边平台,目标是右上方一块踩住开门的压力板,中间隔着沟和一层台阶。
起点进「开放列表」(待考察的候选)。此时 g=0,h=起点到踏板的直线估计距离。
每次从开放列表里取出 f 值最小的节点展开——这就是「方向感」:明明周围的邻居都能展开,但永远优先试探离目标更近的那个。
沿所有可通行边走一步:跳上台阶的边代价 5.0,走路代价 1.2。若新路线让某个邻居的 g 变小,就更新它的 g 并记下「我是从哪来的」。
终点被从开放列表弹出的那一刻搜索结束,沿着「从哪来」指针回溯就是完整路径。本项目刻意采用弹出时判中(而非生成时),保证最优性成立。
同一张地图(灰色为墙,蓝点起点,金点终点),左边 BFS 从起点一图一图地漫染,右边 A* 沿启发式的指向穿过缺口。数一数展开格数——这就是启发式换来的效率。
已展开 0 格
已展开 0 格
演示用曼哈顿距离作启发式(网格四向移动下可采纳);游戏本体用的是「欧氏距离 × 最小代价密度」,原理相同。
A* 的魔力全部押在 h 的一个性质上:可采纳性(admissibility)——h 永远不高估真实剩余代价。打个比方:h 是你说「最多还剩这么远」,只要你不吹牛,A* 就绝不会漏掉真正最短的路。
本项目的启发式是直线距离 × 图中最小代价密度:
它够乐观(绝不高估 → 可采纳 → A* 最优性成立),又够聪明(大方向指得准 → 剪掉绝大部分无用探索)。测试用独立实现的 Dijkstra 做基准,逐条验证生产 A* 的路径代价与之完全一致。
每条边的代价都编码了设计者的经验判断——移动平台在晃,乘它就该更贵:
| 边类型 | 基础代价 | 为什么 |
|---|---|---|
| walk | ~1.2 / px | 最安全,代价密度最低 |
| stairs | 低 | 机械可靠,只是慢 |
| door | +2.0 | 依赖门保持开启,有协作耦合 |
| ride | +3.0 | 平台在动,等待+时机成本 |
| drop | +4.0 | 下落不可逆,落点误差要命 |
| jump | +5.0 | 最贵:起跳点、弧线、落点都可能翻车 |
跑动中平台的位置变化还会实时改写 ride 边的可用性与代价——图不是一张死表,而是随游戏状态呼吸的活结构。
搜索出路径 ≠ 能走通。执行中如果同一条边反复失败(比如起跳点总差半步),A* 把它记进黑名单,下次搜索时那条边带着 blocked:a>b 条件直接不可通行——融合器随之自动换用 BFS / DFS 的备选路线。连续 6 次彻底没路时,AI 会申请一次「回到存档点重来」,而不是在原地无限抽搐。
触发重规划的时机:目标变了、门的开合改变了图的条件、或 1.5 秒没有实质性进展。失败被记成数据,而不是情绪。
A* 说「从节点 33 跳到节点 59」,但游戏帧循环里没有「跳」这种东西——只有每一帧的按键组合。
遇到跳跃边,AI 不会莽。它启动一个无头物理克隆(和真实玩家逐位一致的物理引擎),把候选方案逐个「脑内模拟」一遍:先走 10 帧再跳?先后退助跑再起跳?空中什么时候松方向?二段跳在哪一帧按?
赢家的完整按键序列被提交并逐帧重放——只回放第一帧的话,下一帧又会重新搜索,永远消费不到那个关键的「延迟跳跃帧」。重放期间还有三重保险:真到达目标立刻中止吸收漂移;脚本耗尽仍未落地就按最后漂移惯性滑行到触地;助跑后退阶段挂起「卡死时钟」(故意背离目标不等于迷路)。
游戏的二段跳只在速度接近抛物线顶点(|vy| ≤ 144)时生效——满速下按了也白按,这个窗口一秒只有约 4 帧。模拟器在正确的帧按下二段跳;实机执行时若物理有几帧漂移,重放器会盯着真实 |vy| 等窗口出现再注入按键,而不是死板按帧号按——这一条直接把宽沟跨越的成功率拉了起来。
另一处细节:驻守压力板时用死区控制——在目标带内干脆松开所有键靠摩擦停车,只有真的滑出去了才单侧修正。早期版本用「速度过快就反向按键」,结果在踏板上左右横跳像在跳舞。
tutorial、Farlands、Crucible 三关双 AI 真实物理通关,无传送、无脚本按键。
从新手谷到双出口 SUCCESS 再回主菜单,全自动。
含 4 项全战役通关回归 + 行为质量断言(不抖动、真的会用二段跳)。