Crossin的编程教室

手把手教你用Python写一个俄罗斯方块

大家好,欢迎来到 Crossin 的编程教室。

今天我们来尝试用 Python 写一个《俄罗斯方块》游戏。

底层原理

在写代码之前,我们先把这个经典游戏拆解一下。其实,俄罗斯方块在计算机眼里,根本不是什么炫酷的动画,它只是一个“Excel表格”。

游戏场景 = Excel表格:别把游戏画面想得太复杂,它本质上就是一个大的数字表格,就像你平常在 Excel 里用的那种。有固定方块的地方,格子数值就是 1;空着的地方,格子数值就是 0。 

方块下落 = 盖章:所谓的“方块下落”,其实就是程序每隔一段时间,把方块的形状(一个小范围的数字矩阵)“贴”在网格的不同位置上。

消除一行 = 删掉Excel里全是1的行:当某一行被塞满了(全都是 1 ),就把这一行删掉,并在顶部补上一行全 0 的空行,让上面的所有行往下移一格。

接下来,我们就用 Python 的游戏神器 pygame 库,把这些逻辑一步步实现出来。

代码实现

为了让大家看得更明白,我们将游戏的核心逻辑拆分成 4 个关键部分。

1. 方块与地图初始化

原版的俄罗斯方块一共有 7 种,我们用 0 和 1 的矩阵把它们全部定义出来,同时初始化一个 20 x 10 填满 0 的游戏大地图。

# 游戏网格大小COLS, ROWS = 10, 20
# 补全 7 种传统方块形状SHAPES = [    [[1, 1, 1, 1]],  # I    [[1, 1, 1], [0, 1, 0]],  # T    [[1, 1], [1, 1]],  # O (田字)    [[1, 1, 0], [0, 1, 1]],  # Z    [[0, 1, 1], [1, 1, 0]],  # S    [[1, 1, 1], [1, 0, 0]],  # L    [[1, 1, 1], [0, 0, 1]]   # J]
# 初始化大地图:20行10列的二维列表,初始全为0game_field = [[0 for _ in range(COLS)] for _ in range(ROWS)]

2. 方块旋转与碰撞检测

方块旋转的本质就是矩阵翻转(将行变成列,并反转顺序)。

而方块在移动或旋转时,绝对不能穿墙或者穿过已经固定好的旧方块。因此,每次动作前都要进行“预判”。

# 碰撞检测函数:判断方块在指定位置是否合法def check_collision(shape, offset_x, offset_y):    for r_idx, row in enumerate(shape):        for c_idx, val in enumerate(row):            if val:                new_x = offset_x + c_idx                new_y = offset_y + r_idx                # 检查是否越界(左右边界、底部)或撞到已有方块                if new_x < 0 or new_x >= COLS or new_y >= ROWS:                    return True                if new_y >= 0 and game_field[new_y][new_x]:                    return True    return False
# 顺时针旋转矩阵def rotate_shape(shape):    return [list(x) for x in zip(*shape[::-1])]

3. 消行并得分

当某一行没有 0 的时候,说明被填满了。我们将其剔除,并在大地图最前面(顶部)塞入一行全新的 0。

def clear_lines():    global game_field, score    # 过滤掉全是1的行,只保留没满的行    new_field = [row for row in game_field if any(val == 0 for val in row)]    cleared = ROWS - len(new_field)  # 删掉了几行
    # 补齐上方空行    for _ in range(cleared):        new_field.insert(0, [0 for _ in range(COLS)])
    game_field = new_field    score += cleared * 100  # 每消一行加100分

4. 游戏结束与重玩

当新生成的方块在刚出生的地方就发生碰撞,说明堆积的高度已经到顶,游戏结束。此时按下回车键(`K_RETURN`)可以清空分数和地图,重新开始。

# 每次生成新方块时检测if check_collision(current_shape, block_x, block_y):    game_over = True  # 触发游戏结束开关
# 重置游戏函数def reset_game():    global game_field, score, game_over, current_shape, block_x, block_y    game_field = [[0 for _ in range(COLS)] for _ in range(ROWS)]    score = 0    game_over = False    current_shape = random.choice(SHAPES)    block_x, block_y = 3, 0

完整游戏代码

把上面的逻辑组装起来,加上画面的绘制和键盘事件响应,就得到了下面这个完整版程序:

