news 2026/10/26 8:13:50

基于PyQt5的多智能体博弈五子棋:深搜与α-β剪枝实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于PyQt5的多智能体博弈五子棋:深搜与α-β剪枝实践

简介:基于Python与PyQt5实现的多智能体博弈AI五子棋项目,是一份面向计算机相关专业学生毕业设计、课程设计或期末大作业的完整源码资料。项目覆盖人机博弈、深度搜索与α-β剪枝等核心AI算法,代码附有注释,适合新手阅读与二次开发,也方便快速部署体验。压缩包共17个文件,包含4个Python源码文件、1个PyQt界面文件(.ui)、1个资源文件(.qrc),以及9张PNG图片、1个ICO图标和1份Markdown文档,整体大小约5.39MB。Python源码承载游戏逻辑与AI决策,UI和资源文件负责界面呈现,文档说明有助于理解项目结构;项目结构清晰,适合展示博弈树搜索过程。目前已有338人学习/浏览。通过这份资料,读者可以获得完整的项目源代码、界面与图标素材、启动入口和README说明,既能直接运行体验人机对战,也能依据注释和文档梳理性剪枝、深搜等AI实现思路,为毕业答辩或课程汇报提供扎实参考。

1. 多智能体博弈五子棋:一个能落在简历上的毕设题目

如果你在找毕业设计题目,大概率会盯着“基于Python+PyQt5实现多智能体博弈AI五子棋游戏(人机博弈+深搜+α-β剪枝)”这种长标题琢磨:它是把界面、AI、搜索算法串成一个能玩的程序,还是给外行看的包装词?我的判断是:这个题目很适合做课设和毕设,因为每个词都能拆成独立章节,最后又能合起来交付一个交互项目。

项目核心价值不在“会下五子棋”,而在一条完整的落地链路:用Python和PyQt5搭建图形界面,用深搜实现AI决策,再用α-β剪枝把性能拉到可玩范围,最后把算法分析写进文档。它同时覆盖人机博弈、双AI对弈和算法对比,适合有Python基础但没写过GUI、希望在搜索算法上补课的人。

2. 用PyQt5把棋盘画出来:最小可交互界面

2.1 为什么用PyQt5而不是控制台或网页

做五子棋的第一版,我见过不少同学用控制台打印棋盘,逻辑照样能跑,但答辩时总觉得少点东西。PyQt5的优势在于,它和Python环境配合非常直接:先确认python安装和pip正常,然后执行pip install pyqt5就能开始写,不需要像前端那样再装一套Node或调跨域问题。如果没装成功,多数是源太慢,换国内pip镜像即可;装完之后要重启一下IDE,否则vscode的python环境可能识别不到新的第三方包。

另一个我看中的点是PyQt5的信号槽机制。棋盘点击、AI落子、线程通信都可以用信号和槽串起来,比tkinter的command回调更清晰。QPainter绘制15路棋盘和棋子很快,整个窗口重绘一次毫秒级别,不会成为后续深搜的性能瓶颈。

所以我的项目结构从一开始就分成三层:Board类只管棋盘二维数组和胜负判断,AI搜索类只读棋盘并返回落子坐标,MainWindow负责鼠标事件和重绘。这样“人机博弈”和“多智能体博弈”共用一个Board接口,后面双AI对弈时不需要为窗口操作费心。代码上最大程度避免在UI里直接操作棋盘数据。

2.2 棋盘绘制与落子坐标换算

下面是一段可以直接运行的PyQt5最小界面。它不包含AI,只是用来验证环境和确认坐标映射,代码里有注释。

