PyQt5 tutorial "Tetris game"

 

Tetris in PyQt5

In this chapter, we will create a Tetris game clone.

 

Tetris

Tetris game is one of the most popular computer games of all time. The original game was designed and programmed by Russian programmer Alexey Pajitnov in 1985. Since then, Tetris has been available on almost all computer platforms.

Tetris is called a puzzle game. In this game, we have seven different shapes, called quadrilaterals : S-shaped, Z-shaped, T-shaped, L-shaped, linear, mirror L-shaped and square. Each of these shapes is formed with four squares. The shape fell off the board. The purpose of the Tetris game is to move and rotate the shapes so that they fit as much as possible. If we manage to form a row, then the row will be destroyed and we score. We play Tetris games until we are finished.

Tetrominoes

图:Tetrominoes

PyQt5 is a toolkit designed to create applications. There are other libraries designed to create computer games. However, PyQt5 and other application toolkits can be used to create simple games.

Creating computer games is a great way to improve your programming skills.

development of

We do not have images of Tetris games, and we used the drawing API provided in the PyQt5 programming toolkit to draw the quadruple dominoes. There is a mathematical model behind every computer game. So it is in Tetris.

Some ideas behind the game:

  • We use a to  QtCore.QBasicTimer()create a game cycle.
  • The quadruple is drawn.
  • The shape moves on a square-by-square basis (not pixel-by-pixel).
  • Mathematically, the circuit board is a simple list of numbers.

The code consists of four Tetriscategories: Board, , Tetrominoe and Shape. This Tetriscourse has games. This Boardis where the game logic is written. This Tetrominoecategory contains all Tetris pieces named and the Shapecategory contains a Tetris block code.

#!/usr/bin/python3
# -*- coding: utf-8 -*-


from PyQt5.QtWidgets import QMainWindow, QFrame, QDesktopWidget, QApplication
from PyQt5.QtCore import Qt, QBasicTimer, pyqtSignal
from PyQt5.QtGui import QPainter, QColor 
import sys, random

class Tetris(QMainWindow):
    
    def __init__(self):
        super().__init__()
        
        self.initUI()
        
        
    def initUI(self):    
        '''initiates application UI'''

        self.tboard = Board(self)
        self.setCentralWidget(self.tboard)

        self.statusbar = self.statusBar()        
        self.tboard.msg2Statusbar[str].connect(self.statusbar.showMessage)
        
        self.tboard.start()
        
        self.resize(180, 380)
        self.center()
        self.setWindowTitle('Tetris')        
        self.show()
        

    def center(self):
        '''centers the window on the screen'''
        
        screen = QDesktopWidget().screenGeometry()
        size = self.geometry()
        self.move((screen.width()-size.width())/2, 
            (screen.height()-size.height())/2)
        

