简介:这是一份面向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 自己跟自己下十局,看有没有出现「双方都不堵活四」这种低级局面。这种自对弈测试比人肉试错快得多,也是我调参时最依赖的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取