import pygameimport random
# 初始化 pygamepygame.init()GRID_SIZE = 30COLS, ROWS = 10, 20SCREEN_WIDTH, SCREEN_HEIGHT = COLS * GRID_SIZE, ROWS * GRID_SIZE + 50 # 底部留白显示分数screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))pygame.display.set_caption("Crossin的俄罗斯方块")
# 颜色定义BLACK = (0, 0, 0)WHITE = (255, 255, 255)GRAY = (50, 50, 50)RED = (255, 87, 34)BLUE = (33, 150, 243)
# 7 种经典方块SHAPES = [    [[1, 1, 1, 1]],  # I    [[1, 1, 1], [0, 1, 0]],  # T    [[1, 1], [1, 1]],  # O    [[1, 1, 0], [0, 1, 1]],  # Z    [[0, 1, 1], [1, 1, 0]],  # S    [[1, 1, 1], [1, 0, 0]],  # L    [[1, 1, 1], [0, 0, 1]]   # J]
game_field = [[0 for _ in range(COLS)] for _ in range(ROWS)]score = 0game_over = False
current_shape = random.choice(SHAPES)block_x, block_y = 3, 0
clock = pygame.time.Clock()fall_time = 0fall_speed = 500  # 方块每 500 毫秒下落一格
def check_collision(shape, offset_x, offset_y):    for r_idx, row in enumerate(shape):        for c_idx, val in enumerate(row):            if val:                new_x = offset_x + c_idx                new_y = offset_y + r_idx                if new_x < 0 or new_x >= COLS or new_y >= ROWS:                    return True                if new_y >= 0 and game_field[new_y][new_x]:                    return True    return False
def rotate_shape(shape):    return [list(x) for x in zip(*shape[::-1])]
def clear_lines():    global game_field, score    new_field = [row for row in game_field if any(val == 0 for val in row)]    cleared = ROWS - len(new_field)    for _ in range(cleared):        new_field.insert(0, [0 for _ in range(COLS)])    game_field = new_field    score += cleared * 100
def reset_game():    global game_field, score, game_over, current_shape, block_x, block_y    game_field = [[0 for _ in range(COLS)] for _ in range(ROWS)]    score = 0    game_over = False    current_shape = random.choice(SHAPES)    block_x, block_y = 3, 0
running = Truewhile running:    screen.fill(BLACK)    delta_time = clock.tick(60)  # 游戏主循环每秒运行60次    fall_time += delta_time
    # 1. 事件处理    for event in pygame.event.get():        if event.type == pygame.QUIT:            running = False        if event.type == pygame.KEYDOWN:            if game_over:                if event.key == pygame.K_RETURN:  # 游戏结束时按回车重玩                    reset_game()            else:                if event.key == pygame.K_LEFT:                    if not check_collision(current_shape, block_x - 1, block_y):                        block_x -= 1                if event.key == pygame.K_RIGHT:                    if not check_collision(current_shape, block_x + 1, block_y):                        block_x += 1                if event.key == pygame.K_DOWN:                    if not check_collision(current_shape, block_x, block_y + 1):                        block_y += 1                if event.key == pygame.K_UP:  # 上方向键旋转                    rotated = rotate_shape(current_shape)                    if not check_collision(rotated, block_x, block_y):                        current_shape = rotated
    # 2. 自动下落逻辑    if not game_over:        if fall_time >= fall_speed:            fall_time = 0            if not check_collision(current_shape, block_x, block_y + 1):                block_y += 1            else:                # 触底锁定方块                for r_idx, row in enumerate(current_shape):                    for c_idx, val in enumerate(row):                        if val and block_y + r_idx >= 0:                            game_field[block_y + r_idx][block_x + c_idx] = 1                clear_lines()                # 重新生成新方块                current_shape = random.choice(SHAPES)                block_x, block_y = 3, 0                if check_collision(current_shape, block_x, block_y):                    game_over = True
    # 3. 画面渲染    # 绘制固定的地图方块    for r in range(ROWS):        for c in range(COLS):            if game_field[r][c]:                pygame.draw.rect(screen, BLUE, (c * GRID_SIZE, r * GRID_SIZE, GRID_SIZE - 1, GRID_SIZE - 1))            else:                pygame.draw.rect(screen, GRAY, (c * GRID_SIZE, r * GRID_SIZE, GRID_SIZE, GRID_SIZE), 1)
    # 绘制当前下落的方块    if not game_over:        for r_idx, row in enumerate(current_shape):            for c_idx, val in enumerate(row):                if val:                    x = (block_x + c_idx) * GRID_SIZE                    y = (block_y + r_idx) * GRID_SIZE                    pygame.draw.rect(screen, RED, (x, y, GRID_SIZE - 1, GRID_SIZE - 1))
    # 4. UI 文本显示    font = pygame.font.SysFont("SimHei", 24)   # mac改为 "songti"    score_text = font.render(f"得分: {score}", True, WHITE)    screen.blit(score_text, (10, SCREEN_HEIGHT - 40))
    if game_over:        over_text = font.render("游戏结束! 按回车重新开始", True, RED)        screen.blit(over_text, (SCREEN_WIDTH // 2 - over_text.get_width() // 2, SCREEN_HEIGHT // 2 - 20))
    pygame.display.flip()
pygame.quit()

新手建议

开发过程中有一些新手常会踩到的坑,这里列一下,大家注意“避坑”:

1. 方块一晃眼就掉到底,根本反应不过来 

忘记写 clock.tick(5)。如果没有这个限速器,Python 会以极快的速度运行循环。你也可以根据实际运行的速度调节这个参数值。

2. 旋转时方块有时候会卡进墙里或者报错

没有在旋转之前做碰撞检测的预判。必须先用 check_collision 判断返回 False 后才能执行旋转操作。

3. 中文显示不正确或代码报错 SyntaxError

中文的问题包括两种,一种是游戏中的中文显示,一种是代码中的中文。

游戏中显示中文需要有相应的字体支持。Win 和 Mac 系统自带的字体不一样,要做不同设置。

而代码里的逗号、括号、引号必须是英文半角。新手经常在打完中文之后忘了切换输入法而导致输错了符号,这里尤其要注意。

看到这里,相信你已经理解了开发《俄罗斯方块》的思路。但看懂不等于学会,不如现在就打开电脑,把这段代码复制进去跑一下,然后在此基础上做一些优化吧。这样既玩到了游戏,又提升了代码水平,还收获了成就感,一举多得,何乐而不为?

如果本文对你有帮助,欢迎点赞、评论、转发。你们的支持是我更新的动力~


添加微信 crossin123 ,加入编程教室共同学习~
Image
感谢转发和点赞的各位~