PyQt5 tutorial "Juego de Tetris"

 

Tetris en PyQt5

En este capítulo, crearemos un clon del juego Tetris.

 

Tetris

El juego Tetris es uno de los juegos de computadora más populares de todos los tiempos. El juego original fue diseñado y programado por el programador ruso Alexey Pajitnov en 1985. Desde entonces, Tetris ha estado disponible en casi todas las plataformas informáticas.

Tetris se llama un juego de rompecabezas. En este juego, tenemos siete formas diferentes, llamadas cuadriláteros : en forma de S, en forma de Z, en forma de T, en forma de L, lineal, en forma de L espejo y cuadrado. Cada una de estas formas se forma con cuatro cuadrados. La forma se cayó del tablero. El propósito del juego Tetris es mover y rotar las formas para que se ajusten lo más posible. Si logramos formar una fila, la fila se destruirá y puntuaremos. Jugamos juegos de Tetris hasta que hayamos terminado.

Tetrominoes

图 : Tetrominoes

PyQt5 es un juego de herramientas diseñado para crear aplicaciones. Hay otras bibliotecas diseñadas para crear juegos de computadora. Sin embargo, PyQt5 y otros kits de herramientas de aplicaciones se pueden usar para crear juegos simples.

Crear juegos de computadora es una excelente manera de mejorar tus habilidades de programación.

Desarrollo

No tenemos imágenes de juegos de Tetris, y utilizamos la API de dibujo proporcionada en el kit de herramientas de programación PyQt5 para dibujar los dominós cuádruples. Hay un modelo matemático detrás de cada juego de computadora. Así es en Tetris.

Algunas ideas detrás del juego:

  • Usamos a para  QtCore.QBasicTimer()crear un ciclo de juego.
  • Se dibuja el cuádruple.
  • La forma se mueve cuadrado por cuadrado (no píxel por píxel).
  • Matemáticamente, la placa de circuito es una simple lista de números.

El código consta de cuatro Tetriscategorías: Board,, Tetrominoe y Shape. Este Tetriscurso tiene juegos. Aquí Boardes donde se escribe la lógica del juego. Esta Tetrominoecategoría contiene todas las piezas de Tetris nombradas y la Shapecategoría contiene un código de bloque de Tetris.

#!/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_())

El juego se ha simplificado un poco para que sea más fácil de entender. El juego comienza inmediatamente después de que comienza. Podemos pausar el juego presionando el botón. La tecla Espacio caerá inmediatamente al fondo de una pieza de Tetris. El juego se juega a una velocidad constante sin aceleración. El puntaje es el número de filas que eliminamos.

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

BoardCree una instancia de la clase y configúrela como el widget central de la aplicación.

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

Creamos una barra de estado donde mostraremos el mensaje. Mostraremos tres mensajes posibles: número de líneas eliminadas, mensaje en pausa o mensaje del juego. Esta  msg2Statusbares una señal personalizada implementada en la clase Board. Este showMessage()es un método incorporado para mostrar mensajes en la barra de estado.

self.tboard.start()

Esta línea comienza el juego.

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

Úselo para crear señales personalizadas pyqtSignal. ¿Cuál msg2Statusbares la señal que se envía cuando queremos escribir un mensaje o barra de estado de puntuación?

BoardWidth = 10
BoardHeight = 22
Speed = 300

Estas son Board'svariables de clase. De BoardWidthBoardHeightel tamaño de los bloques definidos en la placa de circuito. Al  Speeddefinir la velocidad del juego. Cada nuevo ciclo de juego de 300 ms comenzará.

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

En este initBoard()método, inicializamos algunas variables importantes. Las self.boardvariables son de 0 a 7. Representa la ubicación de varias formas y mantiene una lista de números en la forma del tablero.

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

Este shapeAt()método determina el tipo de forma de un bloque dado.

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

La placa de circuito puede redimensionarse dinámicamente. Como resultado, el tamaño del bloque puede cambiar. En el squareWidth()cálculo del cuadrado de la anchura de los píxeles individuales, y la devuelve. ¿Cuál Board.BoardWidth es el tamaño del bloque del tablero?

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()

Este pause()método detiene el juego. Parará el temporizador y mostrará un mensaje en la barra de estado.

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

Esta pintura tiene lugar en este paintEvent()método. Esto QPainter es responsable de todas las pinturas de bajo nivel de 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)

El dibujo del juego se divide en dos pasos. En el primer paso, dibujamos todas las formas que caen al fondo del tablero o al resto de las formas. self.boardRecuerde todos los cuadrados en la variable de lista. Use este shapeAt()método para acceder a la 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())

El siguiente paso es dibujar las piezas reales que se caen.

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

En el keyPressEvent()método, verificamos la tecla presionada. Si presionamos la tecla de flecha hacia la derecha, intentaremos movernos hacia la derecha. Decimos intentar porque esta pieza puede no moverse.

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

La tecla de flecha hacia arriba rotará la pieza que cae hacia la izquierda.

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

La tecla Espacio se colocará inmediatamente en la parte inferior de la pieza suelta.

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

Presione la tecla d y el bloque se moverá hacia abajo un bloque. Se puede usar para acelerar una pequeña caída.

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)

En el evento del temporizador, creamos un nuevo segmento después de que el anterior se coloca en la parte inferior, o movemos un segmento descendente una línea hacia abajo.

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

Este clearBoard()método Tetrominoe.NoShapeborra la placa de circuito configurando cada bloque de la placa de circuito.

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)
 ...

Si el trabajo toca el fondo, lo llamamos el removeFullLines()método. Encontramos todas las líneas completas y las eliminamos. Lo eliminamos moviendo todas las líneas sobre la línea continua actual en una línea. Tenga en cuenta que invertimos el orden de las filas que se eliminarán. De lo contrario, no funcionará correctamente. En nuestro ejemplo, usamos la gravedad ingenua . Esto significa que estos fragmentos pueden flotar sobre la brecha.

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")

Este newPiece()método crea aleatoriamente un nuevo Tetris. Si esta pieza no puede entrar en su posición inicial, el juego termina.

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

En el tryMove()método, tratamos de mover nuestra forma. Si la forma está en el borde del tablero o adyacente a otras partes, regresamos False. De lo contrario, colocaremos la parte actualmente caída en una nueva ubicación.

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

Esta Tetrominoeclase contiene los nombres de todas las formas posibles. Todavía tenemos NoShapeun espacio vacío.

Este Shapecurso contiene información sobre Tetris.

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

Esta coordsTabletupla considera todos los valores de coordenadas posibles de nuestro segmento Tetris. Esta es una plantilla, y todas las partes usan sus valores de coordenadas.

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

Después de la creación, creamos una lista de coordenadas vacía. La lista guardará las coordenadas de Tetris.

Coordenadas

Figura: Coordenadas

La imagen de arriba ayudará a comprender más los valores de coordenadas. Por ejemplo, las tuplas (0, -1), (0, 0), (-1,0), (-1, -1) representan la forma de Z. La figura ilustra la forma.

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

Este rotateLeft()método gira un bloque hacia la izquierda. El cuadrado no tiene que girar. Es por eso que solo devolvemos una referencia al objeto actual. Cree un nuevo clip y establezca sus coordenadas en las coordenadas del clip girado.

Tetris

Figura: Tetris

Publicado 59 artículos originales · 69 alabanzas · 270,000+ visitas

Supongo que te gusta

Origin blog.csdn.net/pansaky/article/details/98958078
Recomendado
Clasificación