Article directory
Source code address: PyGame Tetris
logical design
The logic of Tetris is very simple, that is, several blocks are put together and then fall. When they hit the surrounding walls, they cannot move. If a row is filled with squares, the row is deleted and all the squares above it are moved down one row.
In order to code this logic, a Boolean matrix can be used to represent specific block types. For example, long strip blocks can be represented by the following matrix.
BLOCK_I = [[0,1,0,0],
[0,1,0,0],
[0,1,0,0],
[0,1,0,0]]
In actual operations, rotation operations are often encountered. The basic logic is to rotate the i i iline change − i -i −icolumn, the rotation is as follows
def rotateBlock(block):
newBlock = [[[] for _ in block] for b in block[0]]
for i,row in enumerate(block, 1):
for j,r in enumerate(row, 0):
newBlock[j][-i] = r
return newBlock
Define all the blocks and enclose them in the listBLOCK
,
BLOCK_S = [[0,1,1],
[1,1,0],
[0,0,0]]
BLOCK_Z = [[1,1,0],
[0,1,1],
[0,0,0]]
# I型方块
BLOCK_I = [[0,1,0,0],
[0,1,0,0],
[0,1,0,0],
[0,1,0,0]],
# O型方块
BLOCK_O = [[1,1],
[1,1]]
# J型方块
BLOCK_J = [[1,0,0],
[1,1,1],
[0,0,0]]
# L型方块
BLOCK_L = [[0,0,1],
[1,1,1],
[0,0,0]]
# T型方块
BLOCK_T = [[0,1,0],
[1,1,1],
[0,0,0]]
BLOCKS = [BLOCK_S, BLOCK_I, BLOCK_J, BLOCK_L, BLOCK_O, BLOCK_T, BLOCK_Z]
With this, one of the blocks can be randomly generated. This process needs to introduce a little randomness.
import random
# 创建一个图形
def newBlock():
block = random.choice(BLOCKS)
for _ in range(random.randint(0,4)):
block = rotateBlock(block)
return block
Move logic
The principle is very simple, but if you want to write a playable game, you still need to pay attention to some details. Such as the behavior of Tetris when hitting a wall; the method of rotating blocks. Before implementing these specific problems, first set a few constants
SCREEN_WIDTH, SCREEN_HEIGHT = 450, 750
BLOCK_COL_NUM = 10 # 每行的方格数
BLOCK_ROW_NUM = 25 # 每列的方个数
SIZE = 30 # 每个小方格大小
BG_COLOR = (40, 40, 60) # 背景色
RED = (200, 30, 30) # 红色,GAME OVER 的字体颜色
WHITE = (255,255,255) # 白色
BLACK = (0,0,0) # 黑色
GREEN = (60, 128, 80) # 绿色
STOP_LST = [[0 for _ in range(BLOCK_COL_NUM)]
for _ in range(BLOCK_ROW_NUM)]
Next, implement the details of Tetris one by one. When moving the block left or right, if the block touches the wall, a determination function is needed. The code is as follows. When isR
is True, it means moving to the right, otherwise it moves to the left. The judgment logic is very intuitive, just check whether it exceeds the boundary.
# 判断是否可以向左右移动
# isR为True,判断能否向右移动;isR为False,判断能否向左移动
def judgeMoveLR(block, stCol, isR):
nCol = len(block[0])
cols = range(nCol-1, -1, -1) if isR else range(nCol)
for col in cols:
lstCol = [line[col] for line in block]
if 1 not in lstCol:
continue
if not 0 <= stCol + col < BLOCK_COL_NUM:
return False
return True
If the judgment is successful, you need to move the block, then according to its input to move left or right, you can construct a function
# 相左或者向右移动
def moveLR(block, stCol, key):
isR = key == pygame.K_RIGHT
stCol = stCol + 1 if isR else stCol - 1
if judgeMoveLR(block ,stCol, isR):
return 1 if isR else -1
return 0
If the block hits the wall during the falling process, it will be a little troublesome, because some blocks may have been accumulated below. To do this, you need to know not only the information of the currently falling block, but also the information of the blocks that have been accumulated.
# 判断是否可以继续下落
def judgeMoveDown(block, stRow, stCol, stopLst):
# 堆积方块的位置
stopPosition = list()
for row, line in enumerate(stopLst):
for col, b in enumerate(line):
if b: stopPosition.append((row, col))
# 判断碰撞
for row, line in enumerate(block):
if 1 in line and stRow + row >= BLOCK_ROW_NUM:
return False
for col, b in enumerate(line):
if b and (stRow + row, stCol + col) in stopPosition:
return False
return True
Finally, the three are combined to determine whether the block can continue to move.
def canMove(block, stRow, stCol, stopLst):
flag = judgeMoveLR(block, stCol, False)
flag &= judgeMoveLR(block, stCol, True)
flag &= judgeMoveDown(block, stRow, stCol, stopLst)
return flag
elimination and accumulation
The elimination logic is that when a row contains all 1s, delete the row and then add a list of all 0s at the top.
def judgeLines(stopLst):
# 记录刚刚消除的行数
i, N, count = 0, len(stopLst), 0
for i in range(N):
if 0 not in stopLst[i]:
line = [0]*len(stopLst.pop(i))
stopLst.insert(0, line)
count += 10
return count
The stacking logic is relatively simple, just set the stacking part position to 1.
# 将停止移动的block添加到堆积方块
def addToStopLst(stopLst, block, stRow, stCol):
for row, line in enumerate(block):
for col, b in enumerate(line):
if b: stopLst[stRow + row][stCol + col] = 1
At this point, the operation logic of Tetris has been designed, and the next step is drawing.
Parameter logic
The falling speed of Tetris can distinguish different levels. The following is a scheme that uses scores to determine the falling speed. The speed value represents the delay time of the falling block.
def changeSpeed(score):
scores = [20, 50, 100, 200]
infos = "12345"
speeds = [0.5, 0.4, 0.3, 0.2, 0.1]
ind = bisect(scores, score)
return infos[ind], speeds[ind]
Drawing logic
The first is background drawing, which mainly includes background color and grid drawing.
def drawBackground(screen):
screen.fill(BG_COLOR)
width = SIZE * BLOCK_COL_NUM
pygame.draw.line(screen, BLACK, (width, 0), (width, SCREEN_HEIGHT), 4)
for x in range(BLOCK_COL_NUM):
pygame.draw.line(screen, BLACK, (x * SIZE, 0), (x * SIZE, SCREEN_HEIGHT), 1)
for y in range(BLOCK_ROW_NUM):
pygame.draw.line(screen, BLACK, (0, y * SIZE), (width, y * SIZE), 1)
Only the left window is used here, and the right side will be used to store prompt information, mainly including score, speed level and the next falling block.
def drawRight(screen, score, speedInfo):
fontPath = r'C:\Windows\Fonts\msyh.ttc'
font = pygame.font.Font(fontPath, 24) # 黑体24
posDct = {
'得分: ':10, str(score): 50,
'速度: ':100, speedInfo: 150,
'下一个:':200
}
for k,v in posDct.items():
msg = font.render(k, True, WHITE)
screen.blit(msg, (BLOCK_COL_NUM * SIZE + 10, v))
Then there is the block drawing function. As the name suggests, drawBlock draws a block and drawBlocks draws a group of blocks.
def drawBlock(screen, row, col, stRow, stCol):
stRow += row * SIZE
stCol += SIZE * col
rect = pygame.Rect(stCol, stRow, SIZE, SIZE)
pygame.draw.rect(screen, GREEN, rect, 0)
pygame.draw.rect(screen, BLACK, rect, 1)
def drawBlocks(screen, block, stRow, stCol):
for row, line in enumerate(block):
for col, b in enumerate(line):
if not b: continue
drawBlock(screen, row, col, stRow, stCol)
Finally, if the game fails, a failure message will pop up
def drawGamerOver(screen):
fontPath = r'C:\Windows\Fonts\msyh.ttc'
font = pygame.font.Font(fontPath, 72)
w, h = font.size('GAME OVER')
msg = font.render('GAME OVER', True, RED)
screen.blit(msg, ((SCREEN_WIDTH - w) // 2, (SCREEN_HEIGHT - h) // 2))
# 显示"鼠标点击任意位置,再来一局"
font = pygame.font.Font(fontPath, 24)
w, h = font.size('点击任意位置,再来一局')
msg = font.render('点击任意位置,再来一局', True, RED)
screen.blit(msg, ((SCREEN_WIDTH - w) // 2, (SCREEN_HEIGHT - h) // 2 + 80))
main program
The main program content is as follows, in which initBlock is used to initialize the block. Since everyone is familiar with Tetris and all the sub-functions called have been explained, the process will not be described in detail.
def initBlock(nextBlock=None):
stRow, stCol = -2, 4
nowBlock = nextBlock if nextBlock else newBlock()
nextBlock = newBlock()
return stRow, stCol, nowBlock, nextBlock
def main():
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('俄罗斯方块')
# 初始化当前图形的位置,当前图形,以及下一个图形
stRow, stCol, nowBlock, nextBlock = initBlock()
last_time = time.time()
speed, speedInfo = 0.5, '1'
score, gameOver = 0, False
stopLst = deepcopy(STOP_LST)
clock = pygame.time.Clock()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key in [pygame.K_LEFT, pygame.K_RIGHT]:
stCol += moveLR(nowBlock, stCol, event.key)
elif event.key == pygame.K_UP:
rotBlock = rotateBlock(nowBlock)
if canMove(rotBlock, stRow, stCol, stopLst):
nowBlock = rotBlock
elif event.key == pygame.K_DOWN:
# 判断是否可以向下移动,如果碰到底部或者其它的图形就不能移动了
while judgeMoveDown(nowBlock, stRow + 1, stCol, stopLst):
stRow += 1
if gameOver:
stRow, stCol, nowBlock, nextBlock = initBlock()
stopLst = deepcopy(STOP_LST)
score = 0
gameOver = False
# 判断是否修改当前图形显示的起始行
if not gameOver and time.time() - last_time > speed:
last_time = time.time()
# 可以向下移动的情形
if judgeMoveDown(nowBlock, stRow + 1, stCol, stopLst):
stRow += 1
else:
# 不可向下移动,则需新建一个block然后下落
addToStopLst(stopLst, nowBlock, stRow, stCol)
score += judgeLines(stopLst)
gameOver = 1 in stopLst[0] # 第一行中间有1则游戏结束
speedInfo, speed = changeSpeed(score) # 调整速度
stRow, stCol, nowBlock, nextBlock = initBlock(nextBlock)
drawBackground(screen)
drawRight(screen, score, speedInfo)
drawBlocks(screen, nowBlock, stRow*SIZE, stCol*SIZE) # 当前方块
drawBlocks(screen, stopLst, 0, 0) # 堆积方块
drawBlocks(screen, nextBlock, 300, 320) # 下一个方块
# 显示游戏结束画面
if gameOver:
drawGamerOver(screen)
pygame.display.update()
clock.tick(60) # 通过一定的延时,实现1秒钟能够循环60次
The final game effect is as follows