import sys from PyQt5.QtWidgets import QApplication, QMainWindow from PyQt5.QtGui import QPainter, QColor from PyQt5.QtCore import Qt class GobangWindow(QMainWindow): def __init__(self, n=15): super().__init__() self.n = n self.cell = 40 self.margin = 30 self.board = [[0] * n for _ in range(n)] # 0空地 1黑子 2白子 self.size = self.margin * 2 + self.cell * (self.n - 1) self.setWindowTitle("PyQt5 五子棋") self.setFixedSize(self.size, self.size) def cx(self, c): return self.margin + c * self.cell def cy(self, r): return self.margin + r * self.cell def paintEvent(self, event): painter = QPainter(self) painter.fillRect(self.rect(), QColor(235, 176, 89)) painter.setPen(QColor(0, 0, 0)) for i in range(self.n): y = self.cy(i) painter.drawLine(self.margin, y, self.size - self.margin, y) painter.drawLine(y, self.margin, y, self.size - self.margin) for r in range(self.n): for c in range(self.n): if self.board[r][c] == 1: painter.setBrush(QColor(0, 0, 0)) painter.drawEllipse(self.cx(c) - 13, self.cy(r) - 13, 26, 26) elif self.board[r][c] == 2: painter.setBrush(QColor(255, 255, 255)) painter.setPen(QColor(0, 0, 0)) painter.drawEllipse(self.cx(c) - 13, self.cy(r) - 13, 26, 26) def mousePressEvent(self, event): if event.button() != Qt.LeftButton: return x, y = event.x(), event.y() # 像素坐标换算成棋盘坐标,round让点击点自动吸附到最近的交叉点 c = round((x - self.margin) / self.cell) r = round((y - self.margin) / self.cell) if 0 <= r < self.n and 0 <= c < self.n: self.board[r][c] = 1 self.update() if __name__ == "__main__": app = QApplication(sys.argv) win = GobangWindow() win.show() sys.exit(app.exec_())

我对代码里的几个参数说明一下。n=15是棋盘路数,对应标准五子棋;cell=40是每格边长,窗口尺寸是302+4014=620像素,在笔记本上刚好完整显示。margin=30控制棋盘到窗口边缘的距离,如果想做截图再调到40。注意setFixedSize而不是setGeometry,它能让窗口不可拉伸,避免棋盘被拖动变形。

点击事件的坐标换算是最容易错的地方。鼠标x、y是相对于窗口左上角的像素值,而棋盘落子要用行列号。公式是“先减边距再除以格子宽度”,也就是c=(x-margin)/cell;取整要用round而不是int。原因很简单:如果用户点在第7列中心偏右一点,int会把0.8变成0,落子唰地跳到上一格;round则吸附到最近的交叉点。边缘的落子手感全靠这一步。

这段代码里的board二维数组就是后面深搜要操作的数据。界面归界面,数据归数据,后面写AI时你不需要在这个类里动任何绘图代码。

2.3 从“人”这一侧接出博弈流程

光能落子还不够,鼠标点击后要能转入AI决策。下面只列出新增字段和方法,替换2.2里的同名方法即可:

class GobangWindow(QMainWindow): def __init__(self, n=15): # 棋盘、尺寸、paintEvent等初始化见2.2节 self.current_turn = 1 self.locked = False def mousePressEvent(self, event): if self.locked: return if event.button() != Qt.LeftButton: return x, y = event.x(), event.y() c = round((x - self.margin) / self.cell) r = round((y - self.margin) / self.cell) if 0 <= r < self.n and 0 <= c < self.n and self.board[r][c] == 0: self.board[r][c] = 1 # 玩家先手 self.update() self.locked = True self.ai_move() def ai_move(self): # 先放一个占位:用QTimer延迟300毫秒,避免事件循环卡顿 from PyQt5.QtCore import QTimer QTimer.singleShot(300, self.place_ai_stone) def place_ai_stone(self): # 占位逻辑:找第一个空位放白子,后续替换成AI搜索结果 for r in range(self.n): for c in range(self.n): if self.board[r][c] == 0: self.board[r][c] = 2 self.locked = False self.update() return

这个流程里的locked标志很关键。如果没有它,玩家会在AI思考时连续点击,给同一个棋盘同时写入黑白子,坐标全部错乱。我这里用全局的locked=true锁住界面,等AI通过place_ai_stone落子后再解锁。QTimer.singleShot(300, ...)是演示时最常见的做法,它的作用是让界面先刷新一次再执行AI逻辑。真正做答辩版本,建议换成第4.1里的QThread方案,因为深搜耗时时让主线程同步等待会直接把事件循环堵死。

