ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PyQt5五子棋AI实战:α-β剪枝、评估函数与桌面应用全解析

PyQt5五子棋AI实战:α-β剪枝、评估函数与桌面应用全解析 简介基于Python与PyQt5打造的“多智能体博弈AI五子棋游戏”毕业设计项目核心涵盖人机对战、深度优先搜索DFS与α-β剪枝算法通过完整工程展示了博弈树搜索在棋类AI中的实际应用。资源面向计算机类毕业设计、课程设计和期末大作业场景也适合希望借助完整代码学习PyQt5界面开发与博弈算法的读者。压缩包共含17个文件包括4个核心Python源码如AI算法模块、gobang界面主程序、Qt界面定义文件ui/qrc、1份README说明文档以及多张运行效果截图与应用图标整体大小约5.39MB。目前已有338人学习了解。通过该项目可获取可直接运行的源代码、项目结构说明与详细代码注释便于新手快速上手、二次开发与答辩展示系统功能完善、界面美观、操作流畅对完成毕业设计或深入理解多智能体博弈与剪枝优化具有很高的参考价值。1. 用PyQt5和α-β剪枝做五子棋AI这道题真正难在哪很多人选“五子棋AI”这类题目做课设算法课上学过minimax课设却卡在把算法变成能演示的桌面程序。这个标题给了一条完整路径Python写逻辑PyQt5画界面和接收鼠标事件深搜与α-β剪枝做搜索加速再配一份说明文档。它能解决的不只是“AI会下棋”而是把数据结构、搜索算法、GUI事件驱动这几个零散知识点串成一件能演示、能答辩的作品。适合本科毕业设计也适合想把搜索算法应用到实际项目的开发者照着复现。一个容易误判的点决定AI强弱的往往不是搜索深度而是评估函数写得好不好后面会展开说。2. 让AI“看见”棋盘评估函数、深搜与α-β剪枝的落地选型2.1 多智能体博弈在五子棋里指什么人机对战与AI自战两种模式标题里的“多智能体博弈”在毕设语境下通常不是学术论文里那种复杂的多智能体系统而是指棋盘上同时存在两个决策实体。第一种是玩家和AI各执黑白轮流落子这是最基本的人机博弈第二种是做成“AI自战”模式让两个AI实例使用不同搜索深度或不同评估参数互相博弈用来验证剪枝效果、量化调优收益。这两种模式共用同一套15路棋盘和搜索代码差别只在谁去调用搜索、搜索完结果往哪里写。这个设计是文档里可以写清楚的亮点同样的核心代码一种玩法打人一种玩法打自己。答辩老师如果追问“多智能体博弈体现在哪”你可以明确回答——人机和自战模式里每个棋手都是一个独立的博弈智能体在共同棋盘环境下追求自身收益最大化。强烈建议代码里把“人机对战”和“AI自战”做成两个入口自战模式不只是演示功能它还是第5章里做回归测试的“测试夹具”。2.2 评估函数怎么打分棋型计分表与四方向扫描搜索树决定的是“AI能想到哪一步”评估函数决定的是“AI怎么看清楚当前局面”。五子棋不像国际象棋没有车、马、兵的物质优势只能靠棋型判断。常见做法是给“成五、活四、冲四、活三、眠三、活二”等棋型设置梯度分数AI落子时会优先构造高价值棋型。SCORE_TABLE { five: 1000000, # 成五直接赢了 live_four: 100000, # 活四两端都空下一步必胜 rush_four: 20000, # 冲四只有一端开放 live_three: 20000, # 活三有发展成活四的空间 sleep_three: 3000, # 眠三被对方限制了一端 live_two: 3000, # 活二早期布子用 sleep_two: 200, one: 10, } DIRECTIONS [(1, 0), (0, 1), (1, 1), (1, -1)] def shape_at(board, x, y, dx, dy, stone): # 以(x, y)为中心沿(dx, dy)两个方向数连续同色棋子 count 1 blank_l blank_r False px, py x - dx, y - dy while 0 px 15 and 0 py 15 and board[px][py] stone: count 1 px, py px - dx, py - dy if 0 px 15 and 0 py 15 and board[px][py] 0: blank_l True px, py x dx, y dy while 0 px 15 and 0 py 15 and board[px][py] stone: count 1 px, py px dx, py dy if 0 px 15 and 0 py 15 and board[px][py] 0: blank_r True return count, blank_l, blank_r def evaluate(board, stone): # 只让“方向上第一个同色子”负责统计避免一条线被重复计分 score 0 for x in range(15): for y in range(15): if board[x][y] ! stone: continue for dx, dy in DIRECTIONS: px, py x - dx, y - dy if 0 px 15 and 0 py 15 and board[px][py] stone: continue count, blank_l, blank_r shape_at(board, x, y, dx, dy, stone) if count 5: score SCORE_TABLE[five] elif count 4: ends int(blank_l) int(blank_r) score SCORE_TABLE[live_four] if ends 2 else SCORE_TABLE[rush_four] if ends 1 else 0 elif count 3: ends int(blank_l) int(blank_r) score SCORE_TABLE[live_three] if ends 2 else SCORE_TABLE[sleep_three] if ends 1 else 0 elif count 2: ends int(blank_l) int(blank_r) score SCORE_TABLE[live_two] if ends 2 else SCORE_TABLE[sleep_two] if ends 1 else 0 elif count 1: score SCORE_TABLE[one] return scoreshape_at 以某个棋子为中心向两个方向延伸返回连续同色数和两端是否为空。evaluate 遍历棋盘时跳过那些“前一个位置已经是同色”的格子保证一条连续线段只会被统计一次避免同一条线上的五个棋子被重复算成五个活二。参数说明DIRECTIONS 四个方向覆盖了横、竖、两个对角线这是五子棋连线的所有可能方向评分表的分数可以按实际棋感调整但梯队次序不能乱——成五必须最大活四必须明显高于冲四和活三否则AI会为了堵一个眠三而放弃必赢的活四。这里用的是最简单的逐点扫描评估想让性能更好可以改成增量评估每次落子后只更新受影响方向上的那几个交叉点。2.3 深搜加α-β剪枝minimax主流程与剪枝条件minimax 假设双方都走最优AI 层取最大收益玩家层取最小收益。没有剪枝时搜索量是“候选点 × 候选点 × …”15 路棋盘全盘展开根本算不完。α-β剪枝的核心是维护两个值alpha 表示极大方已经确保的最佳收益beta 表示极小方已经确保的最小损失当某个分支不可能再改变父节点的选择时直接放弃剩余子节点。def alpha_beta(board, depth, alpha, beta, is_max, ai_stone, human_stone): winner check_winner(board) if winner ai_stone: return 1000000 depth * 10000 # 越早赢分越高 if winner human_stone: return -(1000000 depth * 10000) if depth 0: return evaluate(board, ai_stone) - evaluate(board, human_stone) moves candidate_moves(board, ai_stone) if is_max: best -float(inf) for x, y in moves: board[x][y] ai_stone val alpha_beta(board, depth - 1, alpha, beta, False, ai_stone, human_stone) board[x][y] 0 # 撤销落子恢复局面 best max(best, val) alpha max(alpha, val) if beta alpha: # 极大方已经不可能超过 beta剪掉 break return best else: best float(inf) for x, y in moves: board[x][y] human_stone val alpha_beta(board, depth - 1, alpha, beta, True, ai_stone, human_stone) board[x][y] 0 best min(best, val) beta min(beta, val) if beta alpha: # 极小方已经不可能低于 alpha剪掉 break return best def candidate_moves(board, ai_stone): # 只在已有棋子周围搜索不枚举整个空棋盘 moves set() for x in range(15): for y in range(15): if board[x][y] ! 0: continue if any(0 x dx 15 and 0 y dy 15 and board[x dx][y dy] ! 0 for dx, dy in NEIGHBORS): moves.add((x, y)) # 按评估分从高到低排序剪枝效率依赖这一步 return sorted(moves, keylambda p: evaluate_point(board, p[0], p[1], ai_stone), reverseTrue)[:10] def ai_search(board, ai_stone, human_stone, depth): best_move None best_val -float(inf) for x, y in candidate_moves(board, ai_stone): board[x][y] ai_stone val alpha_beta(board, depth - 1, -float(inf), float(inf), False, ai_stone, human_stone) board[x][y] 0 if val best_val: best_val val best_move (x, y) return best_movedepth 表示剩余搜索深度毕设级别取 4 到 6再高就要配合更好的评估增量和更强的剪枝。alpha、beta 初始为负无穷和正无穷递归时极大层更新 alpha极小层更新 beta剪枝条件统一是 beta alpha。胜负判断放在搜索函数最前面返回“大数 depth 小奖励”是为了让AI偏向选择能更早获胜的路径。为什么不枚举整个空棋盘15×15 共 225 个空位深度 4 时节点数仍然是灾难。只在已有棋子周围找候选点把分支宽度压到几十个以内再用评估分排序后取前 10 个搜索规模才真正可控。剪枝是否生效强烈依赖走法排序质量如果走法从坏到好排alpha 上升慢剪枝率很低这一点在第4章会展开讲。顺带说一个答辩容易混淆的概念这里的剪枝和模型剪枝完全是两回事。搜索树的 α-β剪枝在递归时提前砍掉不会影响结果的分支属于算法层面的剪枝resnet34 剪枝量化里的权重剪枝、非结构化剪枝是砍掉网络中不重要的权重和通道那是深度学习部署里的操作。两个都叫“剪枝”但解决问题的对象不同文档里最好单独写一节区分。3. 用PyQt5把博弈过程画到界面上棋盘绘制、落子事件与AI线程分离3.1 棋盘组件与QPainter绘制15路棋盘的计算参数PyQt5 在毕设项目里主要承担两件事控件布局和 QPainter 自绘。五子棋棋盘用 QPainter 自绘是更省事、也更可控的做法。常用参数为 board_size15格子间距 cell36 像素边距 margin30 像素。窗口边长等于 margin×2 cell×(board_size-1)也就是 30×2 36×14 564 像素。class BoardWidget(QWidget): def __init__(self, board_size15, cell36, margin30): super().__init__() self.board_size board_size self.cell cell self.margin margin self.board [[0] * board_size for _ in range(board_size)] size margin * 2 cell * (board_size - 1) self.setFixedSize(size, size) def paintEvent(self, event): painter QPainter(self) painter.setRenderHint(QPainter.Antialiasing) edge self.margin (self.board_size - 1) * self.cell for i in range(self.board_size): start self.margin i * self.cell painter.drawLine(self.margin, start, edge, start) # 水平线 painter.drawLine(start, self.margin, start, edge) # 垂直线 for x in range(self.board_size): for y in range(self.board_size): if self.board[x][y] ! 0: cx self.margin x * self.cell cy self.margin y * self.cell painter.setBrush(QColor(black if self.board[x][y] 1 else white)) painter.drawEllipse(cx - 13, cy - 13, 26, 26)drawEllipse 的四个参数是左上角坐标、宽、高所以画棋子时要用 cx-13、cy-13、26、26圆心才落在交叉点上。cell36、margin30 不是随便定的棋子半径 13 像素时要保证相邻棋子有合适的视觉间距如果调整 cell棋子半径也要跟着调。为什么不直接贴一张棋盘图片因为自绘能精确把鼠标像素坐标映射回棋盘格还能在最后一手棋上加高亮标记这些用静态图片做起来很别扭。paintEvent 里只用 self.board 这个二维数组绘制数据和显示分离后续 AI 搜索直接操作同一个 board 数据源。3.2 落子事件流转从鼠标点击到触发AI搜索鼠标事件要做三步像素坐标换算成棋盘坐标、检查合法性、落子后触发 AI。def mouseReleaseEvent(self, event): if self.is_ai_thinking: return px event.pos().x() - self.margin py event.pos().y() - self.margin x round(px / self.cell) y round(py / self.cell) if not (0 x self.board_size and 0 y self.board_size): return if self.board[x][y] ! 0: return self.board[x][y] HUMAN_STONE self.update() if check_winner(self.board, HUMAN_STONE): QMessageBox.information(self, 结果, 玩家胜) return self.start_ai()坐标换算用 round 而不是 int 强转因为鼠标点在两个交叉点中间时应该落到最近的格点上。x 对应横向行索引y 对应纵向列索引后续 check_winner 和 AI 搜索里必须统一这个约定否则复盘时会出现“明明五子连珠却判错方”的麻烦。落子顺序有个细节先落子、先重绘、先判胜再启动 AI。AI 搜索期间把 is_ai_thinking 置 True玩家连点也不会产生第二个事件。QMessageBox 是模态对话框弹窗期间事件循环仍在派发鼠标事件所以弹窗之前要先把状态置好避免关窗后触发重复落子。3.3 AI搜索必须放后台QThread信号槽的最小实现如果直接在 start_ai() 里调用 alpha_beta递归会占住 GUI 线程窗口在这个时间段内完全不刷新拖动窗口时系统甚至会提示“未响应”。depth5、候选点 10 个时单步几千万次的棋盘访问在 Python 里很吃力。常见做法是把 AI 搜索放进 QThread 工作线程只把落子坐标通过信号发回主线程。class AIWorker(QThread): move_ready pyqtSignal(int, int) def __init__(self, board, ai_stone, human_stone, depth): super().__init__() self.board board self.ai_stone ai_stone self.human_stone human_stone self.depth depth def run(self): x, y ai_search(self.board, self.ai_stone, self.human_stone, self.depth) self.move_ready.emit(x, y) # 主窗口里的调用 def start_ai(self): self.is_ai_thinking True self.worker AIWorker(self.board, AI_STONE, HUMAN_STONE, self.depth) self.worker.move_ready.connect(self.on_ai_move) self.worker.start() def on_ai_move(self, x, y): self.board[x][y] AI_STONE self.is_ai_thinking False self.update() if check_winner(self.board, AI_STONE): QMessageBox.information(self, 结果, AI胜) return if is_board_full(self.board): QMessageBox.information(self, 结果, 平局)QThread 里不要直接操作任何控件把 QWidget 传给 worker 是反模式。这里只传 board 和棋子颜色两个纯数据move_ready 信号跨线程连接主线程槽函数里再改 board、再 update()落子的唯一写入口在主线程不会和 paintEvent 抢数据。用 PyQt5 做界面设计时整体结构建议分层BoardWidget 只管绘制和鼠标事件MainWindow 管按钮、棋谱显示和模式切换AIWorker 管搜索。在 pycharm 里跑这个项目时把解释器指向项目独立的虚拟环境断点打在 alpha_beta 的 beta alpha 那一行能直接看到剪枝如何把右边整棵子树跳过去这是理解剪枝最快的方式。4. 避坑从pyqt5装不上到剪枝失效最常见的几个坑4.1 环境与安装python版本、镜像源和labelme的依赖冲突坑1pip install pyqt5 卡住或报错。现象新建环境里执行 pip install pyqt5进度条长时间不动或 Python 3.12 下报 “Building wheel for PyQt5-sip ... error”。原因默认源下载慢另外 Python 3.12 之后 PyQt5 对 sip 依赖的版本兼容更苛刻安装更容易翻车。解决用清华镜像源加指定版本安装例如pip install pyqt55.15.9 -i https://pypi.tuna.tsinghua.edu.cn/simple更省心的组合是 Python 3.9 或 3.10 配 PyQt5 5.15 系列装完立刻执行from PyQt5.QtWidgets import QApplication验证。坑2之前装过 labelme再装 PyQt5 后程序起不来。现象import PyQt5 不报错但一运行就弹 “This application failed to start because no Qt platform plugin could be initialized”或者界面字体异常。原因labelme 这类标注工具依赖 PySide2 或 PyQt5 的特定版本和毕设项目装在同一套环境里两个 Qt 绑定互相覆盖插件文件。解决写完代码后用python -m venv venv建一套干净环境激活 venv 再装 pyqt5和 labelme 彻底分开在 pycharm 的项目设置里给当前项目单独指定这个 venv 的解释器。这个习惯能省掉大量“之前还能跑今天突然不行”的玄学问题。4.2 搜索与界面界面假死、剪枝没效果怎么定位坑3AI 思考时窗口假死标题栏出现“未响应”。现象落子后界面立刻卡住转几秒到几十秒期间窗口不能拖动和关闭。原因搜索函数直接在 GUI 线程里递归递归期间 Qt 事件循环得不到执行窗口消息无法处理。解决把 alpha_beta 放到 AIWorker 工作线程主线程只负责重绘和接收 move_ready。有人想在递归中间用 QApplication.processEvents() 强制刷界面这是反模式搜索不结束界面照样卡而且会拖慢搜索速度。坑4加上 α-β剪枝后运行时长反而更长。现象剪枝条件照抄depth5 还是算到天荒地老。原因剪枝效率取决于走法顺序。候选点按 (0,0) 到 (14,14) 顺序排列时alpha 很难快速逼近真实解剪枝几乎不触发候选点范围太大分支宽度也压不下来。解决candidate_moves 只返回已有棋子周围且先按 evaluate_point 从高到低排序截取前 10 到 12 个点。可以加一个全局节点计数器在 alpha_beta 每次进入时 1对比排序前后的总节点数——排序后通常能差一个数量级。这种在递归前判断“还值不值得展开”的剪枝相当于决策树里的预剪枝先展开再回溯时判断“有没有必要继续”是后剪枝α-β属于前者文档里别把两个概念混着写。坑5depth 调大后 AI 反而变傻。现象depth 从 4 调到 6AI 在某个残局里放着一步赢不走去走了别的棋。原因评估函数精度有限越深的搜索会把视野放到远期的模糊局面上评估误差被反向放大另外胜负返回的 depth 奖励在 depth6 时权重过大会扭曲选择。解决搜到第 5、6 层时把“越早赢加分”的系数调低比如 1000000 depth * 100或者干脆不加时间奖励只保证“赢了就行”。这个坑在答辩演示时最容易翻车务必用固定棋谱回归一遍再上台。4.3 规则与判定胜负判断、坐标约定与禁手处理坑6连成五子不判赢或者判到对方那边去。现象棋盘上明明横向五个同色棋程序没弹胜利有时 AI 落子后反而把玩家判输。原因坐标轴约定不一致。绘制坐标系里 x 是横向、y 是纵向check_winner 里却按 x 当行、y 当列去扫或者只扫了行和列漏了两个对角线鼠标坐标换算用 int() 而不是 round()边界处会落错格子。解决统一用 board[x][y]、x 为行号、y 为列号从最后落子点出发沿四个方向双向统计连续同色数count 5 即返回该方赢。写一个 check_direction(board, x, y, dx, dy, stone) 复用四条方向避免复制粘贴改错一处。坑7AI 下出“三三”“四四”禁手位置。现象用比赛规则对弈时AI 在“三三禁手”位置落子规则上属于黑棋禁手被判违规。原因默认实现只做了“连五验证”没有做黑棋禁手检测。解决如果毕设定位是通用五子棋在文档里明确写“无禁手规则”即可如果想让 AI 遵守正式比赛规则需额外实现黑棋禁手检测落子后同时形成两个活三或两个活四以上时判非法。这是一个可以写进“后续改进”的加分项但不建议在代码里硬塞否则评估函数和搜索逻辑都会跟着复杂化。5. 让AI“变聪明”的最后一公里评分表调参、自战验证与棋谱化演示5.1 评分表先定梯度再谈深度调参方向只有一个原则棋型之间的分差必须拉开。活四要明显大于冲四与活三之和因为一旦形成活四对方怎么堵都堵不住如果分数太平AI 会放弃活四去追两个眠三看起来是在“稳守”实际上是漏掉了必胜手。改分时一次只动一个值动完跑同一组残局否则两个参数同时变你根本分不清是谁在起作用。5.2 用AI自战模式当回归测试自战模式是最实用的调试工具。给 AI-A 和 AI-B 设置不同深度统计胜率和单步耗时验证剪枝排序是否真的降低了节点数。def self_play(depth_a, depth_b, rounds10): for i in range(rounds): board [[0] * 15 for _ in range(15)] turn 1 # 1 表示 AI-A 执黑-1 表示 AI-B 执白 while True: if turn 1: x, y ai_search(board, 1, -1, depth_a) else: x, y ai_search(board, -1, 1, depth_b) board[x][y] turn winner check_winner(board) if winner ! 0 or is_board_full(board): break turn -turn # 记录胜负、步数、单步耗时落盘到 CSV记录字段包括depth_a、depth_b、先手方、胜负、整局步数、平均单步耗时。先手一方有明显的开局优势10 局时最好双方轮流执黑各 5 局胜率才公平。同一个评估函数下深度 5 对深度 4 通常会在胜率上明显占优但单步耗时也可能翻倍这个权衡是文档里很有说服力的实验数据。5.3 把对弈过程写成HTML棋谱每步落子记录成 (x, y, 棋子颜色)结束后生成一个简单 HTML 文件用 QTextBrowser 在窗口右侧展示这也是 pyqt5 里显示 html 比较常用的做法。html [htmlbodyh3对弈记录/h3ol] for i, (x, y, stone) in enumerate(move_list): color 黑 if stone 1 else 白 html.append(fli第{i 1}手{color} ({x}, {y})/li) html.append(/ol/body/html) self.text_browser.setHtml(.join(html))这个棋谱文件可以直接导出进毕设文档的附录答辩时比口头说“我这 AI 挺强”有说服力得多。我自己的习惯是先把界面放一边在纯 Python 脚本里把自战模式跑通再开 PyQt5 人机对战——界面容易让人分心搜索和评估函数的逻辑 bug 在纯脚本下更容易暴露。评分表、搜索深度、候选点数量这三个参数每次只改一个改完就跑固定棋谱回归。如果你打算拿这题做毕业设计把调参记录和自战数据留在项目里答辩时就是一份实打实的工作量希望帮到你。本文还有配套的精品资源点击获取
返回列表