资讯详情 基于PyQt5与α-β剪枝的五子棋博弈搜索实战
📅 2026/10/11 22:18:02
简介这份资源是面向计算机相关专业学生与开发者的毕业设计级五子棋AI项目采用Python与PyQt5构建图形界面核心实现人机博弈并引入深度优先搜索与α-β剪枝算法提升AI决策效率适合作为人工智能、游戏开发方向的课程设计、毕设或算法学习案例。压缩包共17个文件约5.63MB包含4个py源码文件、1个ui界面文件、1个qrc资源文件、1个md说明文档、1个ico图标及9张png截图覆盖界面设计、资源加载、AI逻辑与启动入口等模块结构清晰便于二次开发。目前已有213人学习下载。项目代码经过完整测试可正常运行配套README.md提供使用说明读者可借此理解博弈树搜索、剪枝优化与PyQt5界面交互的完整实现路径也可在此基础上修改扩展功能用于毕设、课设或作业提交。1. 从一份毕设代码说起PyQt5 五子棋里藏着的博弈搜索骨架很多同学做毕业设计时第一反应是找个现成模板改改界面但真正能让答辩老师多问两句的往往是藏在棋盘背后的搜索逻辑。这个标题里的关键词——Python、PyQt5、多智能体博弈、深搜、α-β剪枝——其实指向一个非常经典的组合用 PyQt5 搭出可交互的棋盘用深度优先搜索遍历落子空间再用 α-β 剪枝把明显不划算的分支砍掉最后让 AI 在有限时间内给出一个“像人”的落子决策。它解决的不是“怎么画一个五子棋界面”而是“怎么让程序在成千上万种走法里快速找到相对优解”。适合已经会写 Python 基础语法、想通过一个完整项目理解博弈树搜索的在校生也适合想用 PyQt5 练手桌面应用开发、顺便把算法落地一遍的初学者。下面我按实际复现顺序把选型理由、代码骨架、参数调法和踩过的坑一次讲清。2. 棋盘与搜索骨架PyQt5 负责交互深搜负责推演2.1 为什么用 PyQt5 而不是 Tkinter 或 Web 前端五子棋界面需要频繁重绘棋盘、响应鼠标落子、显示提示信息PyQt5 的信号槽机制和 QPainter 绘图能力比 Tkinter 更顺手。Tkinter 画网格线容易闪烁Web 前端虽然界面漂亮但要把 Python 搜索算法跑在浏览器里还得额外搭后端对毕设来说链路太长。PyQt5 直接在主线程里跑搜索用QThread或QTimer把耗时计算挪出去就能避免界面卡死。安装时注意版本匹配Python 3.8 到 3.11 都能装 PyQt5命令是pip install PyQt5如果下载慢就换国内镜像源。装完用python -c from PyQt5.QtWidgets import QApplication; print(ok)验证能打印 ok 就说明环境通了。2.2 棋盘状态怎么表示才方便搜索棋盘用二维列表board[15][15]表示0 是空位1 是黑棋2 是白棋。搜索时不需要复制整个棋盘而是用“落子-撤销”的方式在同一个数组上回溯这样内存占用小递归返回时状态自动还原。下面是最小可运行的棋盘初始化与落子撤销代码# board.py BOARD_SIZE 15 EMPTY, BLACK, WHITE 0, 1, 2 def create_board(): 创建 15x15 空棋盘 return [[EMPTY for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)] def place(board, row, col, player): 落子返回是否合法 if 0 row BOARD_SIZE and 0 col BOARD_SIZE and board[row][col] EMPTY: board[row][col] player return True return False def undo(board, row, col): 撤销落子用于搜索回溯 board[row][col] EMPTY逻辑说明place先做边界和空位检查避免覆盖已有棋子undo只把对应位置清空不保存历史状态因为搜索树是深度优先撤销顺序和落子顺序相反天然安全。参数上BOARD_SIZE改成 15 是标准五子棋改成 19 就是围棋棋盘但搜索量会指数级上升毕设阶段不建议动。2.3 深度优先搜索的递归框架与终止条件深搜的核心是“当前局面下轮到我走我尝试所有合法空位对每个空位落子后递归到对手回合直到达到深度限制或分出胜负”。伪代码看起来简单但终止条件写错就会无限递归。常见做法是设一个最大深度MAX_DEPTH比如 4 层表示 AI 走一步、对手走一步、AI 再走一步、对手再走一步然后返回评估值。评估函数先简单用“连子数量打分”五连给极大值活四给高分活三次之。下面是一个可运行的深搜骨架# search.py from board import BOARD_SIZE, EMPTY, BLACK, WHITE, place, undo MAX_DEPTH 4 def evaluate(board, player): 简化评估只统计连子实际项目可加棋型识别 score 0 for r in range(BOARD_SIZE): for c in range(BOARD_SIZE): if board[r][c] player: score 1 return score def dfs(board, depth, player, alpha, beta): 深度优先搜索返回当前玩家视角的最佳分数 if depth 0: return evaluate(board, player) best -float(inf) for r in range(BOARD_SIZE): for c in range(BOARD_SIZE): if board[r][c] EMPTY: place(board, r, c, player) val -dfs(board, depth - 1, WHITE if player BLACK else BLACK, -beta, -alpha) undo(board, r, c) if val best: best val if best alpha: alpha best if alpha beta: break # α-β剪枝 return best逻辑说明这里用了负极大值形式-dfs(...)把对手视角的分数取反代码更简洁。alpha和beta是剪枝边界当alpha beta时说明当前分支不可能影响上层决策直接跳过剩余空位。参数MAX_DEPTH每加 1搜索节点数大约翻 10 到 20 倍毕设演示用 4 层足够想更强可以加到 6 层但必须配合剪枝和候选点筛选。3. α-β剪枝怎么改从全量遍历到只搜“值得搜”的点3.1 剪枝的触发条件与代码改造点上一节的dfs已经带了剪枝但很多同学直接抄过去发现没效果原因是空位遍历顺序太差。α-β剪枝的效率极度依赖“先搜好棋”。如果先搜角落的废点alpha涨得慢剪枝触发晚等于白剪。改造方法是在生成候选落子时先按“周围 2 格内有棋子”过滤再按启发式分数排序把可能形成活三、冲四的点排前面。下面给出候选点生成与排序的代码# candidates.py from board import BOARD_SIZE, EMPTY def get_candidates(board, radius2): 只返回有邻居的空位减少搜索宽度 cand [] for r in range(BOARD_SIZE): for c in range(BOARD_SIZE): if board[r][c] ! EMPTY: continue has_neighbor False for dr in range(-radius, radius 1): for dc in range(-radius, radius 1): nr, nc r dr, c dc if 0 nr BOARD_SIZE and 0 nc BOARD_SIZE: if board[nr][nc] ! EMPTY: has_neighbor True break if has_neighbor: break if has_neighbor: cand.append((r, c)) return cand def sort_candidates(board, cand, player): 按简单启发式排序靠近中心、周围棋子多优先 center BOARD_SIZE // 2 def score(pos): r, c pos dist abs(r - center) abs(c - center) return -dist return sorted(cand, keyscore)逻辑说明get_candidates把搜索宽度从 225 个空位降到几十个radius2是经验值太小会漏掉跳活三太大剪枝效果差。sort_candidates只是最简版本实际可以统计每个点周围己方和对方棋子数量己方多加分、对方多也加分因为堵对方也很关键。参数radius建议在 1 到 3 之间调棋盘越大越要用小半径。3.2 搜索深度、候选宽度与响应时间的三角关系毕设答辩时老师常问“你的 AI 为什么有时候快有时候慢”答案就在这三个变量的平衡里。深度决定“看多远”候选宽度决定“每层看多少”响应时间由前两者乘积决定。下面这张表是我在 15x15 棋盘、普通笔记本上实测的参考值最大深度候选半径平均候选数单步耗时棋力感受2225 0.1 秒很弱只会堵一步42250.3 到 0.8 秒能堵活三偶尔犯错41120.1 到 0.3 秒快但容易漏远处威胁62253 到 8 秒明显变强但界面会卡61121 到 3 秒毕设演示比较合适提示如果搜索超过 1 秒一定要把搜索放到QThread里否则 PyQt5 界面会显示“无响应”答辩现场很尴尬。3.3 把搜索接入 PyQt5 主界面界面部分用QMainWindow加一个自定义QWidget重写paintEvent画棋盘和棋子重写mousePressEvent处理落子。AI 走棋时用QTimer.singleShot(50, self.ai_move)延迟一下让界面先刷新。下面是最简接入代码# main_window.py from PyQt5.QtWidgets import QMainWindow, QWidget, QApplication from PyQt5.QtGui import QPainter, QColor from PyQt5.QtCore import Qt, QTimer from board import create_board, place, BLACK, WHITE, EMPTY from search import dfs from candidates import get_candidates, sort_candidates CELL 40 MARGIN 30 class BoardWidget(QWidget): def __init__(self): super().__init__() self.board create_board() self.current BLACK self.setFixedSize(CELL * 15 MARGIN * 2, CELL * 15 MARGIN * 2) def paintEvent(self, event): p QPainter(self) p.setRenderHint(QPainter.Antialiasing) for i in range(15): p.drawLine(MARGIN, MARGIN i * CELL, MARGIN 14 * CELL, MARGIN i * CELL) p.drawLine(MARGIN i * CELL, MARGIN, MARGIN i * CELL, MARGIN 14 * CELL) for r in range(15): for c in range(15): if self.board[r][c] ! EMPTY: color Qt.black if self.board[r][c] BLACK else Qt.white p.setBrush(QColor(color)) p.drawEllipse(MARGIN c * CELL - 15, MARGIN r * CELL - 15, 30, 30) def mousePressEvent(self, event): if self.current ! BLACK: return c round((event.x() - MARGIN) / CELL) r round((event.y() - MARGIN) / CELL) if place(self.board, r, c, BLACK): self.current WHITE self.update() QTimer.singleShot(50, self.ai_move) def ai_move(self): cand get_candidates(self.board) cand sort_candidates(self.board, cand, WHITE) best_val, best_pos -float(inf), None for r, c in cand: place(self.board, r, c, WHITE) val -dfs(self.board, 3, BLACK, -float(inf), float(inf)) place(self.board, r, c, EMPTY) if val best_val: best_val, best_pos val, (r, c) if best_pos: place(self.board, best_pos[0], best_pos[1], WHITE) self.current BLACK self.update()逻辑说明mousePressEvent里用round把像素坐标转成格子坐标place返回 True 才切换回合。ai_move遍历候选点对每个点试下白棋然后调用dfs从黑棋视角搜索取负值就是白棋的收益。参数dfs的深度写 3 是因为外面已经有一层候选遍历总共相当于 4 层。QTimer.singleShot(50, ...)给界面 50 毫秒刷新时间避免点击后直接卡住。4. 避坑与排查五子棋搜索里最容易翻车的 5 个地方4.1 现象AI 第一步下在天元旁边第二步却下到角落原因候选点排序只按距离中心没有考虑对方威胁。第一步周围没棋子排序退化成中心优先第二步对方下了天元角落点因为距离中心远反而排后面但搜索深度不够时评估函数看不出角落是废点。解决在sort_candidates里加入“周围对方棋子数量”权重对方棋子多的地方加分强制 AI 优先考虑堵截。同时把MAX_DEPTH至少设到 4让搜索能推演两步。4.2 现象递归跑着跑着报RecursionError: maximum recursion depth exceeded原因dfs里没有正确递减depth或者undo没执行导致棋盘被填满后仍然递归。检查dfs开头是否有if depth 0: return evaluate(...)以及place和undo是否成对出现。如果深度设到 8 以上Python 默认递归限制 1000 可能不够可以在主程序开头加import sys; sys.setrecursionlimit(5000)但更推荐把深度控制在 6 以内。4.3 现象界面点击后卡死几秒然后一次性显示 AI 落子原因搜索在主线程执行QTimer.singleShot只是延迟启动并没有把计算放到后台。解决用QThread包装ai_move或者把搜索拆成多个QTimer分步执行。毕设演示最简做法是降低深度到 4 并限制候选半径到 1把单步耗时压到 0.3 秒以内界面虽然会顿一下但不至于“无响应”。4.4 现象α-β剪枝加了反而比不加还慢原因候选点没有排序剪枝窗口alpha和beta更新顺序不对。检查dfs里是否先递归再更新alpha以及是否用了负极大值形式。负极大值要求递归调用时传-beta, -alpha返回后取负。如果写成普通极大极小剪枝条件要对应改成alpha beta时break。另外如果候选点顺序是随机的剪枝几乎不触发必须先排序。4.5 现象评估函数给五连打满分但 AI 还是不去连五原因评估函数只在叶子节点调用如果深度不够AI 看不到五连的那一步。比如深度 2 时AI 走一步、对手走一步就返回评估AI 连四但没连五评估分不高。解决在dfs开头加胜负检测如果当前局面已经五连直接返回极大值或极小值不再往下搜。这样即使深度浅也能抓住必赢和必堵的点。5. 让 AI 更“像人”棋型评分表与迭代加深的实战调法前面几章的评估函数只数棋子棋力很粗糙。想让毕设演示时 AI 看起来“有脑子”得把棋型识别加进去。常见做法是扫描四个方向统计连续同色棋子和两端封堵情况给活四、冲四、活三、眠三分别打分。下面这张表是我调参时用的参考值分数差距拉大能让搜索更果断棋型示例1 为己方0 为空2 为对方建议分数活四0 1 1 1 1 0100000冲四2 1 1 1 1 010000活三0 1 1 1 0 08000眠三2 1 1 1 0 0500活二0 0 1 1 0 0300眠二2 0 1 1 0 050实现时不用正则直接遍历每个棋子向四个方向延伸计数。注意边界处理遇到对方棋子或棋盘边缘就停止。评分函数返回己方总分减对方总分这样 AI 既会进攻也会防守。另一个实战技巧是迭代加深先搜深度 2如果耗时小于 0.2 秒就继续搜深度 4再小于 0.5 秒就搜深度 6。这样快棋局面自动算得深复杂局面自动收浅避免固定深度导致的卡顿。代码上用一个for depth in [2, 4, 6]循环记录time.time()超时就break保留上一层的最佳走法。最后说个我自己的习惯每次改完评估函数不要只看 AI 赢不赢而是摆一个活三局面看 AI 是堵还是自己进攻。如果它选择进攻但对方下一步能连五说明防守权重太低把对方棋型分数乘 1.2 再试。这个乘数我调了七八次才找到顺手的值别指望一次到位。希望帮到你。本文还有配套的精品资源点击获取