这段代码跑通后,你已经完成了人机博弈的“人”侧。接下来把place_ai_stone里的循环换成真正的深搜搜索结果,就是完整的AI落子链路。

3. 深搜与α-β剪枝:从枚举全盘到秒出结果

3.1 多智能体博弈:先建立一个零和搜索模型

多智能体博弈听上去复杂,五子棋其实是最适合拆开的例子。这里的“智能体”不是指深度学习模型,而是指能独立读棋盘、做决策的单元:玩家是一个智能体,AI是另一个。双AI对战时,黑棋和白棋各是一个智能体。它们共享同一个棋盘,轮流改变环境状态,目标完全相反,所以这是一个典型零和博弈。

在搜索的角度看,当前局面是根节点,AI落子的每个空位都产生一个分支,对手的每个回应又继续产生分支,最终长成一棵博弈树。深搜要做的是从根一直往深处探索,到搜索深度上限或分出胜负,然后一层一层回传评价值。因为双方目标相反,一层取最大,下一层取最小,这就是极大极小搜索,也是α-β剪枝算法的基础。

需要提醒一点:这里的“深搜”和“图的深度优先遍历”不一样。图遍历只要访问过节点就可以不再访问;博弈搜索里每个节点状态不同,不能简单去重,所以递归返回后必须恢复现场,否则兄弟分支会看到被修改过的棋盘。

3.2 深搜的递归核心:不剪枝版本先算对

我一般先写一个不剪枝的minimax,确认评估函数和恢复逻辑没错,再加入剪枝。评估函数可以先用最简单的连续子计数,让AI先“有反应”:

def evaluate_board(board, player): direction = [(1, 0), (0, 1), (1, 1), (1, -1)] score = 0 for r in range(15): for c in range(15): if board[r][c] != player: continue for dr, dc in direction: cnt = 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: cnt += 1 else: break if cnt >= 2: score += cnt * cnt return score

请不要迷信这个评分。它的作用是“把棋盘状态映射成数值”,如果棋形没识别,搜索再深也很难走出活三。但它足以跑通深搜,等整体流程稳定后再升级。

然后是minimax核心:

def minimax(board, depth, is_maximizing, ai_color, human_color): winner = check_winner(board) if winner == ai_color: return 100000 + depth if winner == human_color: return -100000 - depth if depth == 0: return evaluate_board(board, ai_color) - evaluate_board(board, human_color) candidates = get_candidates(board) if not candidates: return 0 if is_maximizing: best = -float("inf") for r, c in candidates: board[r][c] = ai_color best = max(best, minimax(board, depth - 1, False, ai_color, human_color)) board[r][c] = 0 return best else: best = float("inf") for r, c in candidates: board[r][c] = human_color best = min(best, minimax(board, depth - 1, True, ai_color, human_color)) board[r][c] = 0 return best

注意几个边界。胜负判断要优先于depth==0,否则“已经赢了但还没到深度”会继续搜索;返回100000+depth而不用固定100000,是为了让更早获胜的分支得分更高,AI不会在胜局里故意拖延。递归尝试落子后必须撤销,这是深搜最容易翻车的位置:如果漏掉board[r][c]=0,后面的候选点会基于脏棋盘计算,轻则算出错误着法,重则递归栈溢出。

参数方面,搜索深度我一般从2开始调。深度2是最基础的反应,深度4配合后面要说的候选点限制,已经能走出像样的防守;深度6则要依赖α-β剪枝和合理评分。深度不是越大越好,因为每多一层,时间会变得更长,AI落子超过5分钟会让演示翻车。

3.3 加α-β剪枝:两个阈值让深搜不再全遍历

不剪枝的minimax在空棋盘上几乎不可用。以深度4、候选点10个为例,最坏要搜10^4个局面,虽然这只是理论上限,已经能把Python跑出明显卡顿。α-β剪枝算法的思路是:在深搜过程中维护两个阈值,alpha是MAX层目前已经能保证的下界,beta是MIN层目前能接受的上界。当某个分支让beta<=alpha成立,说明后面的搜索不会影响根节点决策,可以整棵跳过。

