ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

计算机博弈入门:博弈树、Alpha-Beta剪枝与评估函数详解

计算机博弈入门:博弈树、Alpha-Beta剪枝与评估函数详解

简介:计算机博弈竞赛辅导资料是面向竞赛备赛与课程学习的一套专题PPT讲义,内容由东北大学机器博弈研究室整理,适合参赛选手、相关专业学生及入门研究者阅读。讲义先从棋类介绍与分类切入,涵盖中国象棋、国际象棋、围棋、五子棋、六子棋以及一字棋、二虎棋、三通棋、点格棋等典型棋类,通过对比棋盘、规则与复杂度,帮助读者理解不同博弈问题的难点。随后系统讲解计算机博弈的基本原理,包括博弈树展开与分析、棋局评估、alpha-beta剪枝与蒙特卡罗树搜索等关键方法,并介绍计算机博弈软件的构成以及基于搜索的机器博弈方法学特点,兼顾算法原理与程序实现视角。资源仅含1个PPT文件,压缩包大小为2.57MB,内容集中且以章节组织,便于按需浏览和反复学习。目前已有329人学习,对希望快速搭建机器博弈知识框架、备战计算机博弈竞赛或开展相关研究的读者具有实用参考价值。

1. 计算机博弈竞赛辅导资料:从规则到搜索方法的完整讲义

准备计算机博弈竞赛时,很多人第一反应是去找现成引擎源码,结果拿回来跑不通、改不动,最后卡在“不知道引擎每一步在算什么”。这份竞赛辅导资料来自某高校机器博弈研究室的系列讲义,内容覆盖棋类介绍与分类、计算机博弈基本原理、软件构成、棋局评估、博弈树展开与分析,以及机器博弈的方法学特点。它不直接给你可编译的代码,而是把做AI棋手之前必须想清楚的问题按逻辑排好:先懂规则,再懂搜索,最后才是写软件。适合两类人:一是第一次参加计算机博弈竞赛、需要快速建立全局认知的选手;二是已经写过一些搜索代码、但评估函数和规则处理始终不稳定的参赛者。如果你只想抄一段能跑的搜索,这份资料不是速成材料;但如果你想在比赛现场不被规则的边角料绊住,它能省下大量返工时间。

2. 棋类规则与分类:先手优势、复杂度与选型依据

2.1 完全信息动态博弈:为什么棋类是竞赛主力

讲义一上来就把研究对象框定成“不含牌的棋类”,并明确区分棋类与牌类:棋类属于完全信息动态博弈,所有局面信息对双方公开;牌类属于不完全信息动态博弈,还要处理隐藏信息和概率估计。这个区分不是理论空谈,它直接影响程序架构。对于完全信息棋类,搜索算法可以假定双方都看到同一个局面,评估函数也只依赖公共状态;对于牌类,你还得额外实现手牌模型、对手建模和采样。

计算机博弈竞赛大多选择完全信息棋类,也正因为这个特性让“搜索+评估”成为通用框架。我在实际备赛时会把这一条当作选型红线:如果某个棋种规则里存在不可见信息、随机骰子或者复杂的隐藏判定,我会直接跳过,因为同样的引擎骨架适配成本会高出很多。讲义后续讲到的博弈树展开、alpha-beta剪枝、棋局评估,全部建立在这个前提下。牌类博弈虽然也是博弈论研究对象,但竞赛里很少把它当成传统棋类来做,资源资料里也特意把它排除在外。

2.2 按着法分类:走子类、填子类与混合类的搜索差异

讲义按着法把棋分成三类:走子类、填子类和混合类。这个分类比按“棋种名称”分类更接近程序实现。走子类棋种开局前双方已摆好阵型,比如中国象棋、国际象棋、跳棋、苏拉卡尔塔;程序需要处理一个非空初始局面,后续每个节点做“移动”操作。填子类棋种开局前盘面无子,比如围棋、五子棋、点格棋;程序从空盘开始,每次“放入”一个棋子,合法位置非常多,搜索树的分支因子在初期会很大,但越往后可选位置越少。

