简介:100行Python代码的贪吃蛇小游戏源码包,面向Python入门者、大学生及游戏开发爱好者,旨在用最精简的代码实现经典贪吃蛇玩法,帮助读者快速理解游戏程序设计的基本思路与核心框架。整个zip压缩包仅2KB,共包含2个文件:1个py文件是可直接运行的游戏主程序,1个md文件是配套说明文档,清晰梳理了代码结构、运行方式与若干实现要点,方便边读边练。源码虽然精炼,却完整涵盖了游戏循环、键盘控制、蛇身移动、食物随机生成以及碰撞检测等关键机制,可作为课程设计、课后练手或教学演示案例。目前已有2900人学习浏览,说明其简洁直观的风格颇受初学者欢迎。下载后既能对照说明逐行拆解贪吃蛇的代码逻辑,也可自行扩展关卡难度、计分系统或音效动画,在动手修改中进一步巩固Python编程能力。
1. 为什么100行够写一个贪吃蛇
一百行Python的贪吃蛇,听起来像玩具,但对一个刚走完循环和列表语法的中级工程师来说,这个规模刚好卡在“能看懂”和“有挑战”的分界线上。zip里只有main.py和README.md,main.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_x=1, dir_y=0 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_x | dir_y | 坐标变化 |
|---|---|---|---|
| 右 | 1 | 0 | 横坐标+1 |
| 左 | -1 | 0 | 横坐标-1 |
| 下 | 0 | 1 | 纵坐标+1 |
| 上 | 0 | -1 | 纵坐标-1 |
这张表看起来简单,实际开发里最容易错的是“下”方向的dir_y。pygame坐标系里y轴向下,所以按方向键上时,dir_y应该为-1,反之按方向键下时dir_y为1。很多人沿用小游戏里常见的数学坐标,反而会让蛇上下颠倒。
3. 事件循环与帧率:让贪吃蛇“动”得顺手
贪吃蛇能不能玩,取决于两个节奏是否协调:键盘输入节奏和蛇移动节奏。main.py把这两个节奏统一在pygame的循环里,每帧先读事件,再决定是否移动和绘制。
3.1 事件读取与方向互斥
pygame.event.get()返回当前帧所有待处理事件,包括窗口关闭、键盘按下、鼠标点击等。贪吃蛇通常只关心KEYDOWN和QUIT:
for 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 | 一个网格的像素边长 | 20 |
| screen_width | 窗口像素宽度 | 格子数 x CELL_SIZE |
| move_interval | 移动间隔毫秒 | 150 |
| clock.tick | 循环最大帧率 | 60 |
4. 运行依赖与踩坑:从跑起来到改得动
把源码拿到本地,第一反应是python main.py,但新手往往会先被环境和隐藏逻辑绊倒。这个zip里的main.py依赖pygame,README.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里可以直接按Ctrl+Shift+P,选择Python解释器,再指向.venv/bin/python。
4.2 依赖装上了但导入时报错的排查
有一种高频卡点:pip install pygame显示已安装,但import pygame报ImportError。多数原因是你的Python命令和pip命令指向不同解释器,比如python指向Python 3.8,pip却属于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(maxlen=2) 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(maxlen=2)容量只有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,高速回头的问题就解决了。
本文还有配套的精品资源,点击获取