class Board(QFrame):
    
    msg2Statusbar = pyqtSignal(str)
    
    BoardWidth = 10
    BoardHeight = 22
    Speed = 300

    def __init__(self, parent):
        super().__init__(parent)
        
        self.initBoard()
        
        
    def initBoard(self):     
        '''initiates board'''

        self.timer = QBasicTimer()
        self.isWaitingAfterLine = False
        
        self.curX = 0
        self.curY = 0
        self.numLinesRemoved = 0
        self.board = []

        self.setFocusPolicy(Qt.StrongFocus)
        self.isStarted = False
        self.isPaused = False
        self.clearBoard()
        
        
    def shapeAt(self, x, y):
        '''determines shape at the board position'''
        
        return self.board[(y * Board.BoardWidth) + x]

        
    def setShapeAt(self, x, y, shape):
        '''sets a shape at the board'''
        
        self.board[(y * Board.BoardWidth) + x] = shape
        

    def squareWidth(self):
        '''returns the width of one square'''
        
        return self.contentsRect().width() // Board.BoardWidth
        

    def squareHeight(self):
        '''returns the height of one square'''
        
        return self.contentsRect().height() // Board.BoardHeight
        

    def start(self):
        '''starts game'''
        
        if self.isPaused:
            return

        self.isStarted = True
        self.isWaitingAfterLine = False
        self.numLinesRemoved = 0
        self.clearBoard()

        self.msg2Statusbar.emit(str(self.numLinesRemoved))

        self.newPiece()
        self.timer.start(Board.Speed, self)

        
    def pause(self):
        '''pauses game'''
        
        if not self.isStarted:
            return

        self.isPaused = not self.isPaused
        
        if self.isPaused:
            self.timer.stop()
            self.msg2Statusbar.emit("paused")
            
        else:
            self.timer.start(Board.Speed, self)
            self.msg2Statusbar.emit(str(self.numLinesRemoved))

        self.update()

        
    def paintEvent(self, event):
        '''paints all shapes of the game'''
        
        painter = QPainter(self)
        rect = self.contentsRect()

        boardTop = rect.bottom() - Board.BoardHeight * self.squareHeight()

        for i in range(Board.BoardHeight):
            for j in range(Board.BoardWidth):
                shape = self.shapeAt(j, Board.BoardHeight - i - 1)
                
                if shape != Tetrominoe.NoShape:
                    self.drawSquare(painter,
                        rect.left() + j * self.squareWidth(),
                        boardTop + i * self.squareHeight(), shape)

        if self.curPiece.shape() != Tetrominoe.NoShape:
            
            for i in range(4):
                
                x = self.curX + self.curPiece.x(i)
                y = self.curY - self.curPiece.y(i)
                self.drawSquare(painter, rect.left() + x * self.squareWidth(),
                    boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
                    self.curPiece.shape())

                    
    def keyPressEvent(self, event):
        '''processes key press events'''
        
        if not self.isStarted or self.curPiece.shape() == Tetrominoe.NoShape:
            super(Board, self).keyPressEvent(event)
            return

        key = event.key()
        
        if key == Qt.Key_P:
            self.pause()
            return
            
        if self.isPaused:
            return
                
        elif key == Qt.Key_Left:
            self.tryMove(self.curPiece, self.curX - 1, self.curY)
            
        elif key == Qt.Key_Right:
            self.tryMove(self.curPiece, self.curX + 1, self.curY)
            
        elif key == Qt.Key_Down:
            self.tryMove(self.curPiece.rotateRight(), self.curX, self.curY)
            
        elif key == Qt.Key_Up:
            self.tryMove(self.curPiece.rotateLeft(), self.curX, self.curY)
            
        elif key == Qt.Key_Space:
            self.dropDown()
            
        elif key == Qt.Key_D:
            self.oneLineDown()
            
        else:
            super(Board, self).keyPressEvent(event)
                

    def timerEvent(self, event):
        '''handles timer event'''
        
        if event.timerId() == self.timer.timerId():
            
            if self.isWaitingAfterLine:
                self.isWaitingAfterLine = False
                self.newPiece()
            else:
                self.oneLineDown()
                
        else:
            super(Board, self).timerEvent(event)

            
    def clearBoard(self):
        '''clears shapes from the board'''
        
        for i in range(Board.BoardHeight * Board.BoardWidth):
            self.board.append(Tetrominoe.NoShape)

        
    def dropDown(self):
        '''drops down a shape'''
        
        newY = self.curY
        
        while newY > 0:
            
            if not self.tryMove(self.curPiece, self.curX, newY - 1):
                break
                
            newY -= 1

        self.pieceDropped()
        

    def oneLineDown(self):
        '''goes one line down with a shape'''
        
        if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
            self.pieceDropped()
            

    def pieceDropped(self):
        '''after dropping shape, remove full lines and create new shape'''
        
        for i in range(4):
            
            x = self.curX + self.curPiece.x(i)
            y = self.curY - self.curPiece.y(i)
            self.setShapeAt(x, y, self.curPiece.shape())

        self.removeFullLines()

        if not self.isWaitingAfterLine:
            self.newPiece()
            

    def removeFullLines(self):
        '''removes all full lines from the board'''
        
        numFullLines = 0
        rowsToRemove = []

        for i in range(Board.BoardHeight):
            
            n = 0
            for j in range(Board.BoardWidth):
                if not self.shapeAt(j, i) == Tetrominoe.NoShape:
                    n = n + 1

            if n == 10:
                rowsToRemove.append(i)

        rowsToRemove.reverse()
        

        for m in rowsToRemove:
            
            for k in range(m, Board.BoardHeight):
                for l in range(Board.BoardWidth):
                        self.setShapeAt(l, k, self.shapeAt(l, k + 1))

        numFullLines = numFullLines + len(rowsToRemove)

        if numFullLines > 0:
            
            self.numLinesRemoved = self.numLinesRemoved + numFullLines
            self.msg2Statusbar.emit(str(self.numLinesRemoved))
                
            self.isWaitingAfterLine = True
            self.curPiece.setShape(Tetrominoe.NoShape)
            self.update()
            

    def newPiece(self):
        '''creates a new shape'''
        
        self.curPiece = Shape()
        self.curPiece.setRandomShape()
        self.curX = Board.BoardWidth // 2 + 1
        self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
        
        if not self.tryMove(self.curPiece, self.curX, self.curY):
            
            self.curPiece.setShape(Tetrominoe.NoShape)
            self.timer.stop()
            self.isStarted = False
            self.msg2Statusbar.emit("Game over")



    def tryMove(self, newPiece, newX, newY):
        '''tries to move a shape'''
        
        for i in range(4):
            
            x = newX + newPiece.x(i)
            y = newY - newPiece.y(i)
            
            if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
                return False
                
            if self.shapeAt(x, y) != Tetrominoe.NoShape:
                return False

        self.curPiece = newPiece
        self.curX = newX
        self.curY = newY
        self.update()
        
        return True
        

    def drawSquare(self, painter, x, y, shape):
        '''draws a square of a shape'''        
        
        colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC,
                      0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]

        color = QColor(colorTable[shape])
        painter.fillRect(x + 1, y + 1, self.squareWidth() - 2, 
            self.squareHeight() - 2, color)

        painter.setPen(color.lighter())
        painter.drawLine(x, y + self.squareHeight() - 1, x, y)
        painter.drawLine(x, y, x + self.squareWidth() - 1, y)

        painter.setPen(color.darker())
        painter.drawLine(x + 1, y + self.squareHeight() - 1,
            x + self.squareWidth() - 1, y + self.squareHeight() - 1)
        painter.drawLine(x + self.squareWidth() - 1, 
            y + self.squareHeight() - 1, x + self.squareWidth() - 1, y + 1)