在minimax上改成带剪枝版本:

def minimax_ab(board, depth, alpha, beta, is_maximizing, ai_color, human_color): winner = check_winner(board) if winner == ai_color: return 100000 + depth if winner == human_color: return -100000 - depth if depth == 0: return evaluate_board(board, ai_color) - evaluate_board(board, human_color) candidates = get_candidates(board) if not candidates: return 0 if is_maximizing: best = -float("inf") for r, c in candidates: board[r][c] = ai_color val = minimax_ab(board, depth - 1, alpha, beta, False, ai_color, human_color) 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 candidates: board[r][c] = human_color val = minimax_ab(board, depth - 1, alpha, beta, True, ai_color, human_color) board[r][c] = 0 best = min(best, val) beta = min(beta, best) if beta <= alpha: break return best

代码只比原版多了两个参数和两个if break,但性能差距可能非常大。这里最关键的是“谁更新谁”:alpha只能MAX节点更新,beta只能MIN节点更新。如果我在MAX里误写了beta,剪枝条件会因为边界值错乱而提前剪掉正确分支,AI会突然下出莫名其妙的棋。

接下来是剪枝的性价比:候选点顺序。同一个局面,先搜一个“较好的”位置还是先搜一个“明显差”的位置,alpha/beta收敛速度完全不同。我习惯在生成候选点时就排序,顺便把候选列表限制在一个小集合里:

def get_candidates(board, max_candidates=10): points = set() for r in range(15): for c in range(15): if board[r][c] == 0: continue for dr in range(-2, 3): for dc in range(-2, 3): nr, nc = r + dr, c + dc if 0 <= nr < 15 and 0 <= nc < 15 and board[nr][nc] == 0: points.add((nr, nc)) if not points: return [] scored = [] for r, c in points: # 简单启发式:优先落在棋子密集的附近 score = 0 for dr in range(-1, 2): for dc in range(-1, 2): nr, nc = r + dr, c + dc if 0 <= nr < 15 and 0 <= nc < 15: if board[nr][nc] == 1: score += 50 elif board[nr][nc] == 2: score += 30 scored.append((score, r, c)) scored.sort(reverse=True) return [(r, c) for _, r, c in scored[:max_candidates]]

这函数做的事有两件:第一,只考虑现有棋子两格范围内的空位,把225个空位收敛到几十个;第二,按简单启发式倒序排列,保证α-β先搜索有价值的分支。max_candidates是候选点数量上限,我常用10,深度大时可以缩到8。如果你发现AI总在边角落子,多半是把max_candidates放太宽或启发式分值没区分黑白,只看自己棋子的权重,却忘了考虑对手。

这样,搜索层用minimax_ab,候选点用get_candidates,评估函数用evaluate_board,三者串起来就是深搜+α-β剪枝的完整闭环。第4章我会梳理这几部分各自最常踩的坑。

4. 避坑与常见问题:从“能跑”变成“能答辩”

4.1 界面卡死:AI搜索不能放在主线程

现象:点击后窗口白块,鼠标拖动无反应,过几秒才恢复。原因:在mousePressEvent里直接调用minimax_ab,递归计算把PyQt5事件循环占满了。解决:用QThread。代码:

class AIWorker(QThread): move_done = pyqtSignal(int, int) def __init__(self, board_snapshot, depth, parent=None): super().__init__(parent) self.board_snapshot = board_snapshot self.depth = depth def run(self): r, c = ai_search(self.board_snapshot, self.depth) self.move_done.emit(r, c)

窗口侧连接信号:

worker = AIWorker([row[:] for row in self.board], depth) worker.move_done.connect(self.on_ai_move) worker.start()

注意复制board。为什么?UI线程还会继续paintEvent,它读到的棋盘对象如果被AI线程修改,绘制内容会跳变。我会把“搜索用的快照”做成只读,AI线程只输出一个坐标,再由主线程更新正式棋盘。如果你偷懒直接传self.board,偶尔会出现棋子和棋盘点错位,而且很难复现。

