原文: http://zetcode.com/gui/pyqt5/tetris/

在本章中,我们将创建一个俄罗斯方块游戏克隆。

俄罗斯方块

俄罗斯方块游戏是有史以来最受欢迎的计算机游戏之一。 原始游戏是由俄罗斯程序员 Alexey Pajitnov 于 1985 年设计和编程的。此后,几乎所有版本的几乎所有计算机平台上都可以使用俄罗斯方块。

俄罗斯方块被称为下降块益智游戏。 在这个游戏中,我们有七个不同的形状,称为 tetrominoes:S 形,Z 形,T 形,L 形,线形,MirroredL 形和正方形。 这些形状中的每一个都形成有四个正方形。 形状从板上掉下来。 俄罗斯方块游戏的目的是移动和旋转形状,以使其尽可能地适合。 如果我们设法形成一行,则该行将被破坏并得分。 我们玩俄罗斯方块游戏,直到达到顶峰。

PyQt5 中的俄罗斯方块 - 图1

图:Tetrominoes

PyQt5 是旨在创建应用的工具包。 还有其他一些旨在创建计算机游戏的库。 尽管如此,PyQt5 和其他应用工具包仍可用于创建简单的游戏。

创建计算机游戏是增强编程技能的好方法。

开发

我们的俄罗斯方块游戏没有图像,我们使用 PyQt5 编程工具包中提供的绘图 API 绘制四面体。 每个计算机游戏的背后都有一个数学模型。 俄罗斯方块也是如此。

游戏背后的一些想法:

  • 我们使用QtCore.QBasicTimer()创建游戏周期。
  • 绘制四方块。
  • 形状以正方形为单位移动(而不是逐个像素移动)。
  • 从数学上讲,棋盘是一个简单的数字列表。

该代码包括四个类别:TetrisBoardTetrominoeShapeTetris类设置游戏。 Board是编写游戏逻辑的地方。 Tetrominoe类包含所有俄罗斯方块的名称,Shape类包含俄罗斯方块的代码。

