1. 从零理解Minimax与Alpha-Beta剪枝的核心逻辑
1.1 这个算法到底在解决什么问题
第一次接触Minimax的人,往往会觉得它像是某种高深的数学理论。其实说白了,它就是一套在对抗性场景下做最优决策的搜索框架。你下一盘棋,轮到你走的时候,你需要考虑:我走这一步之后,对手会怎么回应?对手回应之后,我又该怎么走?如此往复,直到分出胜负。Minimax做的就是把这棵“博弈树”完整地展开,然后从叶子节点倒推回来,告诉你当前哪一步最好。
这个算法的核心假设非常朴素:你的对手和你一样聪明,而且他一定会选择对你最不利的那一步。所以你在做决策时,不能假设对手会犯错,而要在“对手始终选择最优应对”的前提下,找到让自己收益最大化的走法。这就是Minimax这个名字的由来——在最大化自己和最小化对手之间交替进行。
我最初写这个算法的时候,用的是最朴素的递归实现。一个棋盘状态进来,枚举所有合法走法,递归下去,直到达到终止条件(赢、输、平局或达到搜索深度上限),然后把分数一层层传回来。代码不到五十行就能跑通,逻辑非常清晰。但问题也很明显:搜索空间爆炸。以井字棋为例,状态空间还算小,但换成五子棋、象棋、国际象棋,分支因子动辄三四十,搜索深度稍微加几层,节点数就是指数级增长,普通机器根本扛不住。
Alpha-Beta剪枝就是在这个背景下登场的。它不改变Minimax的最终结果,但能砍掉大量明显不需要搜索的分支,在理想情况下可以把搜索节点数从O(b^d)降到O(b^(d/2)),相当于同样时间内能多搜一倍的深度。这个提升在实际对弈程序里是质变级别的。
1.2 为什么选择Alpha-Beta而不是其他方案
有人可能会问:现在不是有很多更高级的搜索算法吗?比如蒙特卡洛树搜索、神经网络评估等等,为什么还要用Alpha-Beta?这个问题我在实际项目里也反复权衡过。
首先,Alpha-Beta剪枝是精确搜索,只要搜索深度足够,它给出的决策在给定评估函数下是最优的。蒙特卡洛树搜索虽然在大规模博弈中表现优异,但它是概率性的,需要大量采样才能收敛,而且对评估函数的设计依赖不同。其次,Alpha-Beta的实现极其轻量,不需要训练模型,不需要大量数据,纯靠逻辑就能跑起来,非常适合作为博弈类项目的入门方案或者对实时性要求较高的场景。
再者,Alpha-Beta剪枝和Minimax是天然绑定的。你不需要额外设计复杂的启发式规则,只需要在递归过程中维护两个边界值alpha和beta,就能实现剪枝。这种简洁性让它在教学、原型开发、小型棋类游戏中依然是最主流的选择。
当然,纯Alpha-Beta也有它的局限。搜索深度受限于计算资源,评估函数的质量直接决定棋力上限。所以实际工程中,通常会配合迭代加深、置换表、走法排序、静态搜索等优化手段一起使用。这些后面会逐一展开。
1.3 回溯分析在其中的角色
标题里提到的“回溯分析”,其实是指算法在递归搜索过程中,从叶子节点向上返回分数并更新决策的过程。这个过程是理解Minimax的关键,也是调试时最容易出问题的地方。
很多人写递归的时候,只关注“往下搜”,忽略了“往回传”的逻辑。结果就是搜索树建起来了,但分数传不回来,或者传回来的分数对应不上正确的层级。回溯分析要求你在每一层递归返回时,清楚地知道当前是最大化层还是最小化层,alpha和beta应该如何更新,剪枝条件是否满足。
我在第一次实现的时候,就因为层级判断搞反了,导致AI总是选择最差的走法。后来加了一组调试输出,把每一层的返回值、alpha、beta都打印出来,才定位到问题。所以这部分我会在后面的实操环节详细拆解,把回溯的每一步都讲清楚。
2. 核心细节解析与实操要点
2.1 博弈树的节点结构与递归框架
在动手写代码之前,先把博弈树的节点结构想清楚。一个节点代表一个棋盘状态,它需要包含以下信息:当前轮到谁走、棋盘数据、以及可能的走法列表。在递归函数中,我们通常不需要显式地构建整棵树,而是通过函数调用栈隐式地维护搜索路径。
递归函数的基本签名大概是这样:传入当前状态、当前搜索深度、alpha值、beta值,返回当前状态对当前玩家的评估分数。函数内部首先判断是否达到终止条件——赢了、输了、平局或者深度耗尽。如果是,直接返回评估分数。否则,枚举所有合法走法,对每个走法生成新状态,递归调用自身,然后根据当前是最大化层还是最小化层来更新alpha或beta。
这里有一个容易踩的坑:评估函数的视角。评估函数返回的分数,应该是站在“当前行动方”的角度,还是站在“某一固定方”的角度?我建议统一站在“最大化方”的角度,这样在回溯时逻辑更清晰。如果当前层是最小化方,取反即可。这个细节看起来小,但一旦搞混,整个搜索结果就全乱了。
另一个要点是走法生成的质量。如果走法列表里包含非法走法,或者遗漏了合法走法,搜索结果就会出错。我在测试阶段专门写了一个校验函数,把生成的走法和规则引擎的结果做对比,确保没有遗漏和多余。
2.2 Alpha和Beta的更新逻辑与剪枝条件
Alpha和Beta是Alpha-Beta剪枝的灵魂。简单来说,Alpha是最大化方当前能保证获得的最低分数,Beta是最小化方当前能保证获得的最高分数。初始时,Alpha为负无穷,Beta为正无穷。
在最大化层,我们尝试找到一个走法,使得返回值尽可能大。每搜索完一个子节点,就更新Alpha为当前最大值。如果Alpha大于等于Beta,说明这个分支已经不可能影响最终决策了,直接剪掉,返回当前值。
在最小化层,逻辑对称。我们尝试找到一个走法,使得返回值尽可能小。每搜索完一个子节点,就更新Beta为当前最小值。如果Beta小于等于Alpha,同样剪枝。
这个逻辑用文字描述很绕,但用代码写出来非常简洁。关键在于理解:剪枝发生的条件是当前节点的搜索窗口已经无效了。也就是说,父节点已经有一个更好的选择了,当前分支再搜下去也不会改变父节点的决策。
我实测下来,剪枝效率高度依赖于走法排序。如果先搜索最好的走法,剪枝效果会好很多。所以在实际实现中,通常会先用一个简单的启发式函数对走法排序,把看起来好的走法排在前面。这一步的投入产出比非常高,往往能让搜索速度提升好几倍。
2.3 评估函数的设计原则与常见误区
评估函数是Minimax的“眼睛”。搜索再深,如果评估函数不准,AI也下不好棋。设计评估函数时,有几个原则值得遵循。
第一,评估函数要快。它会在叶子节点被调用成千上万次,哪怕多一个循环,整体耗时都会显著增加。所以尽量用简单的线性组合,避免复杂的条件判断和浮点运算。
第二,评估函数要稳定。同样的局面,评估分数应该一致,不能有随机性。否则搜索过程中会出现分数抖动,导致决策不稳定。
第三,评估函数要区分度足够。如果所有局面的分数都差不多,搜索就失去了意义。好的评估函数应该能明显区分优势局面和劣势局面。
常见的误区是过度设计评估函数。我见过有人把评估函数写得极其复杂,包含几十个特征,结果搜索速度慢得离谱,而且因为特征之间权重没调好,棋力反而不如简单版本。我的经验是:先用最少的特征跑通流程,然后逐步增加特征,每次增加都做对比测试,确保有正向收益。
另外,评估函数的量纲要统一。比如棋子价值用整数,位置优势用小数,最后加权求和时要注意尺度匹配。否则某个特征会主导整个评估结果,其他特征形同虚设。
2.4 搜索深度与性能的平衡策略
搜索深度是Minimax的另一个关键参数。深度越深,AI看得越远,棋力越强。但深度每增加一层,搜索节点数就乘以分支因子。在Alpha-Beta剪枝下,理想情况是乘以分支因子的平方根,但实际中受走法排序影响,往往达不到理想效果。
我通常的做法是先确定时间预算,然后通过迭代加深来动态调整搜索深度。具体来说,从深度1开始搜,搜完如果还有时间,就搜深度2,以此类推。每一层的搜索结果可以用来对下一层的走法排序,进一步提升剪枝效率。
这种策略的好处是随时可以中断。在实时对弈场景中,如果时间到了,直接返回当前已完成深度的最佳走法,不会出现“搜到一半没结果”的情况。
还有一个技巧是设置最小搜索深度。即使时间很紧,也要保证至少搜到某个深度,否则AI的决策质量会急剧下降。这个最小深度根据具体游戏和硬件性能来定,一般井字棋3层就够,五子棋可能需要6层以上。
3. 实操过程与核心环节实现
3.1 环境准备与项目结构搭建
我用的开发环境是Python 3.10,没有引入任何第三方库,纯标准库实现。这样做的好处是代码可以直接复制运行,不依赖环境配置。项目结构很简单,三个文件:game.py负责游戏规则和状态管理,ai.py负责Minimax搜索和Alpha-Beta剪枝,main.py负责交互和测试。
如果你用的是其他语言,逻辑完全一样,只是语法不同。我后来用JavaScript重写过一版,核心递归函数的结构几乎没变,只是把列表操作换成了数组操作。
在开始写搜索之前,先把游戏规则实现好。以井字棋为例,棋盘用长度为9的列表表示,0表示空位,1表示先手,2表示后手。合法走法就是所有值为0的位置。胜负判断就是检查所有行、列、对角线是否三连。
这一步看起来简单,但一定要写单元测试。我当初就因为对角线判断写错了索引,导致AI认为某些局面已经赢了,实际上没有。这种bug在搜索过程中会被放大,因为AI会基于错误的胜负判断做出决策。
3.2 Minimax基础版本的完整实现
先上基础版本的代码,不带剪枝,方便理解回溯过程。
def minimax(state, depth, maximizing): if is_terminal(state) or depth == 0: return evaluate(state) if maximizing: best = float('-inf') for move in get_legal_moves(state): new_state = make_move(state, move, current_player) score = minimax(new_state, depth - 1, False) best = max(best, score) return best else: best = float('inf') for move in get_legal_moves(state): new_state = make_move(state, move, opponent) score = minimax(new_state, depth - 1, True) best = min(best, score) return best这段代码的逻辑非常直白:如果是最大化层,就找所有子节点中分数最高的;如果是最小化层,就找分数最低的。递归的终止条件是到达叶子节点或者深度耗尽。
但这段代码有个问题:它只返回分数,不返回对应的走法。在实际对弈中,我们需要知道具体走哪一步。所以通常会在最外层单独写一个函数,枚举当前所有走法,对每个走法调用minimax,然后选择分数最高的那个。
我实测下来,井字棋用这个版本,搜索深度设为9(也就是搜到终局),在普通笔记本上大概需要几秒钟。如果换成五子棋,深度设为4,分支因子按20算,节点数就是20^4=160000,勉强能跑,但再深就不行了。
3.3 加入Alpha-Beta剪枝的改造过程
现在把剪枝加进去。改造的核心是在递归函数中增加alpha和beta两个参数,并在循环中检查剪枝条件。
def alphabeta(state, depth, alpha, beta, maximizing): if is_terminal(state) or depth == 0: return evaluate(state) if maximizing: best = float('-inf') for move in get_legal_moves(state): new_state = make_move(state, move, current_player) score = alphabeta(new_state, depth - 1, alpha, beta, False) best = max(best, score) alpha = max(alpha, best) if alpha >= beta: break return best else: best = float('inf') for move in get_legal_moves(state): new_state = make_move(state, move, opponent) score = alphabeta(new_state, depth - 1, alpha, beta, True) best = min(best, score) beta = min(beta, best) if beta <= alpha: break return best改动很小,但效果立竿见影。同样的井字棋,搜索时间从几秒降到了毫秒级。五子棋深度4的搜索也从勉强能跑变成了瞬间完成。
这里的关键是剪枝条件的判断位置。必须在更新完alpha或beta之后立即检查,不能等到循环结束。我见过有人在循环外检查,那样就完全失去了剪枝的意义。
另一个细节是alpha和beta的传递。在递归调用时,要把当前的alpha和beta传下去,而不是重新初始化为无穷。这样才能让剪枝信息在整条搜索路径上生效。
3.4 走法排序的优化实现
前面提到走法排序对剪枝效率影响巨大。最简单的排序策略是用评估函数对每个走法做一个快速打分,然后按分数从高到低排序。在最大化层,高分走法排前面;在最小化层,低分走法排前面。
def order_moves(state, moves, maximizing): scored = [] for move in moves: new_state = make_move(state, move, current_player) score = evaluate(new_state) scored.append((score, move)) scored.sort(reverse=maximizing) return [move for _, move in scored]这个排序本身也有开销,因为要对每个走法调用一次评估函数。但实测下来,这个开销远小于剪枝带来的收益。在五子棋中,加了排序之后,同样深度的搜索节点数减少了大约60%。
如果评估函数比较慢,可以用更轻量的启发式规则来排序。比如在棋类游戏中,优先考虑靠近中心的位置、优先考虑能形成连线的位置。这些规则计算量小,但排序效果也不错。
还有一个进阶技巧是置换表。用一个哈希表记录已经搜索过的状态及其分数,下次遇到相同状态直接查表。这在有大量重复局面的游戏中效果显著,比如国际象棋。不过置换表的实现需要考虑哈希冲突和替换策略,复杂度较高,建议先把基础版本跑通再考虑。
3.5 迭代加深与时间控制的整合
迭代加深的思路很简单:从深度1开始搜,搜完如果还有时间,就搜深度2,以此类推,直到时间用完或者达到最大深度。
def search_with_iterative_deepening(state, time_limit): best_move = None start_time = time.time() for depth in range(1, MAX_DEPTH + 1): if time.time() - start_time > time_limit: break move, score = search_root(state, depth) best_move = move return best_move这里的关键是每一层的搜索结果要保存下来,用于下一层的走法排序。具体来说,上一层的最高分走法,在下一层应该优先搜索。这样能让剪枝更早发生,提升整体效率。
时间控制的粒度要把握好。如果每搜完一个根节点走法就检查一次时间,开销太大;如果搜完整个深度才检查,可能超时太多。我的做法是在递归函数中每隔一定节点数检查一次时间,如果超时就抛出一个异常,在外层捕获并返回当前最佳走法。
这个异常机制要注意清理工作。因为Python的递归深度有限,如果异常抛出时递归栈很深,可能会有性能问题。实际测试中,只要节点数检查间隔设置合理(比如每1000个节点检查一次),影响可以忽略。
4. 常见问题与排查技巧实录
4.1 搜索结果不稳定或AI选择明显错误的走法
这是最常见的问题,通常有以下几个原因。
第一,评估函数的视角搞反了。如果评估函数返回的是“先手方”的分数,但在最小化层没有取反,AI就会选择对先手方最不利的走法。排查方法很简单:打印几个已知局面的评估分数,手动验证是否符合预期。
第二,alpha和beta的初始值传错了。在根节点调用时,alpha应该是负无穷,beta应该是正无穷。如果传反了,剪枝条件会立即触发,导致搜索直接返回错误结果。我当初就因为把alpha和beta写反了,AI第一步就直接认输。
第三,走法生成有遗漏或多余。如果合法走法列表不完整,AI可能看不到某些关键走法。如果包含非法走法,AI可能走出违规的棋。建议写一个独立的测试函数,用随机局面验证走法生成的正确性。
第四,递归终止条件写错了。比如深度判断用了depth == 0,但实际传入的深度是负数,导致递归无限进行。或者胜负判断遗漏了某些情况,导致AI认为游戏还在继续。
排查这类问题的通用方法是加日志。在递归函数的入口和出口打印当前状态、深度、alpha、beta、返回值,然后手动跟踪几个简单局面的搜索过程。虽然日志量大,但定位问题非常有效。
4.2 剪枝效果不明显或搜索速度没有提升
加了Alpha-Beta之后,如果发现搜索速度没有明显改善,大概率是走法排序没做好。如果走法顺序是随机的,剪枝效果会大打折扣,最坏情况下甚至退化成普通Minimax。
解决方法是先做一轮浅层搜索,用浅层搜索的结果来排序。比如先用深度1搜索所有走法,按分数排序,然后再用深度N搜索。这样虽然多了一轮浅层搜索,但深层搜索的剪枝效率会大幅提升,总体时间反而更短。
另一个可能的原因是剪枝条件写在了错误的位置。比如在最大化层,应该在更新alpha之后立即检查alpha >= beta,而不是在循环结束后检查。如果检查位置不对,剪枝就不会生效。
还有一种情况是评估函数太慢,导致剪枝节省的时间被评估函数消耗掉了。这时候需要优化评估函数,或者减少评估函数的调用次数。比如只在叶子节点调用评估函数,中间节点用快速启发式代替。
4.3 递归深度过大导致栈溢出
Python默认的递归深度限制是1000层。如果搜索深度设得很大,或者游戏本身的状态空间很深,就可能触发RecursionError。
解决方法有两个:一是增加递归深度限制,用sys.setrecursionlimit(10000)。但这只是治标,因为递归太深本身就会消耗大量内存,而且Python的递归效率不高。
二是改写成迭代版本,用显式的栈来模拟递归。这样虽然代码复杂一些,但不受递归深度限制,而且可以更灵活地控制搜索过程。我后来用迭代版本重写过一次,性能比递归版本提升了大约20%,因为减少了函数调用的开销。
如果不想改写成迭代版本,还有一个折中方案:限制最大搜索深度,配合迭代加深使用。这样即使递归深度有限,也能通过多次浅层搜索来逼近深层搜索的效果。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| AI选择明显错误的走法 | 评估函数视角搞反 | 打印已知局面的评估分数 | 统一评估函数视角,最小化层取反 |
| 搜索直接返回错误结果 | alpha和beta初始值传反 | 检查根节点调用参数 | alpha初始为负无穷,beta为正无穷 |
| 剪枝效果不明显 | 走法排序未做或效果差 | 统计剪枝前后节点数 | 加入走法排序,用浅层搜索结果排序 |
| 递归栈溢出 | 搜索深度过大 | 查看报错信息 | 增加递归限制或改写迭代版本 |
| 搜索速度慢 | 评估函数太复杂 | 统计评估函数调用次数和耗时 | 简化评估函数,减少调用次数 |
| AI决策不稳定 | 评估函数有随机性 | 多次运行同一局面 | 移除评估函数中的随机因素 |
| 某些走法被忽略 | 走法生成有遗漏 | 对比规则引擎的走法列表 | 补全走法生成逻辑 |
4.5 几个我踩过的坑和独家技巧
坑一:在递归中修改了全局状态。我最初写的时候,为了省事,直接在原棋盘上落子,递归返回后再撤销。结果因为递归分支太多,撤销逻辑写漏了一个分支,导致棋盘状态错乱。后来改成每次生成新状态,虽然内存开销大一点,但逻辑清晰,不会出错。
坑二:评估函数用了浮点数。浮点数比较有精度问题,而且运算速度比整数慢。后来全部改成整数运算,分数放大100倍,用整数表示,速度和稳定性都提升了。
技巧一:用位棋盘加速。如果棋盘不大,可以用一个整数来表示整个棋盘状态,每个格子占几位。这样走法生成和胜负判断都可以用位运算实现,速度极快。我在五子棋项目里用位棋盘重写后,搜索速度提升了将近3倍。
技巧二:缓存评估结果。如果评估函数计算量大,可以用一个字典缓存已经计算过的局面。因为搜索过程中会反复遇到相同局面,缓存命中率很高。注意缓存要设置大小上限,否则内存会爆。
技巧三:对称性剪枝。很多棋类游戏有对称性,比如井字棋的旋转和镜像。如果两个局面是对称的,评估分数应该相同。利用这个性质可以减少搜索量。实现方法是在评估函数中把局面标准化到某个对称形式,然后查缓存。
技巧四:渐进式加深配合时间管理。不要一开始就设定一个固定的搜索深度,而是根据剩余时间动态调整。我通常设置一个时间预算,比如1秒,然后从深度1开始搜,每搜完一层检查剩余时间,如果时间还够就继续加深。这样在快棋场景下特别有用,AI总能在时间用完之前给出当前最优解。
技巧五:调试时用极小棋盘。如果在大棋盘上调试,搜索树太大,日志太多,很难定位问题。我通常先用3x3的井字棋验证逻辑,确认无误后再迁移到更大的棋盘。井字棋的状态空间小,可以完整搜索,非常适合验证算法的正确性。
5. 性能调优与进阶方向
5.1 节点数统计与性能瓶颈定位
优化之前先测量。我在搜索函数里加了一个节点计数器,每次进入递归就加一。这样能直观地看到不同深度、不同排序策略下的节点数变化。
实测数据:井字棋深度9,无剪枝约55万节点,有剪枝约2万节点,加了走法排序后约8千节点。五子棋深度4,无剪枝约16万节点,有剪枝约3万节点,加了排序后约1.2万节点。这些数据说明剪枝和排序的收益是叠加的,两者都做才能达到最佳效果。
性能瓶颈通常在两个地方:评估函数和走法生成。用Python的cProfile模块可以快速定位。如果评估函数占用时间超过50%,就需要优化评估函数;如果走法生成占用时间高,就需要优化数据结构。
5.2 置换表的实现要点
置换表用一个字典实现,键是局面的哈希值,值是搜索深度和对应的分数。在递归函数入口先查表,如果命中且深度足够,直接返回分数。
transposition_table = {} def alphabeta_with_tt(state, depth, alpha, beta, maximizing): key = hash_state(state) if key in transposition_table: cached_depth, cached_score = transposition_table[key] if cached_depth >= depth: return cached_score # ... 正常搜索逻辑 ... transposition_table[key] = (depth, best) return best注意置换表要区分精确值、上界和下界。如果搜索过程中发生了剪枝,返回的分数可能不是精确值,而是一个边界值。这种情况下不能直接用于替换,需要标记类型。这个细节比较复杂,建议先实现简单版本,确认有效后再加入边界标记。
5.3 静态搜索与地平线效应
地平线效应是指搜索到深度限制时,评估函数可能给出误导性的分数。比如在象棋中,搜索刚好停在对方即将吃子的前一步,评估函数认为局面不错,但实际上下一步就会丢子。
静态搜索的思路是:在达到深度限制后,不立即调用评估函数,而是继续搜索所有“强制走法”(比如吃子、将军),直到局面稳定下来再评估。这样能有效缓解地平线效应。
实现上,静态搜索是一个单独的递归函数,只考虑特定类型的走法。它不需要搜索所有分支,所以开销可控。我在五子棋项目里加了简单的静态搜索,AI的棋力有明显提升,尤其是在中盘缠斗阶段。
5.4 后续可以扩展的方向
如果基础版本已经跑通,可以考虑以下几个扩展方向。
一是加入开局库。对于棋类游戏,开局阶段的变化相对固定,可以预先存储一些常见开局的最佳应对。这样AI在开局阶段可以瞬间响应,节省时间用于中后盘的搜索。
二是用机器学习优化评估函数。传统的评估函数靠人工设计特征和权重,费时费力。可以用简单的线性回归或者小型神经网络,从大量对局数据中自动学习评估函数。这个方向门槛较高,但效果上限也更高。
三是并行化搜索。Alpha-Beta剪枝本质上是串行的,但可以通过根节点并行来加速。把根节点的每个走法分配到不同线程或进程搜索,最后汇总结果。Python的GIL限制了多线程效果,可以用多进程或者用C++重写核心部分。
四是移植到其他游戏。Minimax和Alpha-Beta是通用框架,只要实现了游戏规则和评估函数,就能直接套用。我后来把这套代码移植到了黑白棋和四子棋上,只改了规则部分和评估函数,搜索核心一行没动。
6. 一些个人体会
这套算法我从第一次接触到现在,断断续续用了好几年。最初觉得它就是个教科书上的经典算法,实际写起来才发现细节非常多。每一个参数、每一行逻辑,都可能影响最终效果。
我最大的体会是:不要一开始就追求完美。先把最基础的Minimax跑通,确认能正确决策,然后再加剪枝,再加排序,再加各种优化。每一步都做对比测试,确保改动有正向收益。如果一上来就写一个包含所有优化的大而全版本,出了问题很难定位。
另一个体会是:测试用例比代码更重要。我专门写了一个测试脚本,包含几十个已知局面的最佳走法,每次修改代码后都跑一遍。这样能快速发现回归问题,避免改了一个bug引入两个新bug。
最后,不要迷信深度。搜索深度固然重要,但评估函数的质量同样关键。一个深度6层但评估函数粗糙的AI,往往下不过深度4层但评估函数精细的AI。在资源有限的情况下,优先优化评估函数,再考虑增加搜索深度。
这套代码我后来整理成了一个通用框架,换不同的游戏只需要实现几个接口函数。如果你也在做类似的博弈项目,希望这些经验能帮你少走一些弯路。