class Tetrominoe(object):
    
    NoShape = 0
    ZShape = 1
    SShape = 2
    LineShape = 3
    TShape = 4
    SquareShape = 5
    LShape = 6
    MirroredLShape = 7


class Shape(object):
    
    coordsTable = (
        ((0, 0),     (0, 0),     (0, 0),     (0, 0)),
        ((0, -1),    (0, 0),     (-1, 0),    (-1, 1)),
        ((0, -1),    (0, 0),     (1, 0),     (1, 1)),
        ((0, -1),    (0, 0),     (0, 1),     (0, 2)),
        ((-1, 0),    (0, 0),     (1, 0),     (0, 1)),
        ((0, 0),     (1, 0),     (0, 1),     (1, 1)),
        ((-1, -1),   (0, -1),    (0, 0),     (0, 1)),
        ((1, -1),    (0, -1),    (0, 0),     (0, 1))
    )

    def __init__(self):
        
        self.coords = [[0,0] for i in range(4)]
        self.pieceShape = Tetrominoe.NoShape

        self.setShape(Tetrominoe.NoShape)
        

    def shape(self):
        '''returns shape'''
        
        return self.pieceShape
        

    def setShape(self, shape):
        '''sets a shape'''
        
        table = Shape.coordsTable[shape]
        
        for i in range(4):
            for j in range(2):
                self.coords[i][j] = table[i][j]

        self.pieceShape = shape
        

    def setRandomShape(self):
        '''chooses a random shape'''
        
        self.setShape(random.randint(1, 7))

        
    def x(self, index):
        '''returns x coordinate'''
        
        return self.coords[index][0]

        
    def y(self, index):
        '''returns y coordinate'''
        
        return self.coords[index][1]

        
    def setX(self, index, x):
        '''sets x coordinate'''
        
        self.coords[index][0] = x

        
    def setY(self, index, y):
        '''sets y coordinate'''
        
        self.coords[index][1] = y

        
    def minX(self):
        '''returns min x value'''
        
        m = self.coords[0][0]
        for i in range(4):
            m = min(m, self.coords[i][0])

        return m

        
    def maxX(self):
        '''returns max x value'''
        
        m = self.coords[0][0]
        for i in range(4):
            m = max(m, self.coords[i][0])

        return m

        
    def minY(self):
        '''returns min y value'''
        
        m = self.coords[0][1]
        for i in range(4):
            m = min(m, self.coords[i][1])

        return m

        
    def maxY(self):
        '''returns max y value'''
        
        m = self.coords[0][1]
        for i in range(4):
            m = max(m, self.coords[i][1])

        return m

        
    def rotateLeft(self):
        '''rotates shape to the left'''
        
        if self.pieceShape == Tetrominoe.SquareShape:
            return self

        result = Shape()
        result.pieceShape = self.pieceShape
        
        for i in range(4):
            
            result.setX(i, self.y(i))
            result.setY(i, -self.x(i))

        return result

        
    def rotateRight(self):
        '''rotates shape to the right'''
        
        if self.pieceShape == Tetrominoe.SquareShape:
            return self

        result = Shape()
        result.pieceShape = self.pieceShape
        
        for i in range(4):
            
            result.setX(i, -self.y(i))
            result.setY(i, self.x(i))

        return result