混合类则要分阶段切换行为,比如讲义提到的五道棋、日本将棋,既有填子阶段,又有走子和吃子阶段。程序不能只写一套move规则,根节点阶段判断、状态机切换、阶段边界条件都需要单独测。我第一次把混合类棋的状态转移写成单一函数后,填子转走子时连续出现多个错误局面,后来才把阶段标志纳入局面编码。下面这张表是三类棋在程序实现上的关键差异。

类型代表棋种初始局面每步操作搜索实现重点
走子类中国象棋、国际象棋、苏拉卡尔塔、亚马逊布局固定移动目标棋子或吃子开局库、移动生成合法性、吃子规则
填子类围棋、五子棋、点格棋空盘放入己方棋子候选点剪枝、棋形评估、终局判断
混合类五道棋、日本将棋空盘或固定阵型先填子后走子阶段切换、两类规则共用一套局面表示

以中国象棋为例,讲义标明了棋盘是9×10,红黑双方各7个兵种共16子,还有长将、长捉、60步不吃子判和这类特殊规则。放到程序里,这意味着一套局面编码要同时支持坐标、兵种、行棋范围、胜负判定和重复局面检测。国际象棋虽然棋盘小到8×8,但王车易位、吃过路兵、兵升变这些特殊着法,一样是着法生成器里最容易漏掉的部分。填子类里围棋的贴目(5到7目)和五子棋的禁手,都不是简单的“下子”操作,而是在规则层面对先手优势做补偿。

2.3 先手优势处理:从五子棋禁手到六子棋连下两子

讲义专门指出五子棋已经被证明先手必胜,因此衍生出禁手、换手、金球制等赛制改良;六子棋则改为先手下一子、之后每手下两子的节奏,用来削减先手优势。这是理解“规则设计”和“程序策略”关系的最佳案例。对引擎开发而言,如果竞赛规则不做禁手处理,那么“先手必胜”搜索策略会主导一切,评估函数的价值会被压低;如果采用禁手,程序必须在着法生成器里显式维护禁手判断。

五子棋禁手包括三三、四四和长连,判断逻辑需要在每次黑棋落子后检查落点周围的所有棋形模式,牵扯到横向、纵向、两条斜线四个方向的连续计数。这部分我建议单独写一个模式匹配函数,不要并进普通的“赢棋判断”里。六子棋那种“先手1子、后手2子”的不对称节奏,则会让搜索树的深度和宽度出现明显差异:每一回合不是双方各一步,而是一方一步、另一方两步,评估函数必须反映这种“步数差”带来的补偿。如果你参赛用的是六子棋,最好在局面结构体里显式记录“轮到哪方、当前这一回合已经下过几子”,否则搜索到一半会漏掉强制连续落子分支。

2.4 民间棋类的规则还原难点:一字棋、二虎棋、三通棋、点格棋、苏拉卡尔塔、亚马逊

讲义后半部分列出一批民间棋类,规则看起来简单,翻译成代码却很考验细心。一字棋要求每次可以移动多个棋子但必须停留在同一目标棋位,且只能前进不能逆行,这种“一次动多子”的规则会打破“生成一个move动作”的直觉,程序得把组合移动展开成多个合法着法。二虎棋里虎方跳吃、犬方围困,输赢条件不对称,评估函数不能简单用子力优势,得加入“围困状态”特征。

三通棋的棋盘由64个小正三角形构成,属于非矩形棋盘,坐标映射不能直接用二维数组的(x,y),需要先确定三角格的编号方式。点格棋的“最后占边者再连一边”让回合控制权发生变化,还有死格、双环、长链、环等链式结构,是程序实现时最容易被低估的地方。苏拉卡尔塔必须沿着完整弧线吃子,每个棋子每步只能走一格,只有吃子时才沿弧线跳跃。亚马逊则每个着法由“移动+设障”两部分组成,障碍一旦放下就永久影响后续路径,这类复合动作是着法生成器的高频Bug来源。

