
简介面向Python初学者与希望打通图形化项目开发的爱好者这份PDF以俄罗斯方块Tetris为实战案例讲解如何用Python 3.x与Pygame从零搭建一款可运行的益智游戏。内容按项目流程展开需求分析与技术选型、环境搭建、窗口与界面设计、Tetromino方块类的形状与旋转封装、方块下落与碰撞检测、行消除与计分逻辑、键盘事件响应、渲染刷新与帧率控制并在末尾给出测试调试要点及音效、难度分级、排行榜、双人对战等扩展方向同时配有可直接参照的代码片段与函数骨架。全包仅1个PDF文件约164KB轻量易存适合边看边敲、逐步复现。目前已有204人学习下载可作为课程设计、期末作业或Pygame入门练手的分步教程帮助读者理解游戏主循环与模块拆分思路把语法知识落到完整项目上。1. 从一份 PDF 教程到能跑的俄罗斯方块先把项目边界定清楚很多人手上都有一份《Python项目开发实战俄罗斯方块游戏》的 PDF翻到一半发现代码是分片段贴的变量名前后不一致board一会儿是列表一会儿是字典照着敲完运行起来要么窗口一闪而过要么方块卡在顶部不动。这不一定是教程写错了案例教程天然要把篇幅让给知识点而工程上必须补的那部分——虚拟环境、主循环时序、碰撞边界——常被压缩成两三行。这里要划清的边界是做一个用 Python 加 pygame 实现的单机俄罗斯方块包含 7 种标准方块、棋盘矩阵、旋转与踢墙、消行计分、等级加速、Next 预览和暂停重开。不碰联机对战不碰数据库也不依赖任何在线素材所有图形都用矩形画出来走像素游戏的路线。适合刚学完语法想找一个完整项目练手的人也适合会写脚本、但没正经处理过「游戏循环加状态机」这层结构的开发者。后面按环境搭建、核心数据结构、交互与渲染、工程化四步推进每一步都给出可直接抄的命令和代码。2. Python 俄罗斯方块开发环境从 python 安装到依赖落地2.1 用虚拟环境把 pygame 和系统解释器隔开装完 Python 之后第一个动作不是pip install而是先建虚拟环境。原因很实际pygame 的预编译轮子对 Python 小版本敏感3.13 刚发布那阵子就没有对应轮子pip 会退回去尝试源码编译缺 SDL 头文件时报一长串错误。把依赖锁在项目目录里换解释器、换机器都不用重装系统环境。# 确认解释器版本3.10 到 3.12 的轮子覆盖最全 python --version # 标准库自带的 venv比第三方 virtualenv 少一层依赖 python -m venv .venv # Windows .venv\Scripts\activate # macOS / Linux source .venv/bin/activate python -m pip install --upgrade pip pip install pygame2.6.1这里用.venv这个目录名不是随意取的vscode 的 Python 插件会优先识别它。解释器切换走CtrlShiftP里的Python: Select Interpreter选中.venv下的可执行文件即可不用手动改 PATH。团队协作时更稳的做法是把解释器路径写进工作区配置{ python.defaultInterpreterPath: ${workspaceFolder}/.venv/bin/python, python.terminal.activateEnvironment: true }Windows 下把路径换成.venv/Scripts/python.exe。python.terminal.activateEnvironment保证新开终端自动激活环境避免出现「明明装了 pygame运行却报 ModuleNotFoundError」这种九成来自解释器选错的困惑。安装慢就临时挂镜像源pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple但别写进全局 pip 配置公司网络换代理时会出问题。2.2 一个 30 行的窗口骨架先验证渲染链路通畅环境好了先别急着写方块用最小骨架确认窗口、事件、帧率三条链路都通import sys import pygame CELL 30 # 单格像素边长后面对齐靠它 COLS, ROWS 10, 20 # 标准棋盘 10 列 20 行 SIDEBAR 6 # 右侧预览区按格子数算宽度 WIDTH, HEIGHT (COLS SIDEBAR) * CELL, ROWS * CELL FPS 60 def main(): pygame.init() screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(Tetris) clock pygame.time.Clock() running True while running: # 1 事件先只处理关闭把窗口跑起来 for event in pygame.event.get(): if event.type pygame.QUIT: running False # 2 更新下一节放方块下落与消行 # 3 渲染 screen.fill((18, 18, 22)) pygame.display.flip() clock.tick(FPS) # 返回上一帧耗时(ms)同时把循环压到 60 帧 pygame.quit() sys.exit(0) if __name__ __main__: main()三段式主循环是这类项目的通用骨架事件处理只负责把输入转成意图更新负责改数据渲染只读数据不改数据。把这三件事混在一个大while里加到三百行以后就没法调了。clock.tick(FPS)同时承担限帧和返回时间差两个职责不要用time.sleep控帧它不补偿逻辑耗时掉帧之后手感会飘忽这正是很多人误判成「游戏延迟高」的根源。2.3 启动阶段最常见的四类报错和处理路径报错信息触发场景处理方式ModuleNotFoundError: No module named pygame装到了系统解释器运行时用的是虚拟环境重新 Select Interpreter或在激活环境后重装pygame.error: No available video device无显示环境如远程终端、容器设SDL_VIDEODRIVERdummy只跑逻辑和测试OSError: [WinError 126]缺 VC 运行时库安装对应版本的运行库后重装 pygamepip 卡在Building wheel for pygame解释器版本太新没有预编译轮子降到 3.12 或安装系统级 SDL 开发包无显示环境那一条值得记住。后面写自动下落模拟和消行测试时用SDL_VIDEODRIVERdummy python -m pytest就能在纯终端里跑完全部用例不必为跑测试专门开桌面。3. 俄罗斯方块核心数据结构棋盘矩阵、形状表示与旋转算法3.1 用二维矩阵同时表达棋盘和 7 种标准方块棋盘用嵌套列表最顺手board[y][x]的索引顺序和渲染时的屏幕坐标能一一对应省掉一层坐标换算。方块形状也用矩阵但每种要用最小包围盒别统一补成 4x4COLS, ROWS 10, 20 board [[0] * COLS for _ in range(ROWS)] # 0 为空非 0 是颜色编号 SHAPES { I: [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], O: [[2, 2], [2, 2]], T: [[0, 3, 0], [3, 3, 3], [0, 0, 0]], S: [[0, 4, 4], [4, 4, 0], [0, 0, 0]], Z: [[5, 5, 0], [0, 5, 5], [0, 0, 0]], J: [[6, 0, 0], [6, 6, 6], [0, 0, 0]], L: [[0, 0, 7], [7, 7, 7], [0, 0, 0]], }I 用 4x4、O 用 2x2、其余用 3x3是因为形状矩阵的旋转基准就是矩阵中心而 O 的中心在四个格子的交界上。如果为了「统一好看」把 O 也塞进 4x4旋转函数一跑就会产生横向漂移方块会一格一格往边上爬。这个坑在很多流传的案例教程里复制粘贴了好几轮抄代码时重点核对这里。矩阵表示还有一个好处碰撞检测退化成双重循环加两个越界判断不用维护任何图形学结构。3.2 旋转的坐标变换与踢墙偏移顺序顺时针旋转就是「转置后左右翻转」一行代码搞定前提是形状矩阵是方阵def rotate_cw(shape): 顺时针 90 度先转置再把列顺序倒过来 return [list(row) for row in zip(*shape[::-1])] def rotate_ccw(shape): return [list(row) for row in zip(*shape)][::-1] # 简化版踢墙表按顺序试第一个不碰撞的位置即为旋转后位置 KICKS [(0, 0), (-1, 0), (1, 0), (-2, 0), (2, 0), (0, -1)] def is_valid(board, shape, ox, oy): for y, row in enumerate(shape): for x, cell in enumerate(row): if not cell: continue nx, ny ox x, oy y if nx 0 or nx COLS or ny ROWS: return False # 撞墙或触底 if ny 0 and board[ny][nx]: return False # 撞到已固定的块 return True def try_rotate(board, shape, ox, oy): rotated rotate_cw(shape) for dx, dy in KICKS: if is_valid(board, rotated, ox dx, oy dy): return rotated, ox dx, oy dy return shape, ox, oy # 全部失败则保持原状is_valid里ny 0的判断不能省。方块出生时有一到两行在棋盘上方此时ny为负是合法的只要board下标不做访问就行。漏掉这个判断方块会在出生瞬间被判为碰撞。KICKS的顺序有讲究先试原地再试左右各一格最后允许整体上抬一格。这不是完整的 SRS 超级旋转系统但能覆盖九成以上的贴墙旋转场景代价是十几行代码。追求原版手感再补完整的偏移表先跑通玩法不必上那套复杂度。3.3 消行扫描、计分表与等级速度曲线消行用列表推导最干净留下还有空格的行缺几行就在顶部补几行空行。SCORE_TABLE {1: 100, 2: 300, 3: 500, 4: 800} def clear_lines(board): kept [row for row in board if any(cell 0 for cell in row)] cleared ROWS - len(kept) if cleared 0: return board, 0 return [[0] * COLS for _ in range(cleared)] kept, cleared def drop_interval(level): 下落间隔秒随等级递减最快锁在 0.05 return max(0.05, 0.8 - (level - 1) * 0.07)一次性消四行给 800 分而不是四倍单行分是为了让玩家有攒 I 块的动机这是俄罗斯方块作为棋盘游戏最核心的策略层。等级提升按累计消行数每 10 行升一级而不是「消一次升一级」否则一次四消会让下落速度瞬间跳档节奏断裂。等级下落间隔主观手感适用场景10.80 s从容观察熟悉操作50.52 s需要预判常规游玩100.17 s明显压力有经验玩家150.05 s极限只适合硬降玩法4. 从能跑到能玩输入手感、像素渲染与游戏测试4.1 用 DAS 和 ARR 替代 key.set_repeat手感问题的第一来源是左右移动的重复触发。pygame 自带的pygame.key.set_repeat(200, 50)是全局参数和帧率脱钩高刷新率屏幕上会明显发飘。正确做法是自己计时把首次延迟和重复间隔分开DAS 0.17 # Delayed Auto Shift按住后开始连移前的等待 ARR 0.05 # Auto Repeat Rate连移时每次移动的间隔 class InputState: def __init__(self): self.held {} # key - 已按住时长 self.repeat {} # key - 距上次触发移动的时长 def update(self, keys, dt, move_left, move_right): pairs ((pygame.K_LEFT, move_left), (pygame.K_RIGHT, move_right)) for key, action in pairs: if not keys[key]: self.held[key] self.repeat[key] 0.0 continue self.held[key] self.held.get(key, 0.0) dt self.repeat[key] self.repeat.get(key, 0.0) dt if self.held[key] DAS and self.repeat[key] ARR: action() self.repeat[key] 0.0dt必须取自clock.tick(FPS) / 1000.0的返回值不能写死1/60。写死之后60 帧和 144 帧的机器上 DAS 实际生效时间会差一倍多。调参经验DAS 在 0.13 到 0.20 之间ARR 在 0.03 到 0.08 之间先固定 ARR 调 DAS找到「松手前不会滑过头」的值再收 ARR 提升贴墙速度。硬降和软降要单独处理软降间隔可以取 0.03且不受 DAS 影响。4.2 像素游戏渲染网格、描边与 Next 队列渲染部分尽量只读数据。格子画成矩形加 1 像素描边视觉上就有像素游戏的块感COLORS { 0: (18, 18, 22), # 背景 1: (0, 220, 220), # I 青 2: (220, 200, 0), # O 黄 3: (170, 0, 220), # T 紫 4: (0, 200, 80), # S 绿 5: (220, 60, 60), # Z 红 6: (60, 100, 230), # J 蓝 7: (230, 140, 30), # L 橙 } def draw_cell(screen, gx, gy, value): rect pygame.Rect(gx * CELL, gy * CELL, CELL, CELL) pygame.draw.rect(screen, COLORS[value], rect) pygame.draw.rect(screen, (30, 30, 34), rect, 1) # 描边让相邻块可分方块序列用 7-bag 随机加定长队列避免连续出现同一形状import random from collections import deque def refill(queue): while len(queue) 5: bag list(SHAPES.keys()) random.shuffle(bag) # 每 7 个为一轮每种恰好一次 queue.extend(bag) return queue queue refill(deque(maxlen5)) # maxlen 自动淘汰旧元素渲染元素坐标来源刷新时机已固定方块遍历board每次消行后当前方块形状矩阵加ox/oy每次移动或下落幽灵落点从当前位置向下探测到底当前方块变化时Next 预览queue前 3 个新方块生成时分数与等级运行时状态变量消行后重绘幽灵落点是手感提升最明显的一处改动沿当前列一直下探到is_valid返回 False取上一格就是落点用半透明色画出来。计算量是每帧几十次布尔判断可以忽略。4.3 游戏测试把逻辑和渲染拆开才测得到游戏难测的原因通常是逻辑和绘制绑在一起。只要把clear_lines、rotate_cw、is_valid这类纯函数抽出来测试就回归成普通单元测试。def test_clear_four_lines(): b [[1] * COLS for _ in range(4)] [[0] * COLS for _ in range(ROWS - 4)] b[3] [0] * COLS # 留一行空行防止整板被清 new_board, n clear_lines(b) assert n 3 and len(new_board) ROWS def test_rotation_identity(): shape SHAPES[T] for _ in range(4): shape rotate_cw(shape) assert shape SHAPES[T] # 转四次必须回到原形 def test_no_collision_at_spawn(): for name, shape in SHAPES.items(): assert is_valid(board, shape, 3, -1), name测试项输入期望贴左墙旋转I 竖块在ox0通过踢墙右移后合法贴底消行底行填满该行消失顶部补空行出生位置全部 7 种形状均不与已有块碰撞边界越界ox-1is_valid返回 False无图形界面环境下用SDL_VIDEODRIVERdummy python -m pytest -q跑整个套件在一秒内结束。回归时重点看旋转和消行这两个函数其余代码改动基本不会影响它们。5. 进阶把案例代码整理成能长期维护的工程5.1 拆模块与配置外置单文件写到八百行就会开始难受。按职责拆成四个文件足够config.py放常量和颜色表tetris.py放棋盘、形状和消行这些纯逻辑render.py只管绘制main.py负责主循环和输入。拆完之后逻辑层可以脱离 pygame 运行测试跑得更快。配置不要硬编码在函数里用 TOML 外置Python 3.11 起标准库自带解析器import tomllib with open(config.toml, rb) as f: cfg tomllib.load(f) CELL cfg[view][cell] DAS cfg[input][das] ARR cfg[input][arr] START_LEVEL cfg[game][start_level]tomllib.load只接受二进制模式的文件对象用open(..., rb)这是最常见的调用错误。参数外置之后调手感不用改代码改完保存重跑即可也方便把不同难度预设写成多个 TOML 文件。5.2 从 PDF 教程里搬代码时的文本清洗案例教程的代码片段复制出来常带全角空格、中文引号和页码行粘进编辑器直接语法错误。批量处理一下比逐行手改省事import re def clean_snippet(text): text text.replace(\u3000, ) # 全角空格 text text.replace(“, ).replace(”, ) text text.replace(‘, ).replace(’, ) text re.sub(r^\s*\d\s*$, , text, flagsre.M) # 去掉孤立的页码行 return text如果反过来要把自己的项目笔记导出成 PDF 分享注意中文字体必须先注册否则渲染出来全是方块。这一步在多数 PDF 生成库里都是显式调用不注册不报错但结果不可读属于典型的静默失败。最后给一组可以直接拿去试的参数组合把CELL设成 24 让棋盘在 1080p 屏幕上不顶到边DAS取 0.14、ARR取 0.04下落基准间隔从 0.8 起、每级减 0.06。这组值在多数人的手感偏好区间里先用它跑一局再决定往哪个方向微调。本文还有配套的精品资源点击获取