我用求解器拆穿了一个爆款玩法的宣传语——离阵 Arrow Exodus

本系列:把这两年用 Python 写的十几个小项目挨个开源、挨个说清楚。不讲”我做了什么”,讲”做的过程中被什么打脸、最后怎么想明白的”。 第 1 篇 · 离阵 Arrow Exodus 源码:github.com/ghostgorge/ArrowExodusV2(v3 / v4,当前版本) 初版存档:github.com/ghostgorge/ArrowExodus(v1,含八种机关的完整实现) 技术栈:Python + pygame,PyInstaller 打成单文件 exe,双击即玩,不装 Python


一、起因:App Store 上那一整排箭头游戏

Arrow Away: Tap Maze、Arrow Puzzle、Arrow Maze、Arrow Out……规则简单到一句话说完:

点一支箭,它沿自身方向飞出棋盘;只有整条出路是空的才飞得出去;点到被挡住的箭,扣一条命。

宣传语高度一致,都在强调”一步走错就锁死全盘”。我想照着做一个,顺手写了个求解器来验收关卡——然后求解器告诉了我一件官方绝口不提的事。

二、第一个发现:这类游戏根本不可能”走死”

拆开看,箭头谜题的数学内核是给”谁挡谁”的有向图找一个拓扑序。而移除操作有一个致命性质:

纯移除玩法是”汇合的”(confluent)。 移走一支箭,只会让别人的出路更通畅,绝不会更差。 所以任何一步合法的走法都是安全的——你根本不可能把自己走死。

求解器实测:纯箭头关卡的陷阱率恒为 0。”一步走错锁死全盘”在数学上不成立。它的难点只在”看出哪几支是自由的”——那是眼力题,不是规划题。

这个结论直接决定了整个项目的设计方向:要做出真正的极难,必须引入能制造不可逆错误的机关,而不是单纯把棋盘做大。

三、v1:八种自制机关,和”构造出来”的可解性

于是有了八种机关,重点标出哪些真正破坏了汇合性:

机关规则破坏汇合性
石块永久障碍否(纯眼力)
裂墙挡路,但可拿一支箭去撞碎,箭与墙同归于尽(资源分配)
镜面 / \把经过的箭折 90°,出路不再是直线否,但眼力难度陡增
传送门成对,一进一出、方向不变
步数箭只走指定格数就停,跑到别处继续挡路
旋转箭每有一步成功,它就顺时针转 90°(时机)
双生箭锁链相连同时发射,两条出路都要通,而且互相挡路否,但约束极强
序锁箭必须先清掉 N 支箭才点得动否(强制排序)
陷坑箭掉进去也算清除,等于棋盘中央的出口

关卡不是”随机生成 + 碰运气求解”,而是逆序构造

从空棋盘开始一支支往里放箭,只有当这支箭在”已放好的箭”面前出路通畅才允许落子。 把放置顺序倒过来就是一条合法解——最后放的最先走(它当时路是空的),倒数第二支等它走了路也空……

裂墙的”先砸墙、再过人”依赖关系也能纳进这套构造:追踪时把还没指定砸墙者的裂墙当作已经碎掉处理,因为先放的箭在游戏里最后走,那时墙确实碎了;一旦某支箭被指定为砸墙者,这面墙才对后续放置变成实心——后续放的箭在游戏里更早走,那时墙还立着。顺序自然对上。

v1 交付了 10 章 80 关,全部程序生成 + 求解器验收。然后我自己通关了,感觉:太简单。

四、v2 的自我打脸:三个傻子机器人打穿了 61/80 关

v2 试图用”死亡率”证明自己难:随手乱点最终走死的概率顶到 0.99。但那是 25 步里每步 89% 安全累乘出来的0.89²⁵ ≈ 0.05)——没有任何一步是难的,只是走得久了总会打滑,何况还有撤销键,惩罚约等于零。

我写了三个不含任何判断的机器人扔进 v2 的 80 关:

