news 2026/9/28 19:21:39

Python五子棋AI实战:极大极小值搜索与alpha beta剪枝

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python五子棋AI实战:极大极小值搜索与alpha beta剪枝

简介:这是一份面向Python初学者与AI算法爱好者的五子棋AI实战项目,围绕极大极小值搜索与Alpha-Beta剪枝两大经典决策树技术展开,帮助读者理解零和博弈中的搜索策略与剪枝优化思路。压缩包共12个文件,约150KB,以py源码、xml配置、pyc缓存及doc、pdf文档为主,源码模块划分清晰,涵盖棋盘规则、AI搜索、玩家交互与主程序入口,另附论文与参考资料便于延伸阅读。目前已有3016人学习下载,适合作为课程设计、毕业设计或AI入门练手素材。读者可从中获得完整的可运行代码框架、评估函数与剪枝逻辑的实现范例,以及缓存优化等性能改进思路,并借助文档资料理解算法原理与项目结构,快速搭建属于自己的对弈AI。

1. 从「会下棋的 Python 脚本」说起:极大极小值搜索和 alpha beta 剪枝到底解决什么问题

很多人第一次写五子棋 AI,都会掉进同一个坑:让程序把每一步都算一遍,结果第一步还没落子,风扇已经起飞。问题不在 Python 慢,而在于搜索空间是爆炸的——15×15 的棋盘,空位两百多个,逐层展开就是天文数字。极大极小值搜索(Minimax)和 alpha beta 剪枝,就是把这棵「不可能算完的树」砍到能算完的两把刀。

这篇要讲清楚一件事:怎么用 Python 从零搭一个能真正跟你对弈、还会主动堵你活三的 AI 五子棋。它适合刚学完 Python 基础语法、想找一个「有难度但不劝退」项目练手的人,也适合想复习博弈搜索、把课本伪代码变成能跑代码的开发者。核心不是界面多花哨,而是搜索算法、评估函数、剪枝顺序这三块能不能咬合上。下面按「先立住原理、再动手复现、最后讲坑」的顺序推下去,每一步都给能直接抄的代码和参数。

2. 极大极小值搜索:把「我走一步、对手走一步」变成可计算的分数

2.1 为什么是博弈树,而不是穷举所有落子

五子棋是典型的双人零和博弈:一方赢,另一方就输,没有中间态。极大极小值搜索的核心假设是——双方都足够聪明。轮到我走,我选让局面分最高的那步;轮到对手走,他一定选让局面分最低的那步(因为对我越低,对他越高)。把这两层交替展开,就得到一棵博弈树。

树的根是当前局面,往下每一层代表一步落子,层数就是搜索深度。深度为 1 只看自己这步,深度 4 就是「我走、他走、我走、他走」。深度越深,AI 越强,但节点数按分支因子指数增长。15×15 棋盘开局有 225 个空位,深度 4 就是 225⁴ 量级,纯 Python 根本扛不住。所以极大极小值本身只是「正确」,要「可用」必须靠剪枝和候选点筛选。

这里先明确一个概念:评估函数。搜索到叶子节点(达到设定深度或分出胜负)时,需要一个数字告诉上层「这个局面对我多有利」。五子棋里最直接的做法是数棋型:活四、冲四、活三、眠三、活二……每种给不同权重,我方棋型加分,对方棋型减分。评估函数写得好不好,直接决定 AI 是「会下棋」还是「乱下棋」。

2.2 用 Python 写出可运行的 Minimax 骨架

先给一个最小可跑的骨架,棋盘用二维列表表示,0 是空、1 是 AI、2 是人类。评估函数先用简化版,后面再优化。

# board: 15x15 二维列表,0 空 / 1 AI / 2 人类 # depth: 剩余搜索深度 # is_max: True 表示轮到 AI(极大层) def minimax(board, depth, is_max): # 终止条件:深度耗尽,或某一方已连成五子 score = evaluate(board) if depth == 0 or abs(score) >= 100000: return score if is_max: best = -float('inf') for (r, c) in get_candidates(board): board[r][c] = 1 best = max(best, minimax(board, depth - 1, False)) board[r][c] = 0 # 回溯,必须还原 return best else: best = float('inf') for (r, c) in get_candidates(board): board[r][c] = 2 best = min(best, minimax(board, depth - 1, True)) board[r][c] = 0 return best

