前面的文章《python小欢喜(八)俄罗斯方块 (6) 源码文件的初步组织》将源代码分成了3个独立的文件
config.py,block.py,main.py
在前面的例子中,已经实现了方块自由下落的功能。接下来实现一个功能:当底部的方块填满一行时,应该被清除,游戏积分也要相应增加。目前暂不考虑游戏积分的问题。先来实现如何清除底部已经填满的行。
清除行的实际效果如下
为了实现清除行的功能,修改 block.py中的 BottomGroup 类。
添加一个统计每行有多少方块的方法 getLineSet
#获取行集信息
def getLineSet(self):
lineSet ={}
#逐一检查底部方块组合中的每一个方块
for b in self.sprites():
if b.rect.y in lineSet.keys():
lineSet[b.rect.y].append(b)
else:
lineSet[b.rect.y]=[b]
return lineSet
用一个字典对象记录每行的信息,每行的y坐标做为字典对象的键值,该行所有的方块对象组成一个列表,依键值存放于字典之中
添加了一个打印行集信息的辅助函数,便于在终端上观察底部方块的信息
#输出行集中的内容,输出这一行的高度,行中方块的数目,行中每一个方标的x坐标
def printLineSet(self,lineSet):
print('-------LineSet------------')
for y in lineSet:
xPos=[]
for b in lineSet[y]:
xPos.append(b.rect.x)
print(y,len(lineSet[y]),xPos)
清除底部已经填满的行的核心算法在方法 removeFullLine中实现
#检查底部方块的每一行,如果一行被方块填满后,移除此行
def removeFullLine(self):
#被移除的行的y坐标组成的列表
removeLines =[]
#获取当前的行集信息
lineSet = self.getLineSet()
self.printLineSet(lineSet)
for y in lineSet:
#如果该行的方块数目已经到达一行中最大的方块数目
if len(lineSet[y]) >= int(Config.screenWidth/Config.blockWidth):
#逐一清除方块,注意调用了sprite对象的kill方法
for b in lineSet[y]:
b.kill()
lineSet[y]=[]
removeLines.append(y)
self.printLineSet(lineSet)
#剩下的方块如果比被移除的行的位置要高,则向下移动
for b in self.sprites():
b.downTimes = 0
for height in removeLines:
if height > b.rect.y:
#记录该方块应该下移的次数
b.downTimes+=1
#调用block的移动方法
for b in self.sprites():
b.down(b.downTimes*Config.blockWidth)
完整的python代码
config.py
# config.py
# 配置数据,全局变量的定义
#颜色常量的定义
BLACK = (0,0,0) # 用RGB值定义黑色
WHITE = (255,255,255) # 用RGB值定义白色
#配置参数类
class Config():
def __init__(self):
pass
screenWidth = 320
screenHeight= 800
blockWidth = 40
blockColor = 'blue'
downSpeed = 2
#方块组合形状的二维矩阵图示,1表示该处有方块,0表示没有
shapeGraph=[
(
[1,1,1,1],
[0,0,0,0],
[0,0,0,0],
[0,0,0,0],
),
(
[0,1,0,0],
[1,1,1,0],
[0,0,0,0],
[0,0,0,0],
),
(
[1,0,0,0],
[1,0,0,0],
[1,0,0,0],
[1,0,0,0],
),
(
[1,1,1,0],
[1,0,0,0],
[0,0,0,0],
[0,0,0,0],
),
(
[1,0,0,0],
[1,0,0,0],
[1,1,0,0],
[0,0,0,0],
),
(
[1,1,0,0],
[1,0,0,0],
[1,0,0,0],
[0,0,0,0],
),
(
[1,0,0,0],
[1,1,0,0],
[1,0,0,0],
[0,0,0,0],
),
(
[1,1,0,0],
[0,1,1,0],
[0,0,0,0],
[0,0,0,0],
),
(
[1,1,0,0],
[1,1,0,0],
[0,0,0,0],
[0,0,0,0],
),
]
#将形状图转换为形状坐标列表
def shpaeGraph2List(shapeGraph):
shapeList =[]
for g in shapeGraph:
shape=[]
for y in range(4):
for x in range(4):
if g[y][x] == 1 :
shape.append([x*Config.blockWidth,y*Config.blockWidth])
shapeList.append(shape)
return shapeList
#存放形状初始坐标列表的全局变量 shapeList
shapeList = shpaeGraph2List(shapeGraph)
block.py
# block.py
# 方块及方块组合类的定义
import pygame
from PIL import Image,ImageDraw
from config import *
# 方块类
class Block(pygame.sprite.Sprite):
def __init__(self,x,y):
self.inix = x
self.iniy = y
pygame.sprite.Sprite.__init__(self)
self.image = self.createBlockImg()
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
#下移的次数,在清除底部填满的行时会用到
self.downTimes = 0
#生成方块图片
def createBlockImg(self):
#生成一个指定大小与颜色的矩形图像
im = Image.new('RGB',(Config.blockWidth,Config.blockWidth),Config.blockColor)
draw = ImageDraw.Draw(im)
#画一个黑色的边框
draw.line([(0,0),(0,Config.blockWidth)],'black',1)
draw.line([(0,Config.blockWidth),(Config.blockWidth,Config.blockWidth)],'black',1)
draw.line([(Config.blockWidth,Config.blockWidth),(Config.blockWidth,0)],'black',1)
draw.line([(Config.blockWidth,0),(0,0)],'black',1)
data = im.tobytes() #转换成字节
size = im.size #图形分辨率
mode = im.mode
return pygame.image.fromstring(data,size,mode)
#重置初始位置
def reset(self):
self.rect.x = self.inix
self.rect.y = self.iniy
#方块向下移动
def down(self,distance):
# 向下移动
self.rect.centery += distance
#方块左右移动
def move(self,distance):
# 左右移动
self.rect.centerx += distance
# 表示下落中的多个方块的组合,typeIdx指明组合形状在shapeList中的索引
class FallingGroup(pygame.sprite.Group):
def __init__(self, typeIdx,bottomGroup):
self.bottomGroup = bottomGroup
pygame.sprite.Group.__init__(self)
iniX = int((Config.screenWidth/2)/Config.blockWidth)*Config.blockWidth
shape = shapeList[typeIdx]
for xyPair in shape:
x = iniX+xyPair[0]
y = xyPair[1]
self.add(Block(x,y))
#得到组合对象的包络矩形
self.rect = self.boundingRect()
#记录初始位置,此处要使用copy方法
self.iniRect = self.rect.copy()
#下落速度
self.downSpeed = Config.downSpeed
#重置初始位置
def reset(self):
#恢复初始位置,此处要使用copy方法
self.rect = self.iniRect.copy()
for block in self.sprites():
block.reset()
#方块组合向下移动
def down(self, distance):
if not self.bottomGroup.collided(self):
self.rect.y += distance
for block in self.sprites():
block.down(distance)
else:
#self.reset()
self.bottomGroup.eat(self)
#方块组合左右移动
def move(self, distance):
#print([self.rect.x,self.rect.y,self.rect.width,self.rect.height])
if (distance > 0 and self.rect.x < Config.screenWidth-self.rect.width) or (distance < 0 and self.rect.x > 0):
self.rect.x += distance
for block in self.sprites():
block.move(distance)
#求出包围组合对象的矩形
def boundingRect(self):
minX = Config.screenWidth+100
minY = Config.screenHeight+100
maxX = -100
maxY = -100
for block in self.sprites():
if block.rect.x < minX:
minX = block.rect.x
if block.rect.y < minY:
minY = block.rect.y
if block.rect.x > maxX:
maxX = block.rect.x
if block.rect.y > maxY:
maxY = block.rect.y
return pygame.Rect(minX,minY,maxX-minX+Config.blockWidth,maxY-minY+Config.blockWidth)
#旋转
def rotate(self):
#取组合对象的中心点作为旋转中心,旋转中心应位于网格点上
cx=int((self.rect.x+self.rect.width/2)/Config.blockWidth)*Config.blockWidth
cy=int((self.rect.y+self.rect.height/2)/Config.blockWidth)*Config.blockWidth
for block in self.sprites():
#求出当前方块的中心与旋转中心的距离差
dx = block.rect.centerx -cx
dy = block.rect.centery -cy
#距离差组成的复数 乘上 复数 i ,得到的复数是 原复数逆时针旋转90度的结果
r = complex(dx,dy)*complex(0,1)
#得到旋转之后的结果
block.rect.centerx = cx + r.real + Config.blockWidth
block.rect.centery = cy + r.imag
#取得包络矩形的原始水平位置
lastRectX = self.rect.x
#更新组合对象的包络矩形
self.rect = self.boundingRect()
dx = lastRectX - self.rect.x
#使得旋转后的组合对象的水平位置保持不变
self.rect.x += dx
for block in self.sprites():
block.rect.x+=dx
# 表示底部方块的组合
class BottomGroup(pygame.sprite.Group):
def __init__(self):
pygame.sprite.Group.__init__(self)
#检查下落的方块是否与底部方块发生了碰撞
def collided(self,fallingGroup):
#检查是否碰到了窗口边界
for d in fallingGroup.sprites():
if Config.screenHeight - d.rect.y <=Config.blockWidth:
return True
#检查是否碰到了停在底部的方块
for d in fallingGroup.sprites():
for b in self.sprites():
if b.rect.y - d.rect.y <=Config.blockWidth and b.rect.x == d.rect.x:
return True
return False
#吃掉下落的方块
def eat(self,fallingGroup):
for d in fallingGroup.sprites():
self.add(d)
#清除下落方块组合中的所有元素
fallingGroup.empty()
#清除掉已经填满的行
self.removeFullLine()
#检查底部方块的每一行,如果一行被方块填满后,移除此行
def removeFullLine(self):
#被移除的行的y坐标组成的列表
removeLines =[]
#获取当前的行集信息
lineSet = self.getLineSet()
self.printLineSet(lineSet)
for y in lineSet:
#如果该行的方块数目已经到达一行中最大的方块数目
if len(lineSet[y]) >= int(Config.screenWidth/Config.blockWidth):
#逐一清除方块,注意调用了sprite对象的kill方法
for b in lineSet[y]:
b.kill()
lineSet[y]=[]
removeLines.append(y)
self.printLineSet(lineSet)
#剩下的方块如果比被移除的行的位置要高,则向下移动
for b in self.sprites():
b.downTimes = 0
for height in removeLines:
if height > b.rect.y:
#记录该方块应该下移的次数
b.downTimes+=1
#调用block的移动方法
for b in self.sprites():
b.down(b.downTimes*Config.blockWidth)
#获取行集信息
def getLineSet(self):
lineSet ={}
#逐一检查底部方块组合中的每一个方块
for b in self.sprites():
if b.rect.y in lineSet.keys():
lineSet[b.rect.y].append(b)
else:
lineSet[b.rect.y]=[b]
return lineSet
#输出行集中的内容,输出这一行的高度,行中方块的数目,行中每一个方标的x坐标
def printLineSet(self,lineSet):
print('-------LineSet------------')
for y in lineSet:
xPos=[]
for b in lineSet[y]:
xPos.append(b.rect.x)
print(y,len(lineSet[y]),xPos)
main.py
# main.py
# 俄罗斯方块,主程序所在文件
import pygame
from block import *
import random
#随机生成下落方块组合
def randomFallingGroup():
#return FallingGroup(typeIdx=0,bottomGroup=bottomGroup)
return FallingGroup(typeIdx=random.randint(0,8),bottomGroup=bottomGroup)
# 重绘显示区域,形成动画效果
def animate():
global fallingGroup,bottomGroup,lastCheckPoint
#设置屏幕为黑色
screen.fill(BLACK)
#根据下落速度,计算出两个检查点的间隔时间(单位毫秒),如果到达了下一个检查点,则执行下落方法
if pygame.time.get_ticks() - lastCheckPoint > 1000/fallingGroup.downSpeed:
#下落方块组合执行下落方法
fallingGroup.down(Config.blockWidth)
lastCheckPoint = pygame.time.get_ticks()
#如果下落的方块组合已经被底部方块组合“吃”掉了,则生成新的下落方块组合
if len(fallingGroup.sprites()) <= 0:
fallingGroup = randomFallingGroup()
#下落方块组合执行绘制方法
fallingGroup.draw(screen)
#底部方块组合执行绘制方法
bottomGroup.draw(screen)
#刷新屏幕
pygame.display.flip()
# ------------------------main---------------------------------------------------------------------
# 初始化各种对象
pygame.init()
#游戏窗口的屏幕
screen = pygame.display.set_mode([Config.screenWidth,Config.screenHeight])
#用黑色填充背景
screen.fill(BLACK)
#设置图形窗口标题
pygame.display.set_caption("俄罗斯方块")
#游戏时钟
clock = pygame.time.Clock()
#生成底部方块组合对象
bottomGroup =BottomGroup()
#随机生成一个下落方块组合对象
fallingGroup = randomFallingGroup()
#下落时刻检查点
lastCheckPoint = pygame.time.get_ticks()
# 事件处理循环
running = True
while running:
#设定每秒帧数
clock.tick(30)
for event in pygame.event.get():
if event.type == pygame.QUIT: running = False
if event.type == pygame.KEYDOWN: # 如果按下了键盘上的键
if event.key == pygame.K_LEFT: # 如果按下了向左的方向键
#向左平移一格
fallingGroup.move(-1*Config.blockWidth)
elif event.key == pygame.K_RIGHT: #如果按下了向右的方向键
#向右平移一格
fallingGroup.move(Config.blockWidth)
elif event.key == pygame.K_UP: #如果按下了向上的方向键
#调用旋转方法
fallingGroup.rotate()
elif event.key == pygame.K_DOWN: #如果按下了向下的方向键
#下落速度乘以100
fallingGroup.downSpeed*=100
#调用绘制动画的方法
animate()
pygame.quit() #退出pygame