对参加竞赛的人来说,这一页规律表的真正用途是“选棋种之前先评估规则成本”:如果只是练手,优先选走子类、吃子规则单一的棋种;如果追求竞赛差异化,再考虑点格棋一类有链式博弈结构的棋,但一定要把规则里所有特殊动作画成状态图再动笔。算法可以复用,规则引擎却必须为每个棋种重写,这个成本在选型时要算进去。

3. 博弈树与评估函数:从 Alpha-Beta 剪枝到搜索深度

3.1 博弈树展开:广度、深度与完全展开的不现实

先说博弈树最基本的形态:节点是棋局局面,根节点是当前实际局面,每个子节点对应一个合法着法后的局面,一层一层往下就是双方交替行棋的序列。讲义把“博弈树展开与分析”作为单独主题,就是提醒大家:计算机博弈不是靠记忆棋谱,而是靠实时展开这棵“未来的可能局面树”。如果你跳过这步直接写搜索,很容易把“枚举当前着法”和“博弈树搜索”混为一谈。

完全展开不现实。以国际象棋为例,平均分支因子约35,每加深一层,节点数成倍增长,到第10层已经是天文数字。即便中国象棋的分支因子稍低,纯枚举也扛不住。所以实际引擎都做两件事:用剪枝减少展开的节点,用评估函数提前判断“叶子节点”好坏,而不是一直展开到终局。明白了这个背景,很多参数就好解释了:搜索深度为什么通常设置成4到8层、评估函数为什么比“多算几步”更重要、为什么alpha-beta能把搜索效率提升一到两个数量级。

3.2 棋局评估:从相对子力值到多特征加权

棋局评估的目标是把一个局面的优劣压缩成一个数值。讲义直接给出了国际象棋的相对子力值:兵=1,马=3,象=3,车=5,后=9。这个子力值表是评估函数最常用的基线。单看子力还不够,位置、机动性、兵型结构都要变成特征。讲义里讲“启发式搜索”,本质是用启发式特征给搜索剪枝指方向,而不是真的算出精确胜率。最常见的评估函数形式是线性加权:E = sum(w_i * f_i),w_i是特征权重,f_i是特征值。

我在写评估函数时,会先把子力值表当作唯一权重,让引擎能下出“不吃亏”的棋,再逐步加入位置表和机动性权重。这么做的好处是:每次只改一个变量,翻车时能定位是哪条特征出了问题。讲义里国际象棋的“后=车+象”也说明了一个原则:特征之间不一定是正交的,某些棋子能力可以拆解成多个基础特征,权重可以叠加。这个思路换成中国象棋也一样,车、马、炮、兵各有相对价值,先做硬编码,后做参数化。

需要强调的是,评估函数的输出必须是相对于某一方的分数。我习惯约定为正数表示当前行动方占优,负数表示对方占优,同时在搜索递归里始终从当前节点视角计算,避免正负号在多层递归中整出玄学Bug。另一个常见误用是让评估值只反映子力差异,导致引擎在局面均衡时乱走,这时要用“位置表+兵型结构”给棋子安个“偏好坐标”,比如国际象棋的兵在中心格价值更高。

3.3 搜索算法:Alpha-Beta 剪枝的伪代码与参数取舍

有了评估函数,搜索算法才有意义。讲义提到的alpha-beta剪枝是让搜索深度翻倍的关键。它的核心思路是:搜索过程中维护一个当前最优值的窗口,如果某个分支已经不可能比窗口更好,就剪掉它,不再展开。下面这段是去掉一切工程细节的最小alpha-beta骨架。

