用Python开发俄罗斯方块:矩阵建模、旋转算法与碰撞检测实战 📅 发布时间:2026/9/9 5:54:57 👁 浏览次数: 简介这套 Python 课程设计以俄罗斯方块为实践载体面向正在学习 Python 基础、面向对象编程与图形界面开发的初学者帮助读者通过一个完整游戏项目掌握变量与数据类型、条件与循环、类与继承、画布绘制以及事件驱动编程等核心知识点。压缩包共 3 个文件约 9.33MB内含可直接运行的 exe 程序适合免环境启动试玩源代码 py 文件可按模块阅读与二次修改docx 实验报告则系统梳理了设计思路、实现过程及常见问题排查兼顾动手实践与文档参考。游戏逻辑完整覆盖随机方块生成、旋转规则、碰撞检测、消行计分与游戏结束判定并在此基础上串联起窗体搭建、键盘监听和画布刷新的完整流程。目前已有 7043 人学习适合作为课程设计参考、期末项目模板或入门后的综合练手素材。1. 项目方案是怎么定下来的1.1 选型为什么选pygame而不是纯控制台我当年做这个课程设计的时候第一反应也是用控制台那个黑色窗口硬写俄罗斯方块。打印方块、用键盘方向键控制、回车键旋转……原理上确实能跑但做完以后最大的问题在于它不像是俄罗斯方块。没有颜色区分、没有流畅的动画、没有音效反馈演示给老师看的时候视觉冲击力非常差。后来我把方案换成了pygame理由很简单pygame本身就是Python官方社区里最常用的2D游戏库安装方便一条pip命令搞定API设计直观有现成的键盘事件监听、图形绘制、碰撞检测基础能力。它不像Unity或者Godot那样需要学一大堆编辑器操作也不像Tkinter那样做游戏界面时到处受限。写一个俄罗斯方块pygame大概需要300到500行代码对于课程设计来说量级刚刚好。1.2 功能裁剪先把需求砍到最小可交付版课程设计有个很关键的陷阱一开始想做的功能太多最后连核心逻辑都没跑通。做俄罗斯方块也不例外如果你一开始就想着要加音效、加分数动画、加下一块预览、加等级速度变化、加暂停菜单你会发现代码量翻了不止一倍调试时间更是爆炸式增长。我自己踩过这个坑所以这次给你一个裁剪思路第一版只做四个核心功能——方块生成、方块移动/旋转、方块落定、满行消除。一个可玩的俄罗斯方块本质上只需要这四件事。其余的功能比如暂停、等级提速、下一块预览都属于加分项。我建议你把核心逻辑先跑通再做加分项这样即使最后时间不够交上去的也是完整可运行的项目而不是半成品。另外一个重要的原因是俄罗斯方块的规则足够明确但又包含了一整套典型的游戏开发流程——状态管理、碰撞检测、事件循环、渲染更新。这些知识点恰好覆盖了Python入门阶段的重点内容循环、列表、函数、字典、类和对象。用这个项目来串联Python基础语法比单纯做计算器、图书管理系统一类的东西有意思得多。2. 数据建模方块、面板、旋转与碰撞2.1 用矩阵来表示一切很多新手写俄罗斯方块第一反应是用坐标点去表示方块每个方块由四个坐标构成比如[(0,0), (0,1), (1,0), (2,0)]。这样做不是不行但旋转操作会让你头大——每次旋转都要手动算新坐标还容易算错方向。更成熟的方案是用矩阵。把每一个方块做成一个二维列表1代表有格子0代表空白。俄罗斯方块的七种基本形状我直接定义成7个矩阵SHAPES { I: [[1, 1, 1, 1]], O: [[1, 1], [1, 1]], T: [[0, 1, 0], [1, 1, 1]], S: [[0, 1, 1], [1, 1, 0]], Z: [[1, 1, 0], [0, 1, 1]], J: [[1, 0, 0], [1, 1, 1]], L: [[0, 0, 1], [1, 1, 1]] }游戏面板同样用矩阵来表示——一个宽10格、高20格的二维列表初始全部是0。方块落定的时候把方块矩阵中值为1的位置写入面板矩阵对应的坐标这样面板的状态永远是一张清晰的地图做碰撞检测、满行检测都直接在这个矩阵上操作非常直观。2.2 旋转算法转置加翻转一条龙解决用矩阵表示方块之后旋转就变得简单了。以顺时针旋转为例操作是先把矩阵转置再把每一行反转。转置就是把行列互换这个操作在Python里用zip(*matrix)就可以完成def rotate_clockwise(matrix): return [list(row) for row in zip(*matrix[::-1])]这一行代码干了什么matrix[::-1]先把矩阵上下翻转然后zip(*)把每一列拿出来组成新行本质上就是完成了顺时针旋转90度。你不需要手动算任何坐标算法替你搞定了。这里要特别注意一个问题旋转后可能会导致方块超出边界或者与已有格子重叠所以旋转不能直接生效。我的做法是——先尝试旋转得到新矩阵然后检查新位置是否合法如果合法就应用不合法就保持原样。具体检查逻辑放到碰撞检测里一起说。2.3 碰撞检测边界、堆叠与落定碰撞检测是这个游戏最核心的逻辑没有之一。方块的每次移动、旋转、下落都必须先问一个问题这个新位置合法吗我把合法性判断独立成一个函数输入当前面板、方块矩阵、以及目标坐标方块的左上角位置返回布尔值。判断规则就两条方块矩阵中值为1的格子落在面板坐标系后列坐标必须在0到9之间行坐标必须在0到19之间对应的面板位置必须为空即值为0。代码长这样def is_valid_position(board, shape, offset_x, offset_y): for row_idx, row in enumerate(shape): for col_idx, cell in enumerate(row): if cell 0: continue board_x offset_x col_idx board_y offset_y row_idx if board_x 0 or board_x 10 or board_y 20: return False if board_y 0 and board[board_y][board_x] ! 0: return False return True判断不合法那移动或旋转就取消判断合法就更新方块坐标。方块下落时如果发现下一步不合法就直接落定——把方块矩阵的值写入面板然后进入消行检测。这一步是整个游戏循环的关键衔接点。3. 实操过程环境搭建与代码实现3.1 环境准备Python版本与pygame安装先强调一个前提这个项目对Python版本没有苛刻要求3.8到3.12都能跑。如果你用的是3.8以下建议直接升级因为老版本的语法支持和第三方库兼容性都比较折磨人。安装pygame只需要一条命令pip install pygame如果你用conda管理环境可以先建一个干净环境再装conda create -n tetris python3.9 -y conda activate tetris pip install pygame安装完成后在Python交互环境里执行import pygame不报错就说明环境准备好了。如果提示找不到pip去Python安装目录的Scripts文件夹下执行python -m pip install pygame就行。这是最常见的安装坑我每次写教程都得强调一遍。编辑器方面你用VSCode还是PyCharm都可以没有本质区别。唯一要提醒的是先用VSCode装好Python扩展然后CtrlShiftP选好解释器否则你可能装了pygame但编辑器用的是另一个解释器运行时报ModuleNotFoundError血泪教训。3.2 项目骨架初始化窗口与主循环pygame程序的骨架是固定的先初始化再进入一个死循环不停地处理事件和绘制直到用户关闭窗口。这里我直接给你一个最小的可运行框架import pygame import sys import random # 初始化pygame pygame.init() # 窗口尺寸面板10*20格每格30像素 CELL_SIZE 30 COLS, ROWS 10, 20 SCREEN_WIDTH COLS * CELL_SIZE 200 # 多出来的区域放分数和下一块 SCREEN_HEIGHT ROWS * CELL_SIZE screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(Python俄罗斯方块) # 游戏时钟 clock pygame.time.Clock() # 自定义事件用于控制方块自动下落 FALL_EVENT pygame.USEREVENT 1 def main(): running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False pygame.display.flip() clock.tick(60) pygame.quit() sys.exit() if __name__ __main__: main()注意最后两行if __name__ __main__这个东西的意义在于当这个文件被直接运行时才执行main()如果被其他文件import则不会执行。这是一种良好的代码组织习惯也是Python课程考核里经常被提到的点建议养成习惯。3.3 方块下落机制用自定义事件而不是死循环方块自动下落最简单的实现方式是使用pygame的pygame.time.set_timer功能。它可以让pygame每隔一段时间自动向事件队列里丢一个自定义事件。我的做法是先定义FALL_EVENT pygame.USEREVENT 1然后在主程序最开始设置定时器pygame.time.set_timer(FALL_EVENT, 500) # 每500毫秒触发一次下落事件循环里这样处理if event.type FALL_EVENT: # 尝试向下移动移动不了就落定 if not try_move(0, 1): lock_block()这个方案比用time.sleep好太多。因为sleep会阻塞整个程序导致按键响应变卡而set_timer是异步的下落和按键互不干扰游戏手感非常流畅。你后续如果要加等级提速只需要修改定时器的时间间隔就行。游戏快捷键可以这样绑定左方向键/右方向键控制左右移动上方向键旋转下方向键加速下落空格键直接硬降。这些绑定写在KEYDOWN事件判断里后面完整代码会展示。3.4 U和I两个形状的细节处理在代码实现中有个容易忽略的点I形方块只有一行O形方块旋转后应该保持原样。我的处理方式是写一个特殊判断如果形状名称是O旋转函数直接返回原矩阵如果形状名称是I旋转后宽度可能从4变成1需要额外处理偏移否则会旋转到墙里。这些细节在测试时特别容易暴露如果你玩着玩着发现方块卡到面板边界外大概率就是旋转形状的宽高变化没处理。4. 核心逻辑的完整实现4.1 数据初始化与方块生成游戏运行期间需要维护几个核心变量当前活动方块的形状矩阵、当前方块的坐标用左上角行、列表示、面板矩阵、当前得分、游戏是否结束。我把它们统一放进一个字典或者用一个类来管理。为了符合课程设计的要求建议用类封装这样能多展示面向对象的能力。class TetrisGame: def __init__(self): self.board [[0 for _ in range(COLS)] for _ in range(ROWS)] self.score 0 self.game_over False self.current_shape None self.current_color None self.offset_x COLS // 2 - 1 self.offset_y 0 self.spawn_block()spawn_block负责随机选择一个形状作为当前方块并把它放在面板顶部中间位置def spawn_block(self): name random.choice(list(SHAPES.keys())) self.current_shape [row[:] for row in SHAPES[name]] self.offset_x COLS // 2 - len(self.current_shape[0]) // 2 self.offset_y 0 # 如果生成位置已经被占判定游戏结束 if not is_valid_position(self.board, self.current_shape, self.offset_x, self.offset_y): self.game_over True4.2 移动、旋转与落定移动逻辑很简单不细说。旋转需要特别注意I形方块旋转后的形状变化。完整实现是这样def rotate(self): if self.current_name O: return rotated [list(row) for row in zip(*self.current_shape[::-1])] new_x self.offset_x # 如果旋转后超右边界往左挪 while new_x len(rotated[0]) COLS: new_x - 1 # 如果旋转后超左边界往右挪 while new_x 0: new_x 1 if is_valid_position(self.board, rotated, new_x, self.offset_y): self.current_shape rotated self.offset_x new_x落定函数负责把当前方块写入面板同时触发消行检测def lock_block(self): for row_idx, row in enumerate(self.current_shape): for col_idx, cell in enumerate(row): if cell: board_row self.offset_y row_idx board_col self.offset_x col_idx if board_row 0: self.board[board_row][board_col] 1 self.check_clear_rows() self.spawn_block()这里有个细节落定前为什么不先检测一下游戏是否结束因为如果方块落定后新方块生成时碰撞spawn_block内部会设置game_over True然后主循环就可以显示游戏结束画面了。4.3 消行判定一行一行查简单粗暴但高效消行的逻辑很简单从最后一行往上扫描如果某一行全部为1就删除该行并在顶部补一行0。用代码写def check_clear_rows(self): rows_to_clear [] for row_idx in range(ROWS): if all(self.board[row_idx]): rows_to_clear.append(row_idx) for row_idx in rows_to_clear: del self.board[row_idx] self.board.insert(0, [0 for _ in range(COLS)]) if rows_to_clear: self.score len(rows_to_clear) ** 2 * 100计分公式我用了“消除行数的平方乘以100”比如一次消一行得100分一次消四行经典方块里的Tetris得1600分。这样做的好处是鼓励玩家攒四行一起消比固定分值更有策略性。4.4 渲染界面画出面板和方块绘制环节不难但是要心细。主要分三块画面板背景、画面板里已落定的格子、画当前活动方块。每个格子调用pygame.draw.rect画带边框的矩形def draw_block(surface, color, col, row): x col * CELL_SIZE y row * CELL_SIZE pygame.draw.rect(surface, color, (x, y, CELL_SIZE, CELL_SIZE)) pygame.draw.rect(surface, (40, 40, 40), (x, y, CELL_SIZE, CELL_SIZE), 1)主循环里每次先填充背景色再画所有格子最后画分数文字flip()显示。文字用的是pygame.font.SysFont(Arial, 24)中文的话需要指定中文字体路径否则会显示成方块。我建议直接用英文界面关键词省去字体配置的麻烦。颜色我建议每种形状给一个固定颜色这样游戏界面辨识度高、看起来也更专业。用字典维护COLORS { I: (0, 255, 255), O: (255, 255, 0), T: (160, 32, 240), S: (0, 255, 0), Z: (255, 0, 0), J: (0, 0, 255), L: (255, 165, 0) }5. 常见问题与排查技巧实录5.1 运行报错的三个高频原因第一个坑也是我遇到次数最多的pygame模块没装对。表面上是import pygame报ModuleNotFoundError实际原因可能是在VSCode里用的解释器和装pygame的解释器不是同一个。排查方法很简单——在编辑器里跑一下import sys; print(sys.executable)再把打印出来的解释器路径和pip show pygame的路径对比一下不一样就说明解释器选错了。第二个坑窗口一开就闪退代码还没看清楚就没了。这种情况多半是主循环里的pygame.quit()和sys.exit()被意外执行或者event.type判断写成了pygame.KEYDOWN而不是pygame.QUIT。建议在主程序外层加一个try包裹异常时打印详细信息避免静默退出。第三个坑下落太快导致按键还没反应方块就到底了。这是因为定时器时间设得太短比如200毫秒内你按了好几下方向键方块也同时下移了好几格。我的建议是把初始下落速度设置在500到800毫秒之间如果玩家需要加速下落单独监听pygame.KEYDOWN里的下方向键手动调用一次下落逻辑就行。5.2 游戏逻辑的边界Bug排查俄罗斯方块这类游戏最烦的bug是“旋转后卡进墙里”。排查思路在旋转函数里打印旋转前后的矩阵手动核对超出边界的格子。我之前发现I形方块横着的状态旋转成竖着的时候宽度从4变成1如果方块初始位置靠右旋转后坐标没有修正就会瞬间撞墙。解决方案就是我在4.2节里写的两个while循环左超了就右移右超了就左移。另一个容易被忽略的bug面板底部越界。当你下落到底部时offset_y加上方块矩阵的高度可能刚好是20这实际上已经超过面板底部了。检测函数里必须写board_y 20直接判不合法否则方块会“嵌”进底部墙体。这个bug在初始测试阶段很难发现直到方块堆到一定高度后才会不断触发排查起来比较费劲。还有一个纯逻辑上的坑方块落定后如果同时消了两行分数应该怎么算。我在check_clear_rows里先收集所有需要消除的行号再统一删除可以避免“边清边插”导致的行位置错乱。如果你一开始写成“检测一行消一行”那在同时消多行时很容易计算出错建议统一处理。5.3 课程答辩时老师喜欢问的问题去年我带过几个学弟学妹的课程设计俄罗斯方块是出现率最高的项目之一。答辩时老师提问主要集中在下面几个方向is_valid_position函数的时间复杂度是多少能否优化其实遍历方块矩阵最多4✕4等于16个格子常量级别的开销不需要优化但你要能答出来。如何做到方块的随机生成回答用random.choice从形状字典里随机取一个键。旋转算法为什么能实现顺时针旋转这里你要把转置和翻转的原理解释清楚面试官听到你能从数学角度解释会非常加分。如何扩展新的游戏形状你只需要在SHAPES字典里加一个新的矩阵代码零改动。如何实现砖块的高亮特效或者动画消除这种属于加分区如果你有时间可以做一下。一些个人的收尾建议这个项目做完之后我自己感触最深的一点是所有看起来神奇的“游戏逻辑”拆开来看都是最基础的循环、列表、函数组合。俄罗斯方块的核心就是二维数组的读写和状态判断你把这个吃透了后面再去写贪吃蛇、五子棋、飞机大战思路基本是通用的。最后再分享一个关于做课程设计的小技巧不要憋大招先把完整的代码跑起来哪怕很丑很简陋再一点点往里面加功能、调样式。程序这东西跑起来就成功了一大半。你卡住的时候把报错信息复制去搜索引擎搜绝大多数问题都能找到答案——你要相信你踩过的坑前面一定有人替你踩过并且把解决方案发在了某个角落。先把核心逻辑写对再追求美观和流畅这是我做Python项目以来最实在的一条经验。本文还有配套的精品资源点击获取