
简介课程实训项目基于Python的pygame库开发经典俄罗斯方块小游戏面向高校计算机相关专业学生及游戏开发入门爱好者适合作为课程设计、毕业设计或作业参考。压缩包内共六个文件体积仅6.88MB涵盖核心py源文件、Word设计报告、Markdown说明文档、音频素材及配置文件结构清晰便于对照学习。该资源目前已有四十六人学习浏览。代码经过严格测试功能完善且运行稳定可直接复现游戏效果配套设计报告与说明文档能帮助理解游戏逻辑和开发流程音频素材进一步提升游戏完整性。整体采用模块化设计便于阅读与二次修改零基础用户也能按文档指引快速运行基础较好的学习者还可在此基础上增加关卡、计分规则或界面优化用于项目演示与技术交流。1. 从课程实训题目到完整可运行项目先想清楚再写代码“课程实训-基于Python pygame的经典俄罗斯方块小游戏开发”这类题目真正的难点从来不是 Python 语法而是怎么把“会动的方块”拆成一块块能独立调试的程序结构。很多同学第一次打开编辑器就急着写pygame.draw.rect画到一半才发现旋转、碰撞、消行全揉在一个文件里改一个参数整个游戏跑飞最后拿一份看不懂的报告去答辩。反过来说如果先想清楚俄罗斯方块的几个核心问题——七种方块怎么定义、旋转怎么做不会穿墙、消行后数组怎么压缩、下落速度怎么和帧率解耦——整个项目的代码量也就是 400 到 600 行。这篇文章沿着课程实训的完整路径走一遍环境怎么装、形状矩阵怎么写、碰撞检测分几步、消行计分怎么做最后落脚到源码组织和课程报告里必须写清楚的内容。适合第一次接触 pygame 的初学者也适合需要快速把项目整理成可交付状态的开发者。2. pygame 开发俄罗斯方块的环境准备与主循环设计2.1 pygame 安装的常见失败与锁版本策略在解决任何游戏逻辑之前先把环境跑通。pygame 安装最常见的报错是error: failed to build pygame when getting requirements to build wheel这行提示的含义是pip 尝试从源码构建 pygame而不是使用预编译好的 wheel 包。原因一般是当前 Python 版本太新pygame 尚未发布对应版本的预编译包于是回退到需要 C 编译器的源码安装流程而课程实训机器上往往没有完整的编译工具链。另外如果你的主流 Python 环境装在系统保护目录下也会导致安装权限报错。我一般建议课程实训项目使用虚拟环境加锁版本的方式安装python -m venv tetris_env source tetris_env/bin/activate # Windows 下执行 tetris_env\Scripts\activate pip install pygame2.5.2 python -c import pygame; print(pygame.version.ver)这条命令组合的逻辑是先用venv创建一个干净的独立环境避免和系统 Python 环境互相污染然后指定pygame2.5.2版本安装这是一个经过大量课程项目验证的稳定版本覆盖 Python 3.8 到 3.12 的预编译包不需要本机编译最后用python -c验证导入和版本号确认环境没问题再开始写代码。注意安装前的 Python 版本最好查看一下python --version如果输出的是 3.13 及以上建议换用 3.11 或 3.12 重装环境。很多“安装 pygame 时建立车轮失败”的搜索请求根因就是版本过新加上本地缺少编译依赖。提示不要直接运行pip install pygame不指定版本。最新版本虽然功能更多但课程项目用不到锁定稳定版能减少大量不确定因素。2.2 最小可运行的主循环与帧率参数pygame 的主循环有固定套路但俄罗斯方块对这个循环有特殊要求下落、旋转、移动、消行这些逻辑都必须在一帧内完成“处理输入、更新状态、绘制画面”三个动作。这个最小主循环模板是import pygame import sys FPS 60 WINDOW_WIDTH 300 WINDOW_HEIGHT 600 CELL_SIZE 30 GRID_WIDTH WINDOW_WIDTH // CELL_SIZE # 10 列 GRID_HEIGHT WINDOW_HEIGHT // CELL_SIZE # 20 行 pygame.init() screen pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pygame.display.set_caption(Tetris Course Project) clock pygame.time.Clock() while True: dt clock.tick(FPS) / 1000.0 for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() # 游戏逻辑更新在此处 screen.fill((20, 20, 30)) pygame.display.flip()这里clock.tick(FPS) / 1000.0得到的是上一帧到这一帧的真实间隔时间单位是秒。俄罗斯方块的下落计时依靠这个dt而不是简单地“每帧让方块往下移一格”。如果按帧来移动游戏在 30 FPS 和 144 FPS 的屏幕上速度完全不同课程报告里如果写了“下落速度与帧率无关”这种设计说明是一个加分项。pygame.display.flip()的作用是更新整个窗口画面与pygame.display.update()不同flip强制把后备缓冲区的全部内容呈现到屏幕上。screen.fill()的参数是 RGB 颜色元组这一行如果不写上一帧的方块残影会留在新画面上。2.3 网格坐标系与绘制函数的边界俄罗斯方块的坐标系定位遵循一个约定游戏区域左上角是(0, 0)行号row向下递增列号col向右递增。绘制一个方块时需要把网格坐标换算成像素坐标def draw_cell(screen, color, row, col): x col * CELL_SIZE y row * CELL_SIZE pygame.draw.rect(screen, color, (x, y, CELL_SIZE - 1, CELL_SIZE - 1))这里的CELL_SIZE - 1不是随意写的。相邻格子之间留 1 像素间隙视觉上能明显分辨出每个小方块而不是黏成一整块。这个参数在绘图阶段会反复用到建议在settings.py里单独定义CELL_SIZE和间距后面调整窗口大小时只需要改常量。3. 俄罗斯方块核心算法形状定义、旋转矩阵与碰撞检测3.1 用矩阵定义七种标准方块经典俄罗斯方块有七种形状每种由 4 个格子组成。定义方式有很多种常见做法是用一个嵌套列表表示矩阵1 代表有方块0 代表空位。这样定义的数据结构同时承担了旋转计算和碰撞检测两种用途。SHAPES { I: [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], O: [[1, 1], [1, 1]], T: [[0, 1, 0], [1, 1, 1], [0, 0, 0]], S: [[0, 1, 1], [1, 1, 0], [0, 0, 0]], Z: [[1, 1, 0], [0, 1, 1], [0, 0, 0]], J: [[1, 0, 0], [1, 1, 1], [0, 0, 0]], L: [[0, 0, 1], [1, 1, 1], [0, 0, 0]], }为什么不用坐标点列表而是用矩阵因为矩阵天然支持旋转操作且代码可读性更高。I形状特意用 4x4 矩阵而不是 1x4是为了让旋转中心点保持在方块正中央旋转时不会产生偏移。O形状只有 2x2旋转后不变单独处理它可以减少不必要的计算。每种形状还需要绑定一种颜色pygame 要求绘制时传入 RGB 元组SHAPE_COLORS { I: (0, 240, 240), O: (240, 240, 0), T: (160, 0, 240), S: (0, 240, 0), Z: (240, 0, 0), J: (0, 0, 240), L: (240, 160, 0), }绘制时按形状名取颜色是图形渲染的标准惯例也方便后面扩展阴影方块或显示“下一个方块”预览区。3.2 旋转矩阵的正确实现先转置再反转旋转是俄罗斯方块里最容易出错的部分。顺时针旋转 90 度的数学本质是先将矩阵转置再反转每一行。这个算法对任意尺寸的方形矩阵都成立。def rotate_clockwise(shape): return [list(row) for row in zip(*shape[::-1])]拆开看这行代码的逻辑shape[::-1]把矩阵行上下颠倒zip(*...)把每行对应的元素打包成新行等效于转置最后list(row)把元组转换成列表。组合起来的效果就是顺时针旋转 90 度。注意这里要求矩阵是方形的所以I用 4x4、O用 2x2其他都是 3x3。旋转后的方块不能直接覆盖当前方块应该先计算出旋转结果并检查合法性只有合法才真正应用否则保持原状。所以旋转处理的完整逻辑是rotated rotate_clockwise(current_shape) if is_valid_position(grid, rotated, (row, col)): current_shape rotated这看起来多了一行代码但能避免旋转导致方块嵌入墙里或与已固定的方块重叠。不合法的旋转被静默忽略符合经典俄罗斯方块的操作手感玩家会在旋转碰壁时自然尝试先移动再旋转。3.3 碰撞检测的分层设计与边界条件碰撞检测是俄罗斯方块的支柱逻辑需要用单独的is_valid_position函数处理三种场景边界检测、与已固定方块重叠检测、底部越界检测。签名设计如下def is_valid_position(grid, shape, offset): off_row, off_col offset for r, row in enumerate(shape): for c, cell in enumerate(row): if cell 0: continue board_row off_row r board_col off_col c if board_col 0 or board_col GRID_WIDTH: return False if board_row GRID_HEIGHT: return False if board_row 0 and grid[board_row][board_col] ! 0: return False return True这里offset是方块左上角在网格中的位置。三层检查的顺序有讲究先检查左右越界和底部越界再检查重叠。board_row 0这个条件是必须的因为方块刚生成时矩阵的上半部分可能在网格上方之外即负行号。如果直接拿负行号去索引gridPython 会从列表尾部倒数产生错误的碰撞结果。这意味着游戏网格grid应该是一个GRID_HEIGHT x GRID_WIDTH的二维列表初始化全为 0每次有方块锁定后就把对应格子设成非 0 值。碰撞检测中grid[board_row][board_col] ! 0的写法直接利用了初始值全 0 的约定。3.4 方块锁定与整行消除方块无法继续下移时需要把它“烧录”进网格数组。锁定操作是把当前形状矩阵里的 1 写入 grid 的对应位置def lock_shape(grid, shape, offset): off_row, off_col offset for r, row in enumerate(shape): for c, cell in enumerate(row): if cell 1: grid[off_row r][off_col c] 1消行检测则遍历每一行如果一行里所有格子都不为 0就删除该行并在顶部插入一个全 0 的新行def clear_full_rows(grid): full_row_count 0 new_grid [row for row in grid if any(cell 0 for cell in row)] full_row_count GRID_HEIGHT - len(new_grid) while len(new_grid) GRID_HEIGHT: new_grid.insert(0, [0] * GRID_WIDTH) return new_grid, full_row_count列表推导式在这里是关键手法先过滤掉所有不包含 0 的行即满行再在顶部补空行。注意别用for row in grid: if 0 not in row: grid.remove(row)因为遍历列表时删除元素会导致索引错位。先过滤生成新列表再替换原数据是更安全的做法。计数时利用网格高度差计算消了多少行这个返回值后面要传给计分模块。消行检测的时机在每次方块锁定之后而不是在移动过程中否则逻辑会互相干扰。4. 可玩的交互逻辑事件处理、下落计时与计分规则4.1 按键事件与连续的向左向右移动俄罗斯方块的输入处理有两种形式离散事件和连续按键检测。旋转、硬降、暂停属于离散事件用事件循环处理左移、右移、软降属于连续操作用pygame.key.get_pressed()检测按住状态。两种必须分开处理原因是事件循环里的KEYDOWN只触发一次如果你在KEYDOWN里处理移动按住方向键不会产生连续移动。def handle_keydown(event, game_state): if event.key pygame.K_UP: rotate_current_piece(game_state) elif event.key pygame.K_SPACE: hard_drop(game_state) elif event.key pygame.K_p: toggle_pause(game_state) def handle_continuous_move(game_state): keys pygame.key.get_pressed() if keys[pygame.K_LEFT]: move_current_piece(game_state, -1, 0) elif keys[pygame.K_RIGHT]: move_current_piece(game_state, 1, 0) if keys[pygame.K_DOWN]: soft_drop(game_state)注意第二个函数里的顺序先处理左右移动再处理下落。因为玩家可能同时按住左和下的组合键这属于合理操作。move_current_piece内部要先尝试移动到新位置用is_valid_position检查过了才真正更新坐标。硬降的操作逻辑不同于软降硬降是让方块直接落到最低合法位置而软降是按一次方向键就加速下移一格。实现硬降用循环逐格尝试下移直到碰撞边界为止。硬降的返回值是落下的行数这个数据计分要用。4.2 下落速度与关卡节奏的帧率无关设计俄罗斯方块的核心节奏来自下落计时器。设计要点是下落速度用“多少秒下移一格”定义而不是“多少帧下移一格”。这样无论机器跑 30 FPS 还是 144 FPS方块移动的速度体验完全一致。def update_fall_timer(game_state, dt): game_state[fall_timer] dt if game_state[fall_timer] game_state[fall_speed]: game_state[fall_timer] 0 if is_valid_position(game_state[grid], game_state[current_shape], (game_state[row] 1, game_state[col])): game_state[row] 1 return False else: lock_and_spawn(game_state) return True return False这个函数的返回值表示方块是否因为本次下落失败而完成了锁定主循环根据这个返回值决定是否立即进行消行检测。fall_speed参数的取值范围根据关卡变化下面是一个常用的参数表关卡下落间隔秒说明10.8新手友好20.65小幅提速30.5开始有压迫感40.35需要熟练操作50.2接近街机难度关卡提升的策略可以按消除行数累计比如每消除 10 行提升一关。真实的经典俄罗斯方块在更高关卡会进一步加速到 0.1 秒以内课程项目做到第 5 关就足够展示完整逻辑了。4.3 计分规则单行消与四连消的权重差异俄罗斯方块的计分逻辑有一个重要设计一次消除的行数越多总收益指数上升。常见的计分表如下一次消除行数得分计分规则说明1100单行消除2300两行同时消除3500三行同时消除4800极限四行同时消除游戏最高难度操作如果只按“每消一行 100 分”线性计分玩家就没有动力堆高方块争取一次消四行游戏策略性会大幅下降。实现计分直接在消行函数返回的full_row_count上查表score_table {1: 100, 2: 300, 3: 500, 4: 800} def add_score(game_state, full_row_count): if full_row_count in score_table: game_state[score] score_table[full_row_count]注意full_row_count是 0 时不需要任何操作。现实中一次消 5 行不可能发生因为方块宽度只有 4 格所以字典最多到 4 即可。想要街道风格可以再加一个“软降每格 1 分、硬降每格 2 分”的奖励机制这个细节让报告的设计说明更饱满。4.4 游戏状态与重新开始课程项目一般要求有“游戏结束”和“重新开始”的流程。状态机用字符串常量即可GAME_STATES (PLAYING, PAUSED, GAME_OVER) def restart_game(game_state): game_state[grid] [[0] * GRID_WIDTH for _ in range(GRID_HEIGHT)] game_state[score] 0 game_state[level] 1 game_state[fall_speed] 0.8 game_state[state] PLAYING spawn_new_shape(game_state)重新开始不能只清空网格还要重置分数、关卡、下落速度、当前方块同时把fall_timer归零。最容易漏掉的是下落计时器如果忘记清零重开后第一块方块会瞬间下落一格体验很差。GAME_OVER状态的进入条件是新方块生成时碰撞检测返回 False说明网格顶部堆满了。5. 课程实训的交付技巧报告结构、运行说明与参数收敛5.1 用 main.py 加 modules 的目录组织方式课程实训交付的是源码加报告评审老师最反感的是所有代码堆在一个文件里、没有运行说明、也没有模块划分。常见的可靠做法是下面的结构tetris/ ├── main.py # 入口包含主循环和状态机 ├── settings.py # 所有常量和配置参数 ├── shapes.py # 形状定义与旋转函数 ├── board.py # 网格逻辑、碰撞检测、消行 ├── game.py # 游戏状态控制、计分、关卡 └── requirements.txt # 依赖列表main.py只负责初始化窗口、循环事件、调用各模块接口。这个目录本身就是课程报告“系统设计”章节的插图素材。settings.py里用全大写的常量名集中管理参数比如FPS、CELL_SIZE、INITIAL_FALL_SPEED这样报告里可以直接引用参数说明。5.2 课程报告里的关键内容写作思路课程实训报告的评分重点不是代码量而是“设计说明”和“测试过程”是否完整。值得写清楚的几个部分第一是算法流程图重点是旋转和碰撞检测的判定顺序。画图时直接使用文字描述加箭头不必用专业的面向对象图比如“尝试旋转 - 旋转结果是否越界或重叠 - 合法则应用不合法则忽略”这一句话就是一张合格的设计说明图。第二是运行环境说明。写好pip install -r requirements.txt python main.py这两条命令能大幅降低老师亲测的成本。同时注明开发时的 Python 版本例如 “Python 3.11 pygame 2.5.2”防止环境不兼容被打回。第三是参数调整的收敛过程。课程报告中写“下落速度从 1.0 秒调到 0.8 秒因为 1.0 秒太慢测试者觉得无聊”这样比表格里贴一堆无意义的数字更好。调参的建议是每次只调一个变量记录改动前后的表现差异这也是工程调试的基础习惯。5.3 用边界条件测试替代盲目试玩课程项目交上去之前可以用一组边界条件测试快速验证逻辑是否正确而不是反复手动试玩。常见做法是直接模拟旋转极端位置和底部碰撞def test_rotation_collision(): shape SHAPES[I] rotated rotate_clockwise(shape) grid [[0] * GRID_WIDTH for _ in range(GRID_HEIGHT)] assert is_valid_position(grid, rotated, (0, 3)) True assert is_valid_position(grid, rotated, (0, 9)) False # 右越界这种测试不用 pytest用assert就够了。第 19 行的边界值(0, 9)验证了越界判断的右边界是否正确。加上底部测试assert is_valid_position(grid, SHAPES[O], (GRID_HEIGHT - 1, 0)) True assert is_valid_position(grid, SHAPES[O], (GRID_HEIGHT, 0)) False第一行判断O在底部最后一行是合法的第二行判断超出底部一行直接返回 False。“合法位置恰好卡在边界内”比“完全离边界很远”更容易暴露下标越界问题这类断言能有效抓住边界 bug。测试文件的命名建议是test_collision.py与主代码放在同一目录运行时不并入主程序只在需要验证时用python test_collision.py执行。本文还有配套的精品资源点击获取