打法规则一次通关
剥洋葱永远点最靠近棋盘边缘的自由箭31 / 80
最短出路永远点出路最短的那支27 / 80
扫描顺序从左上到右下,遇到能点的就点41 / 80
三者并集61 / 80

八十关里六十一关能被一条固定规则一次通关。玩家反馈的”从边缘找方向对的往里渗透就行”不是错觉,那就是 v2 的最优解

难度指标被关卡长度刷掉了。这是整个项目最值钱的一课:任何”靠拉长流程累乘出来”的难度都是假难度。

五、v3:把难度做在两根轴上

重做时换了坐标系:

含义
W 决策面宽度每步有几个合法走法
S/W 安全率其中几个不会走死
                  安全率
                    高
      窄×安全        │      宽×安全
   「照着念答案」     │   「剥洋葱」← v2 在这
                     │
  W 窄 ───────────────┼─────────────── W 宽
                     │
      窄×危险        │      宽×危险
   ★真正的谜题★     │   「算不动,只能试」
   三条路,一条活的   │
                    低

主指标换成紧步占比(安全率 ≤ 0.5 的步在全程的占比);无脑打法通关数是一票否决项,第 7 章起必须为 0。

具体三把刀:

  1. 封边——外圈铺石块,出口从 4(k−1) 个压到 1–3 个(配陷坑做内部出口)。镜面从装饰升级成必需的路网。实测 W 从 5.8 掉到 2.8。
  2. 减箭——11~16 支,不是 25~31 支。箭多只会稀释陷阱密度、增加扫描量。
  3. 把不可逆性放到窄面上——同一个陷阱,W=8 时安全率 0.88,W=2 时 0.50,杀伤力翻三倍。封边不是为了自己变难,是给陷阱当放大器。

实测教训:只封边不难。 早期样本 W 压到 2.3 但安全率 1.00——那是”窄×安全”,玩家从扫描变成照着念答案,一样不动脑。两刀必须一起上。

技术上有两个关键点。决策面闸门:裸倒推产生的是随机拓扑序,天然又宽又浅,它只问”这支箭射线通不通”,从不问”这样摆会让正着玩时多出几个自由箭”。所以倒推每摆一支就回头数一遍自由箭,超过 cap 就撤销重摇——这一行判断就是难度总开关。旋转箭的时钟反推d0 = (想要的发射方向 − 步号) mod 4,于是”让某支箭只能在第 7 步点”这种编排可以精确做到。

还有一个反直觉的结论:九个机关里只有两台是真正的陷阱引擎。

  • 裂墙:一次性资源。撞碎它的箭被消耗,墙也没了,后来者穿过豁口继续飞,可能撞死在石块上。在封边盘上,走廊里的裂墙就是一扇要付一支箭才能开的门
  • 旋转箭:方向 = (初始方向 + 已成功步数) mod 4。困在只留一个缺口的石块口袋里时,只有 m ≡ c (mod 4) 那几拍点得动,必须消耗别的箭”垫步”对拍子。

石块 / 镜面 / 传送门 / 陷坑 / 双生 / 序锁全部是单调的,造不出死局,它们只负责路径形状和阅读负担。数据也印证了主次:只上裂墙的章节紧步占比只能做到 0.03,旋转箭一进来就跳到 0.17–0.48。所以 v3 把旋转箭提前到第 3 章。

六、实测曲线(74 关全量精确求解,非抽样)

名称紧步占比安全率W无脑通关箭数
1初识箭阵0.001.002.53/3(就该无脑)8
2一次性的门0.030.884.51.2/312
3周而复始0.170.833.71.1/312
4折出去0.220.813.40.5/312
5咽喉0.220.813.30.4/312
6钥匙链0.280.773.20.4/313
7饿着的拍子0.370.742.70/313
8同频0.370.733.00/313
9全是咽喉0.410.702.80/314
10万象归墟0.480.672.80/312