def alpha_beta(state, depth, alpha, beta, maximizing): if depth == 0 or game_over(state): return evaluate(state) if maximizing: value = -float("inf") for move in legal_moves(state): child = apply_move(state, move) value = max(value, alpha_beta(child, depth - 1, alpha, beta, False)) alpha = max(alpha, value) if beta <= alpha: break # beta 剪枝 return value else: value = float("inf") for move in legal_moves(state): child = apply_move(state, move) value = min(value, alpha_beta(child, depth - 1, alpha, beta, True)) beta = min(beta, value) if beta <= alpha: break # alpha 剪枝 return value

逻辑说明:根节点调用时alpha取负无穷、beta取正无穷,maximizing=True。关键点在beta <= alpha时的break,意味着这个分支下无论再怎么展开都不可能让上层更好。alpha是当前极大值方能接受的保底值,beta是极小值方能接受的保底值,窗口越窄剪枝越快。工程上不要把这个伪代码直接拿去做性能测试,实际引擎还要配合着法排序,先搜最容易产生“当前最优”的分支,剪枝率会显著改善。常见做法是先用上一轮迭代加深的最佳着法排在第一位,再把吃子分值大的着法排前面,MVV-LVA(最大吃最小值)是简单有效的排序标准。

3.4 蒙特卡罗树搜索:当分支因子大到无法穷举

alpha-beta并不是唯一选择。讲义里点名的蒙特卡罗树搜索(MCTS)适合分支因子极大、评估函数不好写的棋种,典型例子是围棋。MCTS不再把每个节点都精确算到搜索深度,而是反复做“选择、扩展、模拟、回溯”:从根节点出发,用置信区间公式选择最值得探索的分支,展开一个新节点,再随机或伪随机模拟对局到终局,把胜负结果回传。它的思想是拿大量随机模拟换稳定的胜率估计,不需要手工设计很复杂的评估函数。

但MCTS也不是银弹。它需要不违背规则的快速模拟器,需要给每次选择留足够的模拟次数,否则前期噪声很大。我见过有同学把alpha-beta代码强行套到围棋上,搜了一层就超时;换成MCTS后虽然局面评估很粗糙,但反而能在时间内完成决策。选型逻辑很简单:分支因子中等且评估特征清晰,优先alpha-beta;分支因子巨大且评估特征模糊,优先MCTS。六子棋这种中间地带可以两者都试,先用alpha-beta跑通,再用MCTS对比胜率,看哪种方式在限时内更稳定。

4. 引擎软件构成:棋盘表示、搜索与评估的接线方式

4.1 四层架构:界面、棋盘模拟、搜索算法、评估函数

讲义把软件构成拆成四个块:用户界面、棋盘模拟、搜索算法、评估函数。这四层是逻辑边界,不是代码目录。用户界面负责显示棋局和接收着法指令,棋盘模拟负责维护当前局面、生成合法着法、执行着法,搜索算法负责在给定时限内选择着法,评估函数负责给局面打分。四者之间只有局面表示和着法表示这两个数据接口,内部实现可以完全隔离。

对竞赛程序来说,最忌讳的是把搜索逻辑和界面代码混在一起。我在第一次写某跨平台系统时,把着法生成直接写在按钮回调里,结果每次点击都相当于重新搜索,后期想加开局库都没地方下手。按讲义的四层划分,搜索和评估应该是纯后台模块,界面只要能调用“让引擎下一步”,不管引擎内部是搜索还是查表。这样后期替换搜索算法、调整评估权重,都不需要动界面代码,也能放到命令行环境跑自对弈测试。

4.2 棋盘表示方案:二维数组与位棋盘的选型

棋盘表示是整个引擎的地基。最简单的方案是二维数组,比如中国象棋9×10、国际象棋8×8、围棋19×19,空格和棋子用枚举值表示。二维数组直观、debug方便,速度和内存其实也够应付竞赛规模。位棋盘方案用一个或多个64位整数表示每种棋子的分布,效率极高,但理解成本和Bug排查成本都更高。对新手来说,位棋盘最大的问题是:局面打印不方便,出问题时很难肉眼定位。

