Python俄罗斯方块

完整代码

import pygame, sys
import random

# 绘制方块和背景的函数
def draw_block():
    y, x = initial_position  # 获取当前方块的初始位置
    for row, column in select_block:  # 遍历当前选择的方块
        row += y  # 更新y坐标
        column += x  # 更新x坐标
        # 800 - row * 40 反转了y坐标
        pygame.draw.rect(screen, (255, 165, 0), (column * 40, 800 - row * 40, 38, 38))  # 绘制动态方块(橙色)
    for row in range(1, 21):  # 遍历背景网格的每一行
        for column in range(0, 10):  # 遍历每一列
            pygame.draw.rect(screen, (200, 200, 200), (column * 40, 800 - row * 40, 40, 40), 1)  # 绘制网格线
            if backgroud[row][column] == 1:  # 如果该位置被占用
                pygame.draw.rect(screen, (0, 0, 255), (column * 40, 800 - row * 40, 38, 38))  # 绘制静态方块(蓝色)

# 方块下落的函数
def block_down():
    global select_block  # 声明select_block为全局变量
    y, x = initial_position  # 获取初始位置,initial_position = [21, 5]
    y -= 1  # y坐标减1,表示方块向下移动一个单位
    for row, column in select_block:  # 调节方块下落的位置
        row += y  # 更新方块的y坐标
        column += x  # 更新方块的x坐标
        if backgroud[row][column]:  # 注意!!!!!检查新位置是否已被占用
            break  # 如果被占用,停止下落
    else:
        initial_position.clear()  # 刷新,对初始位置进行更新
        initial_position.extend([y, x])  # 更新初始位置为新的坐标,第一个值表示row,第二个值表示column
        return  # 结束当前函数 block_down 的执行,并返回到调用 block_down 的地方,不再执行后续的代码

    # 在方块下落时,如方块无法再继续向下移动,则将当前方块的位置标记为占用,并将其固定在背景网格中
    y, x = initial_position  # 获取方块的当前初始位置
    for row, column in select_block:  # 对当前选择的方块
        row += y
        column += x
        backgroud[row][column] = 1  # 将背景网格对应位置标记为1,表示占用

    complete_row = []  # 用来存储静态的block,即完成一行后,将静态block存储在数组中
    # 然后通过删除数组元素达到消除方块的效果
    # 处理和检测背景网格中是不是有完整的行
    for row in range(1, 21):  # 遍历背景网格的每一行(1到20行)
        if 0 not in backgroud[row]:  # 如果该行没有0,说明已被填满
            complete_row.append(row)  # 如果一行是完整的将其行号row添加到complete_row中
    for row in complete_row:  # 遍历所有已完成的行
        backgroud.pop(row)  # 将完整的行移除
        backgroud.append([0 for _ in range(10)])  # 在网格底部添加一个新空行,保持网格大小不变

    score[0] += len(complete_row)  # 更新分数,增加消除的行数
    pygame.display.set_caption('你现在的分数是 ' + str(score[0]) + '分')  # 更新窗口标题显示当前分数

    initial_position.clear()  # 清空初始位置列表
    select_block = list(random.choice(all_block))  # 随机选择一个新的方块形状
    initial_position.extend([20, 5])  # 将[20,5]添加到initial_position中,[20,5]代表下落的初始坐标

    y, x = initial_position  # 获取新方块的初始坐标
    for row, column in select_block:  # 用于确定方块的偏移量
        row += y
        column += x
        if backgroud[row][column] == 1:  # 判断静态block对应位置是否为1,如果是说明当前方块与背景中已有方块重叠
            gameover.append(1)  # 如果重叠,将1添加到gameover列表中,表示游戏结束

# 旋转方块的函数
def rotate():
    y, x = initial_position  # 获取当前方块的初始位置
    # 计算旋转后的位置,旋转90度
    rotating_position = [(-colum, row) for row, colum in select_block]
    for row, colum in rotating_position:  # 遍历旋转后的每个单元
        row += y  # 更新y坐标
        colum += x  # 更新x坐标
        if colum < 0 or colum > 9 or backgroud[row][colum]:  # 检查是否越界或与背景重叠
            break  # 如果有问题,停止旋转
    else:
        select_block.clear()  # 清空当前选择的方块
        select_block.extend(rotating_position)  # 更新为旋转后的方块

# 移动方块的函数,参数d表示方向(-1左移,1右移)
def move(d):
    y, x = initial_position  # 获取当前方块的初始位置
    x += d  # 更新x坐标,根据方向d移动
    for row, column in select_block:  # 遍历当前选择的方块
        row += y  # 更新y坐标
        column += x  # 更新x坐标
        if column < 0 or column > 9 or backgroud[row][column]:  # 检查是否越界或与背景重叠
            break  # 如果有问题,停止移动
    else:
        initial_position.clear()  # 清空初始位置列表
        initial_position.extend([y, x])  # 更新初始位置为新的坐标

