2048求解器的原理

盘面下方的提示和自动玩按钮,给出的不是预先写好的建议。它们会在你的浏览器里,针对眼前这个盘面本身做一次真正的搜索,返回评分最高的一步。提示会在盘面上显示一个箭头,自动玩则会一直走这一步,直到对局结束。什么都不会发送到服务器。

从空盘面开始、全部交给这个搜索,它有91.4%的对局能拼到2048方块。下面讲它是怎么做到的。

为什么用期望最大化,而不是下棋的算法

处理博弈树的经典工具是极小化极大:假设对手总会选最坏的应对,并据此做计划。可是2048没有对手,有的只是随机出现的方块,而这个区别意义重大。

按极小化极大思路下棋的玩家,会把新方块当成恶意的,认为4一定会出现在最糟的格子里。结果是把算力全都花在防备三十次里未必出现一次的盘面上,下出多疑、偏守的棋。期望最大化则不同,它把每一种实际可能出现的情况按各自的概率加权,再取平均。它是照概率下棋,而不是照噩梦下棋;只要随机没有恶意,这就是正确的模型。

所以搜索会在两种节点之间交替。在玩家节点,四个方向全都试一遍,保留结果最好的那个。在机会节点,在每一个空格里以0.9的权重放一个2、以0.1的权重放一个4,分别评估后取平均。这和游戏本身出方块时的90比10比例完全相同。

求解器眼中的“好盘面”

搜索不可能一直展开到对局结束,所以求解器在搜索的最底层,用四项指标给盘面打分,再按下面的权重相加。

指标权重衡量的是什么
空格2.7空着的格子数本身。这是分量最重的一项,因为格子用光正是输棋的原因。
单调性1沿着一行或一列,数字是否朝一个方向平稳增大,而不是忽大忽小。
角落1最大的方块位于四个角落之一时,按它的大小给的加分。
平滑度0.1大小相差悬殊的方块彼此相邻时的扣分。512旁边的2永远合并不了,只是个负担。

四项里有三项,不过是把人们常给的建议翻译成了算式:留出空格,保持排列,把大方块放在角落。把这些建议变成判断的,是权重。只要空格能多出足够多,搜索会心甘情愿地接受角落的位置稍差一点。而人往往恰恰把这笔交易算反了。

每一项用的都不是方块上的数字本身,而是以2为底的对数。这样从2到4的差距,和从1024到2048的差距算得一样大。否则盘面上最大的那个方块会把其他一切都淹没。

把算力花在需要的地方

机会节点在每个空格上都要分叉,而且每次分两支。空格有十个的宽松盘面,每一层就是二十个分支;空格只剩三个的拥挤盘面,只有六个。把两者搜到同样的深度是本末倒置:宽松的盘面容易活下去,搜索起来却很费力;拥挤的盘面正好相反。

所以深度会随盘面调整。

空格搜索深度
7个以上往前看2步
4~6个往前看3步
3个以下往前看4步

宽松的盘面轻轻扫过,变挤了就更加谨慎。这正好复刻了高手的直觉。也正因如此,无论在对局的哪个阶段,从点击到箭头出现的等待时间都能保持在舒服的范围内。

实际水平

在500局完整的对局里,拼到2048的对局是91.4%,得分中位数是37,004分,对局长度的中位数是1,943步。包含对照组的完整结果里,可以结合上下文看这些数字。

有必要说清楚:这并不是最尖端的2048机器人。公开的求解器使用位棋盘表示、预先算好的移动表和置换表缓存,搜得深得多,能拼到32768。这个求解器在浏览器的主线程上用普通数组运行,步与步之间不做缓存,而且为了不让页面卡顿,限制在往前看4步以内。它不是为了刷纪录,而是为了用几乎感觉不到等待的速度,回答“这里该怎么走?”这个问题。

不毁掉游戏乐趣的用法

用好提示的方法,不是照着它走。先自己决定这一步,再按提示。箭头一致的话,没什么新东西可学;可要是不一致,你就找到了自己读错的那个盘面,而水平提高的种子正在那里。

自动玩与其当救火队员,不如当示范赛来用更有价值。在你自己觉得难的局面上让它跑一跑,看它守住哪个方向、刻意不做哪些合并。随时按方向键,就能马上把操作拿回来。

求解器是大约250行、没有任何依赖的JavaScript,作为普通文件放在/js/ai.js。想核实这里写的内容,可以用浏览器的查看源代码直接读它。

准备好试一试了吗?

开始玩2048
试试我们的每日挑战