if __name__ == '__main__':
    
    app = QApplication([])
    tetris = Tetris()    
    sys.exit(app.exec_())

The game has been simplified a bit to make it easier to understand. The game starts immediately after it starts. We can pause the game by pressing the button. The Space key will immediately drop to the bottom of a piece of Tetris. The game is played at a constant speed without acceleration. The score is the number of rows we deleted.

self.tboard = Board(self)
self.setCentralWidget(self.tboard)

BoardCreate an instance of the class and set it as the central widget of the application.

self.statusbar = self.statusBar()        
self.tboard.msg2Statusbar[str].connect(self.statusbar.showMessage)

We create a status bar where we will display the message. We will display three possible messages: number of deleted lines, paused message or game message. This  msg2Statusbaris a custom signal implemented in the Board class. This showMessage()is a built-in method for displaying messages on the status bar.

self.tboard.start()

This line starts the game.

class Board(QFrame):
    
    msg2Statusbar = pyqtSignal(str)
...   

Use to create custom signals pyqtSignal. What msg2Statusbaris the signal is sent when we want to write a message or score status bar.

BoardWidth = 10
BoardHeight = 22
Speed = 300

These are Board'sclass variables. Of BoardWidthand  BoardHeightthe size of blocks defined in the circuit board. In  Speeddefining the speed of the game. Every 300ms new game cycle will begin.

...
self.curX = 0
self.curY = 0
self.numLinesRemoved = 0
self.board = []
...

In this initBoard()method, we initialize some important variables. The self.boardvariables are from 0 to 7. It represents the location of various shapes and maintains a list of numbers in the board shape.

def shapeAt(self, x, y):
    '''determines shape at the board position'''
    
    return self.board[(y * Board.BoardWidth) + x]

This shapeAt()method determines the shape type of a given block.

def squareWidth(self):
    '''returns the width of one square'''
    
    return self.contentsRect().width() // Board.BoardWidth

The circuit board can be dynamically resized. As a result, the block size may change. In the squareWidth()calculation of the square of the width of the individual pixels, and returns it. What Board.BoardWidth is the size of the block of the board.

def pause(self):
    '''pauses game'''
    
    if not self.isStarted:
        return

    self.isPaused = not self.isPaused
    
    if self.isPaused:
        self.timer.stop()
        self.msg2Statusbar.emit("paused")
        
    else:
        self.timer.start(Board.Speed, self)
        self.msg2Statusbar.emit(str(self.numLinesRemoved))

    self.update()

This pause()method pauses the game. It will stop the timer and display a message on the status bar.

def paintEvent(self, event):
    '''paints all shapes of the game'''
    
    painter = QPainter(self)
    rect = self.contentsRect()
...        

This painting takes place in this paintEvent()method. This QPainter is responsible for all low-level paintings of PyQt5.

for i in range(Board.BoardHeight):
    for j in range(Board.BoardWidth):
        shape = self.shapeAt(j, Board.BoardHeight - i - 1)
        
        if shape != Tetrominoe.NoShape:
            self.drawSquare(painter,
                rect.left() + j * self.squareWidth(),
                boardTop + i * self.squareHeight(), shape)

The game's drawing is divided into two steps. In the first step, we draw all the shapes that fall to the bottom of the board or the rest of the shapes. self.boardRemember all the squares in the list variable. Use this shapeAt()method to access the variable.

if self.curPiece.shape() != Tetrominoe.NoShape:
    
    for i in range(4):
        
        x = self.curX + self.curPiece.x(i)
        y = self.curY - self.curPiece.y(i)
        self.drawSquare(painter, rect.left() + x * self.squareWidth(),
            boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
            self.curPiece.shape())

The next step is to draw the actual pieces that are dropped.

elif key == Qt.Key_Right:
    self.tryMove(self.curPiece, self.curX + 1, self.curY)

In the keyPressEvent()method, we check the key pressed. If we press the right arrow key, we will try to move to the right. We say try because this piece may not move.

elif key == Qt.Key_Up:
    self.tryMove(self.curPiece.rotateLeft(), self.curX, self.curY)

The Up arrow key will rotate the falling piece to the left.

elif key == Qt.Key_Space:
    self.dropDown()

The Space key will immediately drop to the bottom of the drop piece.

elif key == Qt.Key_D:
    self.oneLineDown()

Press the d key and the block will move down one block. It can be used to accelerate a little drop.

