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
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 Tetris
categorías: Board
,, Tetrominoe
y Shape
. Este Tetris
curso tiene juegos. Aquí Board
es donde se escribe la lógica del juego. Esta Tetrominoe
categoría contiene todas las piezas de Tetris nombradas y la Shape
categorí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)
Board
Cree 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 msg2Statusbar
es 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 msg2Statusbar
es 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's
variables de clase. De BoardWidth
y BoardHeight
el tamaño de los bloques definidos en la placa de circuito. Al Speed
definir 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.board
variables 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.board
Recuerde 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.NoShape
borra 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 Tetrominoe
clase contiene los nombres de todas las formas posibles. Todavía tenemos NoShape
un espacio vacío.
Este Shape
curso 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 coordsTable
tupla 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.
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.
Figura: Tetris