☰
基于博弈搜索算法的智能五子棋设计:从Alpha-Beta剪枝到评估调优
2026/10/13 5:50:31 网站建设 项目流程

我做这个项目前,心里想得很简单:五子棋嘛,规则比象棋围棋简单太多,写个AI应该不难。真把博弈搜索算法完整落下去之后才发现,五子棋恰恰是那种“规则三分钟能学会、状态空间却能让人头皮发麻”的典型。这篇内容我围绕“基于博弈搜索算法的智能五子棋设计”整套思路来拆,从博弈树的规模、核心搜索框架、评估函数,到候选点剪枝和实测调优,把我踩过的坑和最后稳定运行的方案都整理了出来。

1. 五子棋AI为什么绕不开博弈树——从“必胜棋谱”到“穷举不可行”

1.1 规则简单不等于状态简单

五子棋的落子规则极其直白:黑白双方轮流在15×15的棋盘上落子,谁先在横、竖、斜任一方向连成五子,谁就赢。但“直白”和“容易计算”是两码事。棋盘上有225个交叉点,第一手有225种选择,第二手有224种,不考虑对称和重复棋形,整棵完全展开的博弈树规模是按阶乘级别增长的。虽然博弈论里早有结论:在标准无禁手规则下,黑棋有必胜走法,但这建立在庞大搜索的基础上,不是靠背几手棋谱就能覆盖所有变化的。

我刚开始想学网上那些“贪心回答”的思路:每一步找当前棋盘上分值最高的位置就行。试验后很快发现,静态贪心在几步之内就会被反杀,因为它根本看不到“对方下一步在这里冲四,我如果不挡必输”这种对手风险。所谓智能,核心不是每一步都挑眼前最爽的点,而是能在脑海里预演几条未来线路,选出在足够深的推演里依然占优的走法——这就是博弈树的来源。

1.2 博弈树规模到底有多可怕

用一组具体数字感受一下。假设每步只思考4层的搜索深度(我下、你下、我再下、你再下),第一层可选空位约220个,第二层约219个,第三层约218个,第四层约217个。如果全部展开,节点数大约是220的四次方量级,也就是超过20亿。即便现代CPU单条评估指令只要几纳秒,单纯枚举也是不可接受的。

这就是五子棋AI设计的第一道坎:必须用博弈搜索算法,而不是暴力展开。博弈搜索算法的核心思想是“不必访问所有节点,就能得到和全量搜索接近的结果”,Alpha-Beta剪枝就是最经典的方案。另外还有个思路是用蒙特卡洛树搜索(MCTS)通过随机对局采样代替全量展开,后文会提,但这次项目的主线仍然放在确定性博弈搜索上,因为五子棋在普通深度下的局势判断足够明确,确定性算法的可解释性也更好。

在实际编码之前,我还做了一件很值得的事:先写了一个纯模拟器,把棋盘、落子、胜负判定单独封装好。后续搜索算法和评估函数都只和这个模拟器交互,避免在调试AI逻辑时还要同时排查UI和输入状态。这算是老生常谈的模块化,但在这个项目里格外重要,因为搜索逻辑一旦出现错误,表现往往是“某一步棋特别蠢”,如果没有干净的模拟器环境,根本分不清是棋盘坐标算错了还是评估函数写歪了。

2. 核心搜索框架:极大极小值搜索和Alpha-Beta剪枝的落地配合

2.1 极大极小值搜索如何表达“攻守”

在博弈树里,从当前局面开始,交替轮到双方走棋。对我方(假设执黑)而言,每一层我都在“选最大收益”的分支;对对方而言,对方在“选让我最难受”的分支。把这种交替过程表达成一个递归评分结构,就是极大极小值搜索(Minimax)。

以深度4为例,搜索树的倒数第一层是对方的落子响应,我在这一层评估局势,得到一个分值。倒数第二层是我方节点,我会在这些分支里选择分值的最大值。再往上一层又是对方节点,对方会选最小值。最终回溯到根节点时,根节点选择的落子就是当前最优策略。

这里有个很关键的直觉:搜索深度里的“奇数层”“偶数层”分别代表谁在决策,整个递归过程需要严格交替取max和min,只要有一层取反,AI就会出现“明明在进攻却突然送死”的诡异行为。我调试时就踩过这个坑:写完代码后发现AI经常莫名其妙放弃防守,查了很久才发现是递归深度判断写错了,对方层我误取了max。