逻辑说明:is_max为 True 时是 AI 层,取所有走法里的最大值;为 False 时是人类层,取最小值。board[r][c] = 0这行回溯是血泪经验——忘了还原,棋盘会被填满,AI 后面全是废步。

参数说明:depth建议从 2 起步,跑通再往上加;get_candidates是关键优化点,绝不能返回全部空位,只返回已有棋子周围 2 格内的空点,能把分支因子从 200 降到 20 以内。evaluate返回绝对值超过 100000 时视为胜负已定,直接剪掉后续搜索。

2.3 评估函数怎么写才不「瞎下」

评估函数是 AI 的大脑。常见做法是扫描四个方向(横、竖、两条斜线),把每条线上的连续棋型提取出来打分。下面是一个可用的权重表:

棋型说明建议分值
活四两端都空,无法阻挡100000
冲四一端被堵,仍能成五10000
活三两端空,下一步可变活四8000
眠三一端被堵的三500
活二两端空的两300
眠二一端被堵的二50

打分时我方棋型加正分,对方棋型乘一个略大于 1 的系数(比如 1.1)后减分。这个系数是玄学也是经验:太小 AI 只顾进攻不防守,太大又变得畏手畏脚。我一般从 1.1 开始调,对手活三时能主动去堵就算合格。

提示:评估函数不要每次全盘重算,落子后只更新受影响的四条线,性能能提升好几倍。

3. alpha beta 剪枝:让同样的深度少算一大半节点

3.1 剪枝的直觉:已经知道更差,就别再看了

极大极小值搜索有个巨大浪费:有些分支算到一半,就已经能确定它不会影响最终决策,但程序还在傻算。alpha beta 剪枝就是把这个「已经能确定」提前告诉搜索过程。

两个参数:alpha是极大层目前能找到的最好值(下界),beta是极小层目前能找到的最坏值(上界)。搜索过程中如果发现某个节点的 beta ≤ alpha,说明这个分支对上层毫无价值,直接返回,不再展开。剪枝效果极度依赖走法顺序——先搜好棋,剪得越狠。理想情况下,同样的深度,节点数能从 b^d 降到 b^(d/2),相当于深度翻倍。

3.2 带剪枝的完整搜索代码

def alphabeta(board, depth, alpha, beta, is_max): score = evaluate(board) if depth == 0 or abs(score) >= 100000: return score if is_max: best = -float('inf') for (r, c) in get_candidates(board): board[r][c] = 1 val = alphabeta(board, depth - 1, alpha, beta, False) board[r][c] = 0 best = max(best, val) alpha = max(alpha, best) if beta <= alpha: # 剪枝:极小层不会选这个分支 break return best else: best = float('inf') for (r, c) in get_candidates(board): board[r][c] = 2 val = alphabeta(board, depth - 1, alpha, beta, True) board[r][c] = 0 best = min(best, val) beta = min(beta, best) if beta <= alpha: # 剪枝:极大层不会选这个分支 break return best

逻辑说明:极大层更新 alpha,极小层更新 beta,一旦beta <= alpha立即 break。注意初始调用要传alpha=-inf, beta=+inf,否则第一层就剪没了。

参数说明:get_candidates的返回顺序直接决定剪枝效率。我一般按「离最后落子点距离」排序,再叠加一层启发式打分(能形成活三、冲四的点优先),实测节点数能再降 30% 以上。深度建议 4 起步,配合候选点筛选,普通笔记本一秒内能出招。

3.3 候选点生成:剪枝之外的第二把刀

很多人只盯着 alpha beta,却忽略了候选点筛选才是性价比最高的优化。全盘 200 多个空位,真正值得考虑的通常不超过 20 个。

def get_candidates(board, radius=2): candidates = set() for r in range(15): for c in range(15): if board[r][c] != 0: # 只取已有棋子周围 radius 格内的空点 for dr in range(-radius, radius + 1): for dc in range(-radius, radius + 1): nr, nc = r + dr, c + dc if 0 <= nr < 15 and 0 <= nc < 15 and board[nr][nc] == 0: candidates.add((nr, nc)) return list(candidates)

逻辑说明:遍历所有已落子点,把它们周围 radius 格内的空点收集起来。开局棋盘空时,如果没有任何棋子,直接返回中心点。