# 主程序入口
if __name__ == "__main__":
    # 初始化Pygame
    pygame.init()
    # 定义所有可能的方块形状,每种形状由4个单元组成
    all_block = [
        [[0, 0], [0, 1], [0, 2], [0, 3]],  # 直线形状
        [[0, 0], [0, 1], [1, 0], [1, 1]],  # 正方形形状
        [[0, 0], [1, 0], [0, 1], [0, 2]],  # L形状
        [[0, 0], [1, 0], [0, -1], [0, -2]],  # 反L形状
        [[0, 0], [0, 1], [1, 1], [1, 2]],  # Z形状
        [[0, 0], [0, -1], [1, -1], [1, -2]],  # 反Z形状
        [[0, 0], [0, 1], [0, -1], [1, 0]],  # T形状
    ]

    # 初始化背景网格,23行10列,所有位置初始化为0
    backgroud = [[0 for column in range(10)] for row in range(23)]
    backgroud[0] = [1 for colum in range(10)]  # 将第0行全部设置为1,表示底部边界
    # 设置全局变量
    initial_position = [21, 5]  # 初始位置设置在第21行,第5列
    times = 0  # 初始化计时器
    score = [0]  # 初始化分数列表,用于记录分数
    gameover = []  # 游戏结束标志列表

    press = False  # 按键加速标志
    running = True  # 游戏是否运行

    screen = pygame.display.set_mode((400, 800))  # 设置游戏窗口大小为400x800像素
    select_block = list(random.choice(all_block))  # 随机选择一个初始方块形状

    # 游戏主循环
    while running:
        screen.fill((255, 255, 255))  # 将屏幕填充为白色背景
        for event in pygame.event.get():  # 处理所有事件
            if event.type == pygame.QUIT:  # 如果点击关闭按钮
                sys.exit()  # 退出程序
        # for event in pygame.event.get():
        #     if event.type == pygame.QUIT:  # 如果点击关闭按钮,退出游戏
        #         running = False
        #         break
            if event.type == pygame.KEYDOWN:  # 如果有键被按下
                if event.key == pygame.K_DOWN:  # 如果按下的是向下键
                    press = True  # 设置按键加速标志为True
                elif event.key == pygame.K_UP:  # 如果按下的是向上键
                    rotate()  # 调用旋转函数
                elif event.key == pygame.K_RIGHT:  # 如果按下的是向右键
                    move(1)  # 向右移动
                elif event.key == pygame.K_LEFT:  # 如果按下的是向左键
                    move(-1)  # 向左移动
            if event.type == pygame.KEYUP:  # 如果有键被释放
                if event.key == pygame.K_DOWN:  # 如果释放的是向下键
                    press = False  # 取消按键加速标志

        if times % 60 == 0:  # 每60帧调用一次block_down函数
            block_down()
        times += 1  # 增加计时器
        if press:  # 如果按键加速标志为True
            times += 100  # 快速增加计时器,加速下落
        if gameover:  # 如果游戏结束
            sys.exit()  # 退出程序
        draw_block()  # 调用绘制函数
        pygame.time.Clock().tick(400)  # 设置游戏帧率为400帧每秒

        # 创建字体对象
        font = pygame.font.Font(None, 36)
        # 创建显示文本
        text = font.render("Score: " + str(score[0]), True, (0, 0, 0))
        # 在屏幕上显示文本
        screen.blit(text, (10, 10))
        # 更新显示
        pygame.display.update()

        pygame.display.flip()  # 刷新整个显示
运行截图

注意:

一、本游戏代码在 draw_block() 函数中,绘制方块的位置 使用 (column * 40, 800 - row * 40)800 - row * 40 将 y 坐标反转,以符合游戏的视觉效果,使得 y 轴的坐标是从屏幕顶部到底部逐渐减小的
二、本游戏代码的坐标列表中,[a, b],a表示行row(即y坐标),b表示列column(即x坐标)

疑问

一、

def block_down():
    global select_block  # 声明select_block为全局变量
    y, x = initial_position  # 获取初始位置,initial_position = [21, 5]
    y -= 1  # y坐标减1,表示方块向下移动一个单位
    for row, column in select_block:  # 遍历当前选择的方块
        row += y  # 更新方块的y坐标
        column += x  # 更新方块的x坐标
        if backgroud[row][column]:  # 检查新位置是否已被占用
            break  # 如果被占用,停止下落
    else:
        initial_position.clear()  # 清空初始位置列表
        initial_position.extend([y, x])  # 更新初始位置为新的坐标
        return  # 结束函数,继续下一个循环

这段代码和