我一般建议竞赛初期用二维数组,把规则处理和着法生成跑通,再去考虑位棋盘优化。讲义里的棋类棋盘尺寸都写得很清楚,这其实是初期调试最有用的参数:中国象棋9×10、国际象棋8×8、围棋19×19、五子棋15×15、六子棋19×19、三通棋64个三角格位。先把这些尺寸定义成常量,后面所有函数都从棋盘坐标出发,能避免一大批越界问题。如果你要做三通棋这种三角棋盘,二维数组的(x,y)就不够用了,可能要自定义“格编号+相邻格表”的数据结构,着法生成也从查表开始。

4.3 着法生成与合法性校验:规则复杂度的直接体现

着法生成器是把“当前局面”变成“候选着法列表”的模块。这个模块最体现棋类规则复杂度,也是Bug最密集的部分。比如中国象棋的炮需要隔子吃子,马有蹩马腿;国际象棋的王车易位要检查路径上没有敌方攻击;苏拉卡尔塔的弧线吃子必须经过完整弧;亚马逊的着法由移动+设障两步构成;点格棋的“最后占边者再走一步”是规则层面的强制逻辑,不是策略选择。

我建议把着法生成拆成两段:先根据棋种规则生成所有候选动作,再统一做合法性检查。合法性检查不是简单判断目标位置是否为空,要连带检查“走后是否被将军”“是否形成重复局面”“是否踩中禁手”。这一步宁可多写几个辅助函数,也不要贪快把检查合并到一个表达式里。验证着法生成器最直接的方法,是拿历史棋谱回放:每一步读入后,让程序生成合法着法列表,再去棋谱里找对应的着法,找不到就说明规则实现有洞。这个过程很枯燥,但能把搜索层的“隐性错误”提前暴露出来。

4.4 迭代加深搜索:深度、时限与搜索质量的平衡

实际竞赛引擎很少一次性固定搜索深度,因为不同局面复杂度不同,固定深度要么超时要么浪费算力。更常用的是迭代加深:先从深度1开始搜完,保存当前最佳着法;再搜深度2,用上一轮结果优化着法排序;依次加深,直到时间用完,最后一次完整搜索结果作为最终着法。这样既能在超时前退回到次优着法,又能通过每次加深时的着法历史来提高alpha-beta剪枝效率。

参数上,根节点和子节点的时间分配不要平均分。我一般根节点留一半时间,子节点用迭代加深内部的自适应时间片;比赛时还要在引擎和界面之间留少量余量,避免最后一步因为调度问题掉线。下面是一个最小实现思路。

def iterative_deepening(state, time_limit): result = None start = time.time() depth = 1 while time.time() - start < time_limit: result = search_root_with_ordering(state, depth) depth += 1 return result

逻辑说明:result保存最近一次完整深度搜索的结果,超时后直接返回它,而不是返回一个搜到一半的结果。search_root_with_ordering内部会复用上一轮最佳着法来排序,让alpha-beta剪枝效率尽量提升。实际使用时,time_limit要根据比赛规则设置,比如单步1秒或总时限60秒,不要等到界面催促才动手。从讲义角度理解,这一步是最典型的“工程技巧”,它不改搜索逻辑,只改调用方式,就能把稳定性提升一个档次。

5. 备赛避坑指南:超时、和棋与评估玄学的四个案例

5.1 现象:搜索深度一提就超时

很多同学比赛前一天把固定深度从4改成6,结果每步都超时。原因有两层:一是没有时间控制,搜索直到整棵树展开完才返回;二是着法顺序不好,alpha-beta剪枝效率低,深度变化导致节点数增长远超预期。解决方法是改用迭代加深,给每次搜索设置时间上限;同时对着法生成排序,先用上一步的最佳着法或吃子价值高的着法开路。我一般会在引擎里加一个简短的“当前深度+剩余时间”日志,观察每层耗时,确认是规则生成慢还是搜索慢。若日志里显示某一层的节点数异常高,先检查着法生成器是不是漏了裁剪,再去调排序。