对比 v2:末章紧步占比 0.09 → 0.48,安全率 0.89 → 0.67,决策面 5.6 → 2.8,箭数 25 → 12,被无脑打法通关 61/80 → 26/74

终关:9×9、只有 12 支箭、紧步占比 0.61、安全率 0.58——六成的步里,看得见的路有一半以上是死的。

顺带删掉了”扫描”道具(花 3 星尘把所有自由箭高亮,等于官方发的剥洋葱外挂),第 3 章起关闭出路预览。提示保留——那是帮你想,不是替你看。

七、没有做、也不打算做的

别做为什么
加战争迷雾 / 藏箭头方向完全信息 + 困难搜索 = 谜题;不完全信息 + 简单搜索 = 猜谜
加箭数、加盘面稀释陷阱密度,增加扫描量,不增加思考量
靠累计死亡率刷难度可以靠拉长关卡刷分,且有撤销键时惩罚趋近于零
cap 压到 2 以下退化成唯一解,玩家在照着念答案

八、v4:把生成器交到玩家手里

既然关卡的难度可以被精确度量,那就干脆做成工具。v4 加了关卡设计器:左键画、右键擦、F5 一键验证难度(直接调求解器出报告)、F6 试玩、Ctrl+S 保存,还能生成 AEX1: 分享码发给别人。自制关卡不进星星经济,只记最好成绩。

九、工程与一个关键的性能发现

core/board.py       规则内核(Level 静态 / State 不可变 / Game 可撤销)
core/solver.py      求解器 + 三个无脑机器人 + 精确难度评估
core/levels.py      烘焙好的 74 关(自动生成,勿手改)
core/editor.py      v4 关卡设计器(草稿/体检/倒序生成/分享码)
tools/gen_levels.py 关卡烘焙器(封边 + 母题预留 + 决策面闸门)
tools/verify.py     16 项自动验收

规则内核刻意把静态与动态拆开:Level 是棋盘和地形,全程不变;State = (箭头元组, 未碎裂墙集合, 已清除数)不可变的。这一个决定同时带来三件好事——撤销就是弹栈、求解器可以直接对状态做记忆化、关卡校验能无副作用地重放。

统计安全率要对沿途每个分支各问一次”走完这步还有救吗”,代价极高。两处优化把单关分析从几十秒压到毫秒级:

  • 共享记忆表:整场分析只遍历一次可达状态空间;
  • 死局判据剪枝:箭永不移动,所以它最好的处境就是别人全走光;连那时都飞不出去就直接判死——只会漏判,不会误杀。

当前状态 selftest: OK(74/74 可通关)、verify.py: ALL PASS,其中六项是 v3 新加的红线:终章箭数与决策面上限、第 7 章起无脑打法全灭、全局无脑通关率 < 40%、紧步占比逐章不降、安全率逐章不升、最大箭数 ≤ 18。

十、已知取舍

  • 前两章紧步占比只有 0.03。只用裂墙时,一面墙在十几支箭的局里只贡献一个抉择点,会被平均稀释。这两章定位就是”看路”,难度从第 3 章起飞。
  • 第 9 章没有传送门和序锁——加上之后摇不进难度带,保留了不带它们的版本。
  • 求解器对 9×9 / 16 箭以上开始吃力,终章几关的分析节点数接近预算上限。

尾巴

这个项目真正的产出不是 74 关,是一句结论:

难度不是”会不会输”,是”每一步要不要想”。 前者可以靠拉长流程刷出来,后者只能靠压窄决策面、再往窄面上放不可逆。

顺便,它还教会我一件事——先写求解器,再写游戏。没有那个求解器,我到今天还以为原作宣传的”走错锁死”是真的。

📦 源码https://github.com/ghostgorge/ArrowExodusV2 🕹 Windows 双击 run.bat 直接跑,build.bat 出单文件 exe;游戏内连按两下 Shift 有作弊台。


下一篇:DrCapsule——Dr. Mario 式药丸消除。回复「继续」。