2.2 Alpha-Beta剪枝:砍掉那些不影响结论的分支

Alpha-Beta剪枝最经典的理解方式是:如果在某个max节点,已经找到一个分支的评分是8;此时轮到其兄弟分支内部的一个min节点向下探索,并且min节点发现某个子节点的评分是5且只会更小,那么max节点的最终结果不可能低于8,而这个min节点向上返回的评分上限只有5,这个min节点后续剩余分支不管结果如何,都不可能影响max节点选择8。于是就可以直接截断。

对应代码里,我维护两个参数:alpha表示当前路径上我方已经可以保证的最低分,beta表示对方能容忍的最高分。初始时alpha取负无穷,beta取正无穷。每搜索一个节点,如果该节点的返回值大于alpha就更新alpha,一旦alpha >= beta,说明这个分支已经没有继续探索的必要,直接剪掉。

下面是我在项目里用的核心搜索伪代码,写作Python风格,方便讲清楚逻辑:

def alpha_beta(board, depth, alpha, beta, is_maximizing): score = evaluate(board) if depth == 0 or game_over(board): return score if is_maximizing: best = -float("inf") for move in generate_candidates(board): board.play(move) best = max(best, alpha_beta(board, depth - 1, alpha, beta, False)) board.undo(move) alpha = max(alpha, best) if alpha >= beta: break return best else: best = float("inf") for move in generate_candidates(board): board.play(move) best = min(best, alpha_beta(board, depth - 1, alpha, beta, True)) board.undo(move) beta = min(beta, best) if alpha >= beta: break return best

我特别强调一下递归之前先调evaluate的原因:如果当前局面已经是五子连珠,就不必继续展开子节点。否则在接近终局时,搜索树里会出现大量“已经获胜却还在反复落子”的无意义分支,性能会被白白浪费。

2.3 固定深度和迭代加深的取舍

实际对局中,固定深度的代码最直观,但问题在于:如果中途某一层搜索特别耗时,玩家会明显感觉到AI“卡顿”。更好的做法是迭代加深(Iterative Deepening):先搜深度1,再搜深度2,直到时间预算用完。这样既能在超时前返回一个可用的最优走法,又可以利用浅层搜索的结果给深层候选手排序,大幅提升剪枝效率。

我最终的方案是:前三层按固定深度4跑,如果对局时间比较充裕且候选点数量少,自动加深到深度6。这里没有盲目追求八层深度,因为后续会提到,没有高质量的候选点裁剪和评估函数,单纯加深只会让AI变得又慢又情绪化。搜索深度对棋力提升并不是线性的,深度4到深度6是一个质变点,再往上,收益就被时间和代码复杂度吃掉了。

3. 评估函数设计:从“数连子”到“活三冲四”的量化过程

3.1 棋形识别是评估的基石

搜索算法本身只负责在树里找路,指引“哪条路好”的是评估函数。评估函数就像一个教练,在每盘棋的某个静止局面打分:分数为正说明对黑棋有利,为负说明对白棋有利。五子棋不像围棋那么玄,棋形特征比较突出,所以我的评估核心落在一组棋形概念上:连五、活四、冲四、活三、眠三、活二、眠二。

为什么要区分“活三”和“眠三”?因为分值差距悬殊。活三意为:如果对方不理,下一步就能形成活四并最终大概率获胜;眠三则是某个方向已经被堵死或即将被堵,只能形成冲四(进一步逼对方堵)甚至毫无后续。一个活三的分值通常应该大于好几个眠三之和,因为活三带来的是强制性的胜利节奏。

棋形识别我采用最朴素的方案:以每个棋子为中心,沿着横、竖、左右斜四个方向分别扫描连续的同色棋子,并同时检查两端状态。端点是空,则为活形;端点是对方棋子或者边界,则为眠形;两端都空,则是完全活形。这套逻辑虽然朴素,但稳定可靠,比花哨的神经网络更容易调试。

3.2 分数量化表和局部子力计算

在项目里我用了一张经验分值表:

棋形单方向分值
连五100000
活四10000
冲四1000
活三1000
眠三100
活二100
眠二10

这个表有个值得注意的细节:冲四和活三都给了1000,而不是冲四明显低于活三。因为实战中冲四往往能逼迫对方落子,可以制造先手节奏,如果把冲四分值定得太低,AI可能会忽略“连续冲四后再造活三”的战术组合。

