python小欢喜(八)俄罗斯方块 (10) 清除底部已经填满的行

前面的文章《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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值