尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
纯Python标准库实现终端贪吃蛇:零基础实战全记录
这篇是“初学总结”系列的第五篇主题跟前面四次不太一样。前四篇基本都在复习语法、翻标准库文档、照抄示例代码这次我给自己定了一个有点硬核的目标用纯 Python 标准库写一个能在终端里玩的贪吃蛇。项目不大但写完之后我把输入处理、面向对象、事件循环、碰撞检测、终端渲染这些零散知识点完整串了一遍。如果你也是学了几个月编程、写过一堆 demo 却总觉得缺一次完整实战的人这篇记录里“为什么这么设计”和“实际踩了哪些坑”的部分应该比单纯贴代码更有价值。1. 为什么选终端贪吃蛇做第五次总结1.1 麻雀虽小五脏俱全的练手项目贪吃蛇看起来简单实际拆解之后需要处理的东西一点都不少。玩家输入要实时响应蛇的移动要按固定节奏推进食物生成不能跑到蛇身上撞墙或者咬到自己要立刻结束分数要跟着变化画面还要在终端里持续刷新。这其实就是一个缩小版的游戏循环接收输入、更新状态、渲染画面、再循环。我之前学的字典、列表、函数、类都只是“零件”第一次用贪吃蛇把零件组装成整机才发现组装过程比看零件说明书难得多。选它做第五次总结还有一个原因贪吃蛇的规模刚好合适。如果选“做一个网页”或者“写一套管理系统”对新手来说模块太多容易在半路卡住如果只是写一个简单的计算器脚本又串不起这么多知识点。贪吃蛇处在“半个周末能写完但又能逼你想清楚细节”的档位最适合用来验证自己是不是真的掌握了基础。1.2 只用标准库到底值不值一开始我也纠结过要不要直接引入现成的游戏库毕竟用第三方库几十行就能画出可交互界面。但这次的目的不是“做出一个漂亮的游戏”而是“把 Python 基础摸透”。用第三方库会跳过太多细节按键是怎么读到的画面是怎么刷新的碰撞是怎么判断的这些底层机制一旦被封装掉我又会回到“照着示例跑起来但心里没底”的老路。所以我决定只用标准库甚至在不同系统上做了区分处理。这个选择的好处是强迫自己面对底层问题比如终端默认是“整行缓冲”模式你不按回车程序就读不到字符这意味着游戏必须先切换终端的输入模式。再比如窗口尺寸、光标移动、清屏方式在不同的系统控制台上行为并不完全一致。把这些都手动处理一遍之后以后再遇到“为什么程序在别人电脑上跑不起来”这类问题至少能有一个排查方向。1.3 顶层模块划分把视图和逻辑分开正式动手之前我先画了一个简单的模块划分没有直接开写。整个项目拆成三块逻辑核心、终端适配、主流程入口。最开始我担心这种“过度设计”会不会增加学习成本但写完才知道如果不分开后面调 bug 会非常痛苦。游戏逻辑层只负责计算蛇的位置、方向、食物、分数完全不知道终端是什么。终端适配层只负责两件事读取按键和把画面画出来。主流程入口把两者拼起来。这样做的直接好处是逻辑层可以在没有终端、脱离游戏画面时单独测试。比如判断蛇撞墙、吃食物这些核心逻辑直接写几个断言就能跑不需要打开终端手动操作。对新手来说“能自动验证的代码”比“靠眼睛看是否正常”的代码可靠得多。2. 入门必过的三关输入、状态、渲染2.1 第一关怎么在不按回车的情况下读到一个按键这是整个项目里第一个让我卡住的点。平时写input()读键盘时必须按回车才会返回因为终端默认处于“规范模式”要等一整行输入才交给程序。游戏里显然不能这样玩家按了方向键就该立刻触发移动不可能要求每按一个键都去敲回车。所以在类 Unix 系统上要用termios和tty把终端临时切换到原始模式再配合select设置读取超时就能实现“等待按键但最多等多少毫秒”的效果。在 Windows 上对应的方案是msvcrt.kbhit()加msvcrt.getch()。我封装了一个read_key(timeout)函数统一对外返回up、down、left、right、pause、quit这样的语义按键而不是原始字节流。# terminal.py 简化版跨平台按键读取 import os, sys, select def read_key(timeout0.08): if os.name nt: import msvcrt if msvcrt.kbhit(): key msvcrt.getch() if key in (bH, b\xe0): # 方向键前缀 key msvcrt.getch() return {H: up, P: down, M: right, K: left}.get(key, ) if key in (bw, bW): return up if key in (bs, bS): return down if key in (ba, bA): return left if key in (bd, bD): return right if key in (b ,): return pause if key in (bq, bQ): return quit return # Unix 系列Linux / macOS import termios, tty fd sys.stdin.fileno() old termios.tcgetattr(fd) try: tty.setraw(fd) r, _, _ select.select([sys.stdin], [], [], timeout) if not r: return ch sys.stdin.buffer.read(1) if ch b\x1b: # ESC 开头可能方向键 seq sys.stdin.buffer.read(2) if seq b[A: return up if seq b[B: return down if seq b[C: return right if seq b[D: return left return if ch in (bw, bW): return up if ch in (bs, bS): return down if ch in (ba, bA): return left if ch in (bd, bD): return right if ch in (b ,): return pause if ch in (bq, bQ): return quit return finally: termios.tcsetattr(fd, termios.TCSADRAIN, old)这里有个体验上的取舍为了演示简单我让每次read_key都进入原始模式又恢复这会让终端在每次循环时发生模式切换。真正想手感更好应该让主循环在启动时进入原始模式结束时统一恢复而不是每帧都切换。后者虽然代码多一点但运行会更稳定。2.2 第二关用对象还是用一堆列表表示蛇新手写贪吃蛇最容易陷入“把所有状态晾在散装变量”的写法snake_x列表、snake_y列表、direction字符串、food_x、food_y。这种写法在代码很短时没问题一旦加功能会越改越乱。所以我用了两个小的数据类Point表示坐标带__add__方便做向量加法。Snake管理蛇身内部用deque保存身体坐标序列。# snake.py 逻辑核心片段 from dataclasses import dataclass from collections import deque dataclass(frozenTrue) class Point: x: int y: int def __add__(self, other): return Point(self.x other.x, self.y other.y) UP Point(0, -1) DOWN Point(0, 1) LEFT Point(-1, 0) RIGHT Point(1, 0) class Snake: def __init__(self, start, length3): self.body deque([start]) for i in range(1, length): self.body.append(Point(start.x - i, start.y)) self.direction RIGHT def set_direction(self, new_dir): dot new_dir.x * self.direction.x new_dir.y * self.direction.y if dot 0: # 点积为负说明方向完全相反 return self.direction new_dir用deque的初衷很直接蛇移动时头部新增一格尾部移除一格正好是双端队列最擅长的事。头部用append塞进去尾部用popleft弹掉时间复杂度 O(1)不需要反复insert(0, ...)这种低效操作。更关键的是把蛇身当作一个坐标序列之后判断“新头的位置是否已经在自己身体里”就变成了看“坐标在不在 deque 里”。Point加上frozenTrue之后变成不可变对象这能让模型更安全。如果写游戏时有人不小心修改了方向向量比如把UP.y从 -1 改成 1整个游戏行为会马上崩掉。用只读数据类可以提前挡住这一类低级错误。2.3 第三关每秒刷新几次、怎么渲染最稳蛇移动的速度不是“越快越好”。这个项目里我用 tick 来控制节奏也就是每间隔多少毫秒执行一次step()。太快会让人来不及反应太慢又会觉得卡顿。我实际测下来0.1 秒一格对这个 40x20 的地图比较合适也就是每秒 10 格。渲染方式也有讲究。最粗暴的办法是每次刷新都调用os.system(cls)清屏然后重新打印整个画面。但这样会在终端里产生明显的闪烁感因为清屏之后到重新画完中间有一小段时间是空白的。更好的办法是使用 ANSI 光标控制码把光标移动回左上角再覆盖重画。# 渲染函数思路 def draw(game): lines [] for y in range(game.height): row_chars [] for x in range(game.width): p Point(x, y) if p game.food: row_chars.append(●) elif p in game.snake.body: row_chars.append(█) else: row_chars.append( ) lines.append(.join(row_chars)) sys.stdout.write(\033[H) # 光标移到左上角 sys.stdout.write(\n.join(lines)) # 覆盖重画 sys.stdout.write(f\nscore: {game.score}) sys.stdout.flush()注意终端的字符宽度并不完全统一。一个中文宽度字符占两列英文数字占一列。在地图设计时如果只按“字符个数”来算画面边缘可能出现错位。我统一了地图里的每个单元格都显示为两个英文字符宽度空格用两个空格蛇身用两个实心方块这样整体对齐效果更好。3. 从空目录到能玩的完整实现步骤3.1 先写不依赖终端的逻辑层我习惯先写“不需要打开终端也能验证”的逻辑。这样可以通过简单的 Python 交互环境逐步测试而不是每次都要启动整个游戏。逻辑层包含 Snake、Game、Point 和方向常量。# snake.py 扩展 import random class Game: def __init__(self, width40, height20): self.width width self.height height center Point(width // 2, height // 2) self.snake Snake(center, length4) self.food Point(5, 5) self.score 0 self.alive True def step(self, actionNone): if action in {up, down, left, right}: self.snake.set_direction({ up: UP, down: DOWN, left: LEFT, right: RIGHT }[action]) head self.snake.body[-1] self.snake.direction if self._hit_wall(head) or self._hit_self(head): self.alive False return False # 这一步导致游戏结束 self.snake.body.append(head) if head self.food: self.score 1 self._place_food() else: self.snake.body.popleft() return True def _hit_wall(self, p): return p.x 0 or p.x self.width or p.y 0 or p.y self.height def _hit_self(self, p): return p in list(self.snake.body)[:-1] # 忽略尾巴因为下一步会移动 def _place_food(self): while True: p Point(random.randrange(self.width), random.randrange(self.height)) if p not in self.snake.body: self.food p return这里有一个非常关键的顺序问题必须在“判断是否吃到食物”之前完成碰撞检测但蛇身是否包含食物又取决于下一步是否要移动尾巴。在_hit_self里我会忽略旧尾巴因为正常情况下蛇前进时尾巴会往前走一步新头和旧尾巴不会同时存在于下一帧画面上。如果你直接判断head in self.snake.body会出现“蛇头追着尾巴跑但判定为撞到自己”的误判这个问题后面在踩坑部分还会细说。3.2 再写终端输入和渲染层逻辑层稳定之后开始写 terminal 模块。输入用上一节read_key的封装渲染用 ANSI 光标移动覆盖重画。这里有一个 Windows 和 Unix 的细节Unix 上方向键会读到 ESC 开头的三字节序列Windows 上方向键会返回一个特殊前缀加一个扫描码。我统一把它们映射成up、down这样的语义值主循环完全不需要关心当前跑在哪个系统上。# terminal.py 渲染部分 import sys def draw(game): cells [] for y in range(game.height): line [] for x in range(game.width): p Point(x, y) if p game.food: line.append(●) elif p in game.snake.body: line.append(█) else: line.append( ) cells.append(.join(line)) out \033[H \n.join(cells) f\nscore: {game.score} sys.stdout.write(out) sys.stdout.flush()地图尺寸我固定成 40 列 20 行一是终端窗口默认大小基本放得下二是这个尺寸下蛇移动的节奏比较容易掌控。如果调到 80 列屏幕上能放更多内容但蛇从左到右走一次要近 8 秒玩起来不够紧凑。建议新手先从小到大跑通之后再调整尺寸。3.3 最后组装主循环并调整手感主循环最大的难点不是语法而是“既要及时响应按键又要按固定速度走蛇”。如果简单地while True里读键盘每秒循环几千次蛇会快得没法玩。如果每步都time.sleep(0.1)按键的响应也会延迟 100 毫秒玩起来不够跟手。我的做法是每个循环里先读一次键然后检查距离上次移动是否已经超过 tick 时间超过才执行step和draw。import time from snake import Game def main(): game Game() last_move time.time() tick 0.1 while game.alive: key read_key() if key quit: break if key pause: input(已暂停按回车继续...) continue now time.time() if now - last_move tick: game.step(key) draw(game) last_move now这样按键读取每次循环几乎都在执行所以真实响应延迟基本低于 50 毫秒。蛇的移动则由 tick 控制不会因为 CPU 跑得快就变快。3.4 运行效果与参数调整启动程序后终端中央会有一条小蛇、一个随机食物右上或底部显示分数。按方向键控制方向按空格暂停按 q 退出。如果撞墙或者咬到自己画面会停止刷新程序退出后终端恢复原来的显示模式。这时可以开始调参数。我把早期版本的数字整理了一下参数推荐范围说明地图宽度30 ~ 50太小容易秒死太大视线分散地图高度15 ~ 25高度过高会超过终端可视范围tick0.08 ~ 0.15 秒约 7 ~ 12 格每秒练习手感适中初始蛇长3 ~ 5太长开局压力大太短缺少挑战你可以把这几个参数抽成 Game 构造参数用不同数值跑几局感受完全不同。这也是学编程最直观的乐趣改一个数字整个游戏的“手感”立刻不一样。4. 踩过的坑输入卡死、闪屏与方向Bug排查4.1 终端被搞乱后该如何恢复这是新手最容易忽略的坑。用termios把终端从规范模式切成原始模式后如果程序在异常中断时没有恢复设置你的命令行的输入回显会消失按回车也不换行看起来就像终端“坏掉了”。我第一次遇到时一度以为要重启电脑后来才知道是终端属性没有恢复。解决办法是使用try/finally在finally里用tcsetattr恢复。最稳妥的做法是在主流程里一次性进入原始模式然后在finally里统一恢复而不是像演示代码那样每次read_key都切换。从项目安全角度讲我建议你直接把“恢复终端”当成游戏退出路径的一部分无论是正常退出还是异常退出都要确保termios.tcsetattr被执行。4.2 为什么一按反方向就立刻游戏结束这个 bug 的解释很简单蛇在向右走时如果你按了左方向蛇头会瞬间转向左然后直接撞上自己第二节身体游戏立刻结束。真实贪吃蛇游戏通常不允许这种操作因为在蛇长度大于 1 时反向移动等于自杀。我一开始没有处理这个问题很快就发现“为什么我按错一个键就死了”。解决方案是在Snake.set_direction里做点积判断当前方向是RIGHT(1, 0)新方向是LEFT(-1, 0)点积为 -1说明完全相反直接忽略这次方向修改。这比“用字符串比较当前方向”更简单因为方向是二维向量天然可以用数学关系表达“相反”这个概念。4.3 每次渲染都闪一下怎么办早期版本我用os.system(cls)清屏再重画运行起来终端的闪动非常明显尤其是在地图比较大的时候。这个问题本质上是“先清空画面再绘制新画面”造成的短暂空白。后来改成 ANSI 光标移动\033[H把光标拉回左上角然后直接覆盖输出就不再有明显闪烁。如果你用的终端对 ANSI 兼容不好还是只能用清屏那就尽量在“清屏后立即绘制”之间减少其他操作。但现代终端基本都支持 ANSI这个方案已经足够通用。4.4 不同系统上行为不一致同一个脚本在 Windows 和类 Unix 系统上表现不一样主要体现在读取按键的 API 完全不同。msvcrt只能用在 Windowstermios只能在 Unix 系列上用如果不做平台判断代码直接在某些系统上崩掉。所以我在模块开头判断os.name然后在两个分支里分别处理。Windows 方向键读取到的 key 有两个字节需要先判断前缀字节再取第二个扫描码Unix 方向键则是 ESC 开头的一串序列。如果只处理了其中一种在一台电脑上运行没问题换一台电脑就失灵。5. 性能优化、可测试性与下一阶段规划5.1 从全量重绘到局部更新对于 40x20 的小地图全量重绘完全够用但在更大的地图或者需要处理更复杂逻辑时这种“每次刷新重算所有格子”的做法就不划算。优化思路是引入“脏区域”概念游戏里每次变化实际上只有蛇头新位置、蛇尾旧位置、食物位置这三类区域发生了变化只需要重绘这些格子而不是整个地图。我这次没有做局部更新因为逻辑量和复杂度会明显上升不利于新手先跑通整个流程。但代码里把逻辑层和渲染层分开之后后续要改成局部刷新只需要修改draw函数不碰游戏逻辑。这也是分层的直接回报。5.2 把逻辑层做成能自动测试的样子我用了两个关键技巧保证逻辑层可以被自动验证第一不依赖input、print、sys.stdout第二把纯计算函数和 IO 分开。于是我可以写几行很简单的断言来测试核心规则。from snake import Game, Point def test_eat_food(): game Game(width10, height10) # 构造蛇头在 (5,5)食物也在 (5,5) game.snake.body.append(Point(5, 5)) game.food Point(5, 5) game.step(right) assert game.score 1 def test_crash_wall(): game Game(width10, height10) game.snake.body[-1] Point(9, 5) game.snake.direction Point(1, 0) assert game.step() is False # 右移撞墙这些测试在游戏开发过程中帮我省了不少时间。最有价值的是“吃食物”这个用例它能帮助验证“头部到达食物后尾巴不缩短蛇身长度增加”这个规则是否真的被正确实现。没有自动化测试的话这种边界条件只能靠肉眼反复玩来排查又慢又容易漏。5.3 后续可以加的功能这个项目还有很多可以自然延伸的方向。最直接的是加障碍物在地图里随机生成一些不可通过的格子每吃掉一定数量的食物就刷新地图。其次是记录最高分把最高分保存到本地文件里下次启动自动读取。还可以做速度等级吃食物加到一定分数就减少 tick让蛇越跑越快增加挑战性。这些功能里保存最高分能练习文件读写障碍物能练习地图状态管理速度等级能练习游戏循环的参数调整。每一个都不复杂但都能把某一块基础知识再往深推一层。我下一步准备先加入“最高分文件”因为这个功能会把“纯逻辑层”和“外部存储”的关系又理清一遍。5.4 第五次总结的个人体会写到这里我更强烈地感觉到一件事学编程的进度不是看你背了多少 API而是看你有没有亲手解决过“反直觉”的问题。终端模式、反向碰撞、闪烁渲染这些坑任何教程都不会比我亲身踩一遍更让我印象深刻。以前看文档里“点积为负数表示方向相反”这句话我大概扫一眼就过去了直到写贪吃蛇时需要一个判断规则才真正理解这个数学表达的简洁和价值。如果你现在也卡在“学完基础不知道写什么”的状态我建议试试贪吃蛇。不要急着找现成源码先自己搭一个最简骨架然后一点点把输入、逻辑、渲染填进去。遇到不懂的就停下来查标准库文档再回来看自己代码哪里不对劲。折腾完这一遍很多以前模模糊糊的概念会突然变得很具体。第五篇总结做到这里我总算把 Python 从“看过”变成了“用过”。
RELATED