方向叠加时,我采用“四个方向独立计分再求和”的方式。比如某个黑棋在横方向是活三,在竖方向也是活三,那这条位置就是一个双活三,威胁极强。评估值自然远超单纯的单活三。反过来,如果打分时只取了单方向最大值,AI就看不到“双三必胜”的棋形,这是很多新手实现最容易忽略的。

3.3 从静态棋形到全局步调

光有局部棋形还不够,我还加入了一层全局修正:中心距离权重。开局时,同样的连子,位于棋盘中央附近的位置,可扩展方向更多,价值更高;位于边角时,因为可发展空间受限,实际价值要打折扣。具体实现是对每个落点按棋盘中心距离做一次平滑加权。这层修正让AI在开局阶段自动往天元附近靠,而不是一上来就贴着边线走。

另外就是防守权重的问题。许多人写五子棋AI,评估函数只计算己方棋形,完全不看对方威胁。正确做法是分别计算黑棋全盘分和白棋全盘分,最终评估值用黑方分减去白方分。下棋本质上是不对称博弈,我执黑时希望这个差值越大越好,执白时希望差值越小越好。搜索框架里的max和min自然处理了这一点,但前提是评估函数里本来就包含双方棋形。

4. 候选点生成与启发式剪枝:搜索效率提升的关键一步

4.1 为什么不能真的“遍历所有空位”

在没有候选点约束时,第一层搜索要从200多个空位展开,第二层再乘上近200个空位,哪怕有Alpha-Beta剪枝,效率也低得让人着急。我发现一个简单却极其有效的约束:真正值得落子的位置,绝大多数都在已有棋子的周围2×2范围内。远离所有棋子的空位,既不能形成攻势也不能参与防守,先手价值极低。

所以我的候选点生成策略是:遍历所有已有棋子,收集以该棋子为中心、曼哈顿距离不超过2的空位,去掉重复后作为这一步的候选集。这个操作能把每层候选数从200多压到通常在30到60之间,搜索规模直接下降一个量级。实际对局中,胜率没有明显下降,但搜索速度快了非常多。

4.2 候选点排序:剪枝效率翻倍的魔术

Alpha-Beta剪枝的效率严重依赖分支的访问顺序。如果第一步就访问到最优分支,后面的兄弟分支很快会被剪掉,整棵树可能只扩展很少的节点;如果运气差,先访问一堆烂分支,alpha和beta长期不收敛,几乎等于全量搜索。

因此我在递归搜索之前,会先用一个轻量级的评估函数对候选点做一次排序:这个位置如果落子,己方棋形能得到多少分,同时对方在这个位置落子会对我方造成多大威胁。两者之和作为排序分数。这一步是纯启发式的,不需要递归,开销很小,但对剪枝效果的影响极其显著。

实测数据如下。相同评估函数和相同深度,在某个中盘局面下,不做排序时的搜索节点数是约18万,做了排序后降到约3万。差距接近6倍。这就是“同样的算法,不同实现,性能天差地别”的典型案例。

4.3 多层剪枝时的风险防御

候选点裁剪虽然提升了速度,也引入了风险。当AI评估那些“远离棋子”的空位时,可能漏掉一种罕见场景:某一步落子在看起来无关的位置,但它能同时封堵对方两条潜在线路,或者为未来五连埋下长线伏笔。不过在我对局实测中,这种场景出现的概率很低,同时也在深度增加到6以后基本被覆盖。如果追求极致棋力,可以引入第二层候选集,在常规候选点评估结束后,再补充几个基于全局防守空当的额外点,但项目主流程没有这么做,因为收益不明显,复杂度却会增加很多。

5. 实测联调记录:搜索深度、响应速度与棋力稳定性的取舍

5.1 性能对照:从深度4到深度8的真实数据

我在项目里做了一组控制变量测试:候选点排序固定开启,候选点集合固定生成,唯一改变的是搜索深度。测试机是一台普通笔记本,棋盘为15×15标准规格,每步允许的最大思考时间设定为3秒。

搜索深度平均每步耗时中盘节点数棋力表现
20.01s300只会盲目堵最近威胁,缺乏进攻组织
40.2s1.8万能实现简单活三进攻和基本防守
61.1s12万能实现冲四活三组合,懂得主动做局势
84.5s55万棋力更强,但3秒预算内经常超时

