贪吃蛇AI课设实战:BFS寻路与安全决策让蛇与身体共舞 简介一套以“贪吃蛇之与蛇共舞”为主题的Python游戏编程课程设计大作业源码面向有一定Python基础、正在完成课程设计或想尝试AI体感游戏开发的学生。该项目在pgzero1.2的snake框架上扩展利用百度AI人体关键点分析接口经摄像头识别人体姿态并根据手臂、头部和肩膀的相对位置生成上下左右方向指令再通过socket127.0.0.1:20163传给标准贪吃蛇程序实现了“我做运动、小蛇吃苹果”的趣味交互。打包内容共43个文件约9.47MB包括py主程序与功能模块、ipynb交互式调试笔记、pptx作品展示稿、png/jpg图片素材、mp3背景音乐以及sh启动脚本等同时附有开发文档、代码注释和中文使用说明目录结构清晰便于按模块拆解。这套资料完整覆盖摄像头姿态识别、标准贪吃蛇和音乐播放三大模块可作为课程设计大作业源码参考也可作为百度AI开放平台入门实践的样例。目前已有936人学习浏览。1. 贪吃蛇课设怎么做才不像“作业”先想清楚AI要解决什么一个 python 游戏编程课程设计大作业想拿高分光把贪吃蛇“做出来”是不够的。真正拉开差距的地方是标题后半句“AI 智能小游戏”。我见过很多交上来的源码包蛇能走、能吃、能撞死但所谓 AI 只是随机选方向老师一问“你讲讲这个 AI 的思路”就卡住了。这个题目真正的技术点是让蛇像活物一样和自己的身体“共舞”知道往哪走能吃到食物也知道什么时候该放弃食物去保命甚至能做到永不自撞、一路吃到棋盘占满。这篇就按我实际做这类课设的思路从 pygame 骨架、AI 寻路策略到答辩演示完整讲一遍。适合刚学完 python 基础、想交一份有讲头作业的学生。2. 游戏骨架怎么搭pygame 循环、对象划分与源码包结构2.1 源码包为什么要拆成六个文件很多 python 新手交的贪吃蛇是一个人、一个文件、三百行代码从头写到尾。能跑但扩展性为零一旦要在里面加 AI改一次崩一次。我一般会把源码包拆成下面这几个模块每个模块只干一件事文件职责关键内容settings.py全局参数格子数、像素尺寸、FPS、蛇初试长度snake.py蛇本体身体坐标列表、移动、增长、自撞检测food.py食物管理随机生成、避免生成在蛇身上ai.pyAI 决策BFS 寻路、安全判定、方向选择game.py游戏流程状态切换、得分、死亡判定、绘制调度main.py入口pygame 初始化、事件循环、主循环这么拆有几个实际好处。第一调参数不用满文件找人打开 settings.py 就能改速度、改棋盘大小。第二AI 逻辑和游戏逻辑隔离把 ai.py 整个换掉就能对比“贪心 AI”和“路径规划 AI”的差别答辩时这是一个非常好的展示点。第三出 bug 时定位快蛇行为异常查 snake.pyAI 犯傻查 ai.py不用在一坨代码里面做阅读理解。2.2 主循环三段式事件、更新、渲染pygame 程序的核心是主循环。这个循环每次迭代做三件事处理事件、更新游戏状态、绘制画面。我的 main.py 大概长这样import pygame import sys from settings import CELL_SIZE, GRID_WIDTH, GRID_HEIGHT, FPS from game import Game def main(): pygame.init() screen pygame.display.set_mode( (GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE) ) pygame.display.set_caption(贪吃蛇之与蛇共舞 AI) clock pygame.time.Clock() game Game() while True: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() if event.type pygame.KEYDOWN: game.on_key(event.key) game.update() # 蛇移动一步、AI 决策一次都在这里 screen.fill((20, 20, 30)) game.draw(screen) pygame.display.flip() clock.tick(FPS) if __name__ __main__: main()这里有个关键点game.update()每帧只跑一次那蛇的移动速度就完全由clock.tick(FPS)决定。FPS 设 10就是每秒走 10 格这个手感比较接近经典贪吃蛇。如果 FPS 设得过高又没有给蛇做“计时步进”蛇会快成一道光键盘根本反应不过来。初中级课设这样做是合理的高级一点可以把“逻辑更新频率”和“渲染帧率”分开我会在避坑章节专门讲这个坑。2.3 蛇对象的移动与增长逻辑蛇的本质是一个坐标列表头在body[0]尾在body[-1]。每走一步往头部插入一个新坐标没吃到食物就把尾部弹出去。方向不能用“上下左右”这种语义直接用坐标增量表示# snake.py DIRS { UP: (0, -1), DOWN: (0, 1), LEFT: (-1, 0), RIGHT: (1, 0), } class Snake: def __init__(self, start_len3): self.body [(GRID_WIDTH // 2 i, GRID_HEIGHT // 2) for i in range(start_len)] self.direction LEFT self.growing False def move(self): dx, dy DIRS[self.direction] head (self.body[0][0] dx, self.body[0][1] dy) self.body.insert(0, head) if self.growing: self.growing False else: self.body.pop() # 没吃到食物尾巴正常收回来 def grow(self): self.growing True这段逻辑里容易被忽略的是body.pop()要发生在insert之后顺序反过来就会出现蛇多长一节或者少长一节的怪异现象。另外注意growing标志位只保留一帧吃到食物后move()会在下一帧被调用那时长度才真正加一。这是 pygame 事件驱动模型的常见节奏不是“立刻长”而是“下一帧长”。3. “与蛇共舞”的 AI 核心BFS 寻路、尾部跟随与安全兜底3.1 为什么这个项目用 BFS 而不是 A*做 AI 寻路时很多人第一反应是上 A*。但在贪吃蛇这个场景里我坚持用 BFS广度优先搜索原因很实际棋盘只有 30×20 个格子最多 600 个状态BFS 最坏情况也就是把这 600 个格子全展开一遍耗时可以忽略。A* 的优势靠启发式剪枝在大地图上才能体现在 600 格的小棋盘上省不了几次计算却多了一堆排序和估值函数的代码要写、要讲、要答辩。BFS 还有两个对课设特别友好的特性第一它天然给出最短路径路径长度就是步数演示时你能直接说“蛇走了 6 步吃到食物”第二代码量小用一个deque加一个 parent 字典就能实现老师让现场讲思路三句话能讲完。from collections import deque def bfs_path(body, target): head body[0] grid_blocked set(body[:-1]) # 尾巴下一步会离开先不封死细节见避坑章节 queue deque([head]) parent {head: None} while queue: cur queue.popleft() if cur target: # 从 parent 反向重建路径 path [] while cur is not None: path.append(cur) cur parent[cur] return path[::-1] for dx, dy in ((1,0), (-1,0), (0,1), (0,-1)): nxt (cur[0] dx, cur[1] dy) if (0 nxt[0] GRID_WIDTH and 0 nxt[1] GRID_HEIGHT and nxt not in grid_blocked and nxt not in parent): parent[nxt] cur queue.append(nxt) return Nonegrid_blocked用集合而不是列表是 600 格小地图上的一个有效加速用列表的in判断是 O(n) 的蛇长了以后每轮 BFS 多出几千次无效比较。parent 字典记录“从哪来”到终点后从后往前回溯再[::-1]反转成从头到尾的路径。这个函数是后续所有 AI 决策的地基。3.2 AI 决策三步走吃食物、保命、追尾巴BFS 只是工具AI 的决策策略才是“与蛇共舞”的核心。一般来说每条蛇每轮决策我会按优先级走这三步。第一步如果 BFS 能找到一条去食物的路径而且这条路走完之后蛇还有足够的自由空间活动就走这条路。第二步如果吃食物会把自己困死就改成 BFS 去追自己的尾巴跟着尾巴走能让蛇始终处在一个安全回路里。第三步如果追尾巴也追不上就选一个“走了之后剩余空间最大”的方向苟着等待转机。第二步是“与蛇共舞”这个名字的灵魂。追尾巴的本质是把蛇身体当成一条循环轨道头追着尾走整个身体画圈永远不会撞到自己。我要在 AI 决策里判断“走这一步后还有没有活路”这一步需要模拟def available_space(body, head): 计算从 head 出发能到达的空格子数量 visited {head} q deque([head]) blocked set(body) while q: x, y q.popleft() for dx, dy in ((1,0), (-1,0), (0,1), (0,-1)): nx, ny xdx, ydy if (0 nx GRID_WIDTH and 0 ny GRID_HEIGHT and (nx, ny) not in blocked and (nx, ny) not in visited): visited.add((nx, ny)) q.append((nx, ny)) return len(visited) def decide(snake, food): body snake.body head body[0] min_space len(body) * 2 # 安全阈值至少留出两倍身长的活动空间 path bfs_path(body, food) if path: next_head path[1] simulated [next_head] body[:-1] # 模拟移动一步后的身体 if available_space(simulated, next_head) min_space: return next_head # 吃不到或吃了会死改追尾巴 path bfs_path(body, body[-1]) if path and len(path) 1: next_head path[1] simulated [next_head] body[:-1] if available_space(simulated, next_head) min_space: return next_head # 兜底挑一个让活动空间最大的方向 best None best_score -1 for dx, dy in ((1,0), (-1,0), (0,1), (0,-1)): nx, ny head[0]dx, head[1]dy if (0 nx GRID_WIDTH and 0 ny GRID_HEIGHT and (nx, ny) not in body[:-1]): score available_space([(nx, ny)] body[:-1], (nx, ny)) if score best_score: best_score score best (nx, ny) return bestmin_space len(body) * 2这个阈值是我经验里比较稳的取值。蛇身 10 节时至少要留给蛇 20 格活动空间否则进了窄口子就很难出来。这里每一步都做一次“模拟移动 空间计算”贪吃蛇每帧计算量几十毫秒内就能完成完全不影响游戏流畅度。3.3 可视化调试把路径和死区画出来AI 的坑在于它做的决策你看不到原因。我调试这类 AI 的第一件事就是加一个调试开关把 BFS 路径和可达空间直接画在游戏窗口上。# game.py 的 draw() 里 if ai_debug: path ai.last_path # 保存最近一次 BFS 结果 if path: for i, cell in enumerate(path): x, y cell[0]*CELL_SIZE, cell[1]*CELL_SIZE color (0, 255, 0) if i % 2 0 else (0, 200, 0) pygame.draw.circle(screen, color, (xCELL_SIZE//2, yCELL_SIZE//2), 4)开着这个开关跑一局你能清楚看到蛇的“想法”绿点是它打算去的路线死区一眼就能看出哪条路是死路。答辩现场开这个调试模式效果比任何流程图都好因为它展示的是活生生的决策过程。这是个值得做进课设里的加分项。4. 避坑AI 瞎绕圈、拐角自尽与食物死角的排查记录4.1 现象BFS 给出的路径蛇走到一半反而被自己堵死我刚开始把 BFS 直接接进游戏时蛇经常走到一半突然自杀而且死的位置离食物还有好几格看起来就像 AI“变傻”了。原因在 BFS 里对蛇尾的处理。bfs_path里我最初把整个身体都放进 blocked 集合set(body)把尾巴也算成不可走。但实际上蛇每走一步尾巴会同时让出一格如果你把尾巴封死BFS 会绕远路甚至找不到路反过来如果完全不封尾巴BFS 可能规划一条追着尾巴尖的路线蛇头还没到尾巴已经移走了路径自然失效。解决方法是避坑 3.2 里那个写法blocked 集合用body[:-1]把尾巴那一格视为“下一步会空出来”。但要注意这个开放只对尾巴生效一格如果蛇很长尾巴让出的位置离头很远BFS 还是可能规划出穿过蛇身的路径。所以正确逻辑是“路径规划 模拟移动 空间安全判定”三件套一起用缺一个都会翻车。4.2 现象食物刷在死胡同角落AI 宁死也要吃棋盘是随机的食物偶尔会生成在墙角或者生成在蛇身体围成的口袋里。此时 BFS 能到食物但那条路一旦走进去身后就被自己身体堵死吃完食物直接原地去世。原因是我最初只判断“有没有路径”没判断“路径走完以后还有没有活路”。新手很容易犯这个错BFS 找到路径就一头冲过去完全不考虑生存空间。解决方法是 3.2 的 min_space 阈值判断。发现“吃了食物但剩余空间不够”时立刻放弃食物转身追尾巴。还有一个经验值是连续五次决策都不选食物时宁可原地绕圈也不要强行穿窄缝等食物刷新到开阔地带再去吃。这个逻辑写起来就三行但对于 AI 的生存率提升非常明显。4.3 现象AI 模式下按键盘蛇突然反向自撞在做键盘和 AI 双模式时我遇到过这么个问题AI 接管时我按了一下方向键想手动操作蛇直接一头撞到自己身上。原因事件循环收到键盘事件后立刻改了蛇的 direction但 AI 决策这一帧已经结束了相当于手动方向越过 AI 直接生效。如果新方向跟蛇当前运动方向相反比如正在往右你按了左下一步蛇头就直接踩到自己第二节身体上当场死亡。这是贪吃蛇所有死亡方式里最容易出现、也最容易让评委觉得“这个 AI 不稳”的一种。解决方法是给模式加锁。AI 模式下键盘事件直接跳过只有切到手动模式才处理按键。要演示“玩家介入”的话正确做法是显式按 M 键切换模式而不是直接按方向键。if event.type pygame.KEYDOWN: if event.key pygame.K_m: game.ai_active not game.ai_active elif not game.ai_active: game.snake.set_direction_from_key(event.key)4.4 现象FPS 调高后蛇没变快反而画面漂移加发虚贪吃蛇传统做法是用clock.tick(FPS)同时控制渲染帧率和蛇的移动频率。FPS 从 10 调到 30蛇应该变快但实际跑起来蛇是偶尔抖两格视觉上明显不连贯。原因pygame 的 tick 只是“每帧间隔至少 1000/FPS 毫秒”不是严格让游戏逻辑每固定时间走一步。系统卡顿一帧蛇就跳两格系统流畅蛇又走得慢。对于需要 AI 配合的蛇这种跳帧会直接导致碰撞误判。解决方法是把“游戏步进”和“渲染”分开。维护一个计时器蛇每 100 毫秒固定走一步渲染则尽量保持流畅。step_time 0 STEP_INTERVAL_MS 100 while running: dt clock.tick(60) step_time dt while step_time STEP_INTERVAL_MS: game.update() # 蛇移动固定节奏 step_time - STEP_INTERVAL_MS game.draw(screen) pygame.display.flip()这样蛇 100 毫秒走一格雷打不动。AI 有稳定的思考节奏手工操作的手感也完全可控。4.5 现象AI 活到后期忽然开始原地画圈前期一切正常蛇越长越大到了填满小半个棋盘时AI 忽然在一个没食物的小空地上反复转圈怎么都不出去找食物。原因安全阈值len(body) * 2在蛇变长后变得非常苛刻。蛇 30 节时要 60 格自由空间但棋盘总共 600 格剩余空间可能只有 80 格蛇认为哪条路都不安全于是默认进入“兜底模式”一直选空间最大的方向被困在局部。解决阈值不应该是线性的。我最后改成min_space max(len(body), 20)即至少留出等于身长的空格。蛇身较短时这个阈值足够保守蛇身较长时也不会因为要求过多而不敢行动。另外后期食物刷新可以限制在蛇头前方区域这也是很多商业贪吃蛇游戏的常见处理。5. 答辩还能多演一局录像回放、难度曲线与自动演示5.1 录像回放把 AI 的完整表演存下来现在的课设答辩普遍有个尴尬现状现场演示时 AI 不配合死在奇怪的地方。我的习惯是给游戏加一个“录像回放”功能把每一局的决策序列保存下来答辩时直接放录好的完整局。# 录像记录帧号和方向 record [] def record_frame(game): record.append((game.tick_count, game.snake.direction)) # 回放每帧从记录里取出方向覆盖键盘输入 def replay_frame(game, index): tick, direction record[index] if game.tick_count tick: game.snake.direction direction录像只记录决策不记录画面占用的内存几乎可以忽略。回放时把键盘事件禁用直接按记录驱动蛇。这样你可以在答辩前跑出十条漂亮的长对局从中挑一条最精彩的存下来。演示前注意确认回放速度和原速一致用 5.1 的固定步进写法就不容易出现回放节奏错乱。5.2 难度曲线与自动演示参数课设要想拿高分另一个容易被忽视的点是“游戏性”。AI 再聪明如果玩家一上来就被虐体验也不好。我会在 settings.py 里加一个难度曲线表关卡蛇长区间FPS步/秒13-5826-1010311-1513416-201652120每吃一个食物蛇长加一关卡自动切换速度跟着涨。AI 在这个框架下也会表现得更有层次前期横冲直撞中后期开始频繁追尾游走也就是“与蛇共舞”的完整状态。答辩现场演示时可以准备一个简单的自动演示模式启动后 AI 自动接管打满一盘统计得分、步数、存活时间打印在窗口顶部。这组数据就是 AI 水平和策略优劣的直接证据。还有一个实操细节是命名每个版本的源码包要留版本号比如snake_ai_v2_bfs_tail这种方便答辩时讲“我尝试过单纯 BFS 版本、BFS追尾版本、加空间安全判定版本”版本对比本身就是最能撑起课设深度的材料。现在我再做同类项目第一件事永远是先做回放和对比功能没有对比数据AI 算法讲得再好也容易被看成自说自话。希望这个思路能帮你在答辩现场少一点紧张多一点底气。本文还有配套的精品资源点击获取