def timerEvent(self, event):
    '''handles timer event'''
    
    if event.timerId() == self.timer.timerId():
        
        if self.isWaitingAfterLine:
            self.isWaitingAfterLine = False
            self.newPiece()
        else:
            self.oneLineDown()
            
    else:
        super(Board, self).timerEvent(event)

In the timer event, we either create a new segment after the previous one is placed at the bottom, or we move a descending segment down one line.

def clearBoard(self):
    '''clears shapes from the board'''
    
    for i in range(Board.BoardHeight * Board.BoardWidth):
        self.board.append(Tetrominoe.NoShape)

This clearBoard()method Tetrominoe.NoShapeclears the circuit board by setting each block of the circuit board.

def removeFullLines(self):
    '''removes all full lines from the board'''
    
    numFullLines = 0
    rowsToRemove = []

    for i in range(Board.BoardHeight):
        
        n = 0
        for j in range(Board.BoardWidth):
            if not self.shapeAt(j, i) == Tetrominoe.NoShape:
                n = n + 1

        if n == 10:
            rowsToRemove.append(i)

    rowsToRemove.reverse()
    

    for m in rowsToRemove:
        
        for k in range(m, Board.BoardHeight):
            for l in range(Board.BoardWidth):
                    self.setShapeAt(l, k, self.shapeAt(l, k + 1))

    numFullLines = numFullLines + len(rowsToRemove)
 ...

If the work hits the bottom, we call it the removeFullLines()method. We find all the complete lines and delete them. We remove it by moving all lines above the current solid line by one line. Please note that we reversed the order of the rows to be deleted. Otherwise, it will not work properly. In our example, we use naive gravity . This means that these fragments may float above the gap.

def newPiece(self):
    '''creates a new shape'''
    
    self.curPiece = Shape()
    self.curPiece.setRandomShape()
    self.curX = Board.BoardWidth // 2 + 1
    self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
    
    if not self.tryMove(self.curPiece, self.curX, self.curY):
        
        self.curPiece.setShape(Tetrominoe.NoShape)
        self.timer.stop()
        self.isStarted = False
        self.msg2Statusbar.emit("Game over")

This newPiece()method randomly creates a new Tetris. If this piece cannot enter its initial position, the game ends.

def tryMove(self, newPiece, newX, newY):
    '''tries to move a shape'''
    
    for i in range(4):
        
        x = newX + newPiece.x(i)
        y = newY - newPiece.y(i)
        
        if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
            return False
            
        if self.shapeAt(x, y) != Tetrominoe.NoShape:
            return False

    self.curPiece = newPiece
    self.curX = newX
    self.curY = newY
    self.update()
    
    return True

In the tryMove()method, we try to move our shape. If the shape is on the edge of the board or adjacent to other parts, we return False. Otherwise, we will place the currently dropped part to a new location.

class Tetrominoe(object):
    
    NoShape = 0
    ZShape = 1
    SShape = 2
    LineShape = 3
    TShape = 4
    SquareShape = 5
    LShape = 6
    MirroredLShape = 7

This Tetrominoeclass contains the names of all possible shapes. We still have NoShapean empty space.

This Shapecourse holds information about Tetris.

class Shape(object):
    
    coordsTable = (
        ((0, 0),     (0, 0),     (0, 0),     (0, 0)),
        ((0, -1),    (0, 0),     (-1, 0),    (-1, 1)),
        ...
    )
...  

This coordsTabletuple considers all possible coordinate values ​​of our Tetris slice. This is a template, and all parts use their coordinate values.

self.coords = [[0,0] for i in range(4)]

After creation, we create an empty coordinate list. The list will save the coordinates of Tetris.

coordinate

Figure: Coordinates

The above image will help to understand the coordinate values ​​more. For example, the tuples (0, -1), (0, 0), (-1,0), (-1, -1) represent the Z shape. The figure illustrates the shape.

def rotateLeft(self):
    '''rotates shape to the left'''
    
    if self.pieceShape == Tetrominoe.SquareShape:
        return self

    result = Shape()
    result.pieceShape = self.pieceShape
    
    for i in range(4):
        
        result.setX(i, self.y(i))
        result.setY(i, -self.x(i))

    return result

This rotateLeft()method rotates one block to the left. The square does not have to rotate. This is why we just return a reference to the current object. Create a new clip and set its coordinates to the coordinates of the rotated clip.

Tetris

Figure: Tetris

Published 59 original articles · 69 praises · 270,000+ views

Guess you like

Origin blog.csdn.net/pansaky/article/details/98958078