俄罗斯方块(Tetris)是一个经典的益智游戏,核心逻辑包括:**方块生成与下落、旋转、碰撞检测、消行(line clear)、积分与等级系统、游戏结束判定**等。下面我将为你提供一个**完整、可运行、结构清晰的 Python 版本俄罗斯方块项目**,使用 `pygame` 库实现图形界面(轻量、跨平台、适合教学与原型开发)。
> ✅ 要求环境:Python 3.7+,已安装 `pygame`(可通过 `pip install pygame` 安装)
---
### ✅ 项目结构概览(单文件精简版)
我们将所有逻辑封装在一个 `.py` 文件中(便于理解与快速启动),包含以下关键模块:
- `Tetromino` 类:表示七种标准方块(I, O, T, S, Z, J, L)及其旋转逻辑
- `Board` 类:游戏主面板(10×20 网格),负责碰撞检测、固定方块、消行、检查游戏结束
- 主游戏循环:事件处理(左右/下/旋转/暂停)、自动下落计时、状态管理(running / paused / game_over)
---
### ✅ 完整可运行代码(Python + Pygame)
```python
import pygame
import random
import sys
# -------------------------------
# 配置常量
# -------------------------------
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 700
GRID_SIZE = 30
GRID_WIDTH = 10
GRID_HEIGHT = 20
GRID_OFFSET_X = (SCREEN_WIDTH - GRID_WIDTH * GRID_SIZE) // 2
GRID_OFFSET_Y = 50
# 颜色定义(RGB)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (40, 40, 40)
GRID_COLOR = (50, 50, 50)
# 方块颜色(按经典 Tetris 配色)
COLORS = [
(0, 0, 0), # 空单元格 → 黑色(不绘制)
(0, 255, 255), # I → 青色
(255, 255, 0), # O → 黄色
(128, 0, 128), # T → 紫色
(0, 255, 0), # S → 绿色
(255, 0, 0), # Z → 红色
(0, 0, 255), # J → 蓝色
(255, 165, 0), # L → 橙色
]
# 七种方块的形状(相对坐标,以左上角为原点;中心点为索引[0],用于旋转锚点)
SHAPES = [
[[0, 0, 0, 0]], # placeholder for index 0
[[0, 1, 1, 1, 1]], # I
[[0, 0, 0], [0, 1, 1], [1, 1, 0]], # O
[[0, 0, 1], [0, 1, 1], [0, 0, 1]], # T
[[0, 0, 1], [0, 1, 1], [0, 1, 0]], # S
[[1, 0, 0], [1, 1, 0], [0, 1, 0]], # Z
[[1, 0, 0], [1, 1, 1], [0, 0, 0]], # J
[[0, 0, 1], [1, 1, 1], [0, 0, 0]], # L
]
# -------------------------------
# 方块类:Tetromino
# -------------------------------
class Tetromino:
def __init__(self, x, y, shape_idx):
self.x = x
self.y = y
self.shape_idx = shape_idx
self.shape = SHAPES[shape_idx]
self.color = COLORS[shape_idx]
# 旋转次数(0~3),用于缓存旋转后形态
self.rotation = 0
def rotate(self):
# 顺时针旋转矩阵:先转置,再水平翻转
rows = len(self.shape)
cols = len(self.shape[0])
rotated = [[0] * rows for _ in range(cols)]
for r in range(rows):
for c in range(cols):
rotated[c][rows - 1 - r] = self.shape[r][c]
return rotated
def get_positions(self):
"""返回当前旋转状态下所有占用的 (r, c) 坐标(相对自身)"""
shape = self.shape
if self.rotation % 4 == 1:
shape = self.rotate()
elif self.rotation % 4 == 2:
shape = self.rotate()
shape = self.rotate()
elif self.rotation % 4 == 3:
shape = self.rotate()
shape = self.rotate()
shape = self.rotate()
positions = []
for r, row in enumerate(shape):
for c, cell in enumerate(row):
if cell:
positions.append((r, c))
return positions
def get_absolute_positions(self):
"""返回在游戏板上的绝对坐标列表 [(y, x)]"""
rel_pos = self.get_positions()
return [(self.y + r, self.x + c) for r, c in rel_pos]
# -------------------------------
# 游戏主面板类:Board
# -------------------------------
class Board:
def __init__(self):
self.grid = [[0 for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] # 0=空,1~7=方块类型
self.score = 0
self.level = 1
self.lines_cleared = 0
self.game_over = False
self.paused = False
def is_valid_position(self, tetromino):
"""检查方块是否在合法位置(不越界、不重叠)"""
for r, c in tetromino.get_absolute_positions():
if r < 0 or r >= GRID_HEIGHT or c < 0 or c >= GRID_WIDTH:
return False
if r >= 0 and self.grid[r][c] != 0:
return False
return True
def lock_tetromino(self, tetromino):
"""将方块“固定”到面板上"""
for r, c in tetromino.get_absolute_positions():
if 0 <= r < GRID_HEIGHT and 0 <= c < GRID_WIDTH:
self.grid[r][c] = tetromino.shape_idx
def clear_lines(self):
"""消行并更新分数"""
lines_to_clear = []
for r in range(GRID_HEIGHT):
if all(self.grid[r]):
lines_to_clear.append(r)
# 从下往上删除,避免索引偏移
for r in reversed(lines_to_clear):
del self.grid[r]
self.grid.insert(0, [0] * GRID_WIDTH)
n = len(lines_to_clear)
if n > 0:
# 经典得分:1行=100, 2行=300, 3行=500, 4行=800
points = [0, 100, 300, 500, 800][min(n, 4)]
self.score += points * self.level
self.lines_cleared += n
self.level = self.lines_cleared // 10 + 1
def check_game_over(self, tetromino):
"""检查新生成方块是否一出生就重叠(即游戏结束)"""
for r, c in tetromino.get_absolute_positions():
if r < 0 and self.grid[r][c] != 0:
return True
return False
# -------------------------------
# 主游戏逻辑
# -------------------------------
def main():
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Tetris - Python Pygame")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 28)
small_font = pygame.font.SysFont(None, 24)
board = Board()
falling_piece = None
next_piece = None
last_fall_time = 0
fall_speed = 0.5 # 秒/格(随等级加快)
move_cooldown = 0
move_delay = 0.1 # 连续移动延迟(秒)
def new_piece():
nonlocal falling_piece, next_piece
if next_piece is None:
shape_idx = random.randint(1, 7)
else:
shape_idx = next_piece.shape_idx
next_piece = Tetromino(GRID_WIDTH // 2 - 1, 0, random.randint(1, 7))
falling_piece = Tetromino(GRID_WIDTH // 2 - 1, 0, shape_idx)
if not board.is_valid_position(falling_piece):
board.game_over = True
new_piece()
# 主循环
running = True
while running:
current_time = pygame.time.get_ticks() / 1000.0
dt = clock.tick(60) / 1000.0
# --- 事件处理 ---
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if board.game_over or board.paused:
if event.key == pygame.K_r:
# 重置游戏
board = Board()
new_piece()
board.game_over = False
board.paused = False
elif event.key == pygame.K_ESCAPE:
running = False
else:
if event.key == pygame.K_LEFT:
falling_piece.x -= 1
if not board.is_valid_position(falling_piece):
falling_piece.x += 1
elif event.key == pygame.K_RIGHT:
falling_piece.x += 1
if not board.is_valid_position(falling_piece):
falling_piece.x -= 1
elif event.key == pygame.K_DOWN:
falling_piece.y += 1
if not board.is_valid_position(falling_piece):
falling_piece.y -= 1
board.lock_tetromino(falling_piece)
board.clear_lines()
new_piece()
elif event.key == pygame.K_UP:
falling_piece.rotation += 1
if not board.is_valid_position(falling_piece):
falling_piece.rotation -= 1
elif event.key == pygame.K_SPACE: # 硬降
while board.is_valid_position(falling_piece):
falling_piece.y += 1
falling_piece.y -= 1
board.lock_tetromino(falling_piece)
board.clear_lines()
new_piece()
elif event.key == pygame.K_p:
board.paused = not board.paused
# --- 自动下落逻辑 ---
if not board.game_over and not board.paused:
if current_time - last_fall_time > fall_speed / board.level:
last_fall_time = current_time
falling_piece.y += 1
if not board.is_valid_position(falling_piece):
falling_piece.y -= 1
board.lock_tetromino(falling_piece)
board.clear_lines()
new_piece()
# --- 渲染 ---
screen.fill(BLACK)
# 绘制网格背景
pygame.draw.rect(screen, GRAY, (
GRID_OFFSET_X - 2,
GRID_OFFSET_Y - 2,
GRID_WIDTH * GRID_SIZE + 4,
GRID_HEIGHT * GRID_SIZE + 4
))
for r in range(GRID_HEIGHT):
for c in range(GRID_WIDTH):
color = COLORS[board.grid[r][c]]
rect = pygame.Rect(
GRID_OFFSET_X + c * GRID_SIZE,
GRID_OFFSET_Y + r * GRID_SIZE,
GRID_SIZE, GRID_SIZE
)
pygame.draw.rect(screen, color, rect)
pygame.draw.rect(screen, GRID_COLOR, rect, 1)
# 绘制正在下落的方块
if falling_piece and not board.game_over:
for r, c in falling_piece.get_absolute_positions():
if 0 <= r < GRID_HEIGHT and 0 <= c < GRID_WIDTH:
color = falling_piece.color
rect = pygame.Rect(
GRID_OFFSET_X + c * GRID_SIZE,
GRID_OFFSET_Y + r * GRID_SIZE,
GRID_SIZE, GRID_SIZE
)
pygame.draw.rect(screen, color, rect)
pygame.draw.rect(screen, (30, 30, 30), rect, 1)
# 绘制 UI 信息
score_text = font.render(f"Score: {board.score}", True, WHITE)
level_text = font.render(f"Level: {board.level}", True, WHITE)
lines_text = font.render(f"Lines: {board.lines_cleared}", True, WHITE)
screen.blit(score_text, (20, 20))
screen.blit(level_text, (20, 60))
screen.blit(lines_text, (20, 100))
# 下一个方块预览
next_text = font.render("Next:", True, WHITE)
screen.blit(next_text, (SCREEN_WIDTH - 150, 20))
if next_piece:
# 在右侧绘制 4x4 预览区
preview_x = SCREEN_WIDTH - 130
preview_y = 60
for r, row in enumerate(SHAPES[next_piece.shape_idx]):
for c, cell in enumerate(row):
if cell:
rect = pygame.Rect(
preview_x + c * 20,
preview_y + r * 20,
20, 20
)
pygame.draw.rect(screen, next_piece.color, rect)
pygame.draw.rect(screen, (30, 30, 30), rect, 1)
# 游戏状态提示
if board.game_over:
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 180))
screen.blit(overlay, (0, 0))
game_over_text = font.render("GAME OVER! Press R to Restart", True, (255, 50, 50))
screen.blit(game_over_text, (SCREEN_WIDTH//2 - game_over_text.get_width()//2, SCREEN_HEIGHT//2))
elif board.paused:
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
screen.blit(overlay, (0, 0))
pause_text = font.render("PAUSED — Press P to Resume", True, WHITE)
screen.blit(pause_text, (SCREEN_WIDTH//2 - pause_text.get_width()//2, SCREEN_HEIGHT//2))
# 控制说明
help_lines = [
"CONTROLS:",
"← → : Move",
"↑ : Rotate",
"↓ : Soft Drop",
"SPACE : Hard Drop",
"P : Pause",
"R : Restart",
"ESC : Quit"
]
for i, line in enumerate(help_lines):
txt = small_font.render(line, True, (180, 180, 180))
screen.blit(txt, (20, SCREEN_HEIGHT - 160 + i * 24))
pygame.display.flip()
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()
```
---
### ✅ 代码解释(关键设计说明)
| 模块 | 说明 |
|------|------|
| **`SHAPES` 和 `COLORS`** | 使用二维列表定义7种方块的 0/1 形状模板(兼容旋转),颜色严格对应 Tetris 官方配色(如 I=青色)。注意 `SHAPES[0]` 是占位符,索引 1~7 对应 I/O/T/S/Z/J/L。 |
| **`Tetromino.rotate()`** | 实现顺时针90°旋转:通过矩阵转置 + 行翻转。每次调用返回新矩阵,不修改原 `shape`,靠 `rotation` 属性控制当前形态。 |
| **`get_absolute_positions()`** | 将相对坐标 `(r,c)` 转为全局网格坐标 `(y,x)`,是碰撞检测和渲染的基础。 |
| **`Board.is_valid_position()`** | 核心安全检查:是否出界?是否与已固定方块重叠?确保所有操作(移动/旋转/下落)前验证。 |
| **消行逻辑** | 逐行检查 `all(grid[r])`,用 `del + insert(0,...)` 实现高效下移(比复制更直观)。得分采用经典公式(1/2/3/4行 → 100/300/500/800 × level)。 |
| **硬降(SPACE)** | 循环下移直到触底,然后立即锁定并消行,提升操作反馈速度。 |
| **UI 设计** | 包含实时分数/等级/消行数、右侧“Next”预览、半透明遮罩提示、底部控制说明,兼顾可玩性与可读性。 |
✅ **运行效果**:支持键盘操作、暂停、重开、硬降、自动加速(每10行升一级),符合经典 Tetris 体验。
---
### ⚠️ 注意事项(进阶优化方向)
- 当前旋转未做“踢墙”(wall kick)处理(即旋转卡墙时尝试微调位置),真实 Tetris 有此机制;
- 无“hold”功能(暂存方块);
- 随机序列使用 `random.randint`,非真正随机袋(bag)算法(7个不重复后重排),可升级为 `random.sample([1..7],7)` 循环;
- 音效、粒子特效、更高分辨率适配等可后续添加。
---