Python实现俄罗斯方块:从零构建游戏逻辑与碰撞检测

📅 2026/7/19 4:23:39
Python实现俄罗斯方块:从零构建游戏逻辑与碰撞检测
1. 项目概述为什么现在还要手搓俄罗斯方块“俄罗斯方块”这个名字对很多人来说可能意味着童年、像素块和单调的“滴滴”声。但如果你是一个刚开始学Python或者想通过一个完整项目来巩固编程思维的朋友那这个经典游戏绝对是一个被低估的宝藏。很多人觉得现在游戏引擎这么发达各种炫酷的3A大作都玩不过来谁还去写一个这么“古老”的游戏这恰恰是误区所在。这个项目的核心价值不在于做出一个多么华丽的游戏而在于它是一套近乎完美的、浓缩的软件工程训练模型。它麻雀虽小五脏俱全事件驱动、状态管理、碰撞检测、矩阵运算、渲染逻辑、用户交互这些现代游戏乃至很多应用软件的核心概念你都能在一个几百行的Python脚本里亲手实现一遍。用大白话讲这就是一个“编程思维健身房”你能在这里练到几乎所有基础但至关重要的“肌肉群”。我见过不少新手学了一堆语法看了无数教程但一到自己动手就懵了。问题往往出在“如何把想法变成代码”这个环节。俄罗斯方块提供了一个边界清晰、目标明确的问题域一个7种形状的方块我们叫它“Tetromino”从顶部下落你可以左右移动、旋转它当它落到底部或碰到其他方块时固定然后消除填满的行。这个规则本身就是一个绝佳的逻辑拆解练习题。所以别把它当成一个怀旧小游戏。我们这次“从零开始撸”目标是用最直白的Python代码不用复杂的游戏引擎把背后的每一行逻辑都掰开揉碎了讲清楚。当你跟着走完这一趟收获的不仅仅是一个能运行的.py文件更是一套解决复杂问题的结构化思维方法。下面我们就从最核心的“积木”开始。2. 核心设计如何用代码“描述”俄罗斯方块的世界写代码就像盖房子得先有图纸。在动手敲键盘之前我们必须想清楚怎么用Python的数据结构来构建俄罗斯方块这个虚拟世界。这里没有图形界面那些花哨的东西我们要先搭建一个纯粹的“逻辑模型”。2.1 游戏场地的数字化网格与状态首先我们需要一个游戏场地。在屏幕上它是一个矩形区域在代码里最自然的表示方式就是一个二维列表也就是一个矩阵。我们可以把它想象成一个有很多小格子的棋盘。# 假设我们的游戏区域是10列宽20行高 BOARD_WIDTH 10 BOARD_HEIGHT 20 # 初始化游戏板 # 0 表示该格子为空1 表示该格子已被方块占据 board [[0 for _ in range(BOARD_WIDTH)] for _ in range(BOARD_HEIGHT)]这个board变量就是我们世界的核心。所有方块的移动、固定、消除都围绕着操作这个二维列表展开。为什么用0和1简单高效。检查一行是否填满只需要判断这一行是否全是1检查碰撞只需要看目标位置是否是1。这种用简单数字代表状态的思想在游戏开发里非常普遍。注意这里有一个初学者常踩的坑。如果你用board [[0] * BOARD_WIDTH] * BOARD_HEIGHT这种方式初始化你会得到一个“连环引用”的列表。修改board[0][0]会导致board[1][0],board[2][0]… 全部被修改因为内部的每个子列表都是同一个对象的引用。所以务必使用列表推导式来创建独立的子列表。2.2 七种方块的数学化表达俄罗斯方块有7种基本形状分别是I、J、L、O、S、T、Z。每种形状由4个小方块我们称之为“Minos”组成。在代码里我们不需要存储它们的图片只需要存储它们每个方块相对于形状中心的坐标。一个非常经典且高效的方法是为每种形状预定义其所有可能的旋转状态。每种形状用一个包含多个“形态”的列表来表示每个“形态”是一个由(x, y)坐标元组组成的列表。# 形状定义每个形状是一个列表包含其所有旋转形态。 # 每个形态是一个由 (x, y) 坐标组成的列表坐标是相对于形状“中心”或某个参考点的。 SHAPES { ‘I‘: [ [(0, -1), (0, 0), (0, 1), (0, 2)], [(-1, 0), (0, 0), (1, 0), (2, 0)] ], ‘O‘: [ [(0, 0), (1, 0), (0, 1), (1, 1)] ], ‘T‘: [ [(-1, 0), (0, 0), (1, 0), (0, 1)], [(0, -1), (0, 0), (0, 1), (-1, 0)], [(-1, 0), (0, 0), (1, 0), (0, -1)], [(0, -1), (0, 0), (0, 1), (1, 0)] ], # ... 省略 J, L, S, Z 的定义逻辑类似 }以‘T‘形为例它第一个形态的坐标是 [(-1, 0), (0, 0), (1, 0), (0, 1)]在网格上画出来就是一个“T”字。这种表示法的好处是旋转操作变得极其简单。旋转一个形状本质上就是在它的形态列表里循环切换索引。比如当前是‘T‘形的第0个形态顺时针旋转一次就切换到第1个形态。同时我们需要一个“当前方块”对象来跟踪正在下落的方块。这个对象至少需要包含以下信息形状类型、当前旋转形态的索引、以及它在游戏板上的位置通常用最左上角或中心点的坐标表示。class Tetromino: def __init__(self, shape_type, x, y): self.type shape_type # 形状类型如 ‘I‘, ‘T‘ self.rotation 0 # 当前旋转索引 self.x x # 在游戏板上的列坐标通常指形状的某个参考点 self.y y # 在游戏板上的行坐标这样我们就用纯粹的数据构建起了俄罗斯方块世界的两大支柱静态的棋盘和动态的积木。接下来就要让它们根据规则互动起来。3. 游戏逻辑的心脏碰撞检测与状态更新有了数据模型游戏要“活”起来全靠一套严密的逻辑来驱动。这部分代码就像是游戏的大脑它需要不断回答三个问题方块能移动吗方块该固定了吗有行可以消除吗3.1 碰撞检测一切交互的前提碰撞检测是游戏逻辑中最基础也最关键的一环。在俄罗斯方块里碰撞主要发生在当前下落的方块试图移动或旋转时与“边界”或“已固定的方块”发生冲突。我们需要一个通用的check_collision函数。它的逻辑是给定一个假设的方块状态类型、旋转、位置计算其四个小方块在游戏板上的实际坐标然后逐一检查这些坐标点。def check_collision(board, shape_type, rotation, x, y): “”” 检查给定状态的方块是否与边界或已有方块碰撞。 Args: board: 游戏板二维列表 shape_type: 方块类型 rotation: 旋转索引 x, y: 方块参考点坐标 Returns: bool: True 表示碰撞False 表示安全。 “”” # 1. 获取当前旋转形态的局部坐标 shape_data SHAPES[shape_type] minos shape_data[rotation] # 例如[(0,0), (1,0), (0,1), (1,1)] for dx, dy in minos: # 2. 计算每个小方块在游戏板上的绝对坐标 col x dx row y dy # 3. 进行碰撞判断 # 条件1: 是否超出左、右、下边界(上边界通常允许超出用于新方块出现) if col 0 or col BOARD_WIDTH or row BOARD_HEIGHT: return True # 条件2: 是否已经落到游戏板底部其实已被条件1包含但更严格的判断是 row 0? # 条件3: 该位置是否已被其他方块占据board[row][col] 1 # 注意需要先判断 row 0因为新方块可能有一部分初始位置在板子顶部之外行坐标为负 if row 0 and board[row][col]: return True return False这个函数是后续所有操作移动、旋转、下落的“守门员”。任何试图改变方块状态的操作都必须先通过它的检查。实操心得在写碰撞检测时最容易出错的是对“行坐标”下界的处理。新方块通常在板子顶部上方生成其部分小方块的row坐标可能是负数表示在屏幕外。我们的碰撞检测必须允许这种情况否则方块一出来就“碰撞”了。所以判断board[row][col]之前一定要加上if row 0这个条件。3.2 方块固定与行消除更新游戏世界当方块无法继续下落时即向下移动一格会发生碰撞它就需要被“固定”在游戏板上。这个过程分为两步将方块写入棋盘然后检查并消除填满的行。固定方块很简单遍历当前方块的四个小方块将其在board中对应的位置标记为1已占据。def lock_piece(board, piece): “””将当前方块固定到游戏板上。“”” shape_data SHAPES[piece.type] minos shape_data[piece.rotation] for dx, dy in minos: row piece.y dy col piece.x dx # 再次确保坐标有效虽然理论上不应该无效 if 0 row BOARD_HEIGHT and 0 col BOARD_WIDTH: board[row][col] 1行消除是俄罗斯方块最爽快的时刻也是逻辑上需要小心处理的部分。我们不能直接从中间删除一行因为上面的行会掉下来。一个清晰的做法是从下往上扫描发现满行后将其删除并在顶部插入新的空行。def clear_lines(board): “””消除所有已填满的行并返回消除的行数。“”” lines_cleared 0 row BOARD_HEIGHT - 1 # 从最底部开始扫描 while row 0: if all(board[row]): # 如果这一行全部是1即被填满 # 删除这一行 del board[row] # 在顶部插入一个新的空行 board.insert(0, [0 for _ in range(BOARD_WIDTH)]) lines_cleared 1 # 注意删除一行后当前row索引指向的是原来上一行的内容所以不递减row继续检查这个“新”行 else: # 这一行没满检查上一行 row - 1 return lines_cleared这里用all(board[row])来判断一行是否满非常简洁。del和insert操作模拟了方块下落的物理效果。消除行数可以用来计算分数并且消除的行数越多如一次消除4行即Tetris得分应该呈几何级数增长这是增加游戏策略性的关键。4. 游戏主循环与用户输入让世界动起来逻辑模型和规则都有了现在需要一根“时间线”把它们串起来并响应用户的操作。这就是游戏主循环。对于控制台版本的俄罗斯方块我们可以用time模块和键盘监听如keyboard库或msvcrt来实现。4.1 构建游戏主循环的骨架主循环是游戏持续运行的核心。每一帧或每一个时间间隔它都按顺序做以下几件事处理用户的键盘输入移动、旋转、加速下落。更新游戏状态让方块自动下落一格。检查游戏是否结束。重新绘制游戏画面。import time def main_game_loop(): # 初始化 board new_board() current_piece new_random_piece() # 创建一个随机新方块 game_over False fall_time 0 fall_speed 0.5 # 方块自动下落的时间间隔秒 while not game_over: start_time time.time() # 1. 处理输入这里用伪代码表示实际依赖具体输入库 handle_input(current_piece, board) # 2. 更新状态基于时间的自动下落 fall_time time.time() - start_time if fall_time fall_speed: if not move_piece(current_piece, board, 0, 1): # 尝试向下移动 # 如果无法下落则固定方块 lock_piece(board, current_piece) lines clear_lines(board) # 根据消除的行数更新分数、速度等 update_score(lines) # 创建下一个新方块 current_piece new_random_piece() # 检查新方块是否立即碰撞如果是则游戏结束 if check_collision(board, current_piece.type, current_piece.rotation, current_piece.x, current_piece.y): game_over True fall_time 0 # 重置下落计时器 # 3. 渲染画面 render(board, current_piece) # 4. 控制帧率避免循环跑得太快耗尽CPU time.sleep(0.05)这个循环结构是经典的游戏循环模式。fall_speed控制了游戏难度随着分数增加可以逐渐减小这个值让方块下落更快。4.2 实现用户输入与控制在控制台环境下获取实时键盘输入有点麻烦因为input()函数是阻塞的。在Windows上可以使用msvcrt模块为了跨平台我们也可以使用keyboard库需要安装或者curses库在类Unix系统上更常见。这里以概念清晰的伪代码和keyboard库为例import keyboard # 需要 pip install keyboard def handle_input(piece, board): “””非阻塞地检查按键状态并处理。“”” if keyboard.is_pressed(‘left‘): move_piece(piece, board, -1, 0) # 向左移动 if keyboard.is_pressed(‘right‘): move_piece(piece, board, 1, 0) # 向右移动 if keyboard.is_pressed(‘down‘): move_piece(piece, board, 0, 1) # 加速下落软降 if keyboard.is_pressed(‘up‘): rotate_piece(piece, board) # 旋转 if keyboard.is_pressed(‘space‘): hard_drop(piece, board) # 硬降直接落到底move_piece函数内部会调用我们之前写的check_collision只有安全的情况下才会更新方块的位置。rotate_piece函数则尝试将方块的旋转索引加1取模以循环并检查旋转后是否碰撞。如果碰撞可以尝试进行“踢墙”操作——即旋转后如果发生碰撞将方块稍微左右移动一下看看能否容身这是现代俄罗斯方块的标准规则能让操作更舒适。def rotate_piece(piece, board): old_rotation piece.rotation piece.rotation (piece.rotation 1) % len(SHAPES[piece.type]) if check_collision(board, piece.type, piece.rotation, piece.x, piece.y): # 如果旋转后碰撞尝试“踢墙”向左、向右微调位置 for dx in [-1, 1, -2, 2]: # 尝试不同的偏移量 if not check_collision(board, piece.type, piece.rotation, piece.x dx, piece.y): piece.x dx return True # 所有偏移都失败恢复原旋转状态 piece.rotation old_rotation return False return True5. 控制台渲染在终端里画出游戏世界到目前为止我们的游戏逻辑都在内存里运行。为了让玩家看到并互动我们需要将board和current_piece的状态“画”出来。在控制台里我们只能用字符来模拟图形界面。5.1 设计渲染逻辑与画面合成渲染的基本思路是先准备一个和游戏板一样大小的“画面缓冲区”但这个缓冲区需要包含当前正在下落的方块。所以我们不能直接渲染board而是需要创建一个临时副本把当前方块“画”进去再一起渲染。def render(board, current_piece): # 1. 创建画面缓冲区复制当前游戏板状态 # 我们用一个字符列表的列表来表示方便修改 screen_buffer [[‘.‘ for _ in range(BOARD_WIDTH)] for _ in range(BOARD_HEIGHT)] for row in range(BOARD_HEIGHT): for col in range(BOARD_WIDTH): if board[row][col]: screen_buffer[row][col] ‘#‘ # 用 ‘#‘ 表示已固定的方块 # 2. 将当前正在下落的方块画入缓冲区 shape_data SHAPES[current_piece.type] minos shape_data[current_piece.rotation] for dx, dy in minos: row current_piece.y dy col current_piece.x dx if 0 row BOARD_HEIGHT and 0 col BOARD_WIDTH: screen_buffer[row][col] ‘‘ # 用 ‘‘ 表示当前活动的方块 # 3. 清屏并打印缓冲区 clear_screen() # 清屏函数例如打印多个换行符或使用系统命令 print(‘‘ ‘-‘ * BOARD_WIDTH ‘‘) # 打印上边界 for row in screen_buffer: print(‘|‘ ‘‘.join(row) ‘|‘) # 打印每一行内容 print(‘‘ ‘-‘ * BOARD_WIDTH ‘‘) # 打印下边界 # 打印分数、下一个方块预览等信息 print(f‘Score: {score}‘)clear_screen函数在不同操作系统上不一样。一个简单但可能不是最优的方法是打印大量的换行符print(‘\n‘ * 50)。更专业的方法是使用os.system(‘cls‘)Windows或os.system(‘clear‘)Linux/Mac。5.2 优化显示效果与交互反馈上面的基础渲染能跑但体验很粗糙。我们可以做很多优化颜色使用colorama库跨平台可以为不同的方块类型上色让画面更直观。双缓冲区直接渲染可能导致屏幕闪烁。更高级的做法是先在内存中准备好完整的帧字符串然后一次性打印。信息面板在游戏区域旁边显示下一个方块预览、当前分数、等级、消除的行数等。控制提示在画面底部固定显示操作按键说明。# 使用 colorama 的简单示例 from colorama import init, Fore, Back, Style init(autoresetTrue) # 自动重置颜色 def render_with_color(board, piece, next_piece): # ... 合成缓冲区的逻辑同上 ... # 在绘制时根据格子内容决定颜色 for row in range(BOARD_HEIGHT): for col in range(BOARD_WIDTH): cell screen_buffer[row][col] if cell ‘#‘: print(Fore.CYAN ‘■‘, end‘‘) # 固定方块用青色 elif cell ‘‘: print(Fore.YELLOW ‘■‘, end‘‘) # 当前方块用黄色 else: print(‘ ‘, end‘‘) # 空位用两个空格 print(‘|‘) # ...渲染是游戏给玩家的直接感官虽然控制台有限但通过精心设计的字符和颜色依然可以做出清晰、有吸引力的界面。关键是稳定和清晰确保玩家能准确判断方块的位置和即将落下的地点。6. 完整代码整合与深度优化点将上述所有模块组合起来就是一个可运行的、控制台版的俄罗斯方块。完整的代码会有点长但结构应该是清晰的定义常量、形状数据、游戏板、方块类然后是实现各种功能的函数碰撞检测、移动旋转、消除行、渲染最后是组织这一切的主循环。6.1 代码结构组织建议一个良好的项目结构有助于维护和阅读。建议按以下方式组织你的tetris.py# -*- coding: utf-8 -*- “”” 俄罗斯方块 (Tetris) - Python 控制台实现 “”” import random import time import os # --- 常量定义 --- BOARD_WIDTH 10 BOARD_HEIGHT 20 # ... 其他常量如颜色、初始速度等 # --- 形状定义 --- SHAPES { ... } # --- 游戏状态类 --- class Tetromino: ... class GameState: ... # 可选用于封装板、当前方块、分数、速度等所有状态 # --- 核心逻辑函数 --- def new_board(): ... def check_collision(...): ... def rotate_piece(...): ... def move_piece(...): ... def lock_piece(...): ... def clear_lines(...): ... def new_random_piece(...): ... # --- 渲染函数 --- def draw_board(...): ... def draw_info_panel(...): ... # --- 输入处理平台相关--- def get_input(...): ... # --- 主游戏循环 --- def main(): # 初始化游戏状态 # 游戏主循环 # 游戏结束处理 if __name__ “__main__“: main()6.2 从“能玩”到“好玩”可扩展的优化方向一个基础版本完成后你可以从以下几个方向深化这会让你的项目从“作业级”提升到“作品级”难度曲线实现一个随着消除行数增加而提高下落速度的系统。例如每消除10行下落间隔fall_speed减少10%。计分系统设计一个更有激励性的计分规则。经典规则如单消100分双消300分三消500分四消Tetris800分。连续消除Combo还有额外奖励。下一个方块预览在画面旁显示下一个即将出现的方块这是标准功能能让玩家提前规划。暂存功能按某个键如C键可以将当前方块暂存起来之后可以换出。这大大增加了策略深度。“幽灵”方块在游戏区域用半透明或虚线显示当前方块如果直接硬降会落在哪里这是一个非常友好的辅助功能。音效虽然控制台实现音效较复杂但可以尝试用winsoundWindows或简单的打印字符来模拟消除、移动的声音反馈。游戏状态持久化实现保存/加载游戏进度、记录最高分存入文件的功能。个人经验与避坑指南关于随机性使用random.choice(list(SHAPES.keys()))来随机选择方块类型看似公平但真正的俄罗斯方块如Guideline规则使用“7-Bag”随机生成器。它会将7种形状打乱放入一个“袋子”按顺序出完7个后再重新打乱生成下一个袋子。这能有效防止长时间不出某种形状如长条I的极端情况。自己实现一个“7-Bag”生成器是很好的练习。关于旋转原点我们之前定义的形状坐标是相对于一个“参考点”的。对于‘O‘方块它旋转不变所以只有一个形态。对于其他方块旋转中心的选择会影响旋转的手感。通常选择让旋转看起来更自然、更符合直觉的点。如果你发现某个形状旋转后位置很奇怪可能需要调整其预定义坐标。控制台闪烁问题频繁清屏重绘会导致闪烁。一个有效的解决方法是使用curses库Unix-like系统或colorama配合光标定位如print(‘\033[{y};{x}H‘.format(yy, xx), end‘‘)来只更新屏幕上变化的部分而不是全屏刷新。代码测试在实现过程中特别是碰撞检测和旋转逻辑很容易出bug。不要等到全部写完再测试。可以写一些简单的测试脚本比如手动设置板子和方块位置调用check_collision看结果是否符合预期。或者在开发初期先做一个“单元测试模式”用固定的方块序列来验证核心逻辑。当你把这些优化点一个个实现你会发现自己对状态管理、事件处理、算法效率的理解会上一个大台阶。这个看似简单的项目就像一块试金石能清晰地反映出你对编程基础概念的掌握程度。