tetris.py

  1. #!/usr/bin/python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. ZetCode PyQt5 tutorial
  5. This is a Tetris game clone.
  6. Author: Jan Bodnar
  7. Website: zetcode.com
  8. Last edited: August 2017
  9. """
  10. from PyQt5.QtWidgets import QMainWindow, QFrame, QDesktopWidget, QApplication
  11. from PyQt5.QtCore import Qt, QBasicTimer, pyqtSignal
  12. from PyQt5.QtGui import QPainter, QColor
  13. import sys, random
  14. class Tetris(QMainWindow):
  15. def __init__(self):
  16. super().__init__()
  17. self.initUI()
  18. def initUI(self):
  19. '''initiates application UI'''
  20. self.tboard = Board(self)
  21. self.setCentralWidget(self.tboard)
  22. self.statusbar = self.statusBar()
  23. self.tboard.msg2Statusbar[str].connect(self.statusbar.showMessage)
  24. self.tboard.start()
  25. self.resize(180, 380)
  26. self.center()
  27. self.setWindowTitle('Tetris')
  28. self.show()
  29. def center(self):
  30. '''centers the window on the screen'''
  31. screen = QDesktopWidget().screenGeometry()
  32. size = self.geometry()
  33. self.move((screen.width()-size.width())/2,
  34. (screen.height()-size.height())/2)
  35. class Board(QFrame):
  36. msg2Statusbar = pyqtSignal(str)
  37. BoardWidth = 10
  38. BoardHeight = 22
  39. Speed = 300
  40. def __init__(self, parent):
  41. super().__init__(parent)
  42. self.initBoard()
  43. def initBoard(self):
  44. '''initiates board'''
  45. self.timer = QBasicTimer()
  46. self.isWaitingAfterLine = False
  47. self.curX = 0
  48. self.curY = 0
  49. self.numLinesRemoved = 0
  50. self.board = []
  51. self.setFocusPolicy(Qt.StrongFocus)
  52. self.isStarted = False
  53. self.isPaused = False
  54. self.clearBoard()
  55. def shapeAt(self, x, y):
  56. '''determines shape at the board position'''
  57. return self.board[(y * Board.BoardWidth) + x]
  58. def setShapeAt(self, x, y, shape):
  59. '''sets a shape at the board'''
  60. self.board[(y * Board.BoardWidth) + x] = shape
  61. def squareWidth(self):
  62. '''returns the width of one square'''
  63. return self.contentsRect().width() // Board.BoardWidth
  64. def squareHeight(self):
  65. '''returns the height of one square'''
  66. return self.contentsRect().height() // Board.BoardHeight
  67. def start(self):
  68. '''starts game'''
  69. if self.isPaused:
  70. return
  71. self.isStarted = True
  72. self.isWaitingAfterLine = False
  73. self.numLinesRemoved = 0
  74. self.clearBoard()
  75. self.msg2Statusbar.emit(str(self.numLinesRemoved))
  76. self.newPiece()
  77. self.timer.start(Board.Speed, self)
  78. def pause(self):
  79. '''pauses game'''
  80. if not self.isStarted:
  81. return
  82. self.isPaused = not self.isPaused
  83. if self.isPaused:
  84. self.timer.stop()
  85. self.msg2Statusbar.emit("paused")
  86. else:
  87. self.timer.start(Board.Speed, self)
  88. self.msg2Statusbar.emit(str(self.numLinesRemoved))
  89. self.update()
  90. def paintEvent(self, event):
  91. '''paints all shapes of the game'''
  92. painter = QPainter(self)
  93. rect = self.contentsRect()
  94. boardTop = rect.bottom() - Board.BoardHeight * self.squareHeight()
  95. for i in range(Board.BoardHeight):
  96. for j in range(Board.BoardWidth):
  97. shape = self.shapeAt(j, Board.BoardHeight - i - 1)
  98. if shape != Tetrominoe.NoShape:
  99. self.drawSquare(painter,
  100. rect.left() + j * self.squareWidth(),
  101. boardTop + i * self.squareHeight(), shape)
  102. if self.curPiece.shape() != Tetrominoe.NoShape:
  103. for i in range(4):
  104. x = self.curX + self.curPiece.x(i)
  105. y = self.curY - self.curPiece.y(i)
  106. self.drawSquare(painter, rect.left() + x * self.squareWidth(),
  107. boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
  108. self.curPiece.shape())
  109. def keyPressEvent(self, event):
  110. '''processes key press events'''
  111. if not self.isStarted or self.curPiece.shape() == Tetrominoe.NoShape:
  112. super(Board, self).keyPressEvent(event)
  113. return
  114. key = event.key()
  115. if key == Qt.Key_P:
  116. self.pause()
  117. return
  118. if self.isPaused:
  119. return
  120. elif key == Qt.Key_Left:
  121. self.tryMove(self.curPiece, self.curX - 1, self.curY)
  122. elif key == Qt.Key_Right:
  123. self.tryMove(self.curPiece, self.curX + 1, self.curY)
  124. elif key == Qt.Key_Down:
  125. self.tryMove(self.curPiece.rotateRight(), self.curX, self.curY)
  126. elif key == Qt.Key_Up:
  127. self.tryMove(self.curPiece.rotateLeft(), self.curX, self.curY)
  128. elif key == Qt.Key_Space:
  129. self.dropDown()
  130. elif key == Qt.Key_D:
  131. self.oneLineDown()
  132. else:
  133. super(Board, self).keyPressEvent(event)
  134. def timerEvent(self, event):
  135. '''handles timer event'''
  136. if event.timerId() == self.timer.timerId():
  137. if self.isWaitingAfterLine:
  138. self.isWaitingAfterLine = False
  139. self.newPiece()
  140. else:
  141. self.oneLineDown()
  142. else:
  143. super(Board, self).timerEvent(event)
  144. def clearBoard(self):
  145. '''clears shapes from the board'''
  146. for i in range(Board.BoardHeight * Board.BoardWidth):
  147. self.board.append(Tetrominoe.NoShape)
  148. def dropDown(self):
  149. '''drops down a shape'''
  150. newY = self.curY
  151. while newY > 0:
  152. if not self.tryMove(self.curPiece, self.curX, newY - 1):
  153. break
  154. newY -= 1
  155. self.pieceDropped()
  156. def oneLineDown(self):
  157. '''goes one line down with a shape'''
  158. if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
  159. self.pieceDropped()
  160. def pieceDropped(self):
  161. '''after dropping shape, remove full lines and create new shape'''
  162. for i in range(4):
  163. x = self.curX + self.curPiece.x(i)
  164. y = self.curY - self.curPiece.y(i)
  165. self.setShapeAt(x, y, self.curPiece.shape())
  166. self.removeFullLines()
  167. if not self.isWaitingAfterLine:
  168. self.newPiece()
  169. def removeFullLines(self):
  170. '''removes all full lines from the board'''
  171. numFullLines = 0
  172. rowsToRemove = []
  173. for i in range(Board.BoardHeight):
  174. n = 0
  175. for j in range(Board.BoardWidth):
  176. if not self.shapeAt(j, i) == Tetrominoe.NoShape:
  177. n = n + 1
  178. if n == 10:
  179. rowsToRemove.append(i)
  180. rowsToRemove.reverse()
  181. for m in rowsToRemove:
  182. for k in range(m, Board.BoardHeight):
  183. for l in range(Board.BoardWidth):
  184. self.setShapeAt(l, k, self.shapeAt(l, k + 1))
  185. numFullLines = numFullLines + len(rowsToRemove)
  186. if numFullLines > 0:
  187. self.numLinesRemoved = self.numLinesRemoved + numFullLines
  188. self.msg2Statusbar.emit(str(self.numLinesRemoved))
  189. self.isWaitingAfterLine = True
  190. self.curPiece.setShape(Tetrominoe.NoShape)
  191. self.update()
  192. def newPiece(self):
  193. '''creates a new shape'''
  194. self.curPiece = Shape()
  195. self.curPiece.setRandomShape()
  196. self.curX = Board.BoardWidth // 2 + 1
  197. self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
  198. if not self.tryMove(self.curPiece, self.curX, self.curY):
  199. self.curPiece.setShape(Tetrominoe.NoShape)
  200. self.timer.stop()
  201. self.isStarted = False
  202. self.msg2Statusbar.emit("Game over")
  203. def tryMove(self, newPiece, newX, newY):
  204. '''tries to move a shape'''
  205. for i in range(4):
  206. x = newX + newPiece.x(i)
  207. y = newY - newPiece.y(i)
  208. if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
  209. return False
  210. if self.shapeAt(x, y) != Tetrominoe.NoShape:
  211. return False
  212. self.curPiece = newPiece
  213. self.curX = newX
  214. self.curY = newY
  215. self.update()
  216. return True
  217. def drawSquare(self, painter, x, y, shape):
  218. '''draws a square of a shape'''
  219. colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC,
  220. 0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]
  221. color = QColor(colorTable[shape])
  222. painter.fillRect(x + 1, y + 1, self.squareWidth() - 2,
  223. self.squareHeight() - 2, color)
  224. painter.setPen(color.lighter())
  225. painter.drawLine(x, y + self.squareHeight() - 1, x, y)
  226. painter.drawLine(x, y, x + self.squareWidth() - 1, y)
  227. painter.setPen(color.darker())
  228. painter.drawLine(x + 1, y + self.squareHeight() - 1,
  229. x + self.squareWidth() - 1, y + self.squareHeight() - 1)
  230. painter.drawLine(x + self.squareWidth() - 1,
  231. y + self.squareHeight() - 1, x + self.squareWidth() - 1, y + 1)
  232. class Tetrominoe(object):
  233. NoShape = 0
  234. ZShape = 1
  235. SShape = 2
  236. LineShape = 3
  237. TShape = 4
  238. SquareShape = 5
  239. LShape = 6
  240. MirroredLShape = 7
  241. class Shape(object):
  242. coordsTable = (
  243. ((0, 0), (0, 0), (0, 0), (0, 0)),
  244. ((0, -1), (0, 0), (-1, 0), (-1, 1)),
  245. ((0, -1), (0, 0), (1, 0), (1, 1)),
  246. ((0, -1), (0, 0), (0, 1), (0, 2)),
  247. ((-1, 0), (0, 0), (1, 0), (0, 1)),
  248. ((0, 0), (1, 0), (0, 1), (1, 1)),
  249. ((-1, -1), (0, -1), (0, 0), (0, 1)),
  250. ((1, -1), (0, -1), (0, 0), (0, 1))
  251. )
  252. def __init__(self):
  253. self.coords = [[0,0] for i in range(4)]
  254. self.pieceShape = Tetrominoe.NoShape
  255. self.setShape(Tetrominoe.NoShape)
  256. def shape(self):
  257. '''returns shape'''
  258. return self.pieceShape
  259. def setShape(self, shape):
  260. '''sets a shape'''
  261. table = Shape.coordsTable[shape]
  262. for i in range(4):
  263. for j in range(2):
  264. self.coords[i][j] = table[i][j]
  265. self.pieceShape = shape
  266. def setRandomShape(self):
  267. '''chooses a random shape'''
  268. self.setShape(random.randint(1, 7))
  269. def x(self, index):
  270. '''returns x coordinate'''
  271. return self.coords[index][0]
  272. def y(self, index):
  273. '''returns y coordinate'''
  274. return self.coords[index][1]
  275. def setX(self, index, x):
  276. '''sets x coordinate'''
  277. self.coords[index][0] = x
  278. def setY(self, index, y):
  279. '''sets y coordinate'''
  280. self.coords[index][1] = y
  281. def minX(self):
  282. '''returns min x value'''
  283. m = self.coords[0][0]
  284. for i in range(4):
  285. m = min(m, self.coords[i][0])
  286. return m
  287. def maxX(self):
  288. '''returns max x value'''
  289. m = self.coords[0][0]
  290. for i in range(4):
  291. m = max(m, self.coords[i][0])
  292. return m
  293. def minY(self):
  294. '''returns min y value'''
  295. m = self.coords[0][1]
  296. for i in range(4):
  297. m = min(m, self.coords[i][1])
  298. return m
  299. def maxY(self):
  300. '''returns max y value'''
  301. m = self.coords[0][1]
  302. for i in range(4):
  303. m = max(m, self.coords[i][1])
  304. return m
  305. def rotateLeft(self):
  306. '''rotates shape to the left'''
  307. if self.pieceShape == Tetrominoe.SquareShape:
  308. return self
  309. result = Shape()
  310. result.pieceShape = self.pieceShape
  311. for i in range(4):
  312. result.setX(i, self.y(i))
  313. result.setY(i, -self.x(i))
  314. return result
  315. def rotateRight(self):
  316. '''rotates shape to the right'''
  317. if self.pieceShape == Tetrominoe.SquareShape:
  318. return self
  319. result = Shape()
  320. result.pieceShape = self.pieceShape
  321. for i in range(4):
  322. result.setX(i, -self.y(i))
  323. result.setY(i, self.x(i))
  324. return result
  325. if __name__ == '__main__':
  326. app = QApplication([])
  327. tetris = Tetris()
  328. sys.exit(app.exec_())

游戏进行了简化,以便于理解。 游戏启动后立即开始。 我们可以通过按 p 键暂停游戏。 Space 键将使俄罗斯方块立即下降到底部。 游戏以恒定速度进行,没有实现加速。 分数是我们已删除的行数。

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

创建Board类的实例,并将其设置为应用的中央窗口小部件。

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

我们创建一个状态栏,在其中显示消息。 我们将显示三种可能的消息:已删除的行数,暂停的消息或游戏结束消息。 msg2Statusbar是在Board类中实现的自定义信号。 showMessage()是一种内置方法,可在状态栏上显示一条消息。

  1. self.tboard.start()

这条线启动了游戏。

  1. class Board(QFrame):
  2. msg2Statusbar = pyqtSignal(str)
  3. ...

使用pyqtSignal创建自定义信号。 msg2Statusbar是当我们要向状态栏写消息或乐谱时发出的信号。

  1. BoardWidth = 10
  2. BoardHeight = 22
  3. Speed = 300

这些是Board's类变量。 BoardWidthBoardHeight以块为单位定义电路板的大小。 Speed定义游戏的速度。 每 300 毫秒将开始一个新的游戏周期。

  1. ...
  2. self.curX = 0
  3. self.curY = 0
  4. self.numLinesRemoved = 0
  5. self.board = []
  6. ...

initBoard()方法中,我们初始化了一些重要的变量。 变量self.board是从 0 到 7 的数字的列表。它表示各种形状的位置以及板上形状的其余部分。

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

shapeAt()方法确定给定块上的形状类型。

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

电路板可以动态调整大小。 结果,块的大小可能改变。 squareWidth()计算单个正方形的宽度(以像素为单位)并将其返回。 Board.BoardWidth是板的大小,以块为单位。

  1. def pause(self):
  2. '''pauses game'''
  3. if not self.isStarted:
  4. return
  5. self.isPaused = not self.isPaused
  6. if self.isPaused:
  7. self.timer.stop()
  8. self.msg2Statusbar.emit("paused")
  9. else:
  10. self.timer.start(Board.Speed, self)
  11. self.msg2Statusbar.emit(str(self.numLinesRemoved))
  12. self.update()

pause()方法暂停游戏。 它停止计时器并在状态栏上显示一条消息。

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

绘图以paintEvent()方法进行。 QPainter负责 PyQt5 中的所有低级绘图。

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

游戏的绘图分为两个步骤。 在第一步中,我们绘制所有形状或已放置到板底部的形状的其余部分。 所有正方形都记在self.board列表变量中。 使用shapeAt()方法访问该变量。

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

下一步是下落的实际零件的图纸。

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

keyPressEvent()方法中,我们检查按键是否按下。 如果按向右箭头键,我们将尝试将棋子向右移动。 我们说尝试,因为那条可能无法移动。

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

向上方向键将使下降片向左旋转。

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

空格键将立即将下降的片段降到底部。

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

按下 d 键,乐曲将向下移动一个格。 它可以用来加速一块的下落。

  1. def timerEvent(self, event):
  2. '''handles timer event'''
  3. if event.timerId() == self.timer.timerId():
  4. if self.isWaitingAfterLine:
  5. self.isWaitingAfterLine = False
  6. self.newPiece()
  7. else:
  8. self.oneLineDown()
  9. else:
  10. super(Board, self).timerEvent(event)

在计时器事件中,我们要么在上一个下降到底部之后创建一个新作品,要么将下降的一块向下移动一行。

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

clearBoard()方法通过在板的每个块上设置Tetrominoe.NoShape来清除板。

  1. def removeFullLines(self):
  2. '''removes all full lines from the board'''
  3. numFullLines = 0
  4. rowsToRemove = []
  5. for i in range(Board.BoardHeight):
  6. n = 0
  7. for j in range(Board.BoardWidth):
  8. if not self.shapeAt(j, i) == Tetrominoe.NoShape:
  9. n = n + 1
  10. if n == 10:
  11. rowsToRemove.append(i)
  12. rowsToRemove.reverse()
  13. for m in rowsToRemove:
  14. for k in range(m, Board.BoardHeight):
  15. for l in range(Board.BoardWidth):
  16. self.setShapeAt(l, k, self.shapeAt(l, k + 1))
  17. numFullLines = numFullLines + len(rowsToRemove)
  18. ...

如果片段触底,我们将调用removeFullLines()方法。 我们找出所有实线并将其删除。 通过将所有行移动到当前全行上方来将其向下移动一行来实现。 请注意,我们颠倒了要删除的行的顺序。 否则,它将无法正常工作。 在我们的情况下,我们使用朴素重力。 这意味着碎片可能会漂浮在空的间隙上方。

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

newPiece()方法随机创建一个新的俄罗斯方块。 如果棋子无法进入其初始位置,则游戏结束。

  1. def tryMove(self, newPiece, newX, newY):
  2. '''tries to move a shape'''
  3. for i in range(4):
  4. x = newX + newPiece.x(i)
  5. y = newY - newPiece.y(i)
  6. if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
  7. return False
  8. if self.shapeAt(x, y) != Tetrominoe.NoShape:
  9. return False
  10. self.curPiece = newPiece
  11. self.curX = newX
  12. self.curY = newY
  13. self.update()
  14. return True

tryMove()方法中,我们尝试移动形状。 如果形状在板的边缘或与其他零件相邻,则返回False。 否则,我们将当前的下降片放到新位置。

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

Tetrominoe类包含所有可能形状的名称。 我们还有一个NoShape用于空白。

Shape类保存有关俄罗斯方块的信息。

  1. class Shape(object):
  2. coordsTable = (
  3. ((0, 0), (0, 0), (0, 0), (0, 0)),
  4. ((0, -1), (0, 0), (-1, 0), (-1, 1)),
  5. ...
  6. )
  7. ...

coordsTable元组保存了俄罗斯方块的所有可能的坐标值。 这是一个模板,所有零件均从该模板获取其坐标值。

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

创建后,我们将创建一个空坐标列表。 该列表将保存俄罗斯方块的坐标。

PyQt5 中的俄罗斯方块 - 图2

图:坐标

上面的图片将帮助您更多地了解坐标值。 例如,元组(0,-1),(0,0),(-1,0),(-1,-1)表示 Z 形。 该图说明了形状。

  1. def rotateLeft(self):
  2. '''rotates shape to the left'''
  3. if self.pieceShape == Tetrominoe.SquareShape:
  4. return self
  5. result = Shape()
  6. result.pieceShape = self.pieceShape
  7. for i in range(4):
  8. result.setX(i, self.y(i))
  9. result.setY(i, -self.x(i))
  10. return result

rotateLeft()方法将一块向左旋转。 正方形不必旋转。 这就是为什么我们只是将引用返回到当前对象。 将创建一个新零件,并将其坐标设置为旋转零件的坐标。

PyQt5 中的俄罗斯方块 - 图3

图:俄罗斯方块

这是 PyQt5 中的俄罗斯方块游戏。