参数说明:radius=2是常用值,太小会漏掉远处的关键点,太大会让分支因子回升。如果棋盘上棋子很少,可以先在中心附近落子,避免候选集为空。这个函数配合 alpha beta,是让 Python 版五子棋「能玩」的关键组合。

4. 从零跑通:环境、棋盘、胜负判断和主循环

4.1 环境准备与依赖

这个项目不需要任何第三方库,标准库足够。Python 3.8 以上都行,装好之后用python --version确认。编辑器用 VS Code 或 PyCharm 都可以,VS Code 里装个 Python 扩展,选好解释器就能跑。如果你还在纠结 python 安装教程,记住一点:官网下载安装包时勾选「Add Python to PATH」,能省掉后面一堆环境变量问题。

项目结构建议拆成三个文件:board.py管棋盘和胜负判断,ai.py管搜索和评估,main.py管主循环和输入输出。拆开的好处是调试时能单独测评估函数,不用每次跑整局。

4.2 棋盘表示与胜负判断

def check_win(board, player): # 四个方向:横、竖、主对角、副对角 directions = [(0, 1), (1, 0), (1, 1), (1, -1)] for r in range(15): for c in range(15): if board[r][c] != player: continue for dr, dc in directions: count = 1 for step in range(1, 5): nr, nc = r + dr * step, c + dc * step if 0 <= nr < 15 and 0 <= nc < 15 and board[nr][nc] == player: count += 1 else: break if count >= 5: return True return False

逻辑说明:对每个己方棋子,沿四个方向数连续同色棋子,达到 5 就赢。注意副对角方向(1, -1)要判断列不越界。

参数说明:range(1, 5)表示最多再数 4 个,加上自身正好 5 个。这个判断在每次落子后调用一次即可,不用全盘反复扫。

4.3 主循环:人机交替落子

def main(): board = [[0] * 15 for _ in range(15)] board[7][7] = 1 # AI 先手占中心 print_board(board) while True: # 人类落子 move = input("输入坐标 row,col: ") r, c = map(int, move.split(',')) if board[r][c] != 0: print("该位置已有棋子") continue board[r][c] = 2 if check_win(board, 2): print("你赢了") break # AI 落子 best_val, best_move = -float('inf'), None for (r, c) in get_candidates(board): board[r][c] = 1 val = alphabeta(board, 3, -float('inf'), float('inf'), False) board[r][c] = 0 if val > best_val: best_val, best_move = val, (r, c) board[best_move[0]][best_move[1]] = 1 print_board(board) if check_win(board, 1): print("AI 赢了") break

逻辑说明:外层循环里人类先走,AI 再走。AI 这一层遍历候选点,对每个点调用 alphabeta 得到分数,取最高分对应的落子。注意这里is_max=False,因为 AI 落子后轮到人类,下一层是极小层。

参数说明:alphabeta(board, 3, ...)里的 3 是搜索深度,实际项目里可以设成 4。深度每加 1,耗时大约翻几倍,先用 3 跑通再往上调。best_move理论上不会为 None,因为候选集在正常对局中不会为空,但保险起见可以加个判空。

5. 避坑与排查:AI 五子棋最容易翻车的五个地方

5.1 现象:AI 第一步就卡死,风扇狂转

原因:候选点没做筛选,直接遍历全部空位,深度 4 时节点数爆炸。解决:确认get_candidates的 radius 参数生效,开局阶段候选点应控制在 30 个以内。如果还是慢,先把深度降到 2 验证逻辑,再逐步加。

5.2 现象:AI 明明能赢却不落子,反而去堵无关位置

原因:评估函数里对方棋型权重给太高,AI 变得过度防守。解决:检查活四、冲四的分值是否远大于活三,同时把对方系数从 1.1 往下调,比如 1.05。另一个可能是搜索深度不够,没看到自己的连五机会,把深度加到 4 再试。

5.3 现象:程序报IndexError或棋盘越界

原因:胜负判断或候选点生成时没做边界检查,r + dr * step可能超出 0~14。解决:所有坐标计算后都加0 <= nr < 15 and 0 <= nc < 15判断。这个坑几乎每个新手都会踩一次,写个in_board(r, c)辅助函数统一处理最省心。

5.4 现象:AI 落子后棋盘状态错乱,出现重复落子