从表里能明显看出,深度6是一个比较甜的点:棋力已经不弱,同时能稳定控制在1秒左右返回。深度8虽然棋力更高,但如果不加更激进的边界裁剪,很难用在实时对局里。最终我默认使用深度6,并保留一个“长考模式”给非实时场景。

5.2 两个真实对局暴露的问题

第一次实测时,AI在优势局面下走出了一步让人困惑的棋:明明可以活四直接逼对方防守,却跑到边角去发展。复盘后发现,问题出在评估函数的局部视野:那个边角落子后局部棋形分值极高,但全局有效进攻线路并不多。这说明评估函数里对“进攻效率”的刻画不够,后来我加入了“棋子可发展方向数”这一弱特征,问题明显缓解。

第二个问题是防守过度。有一次AI面对一个并不致命的眠三,投入了大量子力去堵,反而给了对手另一个方向做活三的机会。这暴露了打分时的另一个陷阱:眠三单方向分值100,但多个方向的眠三叠加之后,AI会误判成巨大威胁。后来我在评估函数里对同方向重叠棋形做了“威胁合并”,只有真正能形成多个独立活三的情况才会获得高威胁分。

5.3 参数调优的心得

摸索下来,我认为评估函数的多个特征项如果同时存在,会互相干扰。比如得分项有活三、活二、冲四、眠三、区域权重,每一项的系数改动都可能让AI在攻防之间反复横跳。我最后的建议是:先固定核心兵种(连五、活四、冲四、活三)的分数,把所有其他项都调到很低或先注释掉,观察AI是不是能稳定做出冲四活三的战术动作。确认核心逻辑正确后,再逐步加入眠三、活二等辅助项。每加一项前,都先和旧版本在同一组残局上对弈,只有胜率提升才保留。这套“增量调参法”看起来慢,实际却是最快收敛到稳定版本的路径。

6. 这个项目还能往哪走:禁手规则、开局库与蒙特卡洛树搜索

6.1 加入禁手规则

标准无禁手规则下黑棋有必胜打法,很多对战平台会启用三三禁手、四四禁手、长连禁手来平衡黑白胜负。如果在设计里要支持禁手,搜索框架本身不用改,需要改的是评估函数和胜负判定:在扫描棋形时,如果在某个位置落子会形成双活三或双冲四,这个位置的落子非法。这一步牵扯到一个细节,即禁手判定必须基于“落子后形成的完整棋形”,而不是落子前的静态棋形。我曾经在这上面栽过跟头,因为评估函数检测的是落子后的新形状,可禁手检查却用了落子前的旧形状,导致AI认为某个禁手点可下。

6.2 开局库

搜索算法在开局阶段的效果并不好。原因很简单:开局落子稀疏,棋形特征弱,候选点排序的启发信息有限,搜索到深度6也很难看出明确优势。常见做法是维护一张“开局库”,里面保存若干经典开局前几手的强应对。我的项目里只收录了二三十种常见开局,效果就非常显著:AI前几步不再“思考人生”,而是快速走出稳定结构,中盘再接入搜索框架。

6.3 蒙特卡洛树搜索与神经网络方向

确定性博弈搜索有它的上限:深度越大,时间成本越高,而且评估函数始终是人工设计的。蒙特卡洛树搜索(MCTS)提供了另一种思路:通过大量随机模拟对局,统计每个落子的胜率,避免显式构建完整博弈树。MCTS更强的点在于和神经网络结合后,可以先通过训练得到的策略网络选择少量高概率落点,再结合价值网络评估局面,这也是目前棋类AI的主流方向。

不过对于五子棋这种相对小规模的棋类,纯MCTS在固定时间内的棋力未必比深度6的Alpha-Beta剪枝强,因为随机模拟的收敛速度较慢。如果要做混合方案,我建议把MCTS作为局部残局分析器,在深度搜索超时或局面极度复杂时调用,而不是完全替换搜索框架。

从我的个人实践看,基于博弈搜索算法的智能五子棋设计最让我受益的一点是:它让我明白算法本身的瓶颈往往不在理论复杂度而在工程细节。候选点裁剪、Alpha-Beta剪枝顺序、评估函数的特征设计,这三件事任何一个没做扎实,棋力都会有悬崖式下跌。如果你也要上手类似项目,我建议先把极简版跑通:深度4的Alpha-Beta剪枝加上一个只有一个“连五检测”的评估函数,然后在此基础上一点一点加特征。每一步的改动都能用肉眼观察到棋力变化,这种反馈是优化AI最有意思的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询