5.2 现象:评估函数刚调好就翻车

评估函数是很多人说的“玄学”重灾区。改一个权重,内部测试赢了几盘,正式比赛却连续被碾。原因很直接:评估函数没有经过大量自对弈和标准局面测试,权重变化带来的影响被偶然的对手风格放大。解决路径是固定“搜索深度”和“时间限制”后,用自对弈回放比较前后版本的胜负统计;再把若干标准局面(开局、中局、残局)单独打分,看数值是否符合直觉。血泪经验:评估函数每次只改一组特征,别同时调子力、位置和机动性。连续改三个权重后,即使胜率有变化,你也说不清是哪一项起了作用。

5.3 现象:和棋被判负

现象是对手明显没法赢,但引擎突然走了一步送子,最后判负。原因通常是重复局面检测和“60步不吃子判和”这类规则没实现。中国象棋里有长将、长捉、60步不吃子判和,国际象棋有三次重复局面和五十步规则,五子棋有禁手。程序如果不检测,搜索会把“重复局面”当成正常着法继续走,时间耗尽或者送子。解决方法是把规则裁判逻辑独立成模块,在着法合法性和终局判断两个地方都接入;和棋概率高的棋种还要在评估函数里给重复局面一个负权重,避免引擎主动选择重复。

5.4 现象:先手优势被忽略

现象是同一个引擎执黑胜率远高于执白,或者反过来。原因不是代码Bug,而是规则本身先手优势明显,比如五子棋已被证明先手必胜,六子棋也有特定的先后手次序。解决方法是先确认竞赛赛制是否有禁手、换手这类配套规则;如果赛制不处理先手,就要在评估函数里补偿,或写专门的先手策略库。我在准备某次五子棋竞赛时,把禁手检测逻辑单独测了一个下午,因为规则判断一旦错,整个搜索都会在非法局面上打转。最后我还把先后手分开统计胜率,每调一次权重就看两边的变化,防止评估函数只优化一方。

6. 用讲义反推一个可验证的最小引擎骨架

6.1 从讲义到最小闭环:一张检查清单

如果你拿到的是这份辅导资料,建议不要直接去读“软件构成”那一节,而是先自己反推:如果让我写一个最简单的棋类引擎,最少需要哪几个模块?对应讲义内容,我会这样做:先选定一个规则最干净的棋种,比如五子棋或点格棋,然后按清单完成最小闭环。

步骤对应讲义主题验收标准
1定义棋盘尺寸与棋子表示棋类介绍
2实现着法生成与合法性校验棋类规则
3实现评估函数(用相对子力值起步)棋局评估
4实现固定深度alpha-beta搜索搜索算法
5接入人机界面或命令行软件构成
6用自对弈固定深度对比评估函数调整方法学特点

完成前四条,其实已经有一个“弱但完整”的引擎了。先用二维数组,别碰位棋盘;评估函数只放子力,别加位置特征;搜索深度固定2,先保证合法着法全对,再谈深度。这个骨架的价值在于:一旦出现规则错误,你可以在棋盘打印和着法生成阶段就定位,而不是等到搜索阶段面对一片乱码。每通过一个验收,就把讲义对应章节翻出来对照一次,比从头读完整份讲义更有收获。

从那以后,我每次拿到一套竞赛辅导资料,都会强制自己先画“规则、搜索、评估”的接线图,再动手写引擎:棋盘表示是地基,着法生成是规则翻译机,评估函数是搜索的导向,搜索是在时间里找Minimax窗口内的最优。每次调参翻车,我也都按这个顺序往回查,而不是怀疑随机数。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进