完整代码
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 # 结束函数,继续下一个循环
有什么区别?
解答:
第一段代码
-
for
循环遍历完所有方块位置时,如果没有触发break
,即所有新位置都没有被占用,才会执行else
块的内容。 -
else
块更新initial_position
为新的位置[y, x]
。
第二段代码 - 在
for
循环的每次迭代中,代码检查当前方块的新位置是否被占用。 - 如果未被占用(即
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不会变?
解答:
-
y, x = initial_position
:initial_position
包含当前方块的初始位置,例如[21, 5]
,其中y=21
,x=5
。 -
y -= 1
:这一步将y
减 1,表示方块整体向下移动一个单位。假设新的y
变成了20
。 -
for row, column in select_block
:select_block
包含当前方块的相对坐标(例如,形状或结构)。每个方块由相对row
和column
表示,row
和column
是相对于initial_position
的偏移量。-
row += y
:更新每个方块的y
坐标,实际表示方块在网格中的纵向位置。 -
column += x
:更新每个方块的x
坐标,实际表示方块在网格中的横向位置。
更新initial_position
的目的不是为了改变每个方块的row
和column
,而是更新方块整体的初始位置,也就是新的y, x
坐标。这是为了记录整个方块向下移动后的新位置,便于下次移动时继续以这个新位置作为基准。
-
-
row += y
和column += x
:这些操作的作用是计算每个方块在网格中的绝对位置,以便检查这些位置是否被占用。 -
row
和column
是相对位置,通过加上y
和x
,我们得到每个方块在整个游戏网格中的实际位置。
总结 -
y -= 1
是唯一改变的,表示方块向下移动。 -
x
不变,意味着方块只在纵向移动,横向位置保持不变。 -
row += y
和column += x
的操作是为了计算方块在网格中的实际位置,而不是改变方块的形状或结构。 -
initial_position.extend([y, x])
更新的是方块整体的初始位置,以便下一次移动操作可以基于这个新位置。
三、score和gameover为什么要设置成列表,直接设置成变量可以吗?
在这段代码中,score
和 gameover
被设置成列表是为了在函数内部对它们进行修改,并且这些修改能够在函数外部反映出来。由于Python中的变量是按引用传递的,当传递一个列表或其他可变对象给一个函数时,函数内部对这个对象的修改会影响到外部的对象。这使得代码能够在函数内部修改score
和gameover
,并在函数外部保持这些修改。
如果直接将score
和gameover
设置为普通的变量(比如整数或布尔值),这些值在函数内部修改时不会影响到外部的变量,因为整数和布尔值是不可变的,函数内部的修改只会作用在函数的局部变量上。
score = 0 # 普通变量
gameover = False # 普通变量
在函数内部修改score
和gameover
时,实际上是修改了它们的副本,而不是全局变量本身:
def some_function():
score += 10 # 只会修改函数内部的局部变量 score
gameover = True # 只会修改函数内部的局部变量 gameover
这样,外部的score
和gameover
不会被修改。
相比之下,当使用列表时:
score = [0] # 列表
gameover = [] # 列表
函数内部对列表内容的修改会直接影响到外部的变量:
def some_function():
score[0] += 10 # 直接修改列表中的值,影响全局变量
gameover.append(True) # 直接修改列表内容,影响全局变量
这就确保了函数内部的修改能够保留在全局状态中。
因此,在这种情况下,使用列表是一种简便的方式来确保score
和gameover
在整个程序中都能被正确更新和反映。