ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

100行Python代码实现贪吃蛇:pygame游戏开发核心解析

100行Python代码实现贪吃蛇:pygame游戏开发核心解析 简介100行Python代码的贪吃蛇小游戏源码包面向Python入门者、大学生及游戏开发爱好者旨在用最精简的代码实现经典贪吃蛇玩法帮助读者快速理解游戏程序设计的基本思路与核心框架。整个zip压缩包仅2KB共包含2个文件1个py文件是可直接运行的游戏主程序1个md文件是配套说明文档清晰梳理了代码结构、运行方式与若干实现要点方便边读边练。源码虽然精炼却完整涵盖了游戏循环、键盘控制、蛇身移动、食物随机生成以及碰撞检测等关键机制可作为课程设计、课后练手或教学演示案例。目前已有2900人学习浏览说明其简洁直观的风格颇受初学者欢迎。下载后既能对照说明逐行拆解贪吃蛇的代码逻辑也可自行扩展关卡难度、计分系统或音效动画在动手修改中进一步巩固Python编程能力。1. 为什么100行够写一个贪吃蛇一百行Python的贪吃蛇听起来像玩具但对一个刚走完循环和列表语法的中级工程师来说这个规模刚好卡在“能看懂”和“有挑战”的分界线上。zip里只有main.py和README.mdmain.py去掉空行和注释后真的不到100行却包括了窗口初始化、按键响应、蛇身移动、食物生成、碰撞判定、得分绘制六件事。它没有用复杂类也没有拆分模块而是把pygame游戏循环里最必要的东西按顺序摆了出来。适合人群是两类一类是把Python语法背得很熟但第一次接触游戏循环的人另一类是想快速看一份外部源码、然后改成自己的版本的人。前者能在里面学会事件机制后者能直接用它当骨架换皮肤、加障碍物都很方便。2. 网格模型与移动算法main.py里的核心逻辑先别看渲染和事件贪吃蛇之所以能“动”靠的是三件事把蛇抽象成一个有序列表、每次移动时确定新头部、然后决定尾部去留。main.py里没有用OOP而是用全局变量和几个函数完成这种写法对100行规模反而更容易读。2.1 用列表和元组建模蛇身蛇身的核心数据结构是一列坐标元组比如[(x, y), (x, y), ...]。每个元素是一个格子的坐标列表从头部到尾部依次排列。移动时新头部坐标由当前头部加上方向步长得到然后把它插入到列表头部同时删除列表尾部元素。如果新头部正好落在食物上就不删尾部蛇身长度加一。这个过程恰好和列表操作一一对应所以list.insert加上pop就够了。从main.py里抽出来的核心函数我重新整理后大概是def move_snake(snake, dir_x, dir_y, food): # dir_x和dir_y是步长例如向右时dir_x1, dir_y0 head_x, head_y snake[0] new_head (head_x dir_x, head_y dir_y) snake.insert(0, new_head) # 新头插入列表最前 if new_head ! food: snake.pop() # 没吃到食物就去掉尾巴 else: return True # 表示吃到了方便外部生成新食物参数snake是一个可变列表dir_x参与水平方向dir_y参与垂直方向。这里不判断边界因为边界检查被放在调用方否则函数职责就太重了。insert(0, new_head)的时间复杂度是O(n)但n等于蛇身长度最多几百没有问题。如果你追求极致可以用collections.deque的appendleft代替但那份源码没有这样做保持线性也是可以接受的。2.2 元组与列表的选择可哈希与查找效率的关键差别很多初学者习惯把蛇身写成[[x, y], [x, y]]用[x, y]表示坐标。这在逻辑上没错但一旦你想写new_head not in snake列表是不能被哈希的无法放入set。而贪吃蛇恰恰需要频繁判断“新头是否已经在身体里”。这里我踩过坑所以建议照main.py的做法使用元组因为元组可哈希可以直接参与集合操作def hit_self(new_head, snake): return new_head in set(snake[1:])set(snake[1:])把去掉头后的身体坐标转成集合查找时间复杂度降到O(1)。如果使用二维列表每次查找要从头遍历所有蛇身在经典尺寸20x20网格下感觉不明显但当你把地图改成50x50并且蛇吃到几百格时肉眼可见卡顿。参数new_head是待检测的新蛇头坐标snake[1:]不包含当前头部因为头部和头部自身重叠不算撞自己。2.3 食物随机生成与坐标粒度食物坐标必须在网格点上不能落在窗体的任意像素。main.py里用的是随机整数除以格子大小或者直接以格子为单位生成。常见做法是import random food (random.randrange(grid_w), random.randrange(grid_h))grid_w和grid_h分别代表横向和纵向有多少个格子。生成后还要检查它是否在蛇身坐标内简单写法是while food in snake: food ...。这处的参数并不复杂但要注意randrange不包含上界如果格子数是20生成范围是0-19恰好对应索引。如果你把格子数误当成像素数食物会生成在蛇永远吃不到的位置游戏看起来一切正常但就是死循环。方向步长可以归纳成一张表方便直接抄方向dir_xdir_y坐标变化右10横坐标1左-10横坐标-1下01纵坐标1上0-1纵坐标-1这张表看起来简单实际开发里最容易错的是“下”方向的dir_y。pygame坐标系里y轴向下所以按方向键上时dir_y应该为-1反之按方向键下时dir_y为1。很多人沿用小游戏里常见的数学坐标反而会让蛇上下颠倒。3. 事件循环与帧率让贪吃蛇“动”得顺手贪吃蛇能不能玩取决于两个节奏是否协调键盘输入节奏和蛇移动节奏。main.py把这两个节奏统一在pygame的循环里每帧先读事件再决定是否移动和绘制。3.1 事件读取与方向互斥pygame.event.get()返回当前帧所有待处理事件包括窗口关闭、键盘按下、鼠标点击等。贪吃蛇通常只关心KEYDOWN和QUITfor event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN: if event.key pygame.K_UP and direction ! (0, 1): direction (0, -1) elif event.key pygame.K_DOWN and direction ! (0, -1): direction (0, 1)这里的direction ! (0, 1)是在阻挡反向如果当前正在向下就不能按上如果当前正在向上就不能按下。为什么不阻止直接按左右因为左右和上下不会形成立刻撞到自己身体的局面但先上后下却会让蛇头在下一步直接回到上一格的坐标也就是撞到自己的脖子。所以只排除180度掉头这是贪吃蛇游戏逻辑上最基本的保护。3.2 用MOVE_INTERVAL而不是tick控制移动频率新手容易把clock.tick(10)理解为“蛇每秒钟走10格”但tick只是控制while循环的最大执行频率。如果循环里还承担绘制和事件处理循环可能被卡到每秒不到10次而且不同电脑性能不同蛇的速度就不稳定。处理速度更稳妥的办法是用pygame.time.get_ticks()记录上一次移动时刻然后比较经过的时间move_interval 150 last_move_time pygame.time.get_ticks() while running: now pygame.time.get_ticks() if now - last_move_time move_interval: last_move_time now move_snake(snake, dir_x, dir_y, food) draw(screen, snake, food) clock.tick(60)move_interval的单位是毫秒数值越小蛇移动越快。clock.tick(60)始终让事件循环尽量保持每秒60次这样键盘响应是平滑的而蛇的移动节奏由move_interval单独把持。把这两层分开之后你可以随意调整游戏难度而不会影响按键灵敏度。3.3 绘制顺序与分数刷新绘制本身不算核心但main.py里有一个容易踩的坑分数文本那张surface需要每帧重绘否则旧分数会残留在画面上。一般的顺序是screen.fill(BG_COLOR) # 清理画布 for seg in snake: # 绘制蛇身 pygame.draw.rect(screen, SNAKE_COLOR, (seg[0]*CELL_SIZE, seg[1]*CELL_SIZE, CELL_SIZE, CELL_SIZE)) pygame.draw.rect(screen, FOOD_COLOR, (food[0]*CELL_SIZE, food[1]*CELL_SIZE, CELL_SIZE, CELL_SIZE)) screen.blit(score_surface, (10, 10)) pygame.display.flip() # 翻转缓冲区screen.fill如果不执行上一次的蛇尾残影会一直显示视觉上就变成蛇越走越脏。pygame.draw.rect的第三个参数接受矩形区域其中前两个值是左上角像素坐标所以蛇的位置要乘以CELL_SIZE换算成像素。display.flip()把后台缓冲一次性交换到屏幕避免绘制过程中出现闪烁。顺序不要反如果先画蛇再fill等于白画。这里有一张和运行速度强相关的参数表变量含义常见值CELL_SIZE一个网格的像素边长20screen_width窗口像素宽度格子数 x CELL_SIZEmove_interval移动间隔毫秒150clock.tick循环最大帧率604. 运行依赖与踩坑从跑起来到改得动把源码拿到本地第一反应是python main.py但新手往往会先被环境和隐藏逻辑绊倒。这个zip里的main.py依赖pygameREADME.md应该也写了安装命令。4.1 环境安装与常见启动方式Python安装教程网上很多这里只强调项目级依赖安装。最稳的流程python -m venv venv source venv/bin/activate pip install pygame python main.pypython -m venv venv在当前目录建一个虚拟环境防止把这个项目的依赖装进全局Python。source venv/bin/activate进入虚拟环境在Windows上对应的命令是venv\Scripts\activate。pip install pygame安装pygame库这个库是贪吃蛇源码里唯一的第三方依赖。最后python main.py启动游戏如果窗口出现说明环境OK。如果你是用IDE比如VS Code跑记得把解释器指到venv里的Python否则会出现终端里能跑、IDE里却提示模块不存在的怪问题。VS Code里可以直接按CtrlShiftP选择Python解释器再指向.venv/bin/python。4.2 依赖装上了但导入时报错的排查有一种高频卡点pip install pygame显示已安装但import pygame报ImportError。多数原因是你的Python命令和pip命令指向不同解释器比如python指向Python 3.8pip却属于Python 3.11。检查方法python -c import sys; print(sys.executable) pip show pygame | grep Location如果两个路径不在同一个site-packages下说明解释器不一致。解决办法是直接用python -m pip install pygame这样确保装到当前python对应的环境里。pygame本身对Python版本要求不高3.7到3.12都能用但建议不低于3.8因为一些新版本pygame已经放弃旧解释器。4.3 边界条件与逻辑错误如果运行正常但蛇吃不到食物、或者分数不加大多出在逻辑细节。我把自己调试时常用的判定方式列成一个表格症状检查位置说明蛇不增长食物判断是否用了new_head food而不是in或坐标比较撞墙没反应边界判断是否在移动前检查了x、y是否超出0到grid-1食物生成在蛇身上生成逻辑需要while food in snake循环重新生成蛇越动越长且不断掉尾部弹出pop()是否放在了没吃到食物的分支里关于边界判断常见代码是if not (0 new_x grid_w and 0 new_y grid_h): running False这里的0 new_x grid_w是链式比较翻译过来是new_x 0 and new_x grid_w。如果漏掉了蛇会在最后一格时被判定越界如果漏掉 grid_w蛇头已经跑到窗口外面了才终止。我见过不少游戏在蛇只剩一格时表现正常一旦身体长了会刚好擦着墙消失这就是边界漏判。5. 加一个方向缓冲队列彻底解决高速回头自杀随着游戏提速你一定会遇到连续按键导致蛇瞬间反向、自己撞死的情况。原因很简单键盘的KEYDOWN事件处理是滞后的两个按键可能落在同一帧后一个方向覆盖前一个而蛇头已经在下一帧按新方向移动。禁止反向判断只能挡住一次按键挡不住连续操作的历史残留。我的做法是不再让direction变成单一变量而是用collections.deque做长度为2的缓冲队列。每次按方向键先把方向和队尾比较如果与队尾相反就丢弃否则入队每次移动前从队头取一个方向。这样连续按上、左蛇会先向上再向左而不是像以前那样左键覆盖上键或者更糟上键按下后下一帧直接掉头向下撞自己。from collections import deque direction_que deque(maxlen2) direction_que.append((1, 0)) # 初始方向向右 def push_direction(new_dir): if len(direction_que) 0: direction_que.append(new_dir) return last direction_que[-1] # 阻止180度反向 if (new_dir[0] ! -last[0]) or (new_dir[1] ! -last[1]): direction_que.append(new_dir) def pop_direction(): if len(direction_que) 1: return direction_que.popleft() return direction_que[0]参数new_dir是一个二元组比如(0, -1)。deque(maxlen2)容量只有2满了之后自动把最旧的方向弹出所以即使玩家狂按队列里也只会保留最近两个有效方向。pop_direction在队列只剩一个方向时保持不变相当于对当前方向做重复确认。把main.py里原来对direction的直接赋值替换成push_direction在移动函数之前用dir_x, dir_y pop_direction()取出方向其余逻辑不用动。实测在move_interval降到80时这个缓冲区能有效吸收按键扰动手的抖动不再直接变成蛇头转向。你还可以把maxlen改成3但那样会引入“按了左、左、左但蛇还在向右”的滞后感所以2是比较合适的值。把maxlen改成2然后把direction赋值处全部替换成push_direction高速回头的问题就解决了。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进