资讯动态

Python五子棋人机对战实战:从棋盘设计到AI博弈算法

发布时间:2026/9/14 5:23:13 来源:尧图企业网站定制
简介这是一份面向Python课程设计的五子棋游戏完整源代码适合正在完成pygame相关作业或想学习游戏开发基础的高校学生参考。项目通过pygame库创建窗口支持鼠标点击落子、右侧实时显示当前下棋方、胜负判定、开局时间记录与背景音乐播放棋盘状态使用二维列表维护用0/1/2分别表示空位、白棋、黑棋整体代码结构直观便于理解经典棋类游戏的实现思路。压缩包共7个文件核心为task2_version2.0.py主程序另含Visual Studio工程文件sln与pyproj、两张png棋盘素材、一首mp3背景音乐以及README说明文档总大小仅3.16MB资源紧凑且开箱即用。该资源已有2371人学习浏览读者可基于源码快速搭建自己的五子棋项目也可替换图片与音频完成个性化课程设计适合作为答辩演示或二次开发的起点。1. 五子棋课程作业先想清楚这三件事拿到“五子棋游戏源代码”这个作业题目时多数人第一反应是“不就是画个棋盘、轮流落子、判断五连吗”。真动手后你会发现课程作业的评分点往往不在“能玩”而在“设计得是否清楚、边界有没有处理、AI是不是有自己的思路”。我见过不少同学把全部逻辑塞进一个main函数界面和规则混在一起加一个AI功能就改得面目全非这种代码即使跑通答辩时也容易被追问到漏洞百出。在写第一行代码前先把三件事定下来用什么数据结构表示棋盘、胜负判定怎么做到不重不漏、人机对战要做到什么级别。如果作业要求里只写“双人对战”那实现一个控制台版就能过但多数老师期待你至少交一个带界面的版本并且源码组织得能让人一眼看出“棋盘类、判定类、AI类”的边界。这比花哨的动画和音效重要得多。这个标题里“课程作业”四个字意味着你要在“能运行”和“有深度”之间找平衡。我下面给的方案是以 tkinter 做界面、纯 Python 实现规则和 AI全程不依赖第三方库这样在任何装有 Python 的环境里都能直接跑也方便打包成 exe 提交。代码结构上拆成三个文件负责不同职责考试或答辩时你可以很清楚地讲出自己的设计思路。2. 棋盘数据与胜负判定先让规则跑起来2.1 用二维列表还是位棋盘数据结构的选择五子棋的棋盘标准是 15×15也有 19×19 的但课程作业用 15×15 最常见。数据结构上二维列表board[row][col]是最直观的值 0 表示空1 表示黑棋2 表示白棋。这种做法的优点是查改方便调试时打印出来就是棋盘图形不需要额外转换缺点是当你以后想加 AI 搜索时复制整个棋盘会稍微慢一点但对课程作业完全够用。另一种选择是用位棋盘也就是用两个 15×15 的整数位掩码分别表示黑方和白方每个棋子占一位。位运算判连珠非常快但可读性差调试困难也不利于你向老师解释。我更建议作业场景用二维列表因为 225 个格子的规模下性能差异根本体现不出来而代码清晰度和可维护性才是评分重点。我一般会在一个board.py文件里定义Board类内部维护一个_grid二维列表并提供落子、判断是否为空、获取某个位置的一方棋子这些基础方法。这样后续不管是做双人模式还是人机模式都共用同一套棋盘对象避免界面里直接操作裸列表带来的越界风险。2.2 落子、判赢、禁手最小可用核心代码下面是一条完整的判定流程代码包含落子与胜利检测。先看代码再解释每个参数的含义。class Board: def __init__(self, size15): self.size size self.grid [[0] * size for _ in range(size)] self.current_player 1 # 1 黑, 2 白 self.move_count 0 self.winner 0 def is_valid_move(self, row, col): if not (0 row self.size and 0 col self.size): return False return self.grid[row][col] 0 def place_piece(self, row, col): if not self.is_valid_move(row, col): return False self.grid[row][col] self.current_player self.move_count 1 if self.check_win(row, col): self.winner self.current_player else: self.current_player 3 - self.current_player return True def check_win(self, row, col): piece self.grid[row][col] directions [(1, 0), (0, 1), (1, 1), (1, -1)] for dr, dc in directions: count 1 for sign in (1, -1): r, c row sign * dr, col sign * dc while 0 r self.size and 0 c self.size and self.grid[r][c] piece: count 1 r sign * dr c sign * dc if count 5: return True return Falseplace_piece是唯一入口先校验位置合法性再落子然后调用check_win从当前落子点向四个方向搜索。directions里定义了水平、垂直、主对角线、副对角线四个轴向注意副对角线用(1, -1)两个符号方向都要查否则会漏掉反方向的连子。sign控制正反两个方向延伸例如检查水平时先向右数再向左数加上当前棋子本身得到该方向上的连续同色棋子数。这个实现有个关键点只在落子的位置检查胜利而不是每次全盘扫描。因为新一子落下后只有经过它的连线才可能产生新的五连全盘扫描浪费且代码啰嗦。如果棋盘已满但没人赢需要在place_piece之后判断move_count size * size此时平局。还有一点容易被忽略先手优势。五子棋黑棋有禁手规则但课程作业一般不管如果老师要求严格可以给黑棋加“三三禁手”检测不过这会显著增加复杂度建议先提交无禁手版本在 README 里说明。3. 人机对战AI从随机落子到启发式评分3.1 最简单的AI评分函数怎么设计课程作业里的人机对战AI 不需要达到顶尖水平但至少不能随机下。最常见的做法是给棋盘的每个空位打分然后选分数最高的点落子。评分函数的思路是模拟把棋子放到某个位置看它在四个方向上分别能形成多少连子、有多少能变成活四或冲四同时还要考虑防守也就是如果对手在这个位置落子会有多大威胁。我用的评分表是一个经典的权值法四方向独立计算后求和。下面给出核心评分代码def evaluate_point(board, row, col, ai_player): # 模拟AI落子在后计算进攻分再模拟对手落子计算防守分 def score_for_player(player): total 0 directions [(1, 0), (0, 1), (1, 1), (1, -1)] temp_board [row[:] for row in board.grid] temp_board[row][col] player for dr, dc in directions: count 1 open_ends 0 for sign in (1, -1): r, c row sign * dr, col sign * dc while 0 r board.size and 0 c board.size and temp_board[r][c] player: count 1 r sign * dr c sign * dc if 0 r board.size and 0 c board.size and temp_board[r][c] 0: open_ends 1 total shape_score(count, open_ends) return total attack score_for_player(ai_player) defense score_for_player(3 - ai_player) return attack defense * 0.9shape_score是根据连子数和空端数返回一个权重比如活四给 10000冲四给 1000活三给 500眠三给 100活二给 50。这样 AI 会优先去做自己的活四同时也尽量堵住对手的活三。defense * 0.9是让防守稍弱于进攻因为五子棋有先手优势进攻效率更高一些。这个 0.9 是我常用的系数你可以调到 0.8 到 1.0 之间看效果。这个方案的优点是计算量小每个空位只做 8 个方向扫描整个棋盘最多 225 个点毫秒级出结果。缺点是只有一层静态评估不会考虑“下一步对手怎么应对”所以面对复杂定式容易犯傻。但如果课程只要求“能赢新手”这个强度已经够了。3.2 搜索深度与Alpha-Beta剪枝的取舍如果你想让 AI 更强一点可以加入极小化极大搜索配合 Alpha-Beta 剪枝。五子棋搜索的宽度很大每个节点有几十个合法落子深度到了 4 层就需要剪枝和启发式排序否则课程作业级别的电脑根本算不动。常见做法是限制搜索深度为 2 或 4。深度 2 的意思是AI 走一步考虑对手走一步然后评估局面。四层搜索配合剪枝在 15×15 棋盘上节点数可以接受但每层必须用评估函数裁剪候选落子点一般只对最高分的 10-15 个点继续搜索而不是展开所有空位。def alpha_beta(board, depth, alpha, beta, ai_player): if depth 0 or board.winner ! 0: return evaluate_board(board, ai_player) legal_moves get_candidate_moves(board, 12) if board.current_player ai_player: best -float(inf) for r, c in legal_moves: board.place_piece(r, c) if board.winner ! 0: val 99999 else: val alpha_beta(board, depth - 1, alpha, beta, ai_player) board.grid[r][c] 0 board.winner 0 board.current_player 3 - board.current_player board.move_count - 1 best max(best, val) alpha max(alpha, best) if beta alpha: break return best else: # 对手节点取最小值逻辑对称 ...这里有一个关键细节递归搜索时place_piece会修改棋盘状态所以在回溯后要手动恢复grid、winner、current_player和move_count。很多同学写搜索时忘记恢复current_player导致第二层递归交换棋子颜色错乱整个 AI 行为就变得不可预测。另一种更安全的做法是搜索时显式传入玩家参数而不是依赖棋盘对象内部的状态。get_candidate_moves通常只返回对方最近一步落子周围两格内的空位以及 AI 自己棋子周围的空位这样能大幅减少搜索分支。但注意如果棋盘刚开始这种策略会漏掉角落扩张所以开局阶段要额外加一些中心点作为候选。3.3 让AI“像人”的两个参数哪怕用了最简单的评分法也有两个参数对棋风影响很大一个是前面提到的defense_weight另一个是随机扰动。如果你的 AI 每次都选唯一的最高分点那么相同局面下所有行为完全一致玩家很容易总结出规律下一盘就知道怎么骗它。解决办法是在评分差距小于一定阈值时从并列的高分点里随机选取。def choose_best_move(board, ai_player): best_score -1 best_moves [] for r in range(board.size): for c in range(board.size): if board.grid[r][c] ! 0: continue score evaluate_point(board, r, c, ai_player) if score best_score 10: best_score score best_moves [(r, c)] elif abs(score - best_score) 10: best_moves.append((r, c)) return random.choice(best_moves)这里的阈值 10 就是随机扰动幅度。如果每个候选点的分数相差不超过 10 分AI 就把它们视为同档随机选一个。这样既能保证不会去下明显很差的点又能让棋风有点变化。另一个参数是评分表里的具体数值比如把“活三”的分数从 500 提高到 800AI 会更积极地制造活三而不是去堵对手的眠三。调整时你可以写一个小工具让 AI 和自己对弈几百盘统计胜率变化但课程作业不必做这么重手动试几盘感受一下即可。4. 图形界面与交互用tkinter快速搭出可演示界面4.1 画布绘制与棋盘重绘tkinter 是 Python 自带的 GUI 库不需要安装适合做课程设计。棋盘绘制用 Canvas 控件画线、画圆、画矩形都非常直观。下面是一个最小可运行的棋盘绘制框架import tkinter as tk class GobangGUI: CELL_SIZE 36 MARGIN 30 def __init__(self, root, board): self.root root self.board board self.canvas tk.Canvas(root, widthself.CELL_SIZE * (board.size - 1) self.MARGIN * 2, heightself.CELL_SIZE * (board.size - 1) self.MARGIN * 2, bg#DCB35C) self.canvas.pack() def draw_board(self): self.canvas.delete(all) size self.board.size start self.MARGIN end self.MARGIN self.CELL_SIZE * (size - 1) for i in range(size): self.canvas.create_line(start, self.MARGIN i * self.CELL_SIZE, end, self.MARGIN i * self.CELL_SIZE) self.canvas.create_line(self.MARGIN i * self.CELL_SIZE, start, self.MARGIN i * self.CELL_SIZE, end) for r in range(size): for c in range(size): if self.board.grid[r][c] 1: x self.MARGIN c * self.CELL_SIZE y self.MARGIN r * self.CELL_SIZE self.canvas.create_oval(x - 12, y - 12, x 12, y 12, fillblack, outlineblack) elif self.board.grid[r][c] 2: x self.MARGIN c * self.CELL_SIZE y self.MARGIN r * self.CELL_SIZE self.canvas.create_oval(x - 12, y - 12, x 12, y 12, fillwhite, outlineblack)这里draw_board会先清空画布再重画所有内容。如果界面类里直接操作board.grid确实简单但破坏了封装。更好的做法是给Board类加一个get_piece(row, col)方法界面只调用它不直接碰内部列表。注意画棋盘线时循环变量i从 0 到size-1每条线两端要使用start和end保证横竖线交叉点坐标对齐。棋子半径设为 12与格子间距 36 相比视觉上刚好不会互相压住。4.2 鼠标事件与回合控制点击棋盘某一点要把它换算成行列坐标。换算公式是row round((y - margin) / cell_size)但要注意点击位置可能刚好落在两条线中间round之后得到最近的行号。在做round之前要判断点击点是否离最近交叉点太远比如超过cell_size * 0.4就忽略这次点击否则用户点到格子中央也会落子体验很奇怪。下面绑定鼠标事件并处理回合def on_click(self, event): col round((event.x - self.MARGIN) / self.CELL_SIZE) row round((event.y - self.MARGIN) / self.CELL_SIZE) if not (0 row self.board.size and 0 col self.board.size): return dist_x event.x - (self.MARGIN col * self.CELL_SIZE) dist_y event.y - (self.MARGIN row * self.CELL_SIZE) if dist_x * dist_x dist_y * dist_y (self.CELL_SIZE * 0.45) ** 2: return if self.board.place_piece(row, col): self.draw_board() if self.board.winner ! 0: result 黑棋胜 if self.board.winner 1 else 白棋胜 tk.messagebox.showinfo(结束, result) elif self.board.move_count self.board.size * self.board.size: tk.messagebox.showinfo(结束, 平局) else: self.root.after(100, self.ai_move) # AI 落子root.after是 tkinter 中很常用的定时器这里延迟 100 毫秒再让 AI 走棋主要是为了让玩家看到自己刚下的棋子再看到 AI 的动作。如果不加这个延迟AI 会瞬间落子视觉上像是“同时”下棋影响体验。AI 落子后要再次调用draw_board重绘。一个常被忽略的问题是回合控制。如果玩家点击后棋盘已经产生赢家就不能再让 AI 动作。上面代码里用place_piece的返回值判断落子是否成功再检查winner顺序不能反。另外如果开启了 AI 对战 AI 的演示模式需要把after改成递归调用并在每一轮检查游戏是否结束。4.3 界面卡顿与刷新问题的解决课程作业里常见的卡顿有两个来源一是每落一子就整盘重绘所有线、所有棋子如果界面代码里还创建了多余的对象棋盘大一点就会闪二是 AI 评分函数里重复复制整个二维列表每次 AI 落子要跑几百次复制导致明显停顿。整盘重绘其实开销不大15×15 的画布对象只有几百个现代电脑毫秒级完成。真正的问题是你在draw_board里不小心把背景色也重新设置或者创建了太多 Canvas 对象没有删除。建议canvas.delete(all)只清除图形对象不要重新配置画布尺寸和背景。如果确实需要性能优化可以只重绘变化的那一行、列和对角线但那个复杂度对作业不值得。AI 卡顿的优化方法是在evaluate_point里不要复制整个棋盘。可以把模拟落子的步骤改成临时修改board.grid[row][col]计算完再还原为 0用try/finally保证异常时也能还原。但要注意如果在多线程环境里这样做会有并发问题不过 tkinter 主线程是单线程事件循环只要没有开额外线程就不怕。另一个优化是缓存最近几步的评估结果但是五子棋局面变化快实际效果有限不如直接减少候选点数量来得实在。5. 作业提交前的最后检查这几个坑我替你踩过了5.1 判定函数有没有检测最后一路我见过一个真实案例五子棋 AI 在右边界和底边界时经常漏判因为check_win里的while循环条件写的是r size and c size但负方向延伸时忘了写r 0 and c 0导致数组越界被IndexError中断。界面还能跑因为 tkinter 捕获了异常但 AI 直接卡死。这个 bug 很隐蔽尤其是副对角线方向两个符号方向都要检查负边界。写一个自动测试脚本遍历所有可能的五连位置逼每个位置上都放满同色棋子然后断言check_win返回 True。再随机放置一些四连、禁手局面断言返回 False。提交前跑一遍能抓住绝大多数边界问题。特别是检查从左上角到右下角这条对角线以及从右上角到左下角那条对角线这两个方向最容易写错。5.2 打包成exe时资源文件路径如果老师要求交可执行文件你会用 pyinstaller 打包。那时会遇到一个典型问题如果程序中加载了图标文件或音效文件按照相对路径读取在双击 exe 时会报 FileNotFoundError因为当前工作目录不是 exe 所在目录。解决办法是在打包时把资源文件用--add-data选项放进去然后代码里根据是否打包来切换路径。import sys import os def resource_path(relative): if hasattr(sys, _MEIPASS): return os.path.join(sys._MEIPASS, relative) return os.path.join(os.path.dirname(__file__), relative)在 pyinstaller 打包命令里写上--add-data icon.ico;.Windows 用分号。如果你没有图标文件就用 tkinter 默认的空白窗口图标不要折腾因为自定义图标要处理透明背景和多尺寸 ico很花时间对课程分数帮助不大。5.3 给老师看的README怎么写课程作业的评分点里通常包含文档。不要写大段原理介绍老师最想看的是怎么运行、代码文件结构、算法思路、你做了什么创新。给出一个简洁的 README 模板你可以改成自己的。## 运行环境 Python 3.8 无需第三方库 ## 运行方式 python main.py ## 文件说明 board.py —— 棋盘数据结构与规则判定 ai.py —— 启发式评分AI含Alpha-Beta搜索 gui.py —— tkinter界面与事件处理 main.py —— 程序入口 ## 算法说明 AI采用四方向评分 深度4的Alpha-Beta剪枝 防守权重0.9候选点取最近落子3格内最后再检查一件事源码中不要用中文变量名。有些学校代码规范允许中文但很多老师的检查工具可能处理不好编码问题。统一用英文变量注释可以中文但文件头不要带# -*- coding: utf-8 -*-之外的奇怪内容。把这个 zip 包里的__pycache__文件夹删掉再重新压缩避免老师解压后看到一堆缓存文件影响第一印象。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价