五子棋 AI:算杀层与搜索层怎么分工
纯 alpha-beta 在五子棋上会漏掉必杀。我把决策拆成三层——成五、堵五、算杀各司其职,剩下的才交给迭代加深加置换表,快棋也能下出像样的棋。
五子棋 的 AI 一开始只有一个 alpha-beta 搜索,深度开到 4 层,表现却很糟:对手摆出一个活三它不管,自己有成五的机会有时候也不走。原因是搜索深度不够时,估值函数把「三步之后必输」看成了普通局面。
解决办法不是把深度加上去,而是把可以确定的事情从搜索里拿出来。
三层决策,顺序不能换
每走一步,AI 按固定顺序过三道闸:
| 顺序 | 判断 | 处理 |
|---|---|---|
| 1 | 我能成五吗 | 直接走,不搜 |
| 2 | 对手下一步能成五吗 | 堵,且优先选「堵了之后对手造不出活四」的点 |
| 3 | 我能造活四吗 / 对手能造活四吗 | 走 / 破,不搜 |
| 4 | 以上都不成立 | 交给 alpha-beta |
前三条叫算杀层。它们的共同点是:结论由棋形唯一确定,跟后面怎么走无关。这类手交给搜索是浪费——搜索还要花节点去「验证」一个已经确定的事实,而且深度不够时验证不了,反而出错。
搜索该用来处理不确定性,不该用来处理已经确定的棋形。
活四为什么必须单独判
「活四」是两头都能成五的四连(.XXXX.)。一旦有一方摆出活四,另一方就输了——对手只有一个子,堵不住两头。
第三层如果只看「我有四连了吗」,会把冲四(一头被堵的四连)也算进去。冲四对手一堵就没了,走它等于白送一手。所以判据是棋形字符串:011110 是活四,011112 或 211110 是冲四。
我对每一条直线(横、竖、两个斜向)取 9 格窗口,转成 0/1/2 的串再匹配:
const PATTERN = {
FIVE: 1e7,
OPEN_FOUR: 3e5,
FOUR: 2.6e4,
OPEN_THREE: 2e4,
THREE: 1100,
OPEN_TWO: 600,
TWO: 90,
ONE: 6,
};
这些数字不是随便定的。它们要满足两条:成五压倒一切(1e7 比所有别的加起来还大),以及活四必须高于「两个活三」的和(3e5 大于 2e4 × 2)——否则 AI 会拿一个必杀的活四去换两个活三。
搜索层的三件常规武器
第 4 层才是真正的搜索,用了三个标准件:
- 迭代加深:从深度 1 往上加,每层都留一份结果。时间到就用最后一层搜完的结论,不会半途而废。
- 置换表:局面用 Zobrist 哈希做键,存深度、分值和一个界限标志(精确值 / 只知上界 / 只知下界)。不同路径走到同一个局面时直接取用。
- 候选裁剪:只搜已有棋子附近的位置。棋盘 225 格,真正值得考虑的一手通常不到 20 个。
还有一个容易忽略的细节:时间预算要在搜索内部检查,不是在外面设 setTimeout。我每搜 1024 个节点看一次时钟,超时就置中止标志,逐层原路返回。否则一层搜到一半被打断,拿到的分值是不完整的。
难度就是时间预算
五档难度不是五套代码,是同一个引擎的五组参数:
| 难度 | 深度 | 时间 | 扰动 | 候选宽度 |
|---|---|---|---|---|
| 1 新手 | 1 | 90ms | 0.9 | 8 |
| 2 入门 | 2 | 200ms | 0.5 | 10 |
| 3 进阶 | 4 | 450ms | 0.18 | 12 |
| 4 困难 | 6 | 900ms | 0.04 | 14 |
| 5 大师 | 8 | 1800ms | 0 | 16 |
低难度的「扰动」是在靠前的候选里随机挑一个——纯粹的弱化靠减深度效果不好,因为算杀层还在,新手档照样能一步成五。加噪声才有「这档子会犯错」的感觉。
让 AI 把想法说出来
调优时最麻烦的是「它为什么走这一步」。加搜索迹不难:搜完把每层的深度、最佳手、分值记下来,根节点再存前几个候选的明细。
真正花了点功夫的是分值。alpha-beta 返回的大部分是边界值——「不会比 X 好」——直接拿来展示,一排候选会显示成一模一样的数字,看着就像坏了。所以用户主动点开工具树时,根层每个候选改用全窗口搜一遍,换到真实分。代价是慢一截,但只在有人真想看的时候付。
给用户看的数字必须是真数字。用边界值凑出来的「过程」比没有过程更糟。
一点体会
这个 AI 现在的棋力大概到「会把活三当回事」的程度,离真正的强引擎还差得远(没有 VCF/VCT 搜索,没有启发式评估的学习)。但对一个网页小游戏来说,算杀层带来的提升远比把深度从 4 加到 8 明显——因为决定胜负的往往就是那几步一口气。

评论
…