原因:回溯时忘了把board[r][c]还原成 0,或者还原成了错误的值。解决:搜索函数里落子和还原必须成对出现,建议用 try/finally 或者写一个place_and_undo辅助函数。调试时可以在每次落子后打印棋盘,肉眼确认状态。

5.5 现象:剪枝后 AI 棋力反而下降,走出明显臭棋

原因:alpha beta 的初始值传错,或者候选点排序把好棋排在后面导致剪枝过度。解决:确认初始调用是alpha=-inf, beta=+inf;候选点排序时把「能形成活三、冲四」的点放前面。如果还不对,先关掉剪枝跑一遍纯 Minimax,对比结果是否一致,能快速定位是剪枝逻辑还是评估函数的问题。

6. 进阶技巧:用置换表和迭代加深把 AI 再提一档

跑通基础版之后,如果想让 AI 更强,有两个性价比很高的方向。第一个是置换表(Transposition Table):不同走法顺序可能到达同一个局面,用字典把「局面哈希 → 分数」缓存下来,遇到重复局面直接查表,能省掉大量重复搜索。局面哈希可以用 Zobrist 哈希,给每个位置、每种棋子分配一个随机数,落子时异或更新,速度很快。

# Zobrist 哈希示意 import random zobrist = [[[random.getrandbits(64) for _ in range(2)] for _ in range(15)] for _ in range(15)] trans_table = {} def board_hash(board): h = 0 for r in range(15): for c in range(15): if board[r][c] != 0: h ^= zobrist[r][c][board[r][c] - 1] return h

逻辑说明:每个位置每种棋子对应一个 64 位随机数,局面哈希就是所有已落子的随机数异或。落子或撤销时只需异或对应随机数,不用全盘重算。

参数说明:trans_table建议设个上限,比如 100 万条,超了就清空,避免内存无限增长。查表时要同时校验深度,浅层结果不能直接用于深层。

第二个方向是迭代加深:从深度 1 开始搜,逐步加深到 4 或 6,把上一层的最优走法作为下一层的首选排序。这样配合 alpha beta,剪枝效率会明显提升,而且时间可控——设定一个时间上限,超时就返回当前最优。我一般会先跑深度 2 热身,再逐层加到 4,实测比直接搜深度 4 快不少,棋力还更稳。

最后说个习惯:每次改完评估函数或剪枝逻辑,别急着跟人下,先让 AI 自己跟自己下十局,看有没有出现「双方都不堵活四」这种低级局面。这种自对弈测试比人肉试错快得多,也是我调参时最依赖的后悔药。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 19:21:31

OpenClaw从入门到应用——Slack 频道接入的令牌与 Socket/HTTP 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 19:21:18

嵌入式偶发Bug排查三板斧:换机排除、录屏取证与批次对照

偶发 bug&#xff0c;这三个字在嵌入式开发圈里几乎等于噩梦。你把代码 review 了三遍、逻辑抠到每一行&#xff0c;它还是会在某个周二的下午、某块特定板子上突然出现。更难受的是&#xff0c;当你插上调试器想抓现场&#xff0c;它又消失得无影无踪&#xff0c;仿佛从来没存…

作者头像 李华
网站建设 2026/9/28 19:18:35

背景抑制光电传感器原理选型与现场调试:德宝DOB-L61-BG系列实战解析

前阵子帮客户调试一条包装线的检测工位&#xff0c;遇到一个很典型的麻烦&#xff1a;产品到位后&#xff0c;普通漫反射式光电传感器经常把背景纸箱当成目标&#xff0c;导致计数翻倍&#xff0c;甚至空托盘也被当成有货。换过对射、换过回归反射&#xff0c;效果都不理想。最…

作者头像 李华
网站建设 2026/9/28 19:16:56

从对话到操控:用OpenClaw+TaoToken打造产线指挥官Shell骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 19:16:49

STM32理论实战笔记:从内核架构、时钟树到定时器与串口调试

不想把"STM32理论"讲成一本翻不动的数据手册。这是我一开始踩过最深的坑&#xff1a;以为理论就是背时钟树、背寄存器、背各种总线框图&#xff0c;结果背完就忘&#xff0c;代码照样写不明白。后来带过几届学弟做课设和毕业设计&#xff0c;才慢慢摸到门道——真正的…

作者头像 李华
网站建设 2026/9/28 19:16:40

5分钟搞定 Claude Code 接入本地大模型:TaoToken 统一 Key 配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华