相关推荐

CHI760E电化学工作站软件安装与数据采集全攻略:从驱动配置到批量处理

CHI760E电化学工作站软件安装与数据采集全攻略:从驱动配置到批量处理

简介:CHI760E辰华电化学工作站软件最新实用版,面向电化学分析、材料研究及高校实验教学人员,解决CHI760E仪器配套操作软件与USB驱动安装、测量控制的问题。压缩包共59个文件,约13.37MB,以bin实验数据文件、pdf用户手册…

📅 2026/10/13 15:50:31
LiteLLM统一多模型API接入与网关部署实战

LiteLLM统一多模型API接入与网关部署实战

最早把我逼着去研究 litellm 的,是手头一个客服知识库项目。业务上要求同一套能力能随时切换到不同模型服务,结果代码里全是 if 分支:这家平台的鉴权方式不一样,那家的请求结构不一样,返回来的字段名也对不上&#xff…

📅 2026/10/13 15:50:31
TPC-H V3.0.1 基准测试实战:从数据生成到性能调优的完整指南

TPC-H V3.0.1 基准测试实战:从数据生成到性能调优的完整指南

简介:TPC-H V3.0.1 是面向数据仓库与商业智能方向的标准化基准测试套件,适合数据库内核研发、数仓性能调优及数据分析学习者使用,用于在统一口径下评估复杂查询与商业分析场景的响应能力。压缩包共约2000个文件,整体24.07MB&#…

