我一直觉得贪吃蛇是学Python最“值”的一个小项目。它不大几百行代码搞定但里面包含了游戏开发最核心的循环机制、事件处理、坐标运算、碰撞检测甚至还能摸到一点算法设计的边。很多朋友问我Python入门之后该做什么练手我的答案从来都是先写一个贪吃蛇别一上来就爬网页、做数据分析。爬虫有反爬数据分析有各种库要学而贪吃蛇只要你把逻辑捋顺了代码写出来能跑、能吃、能死、能计分你对Python的信心会一下子立起来。这篇文章我就用pygame库从零开始把贪吃蛇的每一个细节拆开讲包括网格怎么设计、蛇怎么移动、食物怎么随机生成、撞墙撞自己怎么判定、分数怎么显示以及那些新手最容易踩的坑比如方向反转BUG、按键抖动、游戏卡顿等等。适合刚学完基础语法、想用项目巩固知识的朋友也适合想给学生讲游戏编程的老师参考。我会尽量做到“超详细”每一步都告诉你为什么要这么写。1. 为什么非要用贪吃蛇学Python它能练到什么硬功夫很多人对贪吃蛇有误解觉得它太简单不够“高级”。但实际上贪吃蛇是一个极其典型的游戏开发教学案例它把游戏程序里几乎所有通用概念都浓缩在了一个极小的规模里。当你把贪吃蛇写明白后面再去学俄罗斯方块、Flappy Bird、甚至一些2D横版游戏都会觉得思路非常清晰。首先是**主循环game loop**的概念。几乎所有的游戏都有一个永远在跑的大循环处理输入 - 更新状态 - 绘制画面 - 控制帧率循环往复。贪吃蛇是理解这个循环最直观的载体没有物理引擎、没有复杂的渲染管线你能清清楚楚地看到每一次循环里程序做了什么。这个“循环思维”一旦建立起来不只是游戏很多常驻运行的程序、服务器程序、GUI程序你都更容易理解。其次是数据结构的选择。蛇怎么表示身体是一节一节的每一节有坐标蛇会移动会变长。这不就是一个天生的列表list吗蛇头在列表头部每次移动往头部插入新坐标如果吃到食物就保留尾巴如果没吃到就弹出尾巴。用列表的insert(0, ...)和pop()就能优雅地模拟蛇的爬行。这种“选择合适的数据结构来建模现实对象”的能力比任何语法知识都重要。第三是坐标系统和碰撞检测。游戏窗口是一个二维坐标系蛇和食物都在这个坐标系里有自己的位置。判断蛇有没有吃到食物本质就是判断蛇头坐标和食物坐标是否相等。判断是否撞墙就是判断坐标是否超出了窗口边界。判断是否撞自己就是判断蛇头坐标是否出现在身体的坐标列表里。这些听起来简单但其实就是游戏逻辑的核心——游戏本质上是规则驱动的状态机。第四是代码的组织能力。虽然一个贪吃蛇可以用几百行堆完但如果你想让它清晰、可维护、方便加功能就得考虑怎么拆函数、怎么处理全局变量和局部变量的关系、怎么让逻辑和绘制分离。很多初学者写出来的贪吃蛇代码“能跑但不敢动”加一个功能就崩就是因为代码全都糊在一起。这篇文章我会采用一种清爽的写法每个功能一个函数逻辑层和界面层分开方便你后续继续扩展。也正是因为贪吃蛇同时踩中了数据结构、事件驱动、坐标运算、游戏循环这几个关键点它才能在计算机编程教学里几十年长盛不衰。不管是用C语言、Java还是Python写核心思路都是相通的。你学会了Python版再去看C语言版的贪吃蛇解析会发现只是在语法层面翻译了一遍逻辑完全一致。2. 开工前的环境准备与整体架构规划2.1 pygame的安装与版本选择做贪吃蛇图形界面方案里我强烈推荐pygame。虽然Python自带的tkinter也能写但tkinter的事件模型、画布刷新和游戏循环并不是为游戏设计的写起来绕性能也一般。pygame就是专门做2D游戏的一套库跨平台API设计得很直观社区资料也最多。安装很简单打开终端或命令行执行pip install pygame如果你在国内下载可能比较慢可以换用清华源pip install pygame -i https://pypi.tuna.tsinghua.edu.cn装好之后可以用一行代码验证是否成功python -m pygame.examples.aliens这个命令会打开一个pygame自带的小游戏窗口如果你能看到画面、听到声音说明环境没问题。如果报错大部分情况是Python版本不匹配或者pip没有配好建议先检查python --version和pip --version确保它们指向同一个Python解释器。2.2 文件结构与代码组织方式我不建议把整个游戏塞进一个几百行的.py文件里然后一口气跑起来。更好的做法是先想清楚游戏需要哪些“角色”和“逻辑”在文件里用注释分区块或者直接拆成几个模块。当然为了新手学习的方便我这里先用单文件实现但内部会严格按照功能拆分函数顺序是初始化部分设置窗口、常量、pygame初始化工具函数生成食物坐标、绘制蛇身、显示文字游戏主循环事件处理、逻辑更新、碰撞检测、画面绘制这样拆分的好处是你看到主循环的时候不会被一堆绘制细节淹没一眼就能看懂游戏每帧都在干什么。2.3 用网格坐标系代替像素坐标这是贪吃蛇的地基贪吃蛇最基础的设计决策就是——不要直接用像素坐标来控制蛇的位置。为什么因为贪吃蛇的移动是“一格一格”的蛇身每一节都对齐在一个固定的网格上。如果你用像素坐标蛇头移动几个像素、蛇身怎么跟随、食物怎么对齐全部都会变得很难处理。正确的做法是把整个窗口划分成若干个大小相等的网格单元蛇头蛇身的位置都用“网格坐标”来表示比如(5, 3)表示第5列第3行的格子。绘制的时候再把网格坐标乘以格子边长换算成像素坐标画出来。这里我选一个600x600的窗口每个格子边长20像素那么一共就是30行、30列用代码表示WIDTH 600 HEIGHT 600 GRID_SIZE 20 COLS WIDTH // GRID_SIZE # 30列 ROWS HEIGHT // GRID_SIZE # 30行为什么要用“//整数除法”因为窗口尺寸除以格子边长必须得到整数否则最后一列格子会画到窗口外面去。这也是一个新手容易忽略的细节你在设计阶段就要保证COLS和ROWS都是整数最好让窗口尺寸能被格子尺寸整除。蛇的初始位置怎么定为了对称美观一般放在窗口正中间。也就是第15列第15行换算成网格坐标是(COLS // 2, ROWS // 2)。初始身体长度可以是3节蛇头在最前面后面两节依次排列。比如默认向右移动初始身体就是snake [ (COLS // 2, ROWS // 2), # 蛇头 (COLS // 2 - 1, ROWS // 2), # 第一节身体 (COLS // 2 - 2, ROWS // 2) # 第二节身体 ]注意列表的第0个元素永远是蛇头。这个约定非常重要后面所有移动、碰撞、吃食物逻辑都依赖这个约定。2.4 明确一个方向常量表和移动速度的关系蛇的运动是持续的它会根据当前的方向每隔一段时间自动向前移动一格。所以我们需要一个“当前方向”变量和一个“移动速度”控制量。方向用二维向量表示最方便DIRECTIONS { pygame.K_UP: (0, -1), pygame.K_DOWN: (0, 1), pygame.K_LEFT: (-1, 0), pygame.K_RIGHT: (1, 0) }为什么要用向量因为移动的本质就是坐标增量。如果当前方向是(0, -1)说明下一步的y坐标要减1也就是向上移动一格。把方向映射成“坐标变化量”移动代码就变得极其简洁dx, dy DIRECTIONS[direction] new_head (snake[0][0] dx, snake[0][1] dy)速度方面pygame里控制游戏节奏靠的是pygame.time.Clock()的tick(FPS)方法。tick(10)表示每秒最多刷新10帧。对贪吃蛇来说这个值就代表了蛇的移动速度每帧移动一格每秒10格。我建议初始速度设在10左右后面如果想加难度可以在吃到食物时把帧率上限提高蛇就越跑越快。3. 贪吃蛇的核心机制拆解网格、移动、碰撞3.1 蛇到底是怎么“前进”的头插尾删的列表艺术很多刚接触贪吃蛇的朋友会想当然地认为蛇身移动是把每一节身体都往下一节的方向挪动就像蜈蚣一样一节推一节。这个理解没有错但如果真按这个思路写代码你得遍历整条蛇把每一节的坐标都改成前一节的坐标代码会显得很啰嗦。更聪明的做法是利用列表的“头插尾删”操作。想象蛇的每一节身体都记录在列表里蛇头在list[0]。每一帧要做的事情只有两步计算新蛇头的位置把它插入到列表头部如果这一帧没有吃到食物就把列表尾巴pop掉如果吃到了就不pop相当于身体多了一节。用代码表达# new_head 已经算好 snake.insert(0, new_head) if not ate_food: snake.pop() else: score 1 # 生成新食物...你可以亲手推演一下初始蛇是[(15,15), (14,15), (13,15)]向右移动一格新头是(16,15)插入后变成[(16,15), (15,15), (14,15), (13,15)]没吃到食物pop掉尾巴(13,15)最终变成[(16,15), (15,15), (14,15)]。整条蛇看起来就是整体向右平移了一格完美。如果吃到食物尾巴不pop新头插入后蛇就长了一节效果也正是我们想要的。这个“头插尾删”的思路是贪吃蛇实现里最精妙的地方也是面试时别人问“你怎么实现贪吃蛇的移动”时最想听到的答案。3.2 食物的随机生成与“不准生成在蛇身上”食物位置必须是随机的而且绝对不能和蛇的任何一个身体节点重叠。否则食物长在蛇身上却看不见游戏就没法玩了。我一般用一个while循环来生成食物坐标思路非常朴素import random def spawn_food(snake): while True: x random.randrange(COLS) y random.randrange(ROWS) if (x, y) not in snake: return (x, y)有人会担心如果蛇很长几乎占满了整个屏幕这个循环会不会死循环理论上存在这种可能但实际操作中只有当蛇长度接近900节30x30900时才可能出现极端情况而那个局面玩家早就赢了或者游戏早就结束了所以完全不用担心。真正的游戏开发里有时会用“可从所有空格子里随机取”的列表方式但你得维护一个空闲格子列表代价更高贪吃蛇这个规模完全不需要。另外还要注意食物生成的坐标也是网格坐标不是像素坐标。绘制时再乘GRID_SIZE无需在生成阶段做任何像素换算。3.3 三种死亡判定撞墙、撞自己、以及方向反转陷阱贪吃蛇的死亡条件有两类撞墙和撞到自己。代码写起来很直接# 撞墙 if new_head[0] 0 or new_head[0] COLS or new_head[1] 0 or new_head[1] ROWS: game_over True # 撞自己 if new_head in snake: game_over True这里有一个细节很多人会忽略撞自己的判断里new_head in snake是否会误判假设蛇没有吃到食物尾巴这一帧肯定会pop掉。如果蛇头下一步刚好移动到尾巴原本的位置算不算撞到自己严格来说不算因为尾巴已经移走了那个位置是空的。但如果你在插入新头之前就用new_head in snake判断就会把“移动到即将消失的尾巴位置”误判成撞自己导致游戏提前结束。要解决这个问题有两种方案。第一种是先把尾巴pop掉再判断第二种是判断时排除最后一个元素if new_head in snake[:-1]。我习惯用第二种因为它不改变插入顺序逻辑上更容易保持前后一致。# 撞自己排除尾巴因为尾巴这一帧会移走 if new_head in snake[:-1]: game_over True还有一个非常经典的坑就是方向反转BUG。假如蛇正在向右走玩家快速按了“上”再按“左”而你的逻辑没有限制那么蛇头下一秒可能就朝左走了。可是蛇头往左走紧接着就会撞上自己的第二节身体当场暴毙。真实游戏里玩家会觉得很冤我只是连续按了两个键蛇怎么会死解决方法是判断新方向是否与当前移动方向相反如果相反就忽略掉这个按键。怎么判断相反当前方向是(1, 0)右你按左(-1, 0)两者相加等于(0, 0)这就是反向。代码def is_reverse(new_dir, current_dir): return (new_dir[0] current_dir[0] 0 and new_dir[1] current_dir[1] 0)注意这个检查只对长度大于1的蛇有意义。如果蛇刚出生只有1节它往左还是往右本质上是无所谓的因为后面没有身体可撞。所以在响应用户输入时如果当前蛇长为1可以放行反向输入否则拒绝。3.4 帧率控制与“按键抖动”的处理pygame的pygame.time.Clock()是控制游戏速度的关键。每个循环末尾调用clock.tick(FPS)程序就会尽量保持每秒不超过FPS帧。对蛇来说这个FPS越低蛇移动越慢越高蛇移动越快。新手容易在这里遇到“键抖”的问题我明明只按了一次方向键蛇怎么连续转了两个弯这往往是因为处理输入时直接用了pygame.key.get_pressed()这个函数返回所有按键的当前状态如果你同时按了上和左或者按键被系统连续触发就可能在一帧里判定多次方向变化。更稳妥的做法是使用事件循环里的pygame.KEYDOWN事件每当按键被按下只处理一次。而且最好只取最后一个有效的方向变更或者每帧只允许一次方向改变。我采取的方案是在事件循环里遇到KEYDOWN就更新方向但因为事件是顺序处理的理论上同一帧如果出现多个KEYDOWN方向会被连续更新。不过对于单个玩家手动按键来说两只手同时精准按到两个键的概率极低而且即便发生了最终的is_reverse检查也会拦住大多数危险情况。如果你想把严谨度拉满可以加一个direction_changed_this_frame的标志每帧只允许第一次按键生效。if event.type pygame.KEYDOWN: if event.key in DIRECTIONS: new_direction DIRECTIONS[event.key] if not is_reverse(new_direction, direction) or len(snake) 1: direction new_direction这样处理下玩家手感会明显变好不会出现“明明没想转向却死了”的体验。4. 从横到竖完整代码实现与逐段精讲4.1 初始化窗口与全局常量下面进入完整代码环节。我会分块展示代码并逐块解释。建议你打开编辑器跟着敲一遍而不是直接复制粘贴这样才能记住结构。import pygame import random import sys # 窗口与网格参数 WIDTH, HEIGHT 600, 600 GRID_SIZE 20 COLS WIDTH // GRID_SIZE ROWS HEIGHT // GRID_SIZE # 颜色定义RGB BACKGROUND (30, 30, 30) SNAKE_HEAD_COLOR (0, 255, 0) SNAKE_BODY_COLOR (0, 180, 0) FOOD_COLOR (255, 60, 60) TEXT_COLOR (255, 255, 255) # 方向映射 DIRECTIONS { pygame.K_UP: (0, -1), pygame.K_DOWN: (0, 1), pygame.K_LEFT: (-1, 0), pygame.K_RIGHT: (1, 0), } def main(): pygame.init() screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(贪吃蛇 - Python实战) clock pygame.time.Clock() ...这里有一个很实用的经验把颜色、尺寸、方向映射都定义为模块级常量而不是在代码里到处写魔法数字。这样以后想改窗口大小、配色只需要改常量区域不需要在逻辑代码里翻找。配色方面我建议别用纯黑用深灰或深蓝做背景蛇用绿色食物用红色这样对比度高长时间盯着也不刺眼。pygame.init()是必须的它会初始化所有pygame模块。set_mode创建窗口set_caption设置标题。clock是帧率控制器后面主循环末尾会用到。4.2 工具函数绘制方块、生成食物、显示分数接下来写三个工具函数。为什么要把绘制方块单独拆一个函数因为蛇头、蛇身、食物都要画方块但它们颜色不同、格子位置不同一个函数统一处理可以大大减少重复代码。def draw_block(screen, color, col, row): 在网格坐标(col, row)处绘制一个方块留1像素间距让蛇身纹理更清晰 rect pygame.Rect( col * GRID_SIZE 1, row * GRID_SIZE 1, GRID_SIZE - 2, GRID_SIZE - 2 ) pygame.draw.rect(screen, color, rect, border_radius4)这里我用了border_radius4让方块带一点圆角视觉上比尖角方块柔和很多。如果你用的pygame版本较老不支持border_radius参数可以把它删掉效果也不会差太多。食物生成函数前面已经写过这里直接复用。有点经验的朋友可能会注意到我把spawn_food里的随机数也限定在COLS和ROWS范围内因为食物不应该出现在窗口之外。显示分数需要一个能渲染文字的surface这块放到主循环里调用比较自然。但字体初始化我建议放在main()开头只做一次不然每帧都创建字体对象性能会有无谓的开销。def draw_score(screen, font, score): score_surface font.render(fScore: {score}, True, TEXT_COLOR) screen.blit(score_surface, (10, 10))4.3 游戏主循环输入、更新、碰撞、绘制一体流所谓主循环就是每帧都在重复执行的一套流程。一般来说顺序是先处理事件用户按了什么键然后更新游戏状态蛇移动、食物是否被吃、是否死亡最后绘制画面等一帧时长走完再进入下一帧。def main(): pygame.init() screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(贪吃蛇 - Python实战) clock pygame.time.Clock() font pygame.font.SysFont(simhei, 28) snake [(COLS // 2, ROWS // 2), (COLS // 2 - 1, ROWS // 2), (COLS // 2 - 2, ROWS // 2)] direction (1, 0) # 初始向右 food spawn_food(snake) score 0 fps 10 running True while running: # 1. 事件处理 for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN: if event.key pygame.K_ESCAPE: running False elif event.key in DIRECTIONS: new_dir DIRECTIONS[event.key] if not is_reverse(new_dir, direction) or len(snake) 1: direction new_dir # 2. 计算新蛇头 dx, dy direction new_head (snake[0][0] dx, snake[0][1] dy) # 3. 碰撞检测 if (new_head[0] 0 or new_head[0] COLS or new_head[1] 0 or new_head[1] ROWS or new_head in snake[:-1]): running False break # 4. 移动蛇身 snake.insert(0, new_head) # 5. 吃食物判断 if new_head food: score 1 food spawn_food(snake) if score % 5 0: fps 2 # 每吃5个食物加速一次 else: snake.pop() # 6. 绘制 screen.fill(BACKGROUND) for seg in snake[1:]: draw_block(screen, SNAKE_BODY_COLOR, seg[0], seg[1]) draw_block(screen, SNAKE_HEAD_COLOR, snake[0][0], snake[0][1]) draw_block(screen, FOOD_COLOR, food[0], food[1]) draw_score(screen, font, score) pygame.display.flip() # 7. 限制帧率 clock.tick(fps) pygame.quit() sys.exit()这段代码就是整个游戏的心脏。我们来逐个看关键点。第1步事件处理里我额外加入了ESC键退出这算是一个对玩家友好的小功能。is_reverse的判断在前面讲过这里不再重复。第2步把当前方向解包成dx, dy然后和蛇头坐标相加得到新蛇头坐标。这里用的是网格坐标不是像素坐标所以每次移动恰好是一格。第3步碰撞检测我把撞墙和撞自己合并到了同一个if里。注意new_head in snake[:-1]而不用snake就是为了解决前面说的“尾巴即将移开不算撞”的边界情况。第4步和第5步的顺序很重要先插入新头再判断是否吃到食物。如果你先判断食物再插入新头逻辑也能写但代码会多出临时变量没那么清爽。我的写法里new_head food比较的就是新蛇头坐标和食物坐标是否重合。吃到食物分数加1重新生成食物并且在一个“每5分加速”的规则下提高fps。这里fps 2是我实测比较舒适的一个增量不会突然快得失控。没吃到食物的情况就执行snake.pop()把尾巴删掉。注意追加了新的头、删掉了尾蛇长度不变吃到食物只追加不删除长度加1。第6步绘制时我先填背景色再画蛇身、蛇头、食物、分数。绘制顺序决定了层叠关系背景在最下面食物和蛇在上面。蛇头我单独用更亮的颜色绘制玩家能一眼看出当前的控制目标在哪儿。pygame.display.flip()是双缓冲机制里的翻页操作把后台画好的画面一次性推到屏幕上能有效减少闪烁。关于双缓冲和flip、update的区别你暂时只需要记住完整窗口刷新就用flip()。第7步的clock.tick(fps)把帧率限制在fps。每调用一次tick程序都会等到上一帧过去足够时间所以这里的fps直接决定了蛇的速度。4.4 把零散函数拼装成可运行的单文件为了让代码可以单文件运行我们需要把is_reverse、spawn_food、draw_block这些函数补全并确保所有函数定义在main()调用之前。完整的文件结构大致长这样import pygame import random import sys # 常量定义... def is_reverse(new_dir, current_dir): return (new_dir[0] current_dir[0] 0 and new_dir[1] current_dir[1] 0) def spawn_food(snake): while True: x random.randrange(COLS) y random.randrange(ROWS) if (x, y) not in snake: return (x, y) def draw_block(screen, color, col, row): ... def draw_score(screen, font, score): ... def main(): ... if __name__ __main__: main()if __name__ __main__:这个写法值得单独提一下它保证了只有直接运行这个文件时才会启动游戏。如果别人在别的代码里import这个文件不会马上弹窗这是Python社区里一个优雅的惯例。运行后你应该能看到一条3节的绿色小蛇在30x30的网格上游走用方向键控制它去吃红色食物每吃一个长一节分数加1每吃5个加速一次撞墙或撞到自己游戏结束。5. 把游戏做顺手方向BUG、加速与边界这些坑怎么填5.1 方向反转BUG最容易被新手忽略的“瞬间死亡”我在调试自己第一版贪吃蛇的时候遇到过一种很诡异的现象蛇明明在向右走我手速快一点连按“上”和“左”蛇立刻死亡而我根本来不及反应。排查了很久才发现问题不在碰撞检测而在方向控制缺少“反向拦截”。这个坑的原理其实很简单向右走的时候蛇的身体在蛇头左侧排列。如果下一秒强行改为向左蛇头会直接扎进自己的身体第二节里于是new_head in snake[:-1]立即成立游戏结束。玩家会觉得“我还没转向呢怎么就死了”实际上是两帧之内完成了两次转向先向上再向左而向左与初始向右是反向。解决办法就是前面写过is_reverse检查。但有几个细节务必注意检查应放在事件处理里而不是移动逻辑里。因为事件处理阶段你还知道“当前方向”是多少可以通过比较判断等真正移动时再检查就晚了。蛇长为1时应当允许任意方向包括反向。否则游戏刚开局蛇头只有一节时如果你在极短时间里按了“上”和“下”第二次按键被忽略玩家会觉得指令失灵。而在长度1的情况下反向移动没有风险。不要只拦和当前方向完全相反的按键也要注意“对角转向”的组合。比如当前向右玩家先按上再按左第二次的“左”需要被拦截。这个例子里的第二次按键其实和当前方向已变为上不构成反向但它会导致蛇在下一帧向左若此时蛇头下方就是自己身体仍然会撞。严格来说这不算“方向反转BUG”而属于操作衔接问题通常玩家自己手速过快导致。要不要处理我的建议是可以处理得更严格一些记录本帧是否已经发生过方向变化如果同一帧里有第二次方向键直接忽略。这样能最大限度避免“瞬间死亡”的负面体验。5.2 速度曲线设计怎么让游戏难度平滑上升如果不做任何加速贪吃蛇到了后期就变成了“长蛇慢爬”玩家只要不犯低级错误就能无限玩下去。但加速太猛也会让游戏变得不可玩。怎么设计一个平滑的速度曲线我用的方案是基础fps10每吃5个食物fps2。这个增长速度在中前期比较温和到了第30个食物时fps大约22操作难度已经明显提升但还不至于让人崩溃。如果你想让后期更刺激可以改成每吃3个食物就加速或者加速幅度递增比如第一次1第二次2第三次3。加速逻辑写在吃食物分支里if new_head food: score 1 food spawn_food(snake) if score % 5 0: fps 2这里要注意fps是局部变量但因为它在main()函数内部且整个游戏循环都在同一作用域里修改它是没有问题的。如果你把fps定义成了模块级常量反而不能直接在函数里改得用global那就绕了。所以我在设计时把fps放在主函数内部作为可变变量而不是常量区这一点很重要。5.3 窗口关闭、游戏结束提示与资源释放很多新手跑pygame程序点窗口右上角的X发现程序卡死或者报错就是因为没有处理pygame.QUIT事件。我在主循环里已经写了if event.type pygame.QUIT: running False这样窗口关闭按钮就能正常退出循环。退出循环后主函数末尾执行pygame.quit()和sys.exit()关闭所有pygame模块并结束进程。这两个调用不能省否则在某些操作系统上可能残留窗口句柄。如果你希望游戏结束后不立即退出而是显示“Game Over按任意键退出”的提示可以在while循环结束后加一段等待代码。不过那样会增加代码行数我这里为了专注讲解核心逻辑没有做游戏结束画面而是直接退出并让终端打印结算信息# 退出前输出成绩 print(f游戏结束最终得分{score})对小项目来说这种处理够用了。等你以后再做更完整的游戏可以专门抽离出“游戏状态机”运行中、暂停、结束。那又是另一个进阶话题了。5.4 常见运行报错与排查思路我在带朋友写贪吃蛇时见过各种报错有几类出现频率特别高。第一类是ModuleNotFoundError: No module named pygame。这个基本就是没装pygame或者装了但当前Python解释器不是同一个环境。如果你用了虚拟环境记得先激活虚拟环境再pip install pygame。如果是Windows User安装的多个Python版本在命令行先python --version确认一下再用python -m pip install pygame而不是直接pip install pygame。第二类是ValueError: invalid rectstyle argument或者TypeError出现在pygame.draw.rect多半是颜色或者Rect参数传错了。比如颜色值超出0-255、Rect用了浮点数。我建议检查一下RGB值以及draw_block的col * GRID_SIZE 1这些计算是否都返回了整数。Python除法会产生浮点数如果用WIDTH // GRID_SIZE已经保证了整数但如果手滑从WIDTH / GRID_SIZE复制过来的代码就可能出现浮点Rect导致pygame报错。第三类是游戏窗口一闪而过。这种情况通常是程序主循环没有被正确执行fps过快或者循环条件一开始就不满足。排查建议是先注释掉clock.tick(fps)改成clock.tick(60)如果还是闪退就在main()里加一些print调试输出看看是到了哪一步退出的。还有一个很隐蔽的问题如果pygame.display.set_mode之后没有调用pygame.display.flip()或者pygame.display.update()画面可能一直显示黑色或无响应。很多新手抄代码时漏掉flip游戏逻辑明明在跑屏幕上却什么都没有。所以绘制步骤里flip和fill一样重要不可省略。6. 从能玩到好玩记分徽标、进阶玩法与后续扩展思路6.1 让界面更好看画格子线和圆角蛇身写完一个“能玩”的贪吃蛇之后我建议你在界面美观度上花一点时间。不要小看这一步它能帮你练习pygame的绘制API也能让游戏从“练习作业”变成“拿得出手的作品”。画背景网格线是很简单的加分项def draw_grid(screen): for x in range(0, WIDTH, GRID_SIZE): pygame.draw.line(screen, (50, 50, 50), (x, 0), (x, HEIGHT)) for y in range(0, HEIGHT, GRID_SIZE): pygame.draw.line(screen, (50, 50, 50), (0, y), (WIDTH, y))线宽1像素颜色比背景稍微亮一点就能让整个网格隐约可见玩家对蛇的每一步位移有更清晰的感知。不过如果你觉得网格线太干扰视线也可以不画。这纯粹是审美选择。蛇身圆角、蛇头画成眼睛、食物画成圆点这些都是很经典的视觉升级。圆点可以用pygame.draw.circlepygame.draw.circle(screen, FOOD_COLOR, (food[0] * GRID_SIZE GRID_SIZE // 2, food[1] * GRID_SIZE GRID_SIZE // 2), GRID_SIZE // 2 - 2)圆心位置是格子的中心点半径略小于半格看起来就像一颗红色的果实。相比方块食物圆形更耐看。6.2 加入穿墙模式、障碍物和双人对战如果你已经熟练掌握单机版想挑战更多玩法我给三个最推荐的扩展方向按难度排序穿墙模式是最简单的一种。撞墙不再死亡而是从对面穿过来。实现方式就是判断新蛇头坐标如果小于0就加上COLS如果大于等于COLS就减去COLSy方向同理x, y new_head if x 0: x COLS - 1 elif x COLS: x 0 if y 0: y ROWS - 1 elif y ROWS: y 0 new_head (x, y)这个玩法能让蛇在后期变得非常灵活但也因为可以无限穿墙撞自己的风险更大操作难度实际上提升了。适合想练微操的玩家。障碍物模式需要在开局时随机生成一些固定障碍用另一个列表存储食物生成时避开障碍物移动时碰到障碍物也判定死亡。这个扩展会让你更深入地理解“多对象碰撞检测”——你不再只判断蛇和食物的关系还要判断蛇和障碍、食物和障碍的关系。双人对战是最有挑战性的扩展。你需要维护两条蛇两个方向两个分数用一个键盘的不同按键来控制比如WSAD控制玩家1方向键控制玩家2或者支持两个手柄/两个键盘。难度在于两条蛇互相碰撞的逻辑玩家1的蛇头碰到玩家2的身体也算死亡。这会逼着你把数据结构设计得更抽象比如把“蛇”抽象成一个类或者字典对象。我见过有人用这个双人对战版在公司年会当暖场小游戏效果非常欢乐。6.3 数据持久化把最高分存到本地文件让游戏“留得住玩家”的另一个小技巧是记录历史最高分。Python里最简单的持久化方式是用文件读写配合json模块或者直接文本保存。import os HIGH_SCORE_FILE high_score.txt def load_high_score(): if os.path.exists(HIGH_SCORE_FILE): with open(HIGH_SCORE_FILE, r, encodingutf-8) as f: return int(f.read().strip()) return 0 def save_high_score(score): with open(HIGH_SCORE_FILE, w, encodingutf-8) as f: f.write(str(score))在游戏结束的时候比较当前分数和历史最高分如果打破了就保存。主界面可以把“最高分”也渲染出来。这个功能虽然只有十几行代码但能让你的游戏从“一次性玩具”变得更完整同时也是练习文件IO的好机会。6.4 当我用AI辅助写贪吃蛇之后我发现了什么近几年AI编程工具越来越强大我也试过让AI直接生成一个贪吃蛇游戏。结果很有意思它确实能在几秒钟内给我一个能运行的代码但只要我提出具体需求比如“增加穿墙模式”“蛇身加圆角”“食物避开障碍物”AI生成的代码经常出现逻辑漏洞比如穿墙模式里坐标变成负数导致绘制错位、食物生成卡进蛇身里或者两段代码之间变量名不一致。原因很简单AI擅长“见过的常见模式”的拼接但它并不理解你的游戏状态在每个时刻是怎么流转的。所以我给新手的建议是先自己把贪吃蛇的每一行代码都搞懂再用AI做辅助扩展。你可以让AI帮你写某个独立的小函数比如draw_grid但核心的游戏循环、碰撞逻辑和方向控制一定要自己亲手写过一遍走过一遍错误排查的过程。这个过程带来的理解深度远非复制粘贴能比。打开编辑器把文章中这份代码敲一遍运行起来然后开始改换个颜色、改个速度、加个穿墙、加个最高分。每个改动都会让你对这个十几行核心循环的理解更深一层。等你把游戏改成自己满意的样子再回头看最初的代码你会明显感觉到自己在逻辑建模、代码组织这些事上迈出了一大步。