4.2 加了剪枝却没提速:节点顺序比剪枝本身更重要

现象:把minimax换成minimax_ab后,时间几乎没降,节点数也没变。原因:候选点没排序,alpha/beta很难触发;或者剪枝条件写错,例如在MAX层却比较beta。解决:先在get_candidates里按启发式排序,再在递归入口放一个计数器验证。剪枝的正确性有个很实用的验证方法:同一局面跑“不剪枝”和“剪枝”两版,两者最佳落子坐标必须完全一致。如果出现不一致,说明剪枝代码边界写错了,而不是“剪枝改变了策略”。

计数器代码:

search_cnt = 0 def minimax_ab(...): global search_cnt search_cnt += 1 # 其余逻辑保持不变

在递归函数入口自增,跑一个中盘局面,分别统计剪枝前后次数。如果次数没掉一个数量级,回去改搜索顺序;如果时间降了几十倍而节点数没降多少,可能是评估函数耗时小、minimax_ab递归外开销占主导,这时优先减少候选点数。

4.3 候选点全遍历:225的空位会把深搜拖死

现象:棋盘只下了两三子,AI就已经想得满头汗;深度调到4,直接等不出结果。原因:get_candidates未做约束,每次递归都扫描全部225个空位。解决:只在已有棋子的2格范围内找空位,并取前10~15个。搜一个固定的中盘局面,候选点通常会降到30个以内,再截断到10个,深度4最多一万次叶节点评估,配合剪枝实际会少很多。这张取舍直接决定AI速度,比优化评估函数更立竿见影。

如果棋盘上一枚棋子都没有,get_candidates会返回空列表。这时我一般让AI直接下天元附近,而不是强行搜索。这种边界要写进文档,否则评审试一下AI先手却不下棋,印象分很受影响。

4.4 评估函数只看“连续子数”:AI没有进攻意识

现象:AI会应“马上要成五的点”,但不会主动制造活三;评优时总选一个看似安稳但没威胁的点。原因:评分只用了连子长度的平方,没有区分活形和眠形。比如一个被两边堵死的眠三,和两边都空的活三,对局意义完全不同。解决:给棋形加权。常见做法是:

棋形权重
成五100000
活四8000
冲四2000
活三800
眠三100
活二20

把上面evaluate_board里的连续计数换成统计每种棋形的数量,乘以不同权重。答辩时把这张权重表放进文档,说明“深搜负责找路线,评估函数负责判断路线价值”,比空谈算法更有说服力。

4.5 “多智能体”被追问:题目里每个词都要有落点

现象:演示时只打开人机对战,评委问“多智能体体现在哪”,答不上来。原因:题目里的多智能体博弈没有对应可玩功能。解决:在菜单里加一个“AI对战”模式,黑棋用深搜+剪枝,白棋用随机或贪心策略,让两个智能体自行下完一整局,并在界面下方打印落子数、搜索深度、剪枝前后节点数。这样既能把“多智能体”落地,还能顺带收集胜率数据。文档里就写:两个独立决策单元共享棋盘状态,各自优化自身收益,构成一个零和博弈系统。这一条回答能直接把题目里的三个关键词全部引出来。

5. 验证剪枝效果:用节点数让答辩眼前一亮

5.1 用节点数和双AI胜率验证算法

代码:

from copy import deepcopy board = load_midgame_board() # 读一个中盘局面 cnt_no_prune = search_stats(deepcopy(board), depth=4, use_prune=False) cnt_prune = search_stats(deepcopy(board), depth=4, use_prune=True) print("no prune:", cnt_no_prune) print("prune:", cnt_prune)

search_stats是一个把minimax包了一层计数的函数。注意两版必须用同一个board对象深拷贝,不能先跑一版再跑另一版时棋盘已经被改掉;另外只统计叶节点还是所有节点要在文档里说明,否则数据对不上。我的习惯是统计“进入递归的总次数”,因为这个数最直观,且不受机器波动影响。

