Previous articles< python little joy (eight) preliminary organization of Tetris (6) source files > Divide the source code into three separate files
config.py,block.py,main.py
In the previous example, the function of free falling of blocks has been realized. Next comes a function: when the bottom box fills a row, it should be cleared, and the game points should be increased accordingly. At present, the problem of game points is not considered. First, it realizes how to clear the rows that have been filled at the bottom.
The actual effect of clearing rows is as follows
To achieve row cleanup, modify the BottomGroup class in block.py.
Add a method getLineSet that counts the number of squares per row
#Getting rowset information def getLineSet(self): lineSet ={} #Check each box in the bottom box combination one by one 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
A dictionary object is used to record the information of each line. The y coordinate of each line is used as the key value of the dictionary object. All the block objects in the line form a list and are stored in the dictionary according to the key value.
An auxiliary function for printing row set information is added to facilitate viewing bottom box information on the terminal.
#Output line set content, output line height, number of blocks in line, x coordinates of each square mark in line 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)
The core algorithm for clearing rows that have been filled at the bottom is implemented in method removeFullLine
#Check each line of the bottom box and remove it if the line is filled with the box. def removeFullLine(self): #A list of y coordinates of removed rows removeLines =[] #Get the current row set information lineSet = self.getLineSet() self.printLineSet(lineSet) for y in lineSet: #If the number of squares in a row has reached the maximum number of squares in a row if len(lineSet[y]) >= int(Config.screenWidth/Config.blockWidth): #Clear the boxes one by one, noting that the kill method of the sprite object is called for b in lineSet[y]: b.kill() lineSet[y]=[] removeLines.append(y) self.printLineSet(lineSet) #If the remaining blocks are higher than the removed rows, move down for b in self.sprites(): b.downTimes = 0 for height in removeLines: if height > b.rect.y: #Record the number of times the box should be moved down b.downTimes+=1 #Calling block's Mobile Method for b in self.sprites(): b.down(b.downTimes*Config.blockWidth)
Complete python code
config.py
# config.py # Configuration data, definition of global variables #Definition of color constants BLACK = (0,0,0) # Defining Black with RGB Value WHITE = (255,255,255) # Defining White with RGB Value #Configuration parameter class class Config(): def __init__(self): pass screenWidth = 320 screenHeight= 800 blockWidth = 40 blockColor = 'blue' downSpeed = 2 #A two-dimensional matrix illustration of the shape of a block combination. 1 indicates that there is a square at that place, and 0 indicates that there is no square at that place. 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], ), ] #Converting Shape Diagram to Shape Coordinate List 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 #Global variable shapeList that stores the initial coordinate list of shapes shapeList = shpaeGraph2List(shapeGraph)
block.py
# block.py # Definition of Block and Block Combination Class import pygame from PIL import Image,ImageDraw from config import * # Cube class 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 #Number of downshifts, used to clear rows filled at the bottom self.downTimes = 0 #Generating Block Pictures def createBlockImg(self): #Generate a rectangular image of specified size and color im = Image.new('RGB',(Config.blockWidth,Config.blockWidth),Config.blockColor) draw = ImageDraw.Draw(im) #Draw a black border 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() #Convert to bytes size = im.size #Graphic Resolution mode = im.mode return pygame.image.fromstring(data,size,mode) #Reset the initial position def reset(self): self.rect.x = self.inix self.rect.y = self.iniy #The box moves down def down(self,distance): # Downward movement self.rect.centery += distance #Square moves left and right def move(self,distance): # Left and right movement self.rect.centerx += distance # Represents the combination of multiple blocks in the drop, and typeIdx indicates the index of the combined shape in the 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)) #Envelope Rectangle of Composite Objects self.rect = self.boundingRect() #Record the initial location, using the copy method here self.iniRect = self.rect.copy() #Falling velocity self.downSpeed = Config.downSpeed #Reset the initial position def reset(self): #To restore the initial location, use the copy method here self.rect = self.iniRect.copy() for block in self.sprites(): block.reset() #Block combinations move downward 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) #Square Combination Moves Left and Right 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) #Finding the Rectangle Surrounding the Composite Object 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) #rotate def rotate(self): #The center of the combined object is taken as the center of rotation, and the center of rotation should be located at the grid point. 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(): #Calculate the distance difference between the center of the current square and the center of rotation dx = block.rect.centerx -cx dy = block.rect.centery -cy #The complex number consisting of the distance difference multiplied by the complex number i is the result of 90 degrees counter-clockwise rotation of the original complex number r = complex(dx,dy)*complex(0,1) #Get the result after rotation. block.rect.centerx = cx + r.real + Config.blockWidth block.rect.centery = cy + r.imag #Get the original horizontal position of the envelope rectangle lastRectX = self.rect.x #Updating Envelope Rectangle of Composite Objects self.rect = self.boundingRect() dx = lastRectX - self.rect.x #Keep the horizontal position of the rotated composite object unchanged self.rect.x += dx for block in self.sprites(): block.rect.x+=dx # Represents a combination of bottom squares class BottomGroup(pygame.sprite.Group): def __init__(self): pygame.sprite.Group.__init__(self) #Check whether the falling square collides with the bottom square def collided(self,fallingGroup): #Check if window boundaries are encountered for d in fallingGroup.sprites(): if Config.screenHeight - d.rect.y <=Config.blockWidth: return True #Check if you encounter a box parked at the bottom 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 #Eat the falling square def eat(self,fallingGroup): for d in fallingGroup.sprites(): self.add(d) #Clear all elements in the drop box combination fallingGroup.empty() #Clear up the filled rows self.removeFullLine() #Check each line of the bottom box and remove it if the line is filled with the box. def removeFullLine(self): #A list of y coordinates of removed rows removeLines =[] #Get the current row set information lineSet = self.getLineSet() self.printLineSet(lineSet) for y in lineSet: #If the number of squares in a row has reached the maximum number of squares in a row if len(lineSet[y]) >= int(Config.screenWidth/Config.blockWidth): #Clear the boxes one by one, noting that the kill method of the sprite object is called for b in lineSet[y]: b.kill() lineSet[y]=[] removeLines.append(y) self.printLineSet(lineSet) #If the remaining blocks are higher than the removed rows, move down for b in self.sprites(): b.downTimes = 0 for height in removeLines: if height > b.rect.y: #Record the number of times the box should be moved down b.downTimes+=1 #Calling block's Mobile Method for b in self.sprites(): b.down(b.downTimes*Config.blockWidth) #Getting rowset information def getLineSet(self): lineSet ={} #Check each box in the bottom box combination one by one 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 #Output line set content, output line height, number of blocks in line, x coordinates of each square mark in line 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 # Tetris, main program file import pygame from block import * import random #Random Generation of Falling Block Combination def randomFallingGroup(): #return FallingGroup(typeIdx=0,bottomGroup=bottomGroup) return FallingGroup(typeIdx=random.randint(0,8),bottomGroup=bottomGroup) # Redraw the display area to form the animation effect def animate(): global fallingGroup,bottomGroup,lastCheckPoint #Set the screen to black screen.fill(BLACK) #According to the falling speed, the interval time between two checkpoints (unit milliseconds) is calculated. If the next checkpoint is reached, the falling method is executed. if pygame.time.get_ticks() - lastCheckPoint > 1000/fallingGroup.downSpeed: #Dropping Block Combination Executing Dropping Method fallingGroup.down(Config.blockWidth) lastCheckPoint = pygame.time.get_ticks() #If the falling block combination has been "eaten" by the bottom block combination, a new falling block combination is generated. if len(fallingGroup.sprites()) <= 0: fallingGroup = randomFallingGroup() #Dropping Block Combination Execution Drawing Method fallingGroup.draw(screen) #Drawing method of bottom block combination bottomGroup.draw(screen) #Refresh screen pygame.display.flip() # ------------------------main--------------------------------------------------------------------- # Initialize various objects pygame.init() #Screen of Game Window screen = pygame.display.set_mode([Config.screenWidth,Config.screenHeight]) #Fill the background with black screen.fill(BLACK) #Setting the title of the graphics window pygame.display.set_caption("Tetris") #Game clock clock = pygame.time.Clock() #Generating Bottom Block Combination Objects bottomGroup =BottomGroup() #Random Generation of a Drop Block Combination Object fallingGroup = randomFallingGroup() #Checkpoint at Falling Time lastCheckPoint = pygame.time.get_ticks() # Event Processing Loop running = True while running: #Set the number of frames per second clock.tick(30) for event in pygame.event.get(): if event.type == pygame.QUIT: running = False if event.type == pygame.KEYDOWN: # If you press the key on the keyboard if event.key == pygame.K_LEFT: # If you press the left direction key #Shift one lattice to the left fallingGroup.move(-1*Config.blockWidth) elif event.key == pygame.K_RIGHT: #If you press the right direction key #Shift a grid to the right fallingGroup.move(Config.blockWidth) elif event.key == pygame.K_UP: #If the upward direction key is pressed #Call the rotation method fallingGroup.rotate() elif event.key == pygame.K_DOWN: #If the downward direction key is pressed #Falling speed multiplied by 100 fallingGroup.downSpeed*=100 #Call the method of drawing animation animate() pygame.quit() #Quit pygame