def block_down():
    global select_block  # 声明select_block为全局变量
    y, x = initial_position  # 获取初始位置,initial_position = [21, 5]
    y -= 1  # y坐标减1,表示方块向下移动一个单位
    for row, column in select_block:  # 遍历当前选择的方块
        row += y  # 更新方块的y坐标
        column += x  # 更新方块的x坐标
        if backgroud[row][column]:  # 检查新位置是否已被占用
            break  # 如果被占用,停止下落
        else:
            initial_position.clear()  # 清空初始位置列表
            initial_position.extend([y, x])  # 更新初始位置为新的坐标
            return  # 结束函数,继续下一个循环

有什么区别?
解答:
第一段代码

  1. for循环遍历完所有方块位置时,如果没有触发break,即所有新位置都没有被占用,才会执行else块的内容。
  2. else块更新initial_position为新的位置 [y, x]
    第二段代码
  3. for循环的每次迭代中,代码检查当前方块的新位置是否被占用。
  4. 如果未被占用(即else块被执行),initial_position就会立即更新,并且函数会返回,跳过剩余的方块检查。
    关键区别
  • 第一段代码:只有在for循环遍历完所有方块,并且没有发现占用位置时,才会更新initial_position。它确保所有方块的位置都是安全的。
  • 第二段代码:一旦发现一个未占用的位置,就立即更新initial_position并返回,可能忽略了其他方块的位置检查。这可能导致initial_position被错误地更新到一个部分有效的位置。

二、

initial_position.extend([y, x])  # 更新初始位置为新的坐标

这个为什么更新为了新的坐标?改变的不是row和column吗?这里输入的是y, x,那row += y 和column += x 的意义在哪?是不是只有y-=1这一个改变,x不会变?
解答:

  1. y, x = initial_positioninitial_position 包含当前方块的初始位置,例如 [21, 5],其中 y=21x=5
  2. y -= 1:这一步将 y 减 1,表示方块整体向下移动一个单位。假设新的 y 变成了 20
  3. for row, column in select_blockselect_block 包含当前方块的相对坐标(例如,形状或结构)。每个方块由相对 rowcolumn 表示,rowcolumn 是相对于 initial_position 的偏移量。
    • row += y:更新每个方块的 y 坐标,实际表示方块在网格中的纵向位置。
    • column += x:更新每个方块的 x 坐标,实际表示方块在网格中的横向位置。
      更新 initial_position 的目的不是为了改变每个方块的 rowcolumn,而是更新方块整体的初始位置,也就是新的 y, x 坐标。这是为了记录整个方块向下移动后的新位置,便于下次移动时继续以这个新位置作为基准。
  • row += ycolumn += x:这些操作的作用是计算每个方块在网格中的绝对位置,以便检查这些位置是否被占用。
  • rowcolumn 是相对位置,通过加上 yx,我们得到每个方块在整个游戏网格中的实际位置。
    总结
  • y -= 1 是唯一改变的,表示方块向下移动。
  • x 不变,意味着方块只在纵向移动,横向位置保持不变。
  • row += ycolumn += x 的操作是为了计算方块在网格中的实际位置,而不是改变方块的形状或结构。
  • initial_position.extend([y, x]) 更新的是方块整体的初始位置,以便下一次移动操作可以基于这个新位置。

三、score和gameover为什么要设置成列表,直接设置成变量可以吗?
在这段代码中,scoregameover 被设置成列表是为了在函数内部对它们进行修改,并且这些修改能够在函数外部反映出来。由于Python中的变量是按引用传递的,当传递一个列表或其他可变对象给一个函数时,函数内部对这个对象的修改会影响到外部的对象。这使得代码能够在函数内部修改scoregameover,并在函数外部保持这些修改。
如果直接将scoregameover设置为普通的变量(比如整数或布尔值),这些值在函数内部修改时不会影响到外部的变量,因为整数和布尔值是不可变的,函数内部的修改只会作用在函数的局部变量上。

score = 0  # 普通变量
gameover = False  # 普通变量

在函数内部修改scoregameover时,实际上是修改了它们的副本,而不是全局变量本身:

def some_function():
    score += 10  # 只会修改函数内部的局部变量 score
    gameover = True  # 只会修改函数内部的局部变量 gameover

这样,外部的scoregameover不会被修改。
相比之下,当使用列表时:

score = [0]  # 列表
gameover = []  # 列表

函数内部对列表内容的修改会直接影响到外部的变量:

def some_function():
    score[0] += 10  # 直接修改列表中的值,影响全局变量
    gameover.append(True)  # 直接修改列表内容,影响全局变量

这就确保了函数内部的修改能够保留在全局状态中。
因此,在这种情况下,使用列表是一种简便的方式来确保scoregameover在整个程序中都能被正确更新和反映。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容