📅 2026/10/13 15:45:28
MORE NEWS

更多资讯

📰

COMSOL仿真波导BIC现象:原理与建模实践

1. 波导BIC现象初探:当电磁波遇见拓扑缺陷在电磁学领域,Bound states in the continuum(连续谱中的束缚态,简称BIC)堪称一种反直觉的物理现象。想象一下:一束光被完美地囚禁在波导结构中,即使周…

📰

虚拟电厂多时间尺度调度与储能容量衰减的Matlab复现

提到“虚拟电厂的多时间尺度调度”,搞电力系统优化的人应该都不陌生。我复现过好几篇相关方向的高水平论文,最大的感受是:真正把储能容量衰减、发电侧调节能力和用户负荷灵活性放进同一个框架里的工作不多,而能把这个框架跑通、跑…

📰

燃烧后CO2捕集MEA吸收过程模拟教程(18):能耗与成本评估——SRD、STEC 与 USD/t CO₂ 的经济性口径

燃烧后CO2捕集MEA吸收过程模拟教程(18):能耗与成本评估——SRD、STEC 与 USD/t CO₂ 的经济性口径版本声明块 工具/软件:本篇不依赖任何特定模拟器版本;能耗输入来自第 17 篇的全流程结果或公开中试数据语言/环境&…

📰

MFC多文档选项卡右键菜单实现指南

1. 项目概述:为什么MFC多文档界面必须支持选项卡右键菜单?在Windows桌面应用开发中,MFC(Microsoft Foundation Classes)仍是大量遗留系统、工业控制软件、内部管理工具的核心技术栈。尤其当一个应用需要同时打开多个文…

📰

Kubernetes调度器实战:从Pending诊断到自定义调度开发

1. 这不是“调度器说明书”,而是一份真实跑过200生产Pod的调度实战手记kubernetes scheduler,这个词在K8s学习路径里常被简化为“负责把Pod分配到Node上”的黑盒模块。但如果你真在某公司核心业务集群里亲手调过调度策略、改过亲和性规则、排查过Pod卡在…

📰

Laravel+QueryList电影站源码:自适应内容聚合实战

简介:这份资源是一套基于 Laravel 与 QueryList 框架搭建的影视站点源码,面向希望快速上线影视内容站点的开发者与站长,尤其适合具备一定 PHP 基础、想省去从零开发成本的中级用户。源码主打自动采集影视资源,前端采用电脑与手机自…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