
聊到用Python写小游戏我通常会建议把贪吃蛇放在第一个项目。原因很简单它恰好能把Python里最常用的列表操作、循环、条件判断、函数拆分全部串起来而且一旦跑起来成就感是即时的——看着一条小蛇在屏幕上灵活转向、吃到食物、慢慢变长那种正反馈比做一百道语法练习题都来得直接。今天要分享的是一个可以直接拿来用的实战版本200行左右的Python代码基于Pygame实现画面完全靠代码绘制不依赖任何图片、音频素材复制到本地就能跑。这篇文章会把这200行里该有的知识点全部拆开讲透包括为什么这样设计数据结构、碰撞检测为什么必须留一个尾巴位置、怎样让游戏手感丝滑不卡顿。无论你是刚学完Python基础语法想找项目练手的新人还是想快速给朋友写个小游戏解闷的开发者这篇内容都能直接带你走完从0到1的整个过程。1. 项目定位为什么值得写一个贪吃蛇1.1 贪吃蛇是Python列表操作的绝佳演练场很多教程让你写“学生管理系统”或者“通讯录”虽然也练列表但缺少视觉反馈写着写着就容易犯困。贪吃蛇不一样它的整个核心逻辑都建立在一个动态变化的列表上。蛇身就是一个列表列表里每个元素是一个(x, y)坐标元组。蛇头永远在snake[0]蛇尾在snake[-1]。每一次移动本质上是“在列表头部插入一个新坐标如果没吃到食物就在尾部弹出一个坐标”。一个insert(0, ...)加一个pop()就把整条蛇的移动表现出来了。这个过程直接覆盖了列表最核心的几种操作头部插入、尾部删除、用in判断元素是否存在、用切片snake[:-1]排除最后一个元素。更妙的是列表长度会随着吃食物动态增加这种“长度可变”的需求如果放到C语言里要么用链表、要么用动态数组还得自己管理内存但在Python里list天生就支持写起来非常舒服。所以这个项目对新手最大的价值不是“做出了一个游戏”而是把之前零散学到的列表知识全部串了起来。等你写完这200行再看那些“列表增删改查”的练习题会觉得明显轻松不少。1.2 无素材依赖的工程思路用代码当美术很多人在做小游戏的时候卡住自己的不是逻辑而是“素材从哪里找”。网上找图片要抠背景、要调尺寸还要考虑版权问题音频素材更是麻烦一张合适的音效可能搜半天。最后项目还没开始写人先被素材折腾累了。这个实战项目直接绕开了所有素材问题。蛇身用pygame.draw.rect画圆角矩形食物用pygame.draw.circle画圆形背景网格用循环画直线。整份代码只有一个.py文件没有assets目录没有外部依赖拷给别人就一个文件双击就能跑。这种“用代码生成画面”的思路放到真实的工具类小项目里也很值得借鉴。只要绘制逻辑不复杂能用代码生成的视觉元素就尽量不引入外部资源一来项目更整洁二来后续维护成本低。你不需要会做美术设计只需要会调几个RGB颜色值。1.3 五分钟跑起来环境准备与最小验证写代码之前先把运行环境准备好。我用的是Python 3.8以上版本实测3.8、3.9、3.10、3.11都能直接跑。如果你用的是更老的版本建议先升级到3.8以上。第一步安装Pygame。打开终端或命令行执行pip install pygame如果想确认是否装好可以执行python -c import pygame; print(pygame.version.ver)能打印出版本号就说明安装成功。第二步新建一个snake.py文件把后面第三部分给出的完整源码复制进去然后在终端执行python snake.py窗口弹出来游戏就能跑了。如果安装过程中遇到pip版本过旧、Python版本兼容性等问题不要急我在第五部分专门整理了排查方法那部分是多次踩坑之后总结出来的。2. 核心算法拆解让蛇“动”起来的关键2.1 蛇身的数据结构为什么是列表而不是链表贪吃蛇的移动逻辑本质上可以抽象成一句话每一帧蛇头向当前方向前进一格如果没吃到食物蛇尾就缩回一格。这个逻辑用什么数据结构表达最合适很多有C语言基础的人第一反应是“链表”因为蛇身会长长链表天然支持动态长度。但在Python里list本身就是一个动态数组底层的扩容机制已经帮你处理好了“变长”的问题你不需要自己维护节点和指针。还有人会想用collections.deque因为它在头部插入和尾部删除的效率更高。但问题也很明显deque不能直接用切片判断某个坐标是否在蛇身里的in操作在deque上会比list慢不少。而贪吃蛇的网格总共才几十乘几十个格子蛇身长度很少超过几百list的头部插入复杂度虽然是O(n)但n在几百以内完全没压力。为了延时节省这几微秒去牺牲代码可读性不值当。真正让我坚持用list的原因还有一个它支持切片。后面讲碰撞检测时你会看到判断蛇头是否撞到身体需要判断新蛇头是否落在snake[:-1]里这个写法在deque里做不到只能手动转成list或者用循环遍历。用list一句话就写完了。2.2 方向控制转向队列与防掉头逻辑方向怎么表示最简单的方式是用方向向量。比如定义四个常量UP (0, -1) DOWN (0, 1) LEFT (-1, 0) RIGHT (1, 0)向右移动时蛇头坐标加上(1, 0)向上移动时加上(0, -1)。这种表示方式让“移动”变成了纯坐标加法运算逻辑非常干净。但方向控制有一个经典问题如果蛇当前向右移动玩家按了左键蛇会直接180度掉头撞上自己身体。所以每次按键时必须检查新方向是否与当前方向完全相反。这里需要特别强调一个细节你没有必要按下按键的瞬间就修改当前方向而是可以先把方向指令存到一个队列里等到真正移动的那一帧再取出来用。为什么要这么做因为游戏帧率通常在12帧左右一帧持续约83毫秒玩家在这短短一帧内可能连续按了两次方向键。如果你每按一次键就立即修改direction第二次按键会覆盖第一次玩家会觉得“我明明按了两个方向蛇怎么只响应了最后一个”。更糟糕的是如果两次按键方向相反蛇可能会瞬间反向然后一头撞死。使用转向队列后每次移动只消费一个方向指令剩下的排队等待操作既跟手又不会出现反向抽风。2.3 碰撞检测的三层设计贪吃蛇的碰撞检测看起来简单实际写起来有三个地方容易出问题。第一层是边界碰撞。窗口大小是SCREEN_WIDTH x SCREEN_HEIGHT每格大小为CELL所以横向格子数是SCREEN_WIDTH // CELL纵向格子数是SCREEN_HEIGHT // CELL。当蛇头坐标new_head的横坐标小于0或者大于等于格子数都是撞墙。这里注意是用而不是因为坐标从0开始计数。第二层是自身碰撞。判断新蛇头是否碰到了身体但不能直接用new_head in snake因为蛇尾这一帧是要移动的。假设蛇身有三节蛇头向前走一步如果这一步恰好走到的位置是原来蛇尾所在的位置那本来就不算碰撞——因为蛇尾会同时前移把那个格子让出来。所以要用new_head in snake[:-1]把蛇尾排除在外。第三层是食物碰撞。判断比较简单直接比较蛇头坐标和食物坐标是否相等。吃到食物后蛇头照常前进但尾巴不缩回长度就增加了1。这三层检测的写法和顺序都很重要。先判断边界再判断自身碰撞最后处理食物顺序反了容易出现“蛇撞墙之后还能吃到食物”这种逻辑循环问题。2.4 食物生成如何保证不刷在蛇身上食物生成也是一个有讲究的地方。最简单的方案是在网格范围内随机取一个坐标判断它是否在蛇身上如果在就重新随机。当蛇比较短时这个方案效率很高通常一两次就能找到空位。但随着蛇越长问题开始出现。网格总共600格蛇长500时随机到一个空格的概率只有六分之一说不定要循环十几次才能找到。极端情况下蛇占满整个网格随机算法会陷入死循环。所以在写create_food函数时我会加一个尝试次数的上限比如最多尝试10000次。如果这么久还没找到空位说明网格基本满了此时直接返回None在主循环里判定为通关胜利。这个保护性设计正常情况下永远不会触发但一旦触发就能避免程序卡死。写任何代码都应该在“边界情况”上多想一想。3. 完整源码与逐段拆解3.1 代码组织与运行流程整个项目文件结构非常简单只有一个snake.py文件按顺序分为几个区块常量区定义窗口尺寸、格子大小、帧率、颜色、方向向量。工具函数区init_game负责初始化游戏窗口create_food负责生成食物draw_snake负责画蛇draw_food负责画食物load_best_score和save_best_score负责读写最高分。main函数承载游戏主循环包括事件处理、逻辑更新、画面渲染三个步骤。入口if __name__ __main__调用main()。我没有刻意把代码封装成类。200行以内的项目用函数加全局常量的组织方式读起来反而更直观。等你掌握了这版代码想再往工程化方向改可以自己用类再封装一遍那是后话。3.2 完整源码可直接运行下面是全部源码我已用“200行内完成”作为目标来组织代码并加入了暂停、最高分记录、重新开始等实用功能import pygame import random import sys # ---------- 窗口与网格 ---------- SCREEN_WIDTH, SCREEN_HEIGHT 800, 600 CELL 20 GRID_WIDTH SCREEN_WIDTH // CELL GRID_HEIGHT SCREEN_HEIGHT // CELL FPS 12 HIGH_SCORE_FILE snake_highscore.txt # ---------- 方向向量 ---------- UP (0, -1) DOWN (0, 1) LEFT (-1, 0) RIGHT (1, 0) DIRECTION_KEYS { pygame.K_UP: UP, pygame.K_DOWN: DOWN, pygame.K_LEFT: LEFT, pygame.K_RIGHT: RIGHT, } # ---------- 颜色 ---------- BG_COLOR (20, 20, 30) GRID_COLOR (35, 35, 45) SNAKE_COLOR (80, 220, 120) HEAD_COLOR (200, 255, 200) FOOD_COLOR (230, 80, 80) TEXT_COLOR (240, 240, 240) def init_game(): pygame.init() screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(Snake) clock pygame.time.Clock() return screen, clock def create_food(snake): limit 10000 while limit 0: pos (random.randint(0, GRID_WIDTH - 1), random.randint(0, GRID_HEIGHT - 1)) if pos not in snake: return pos limit - 1 return None def draw_snake(screen, snake): for i, (x, y) in enumerate(snake): color HEAD_COLOR if i 0 else SNAKE_COLOR pygame.draw.rect( screen, color, (x * CELL 1, y * CELL 1, CELL - 2, CELL - 2), border_radius4 ) def draw_food(screen, food): if food is None: return fx, fy food pygame.draw.circle( screen, FOOD_COLOR, (fx * CELL CELL // 2, fy * CELL CELL // 2), CELL // 2 - 3 ) def load_best_score(): try: with open(HIGH_SCORE_FILE, r, encodingutf-8) as f: return int(f.read().strip()) except Exception: return 0 def save_best_score(score): try: with open(HIGH_SCORE_FILE, w, encodingutf-8) as f: f.write(str(score)) except Exception: pass def draw_game_over(screen, score, best, winFalse): font pygame.font.SysFont(None, 36) title YOU WIN! if win else GAME OVER lines [ title, SCORE: {} BEST: {}.format(score, best), PRESS R TO RESTART, Q TO QUIT ] screen.fill(BG_COLOR) y SCREEN_HEIGHT // 2 - 50 for line in lines: surf font.render(line, True, TEXT_COLOR) screen.blit(surf, (SCREEN_WIDTH // 2 - surf.get_width() // 2, y)) y 45 pygame.display.flip() while True: for event in pygame.event.get(): if event.type pygame.QUIT: return False if event.type pygame.KEYDOWN: if event.key pygame.K_r: return True if event.key pygame.K_q: return False def main(): screen, clock init_game() # 预渲染背景主循环里只需要 blit 一次 bg pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) bg.fill(BG_COLOR) for x in range(0, SCREEN_WIDTH, CELL): pygame.draw.line(bg, GRID_COLOR, (x, 0), (x, SCREEN_HEIGHT)) for y in range(0, SCREEN_HEIGHT, CELL): pygame.draw.line(bg, GRID_COLOR, (0, y), (SCREEN_WIDTH, y)) font pygame.font.SysFont(None, 28) best load_best_score() # 外层循环支持游戏结束后按 R 重新开始 while True: snake [ (GRID_WIDTH // 2, GRID_HEIGHT // 2), (GRID_WIDTH // 2 - 1, GRID_HEIGHT // 2), (GRID_WIDTH // 2 - 2, GRID_HEIGHT // 2) ] direction RIGHT turn_queue [] food create_food(snake) score 0 running True win False while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN: if event.key pygame.K_SPACE: pause True while pause: for e in pygame.event.get(): if e.type pygame.QUIT: running False pause False elif e.type pygame.KEYDOWN and e.key pygame.K_SPACE: pause False continue if event.key in DIRECTION_KEYS: new_dir DIRECTION_KEYS[event.key] # 禁止直接掉头 if (new_dir[0], new_dir[1]) ! (-direction[0], -direction[1]): if not turn_queue or turn_queue[-1] ! new_dir: turn_queue.append(new_dir) if not running: break # 每次移动最多消费一个方向指令 while turn_queue: new_dir turn_queue.pop(0) if (new_dir[0], new_dir[1]) ! (-direction[0], -direction[1]): direction new_dir break head_x, head_y snake[0] dx, dy direction new_head (head_x dx, head_y dy) # 边界碰撞检测 if (new_head[0] 0 or new_head[0] GRID_WIDTH or new_head[1] 0 or new_head[1] GRID_HEIGHT): running False break # 自身碰撞检测蛇尾会移动所以排除最后一个 if new_head in snake[:-1]: running False break # 蛇头前移 snake.insert(0, new_head) # 判断是否吃到食物 if new_head food: score 1 food create_food(snake) if food is None: running False win True break else: snake.pop() # ---------- 渲染 ---------- screen.blit(bg, (0, 0)) draw_food(screen, food) draw_snake(screen, snake) score_surf font.render(SCORE: {}.format(score), True, TEXT_COLOR) screen.blit(score_surf, (10, 10)) pygame.display.flip() clock.tick(FPS) if score best: best score save_best_score(best) restart draw_game_over(screen, score, best, win) if not restart: break pygame.quit() sys.exit() if __name__ __main__: main()3.3 关键代码逐段拆解先看初始化部分。init_game里执行pygame.init()创建窗口和时钟。这里的clock对象是后面控制帧率的核心调用clock.tick(FPS)可以保证循环每秒最多运行FPS次。然后是预渲染背景的部分。我在进入主循环前先把背景色和网格线全部画到一个Surface对象bg上。主循环里每次渲染只要screen.blit(bg, (0, 0))就能把整个背景贴上去。如果没有这一步每次循环都要重新画几十条网格线浪费CPU。这个优化在12帧率下差别不大但它体现了一种思维把不变的画面内容提前“烘焙”好运行时只做最少的绘制工作。再看事件处理。for event in pygame.event.get()遍历一帧内所有事件QUIT事件对应窗口关闭按钮KEYDOWN处理按键。空格键的暂停逻辑是一个内层小循环在这个循环里不断监听事件只有再次按下空格或关闭窗口才会退出暂停这样不会在暂停期间继续接受方向指令。方向键处理后存入turn_queue。存入之前要做一次防掉头检查如果新方向和当前方向的负方向相同就忽略。这里的检查有一个细节判断时应该基于当前direction而不是队列尾部这样即使排队中已经有一个方向也不会因为中间方向导致掉头。主循环里的移动逻辑非常直观。取蛇头坐标加上方向向量得到new_head然后依次做边界碰撞检测和自身碰撞检测。这里的snake[:-1]是整段代码最容易被忽略的点。如果忘了排除蛇尾会出现“蛇头走到尾巴即将离开的格子时被判为撞到自己”的误判明明可以安全通过的移动反而导致游戏结束。吃到食物后直接snake.insert(0, new_head)而不执行pop()蛇身长度自然增加1。没吃到食物时插入新头后还要把蛇尾弹出去。这个“先插头再决定删不删尾”的顺序非常重要逻辑上恰好模拟了真实的移动过程新头一定先出现尾巴是否保留取决于有没有吃到食物。最后一行的clock.tick(FPS)是整个游戏“丝滑不卡顿”的关键。它做的事情是让循环每秒最多执行12次。如果没有这一行游戏循环会以每秒几千次的频率疯狂运行蛇会瞬间冲到屏幕外面你根本反应不过来。tick就像给游戏循环装上了节拍器节奏稳定不随电脑性能波动。4. 丝滑不卡顿的性能细节4.1 帧率控制为什么12FPS最合适如果之前没接触过游戏开发你可能会觉得“越高的帧率才越丝滑”。这个直觉在3D射击游戏里是对的但在贪吃蛇里恰恰不适用。贪吃蛇的核心节奏是“每帧走一格”帧率的高低直接决定蛇的移动速度。12FPS意味着平均每83毫秒蛇头前进一格这个速度比较接近经典掌机的手感操作有足够的反应时间视觉上也足够连贯。如果调成8FPS蛇会明显变得迟缓玩起来有“一格一卡”的拖沓感调到20FPS以上蛇又变得太快眼睛根本追不上还没反应过来就撞墙了。我在实际体验中还会做一个动态调速度的设计每吃5个食物就让FPS加1最高跑到30。这样游戏越到后面节奏越快难度自然递增玩家的心跳也会跟着提起来。4.2 渲染层优化减少每帧重复工作量Pygame 的画面渲染本质上就是把Surface上的一块像素区域“贴”到窗口上。一次blit操作的开销取决于像素面积和绘制指令的数量。为了让这个开销降到最低我做了三个优化。第一个优化是预渲染背景。网格线和背景色在游戏过程中完全不变所以提前渲染到bg上。主循环里一次blit就完成不再需要画几十条线。第二个优化是避免在循环内反复创建Font对象。font pygame.font.SysFont(None, 28)放在主循环外循环里只调用font.render生成文字图像。render本身也有开销但分数变化时才会真正重新生成频率不高。第三个优化是减少无效绘制区域。整个游戏里画面元素就三个蛇、食物、分数文字。每一帧都按照bg - food - snake - text的顺序blit最后pygame.display.flip()一次性把内容提交到窗口。没有额外特效和粒子也没有复杂的图像处理所有元素都是最简单的矩形和圆形渲染开销已经压到了最低。有个小知识可以补充一下pygame.display.update()可以传入矩形区域来只更新局部但前提是你知道哪些区域发生了变化。贪吃蛇的每一帧变化范围很小理论上可以做局部更新优化。不过代码复杂度会明显上升对200行目标来说得不偿失。在这个规模下flip()整屏提交的开销已经足够低完全没必要做局部更新。4.3 输入响应事件处理与游戏循环的解耦“丝滑”不仅体现在画面流畅度上更体现在操作跟手上。我见过太多贪吃蛇代码方向键按下去要么没反应要么延迟一拍玩起来特别难受。这里的关键在于事件处理的位置。pygame.event.get()返回的是从上次调用到这次调用之间累积的所有事件。如果事件处理放在移动逻辑之后那么玩家按下方向键后要等到下一帧循环才会被读取到响应就慢了半拍。正确做法是每一帧循环最开始就处理事件让输入尽早进入游戏状态。另外转向队列也是“跟手”的重要功臣。有了队列玩家快速连按时每个按键都会依次被消费不会丢失输入。想象一下极限操作蛇向右移动时你在一帧内快速按了“上”和“左”如果没有队列只有最后一个“左”生效蛇会直接朝左撞上身体用了队列先“上”再“左”蛇会安全地拐个直角弯出来。这种手感差异只有玩过对比版本才会明显感觉到。4.4 调优参数速查表参数推荐值影响CELL20格子越大画面越粗犷蛇身越粗格子数少则碰撞判定更宽松FPS12越低蛇走得越慢越高反应时间越短SCREEN_WIDTH/HEIGHT800x600窗口越大可视范围越广但移动耗时相应增加SNACK_COLOR(80, 220, 120)蛇身颜色改成自己舒服的配色即可FOOD_COLOR(230, 80, 80)食物颜色注意与背景和蛇身区分开如果你想给小孩玩可以降低FPS到8同时调大CELL让画面更慢更柔和。如果你想让自己玩出挑战感把FPS调到15以上一秒内决策时间大幅缩短爽快感会上一个台阶。5. 常见问题与排查技巧实录5.1 蛇不听话方向键失灵与反向抽风我调试这个项目时遇到最多的问题就是转向异常。“我朝右走按下‘上’再按‘左’蛇为什么要往下冲”这个现象背后是经典的“同帧双按键覆盖”。如果没有方向队列两次按键发生在同一帧内第二次按键直接覆盖了第一次蛇的最终方向是“左”而玩家预期的是“先上后左”。更严重的版本是第二次按键的方向与当前方向相反蛇直接掉头撞死。排查方法很简单在按方向键的地方打一行print(direction, turn_queue)然后慢慢按观察输出顺序很快就能定位问题。我给出的解决方案是转向队列加双重防掉头校验。这样既保证输入不丢失又保证蛇永远不可能直接掉头。5.2 运行卡顿一顿一顿到底为什么如果你运行代码时感觉画面一顿一顿的首先检查是不是真的用了clock.tick(FPS)。有些教程会忽略这一行结果循环疯狂空转蛇跑得飞快还伴随CPU风扇狂转。另一个卡顿原因藏在背景绘制里。如果每次循环都用pygame.draw.line重新画几十条网格线虽然pygame.draw本身很快但叠加起来也会有可感知的开销。我的解决方案是预渲染背景你也可以顺手把分数文字也预渲染好只在上分时更新一次。再一个容易忽略的点是不要在循环里做文件读写。我见过有人把最高分写在pygame.display.flip()之前每帧都写一次文件那磁盘IO直接拖垮帧率。最高分只需要在游戏结束时写一次。还有一个来源是事件积压。如果游戏逻辑复杂导致一帧耗时超过tick的间隔事件会堆积玩家按键后要过好几帧才被处理就会感觉“延迟”。这个项目的逻辑量非常小基本不会出现这种情况但如果你以后开发更复杂的游戏可以考虑把逻辑帧和渲染帧分开处理。5.3 pygame安装失败整理的完整解决方案安装Pygame时最常见的报错是ModuleNotFoundError: No module named pygame。这种问题通常有两种原因第一种是没安装解决办法直接pip install pygame第二种是安装了但装到了另一个Python环境。很多人电脑里有系统自带的Python又有Anaconda还有VS Code配的虚拟环境命令行里的pip和python不一定指向同一个环境。最稳妥的安装命令是python -m pip install pygame用python -m pip可以保证pip和python属于同一个环境避免“明明装好了却导入失败”的问题。如果安装过程中出现编译错误或者找不到匹配版本优先升级一下pip本身python -m pip install --upgrade pip如果用的是Python 3.12或更新的版本某些老版本Pygame可能存在兼容性问题可以尝试安装预览版python -m pip install pygame --pre如果从默认源下载速度很慢可以配置一个可信的pip镜像源来加速安装这在公司网络或某些网络环境下尤其常见。5.4 其他容易被忽视的坑第一个坑是窗口标题显示中文乱码。pygame.display.set_caption(贪吃蛇)在部分系统上可能显示成方块或问号原因是Pygame默认字体不支持中文字符。最简单的方案是用英文标题比如Snake等以后想深入学字体渲染再处理中文显示。第二个坑是窗口点关闭后进程不退出。如果只写了running False主循环会退出但程序不会结束因为Pygame的底层资源没有被释放。正确做法是在程序退出前调用pygame.quit()最后再用sys.exit()退出Python进程。第三个坑是食物生成后立即被蛇头撞上。这种情况并不算bug只是随机因素导致的“运气差”玩几分钟就会遇到一两次。如果觉得影响体验可以在生成食物时额外判断一下距离蛇头至少两格算是游戏难度调节的小技巧。第四个坑是修改窗口尺寸后忘了同步格子数。比如把SCREEN_WIDTH改成1000但CELL还是20那么GRID_WIDTH会自动算成50如果代码里写着基于GRID_WIDTH的逻辑反而没有问题。真正容易错的是手动把GRID_WIDTH写死成40改窗口尺寸后网格和边界就对不上了。我建议始终用SCREEN_WIDTH // CELL来计算格子数不要写死。最后再分享一个调试技巧把create_food里的随机函数临时改成固定返回值比如(5, 5)可以让你稳定复现“食物在蛇头路径上”的各种情况方便测试。等调试结束再改回来就行。这种“把随机性暂时拿掉”的做法在排查任何游戏逻辑问题时都特别高效。我个人实际写完这个项目最大的体会是代码越短越考验你对每个细节的把控。碰撞检测里的snake[:-1]、转向队列的双重校验、背景预渲染这些看起来不起眼的细节加在一起就是“丝滑”和“卡顿”的区别。如果你之前只是看过贪吃蛇的教程但还没动手写过建议现在就把代码敲一遍然后试着改改颜色、调调速度、加上穿墙模式或障碍物。等你把这些扩展都试过一遍Python的基础功底会比刷十遍语法题都扎实。