第二个验证是双AI胜率。让黑棋AI用剪枝,白棋AI用随机落子,连续跑20局,看黑棋胜率。如果剪枝实现有问题,黑棋胜率不会比随机高多少;如果棋力正常,胜率应该接近100%。比较两件事:剪枝前后节点数显著下降,剪枝后的棋力不下降,这才算有效优化。

5.2 把参数表写进文档

文档说明别照抄代码,要写清楚四个数:搜索深度、候选点数、评分权重、剪枝开关。我用过一个最简单的方式:写一个CONFIG.py集中管理这些参数,文档里放“depth=4,max_candidates=10,成五=100000,活四=8000……”表格,并说明不同参数组合在演示机上耗时约多少秒。答辩时直接展示这一页,比讲十分钟原理更有效。

我现在做AI相关项目,第一件事就是给搜索加一个节点计数开关。没有计数,剪枝效果全靠感觉,就是玄学;有了计数,你能一眼看出优化是不是真的发生了。希望帮到你。

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

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

AI文献综述工具:学术透镜技术解析与应用

1. 学术写作的范式变革&#xff1a;当AI遇上文献综述去年我在指导研究生论文时发现一个有趣现象&#xff1a;学生们平均要花费200小时在文献综述环节&#xff0c;却仍有80%的初稿存在逻辑断裂或文献覆盖不全的问题。这种现象正在被一款名为"书匠策AI"的工具改变——它…

作者头像 李华
网站建设 2026/10/26 8:13:34

邹博机器学习全套代码复现:环境、回归、SVM与聚类避坑指南

简介&#xff1a;压缩包收录了邹博机器学习课程的配套代码&#xff0c;涵盖回归、支持向量机&#xff08;SVM&#xff09;、聚类等常规算法&#xff0c;同时内置xgboost-master源码库&#xff0c;既适合初学者对照练习&#xff0c;也可供开发者快速搭建模型。包内共391个文件&a…

作者头像 李华
网站建设 2026/10/26 8:13:20

凸优化算法实践:自动微分、牛顿法与拟牛顿法的工程实现

简介&#xff1a;面向北京大学信息科学技术学院智能科学与技术系“凸分析与优化方法”课程的编程作业合集&#xff0c;适用于正在系统学习自动微分、梯度类优化算法与神经网络参数调优的学生和研究者。压缩包共63个文件&#xff0c;以23个Python脚本为核心&#xff0c;包含自动…

作者头像 李华
网站建设 2026/10/26 8:13:04

2026网络安全毕业设计热点:云原生与AI安全趋势

1. 网络安全毕业设计选题趋势与2026年热点预测毕业设计选题往往需要兼顾技术前瞻性与实际应用价值。从当前网络安全领域的技术演进路线来看&#xff0c;2026年的热点方向可能会集中在以下几个维度&#xff1a;云原生安全架构的纵深防御AI驱动的威胁检测与自动化响应物联网边缘计…

作者头像 李华
网站建设 2026/10/26 8:11:31

AI工程化落地:动态环境适配与混合推理引擎实战

1. 从5000实战案例看AI落地的本质困境在2023年全球AI应用峰会上&#xff0c;Kymo AI首席架构师张工展示了一组令人震撼的数据&#xff1a;他们的AI系统已在制造业质检、医疗影像分析、金融风控等12个垂直领域完成5000实际场景部署。这个数字背后是超过2000次模型迭代和300余次架…

作者头像 李华
网站建设 2026/10/26 8:11:00

RM机器人上位机工程实践:ROS 2 + Qt6实时可视化方案

1. 项目概述&#xff1a;这不是一个“软件推荐”&#xff0c;而是一套面向RM机器人开发者的上位机工程实践方案“吃点好的&#xff01;适合RM宝宝的上位机&#xff1f;”——这个标题乍看像一句调侃&#xff0c;但背后藏着一群高校RoboMaster参赛队员的真实痛点&#xff1a;他们…

作者头像 李华