news 2026/5/1 6:07:43

Python PyQt6教程十一-俄罗斯方块

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python PyQt6教程十一-俄罗斯方块

这是PyQt6教程。本教程适合初学者和中级程序员。阅读本教程后,您将能够编写非平凡的PyQt6应用程序。

代码示例可在本站下载:教程源代码

目录

  • 引言
  • 日期和时间
  • 第一个工程
  • 菜单与工具栏
  • 布局管理
  • 事件和信号
  • 对话框
  • 控件
  • 拖放
  • 绘画
  • 自定义控件
  • 俄罗斯方块

俄罗斯方块设计

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

俄罗斯方块

俄罗斯方块游戏是有史以来最受欢迎的电脑游戏之一。最初的游戏是由俄罗斯程序员Alexey Pajitnov于1985年设计和编程的。从那时起,俄罗斯方块几乎在每个计算机平台上都有很多变体。


俄罗斯方块被称为落块益智游戏。在这个游戏中,我们有七种不同的形状,称为tetrominoes:S形、Z形、T形、L形、线形、镜像L形和方形。这些形状中的每一个都由四个正方形形成。形状从木板上掉下来。俄罗斯方块游戏的目标是移动和旋转形状,使其尽可能地贴合。如果我们设法排成一排,那排就被摧毁了,我们得分。我们玩俄罗斯方块游戏,直到达到极限。

PyQt6是一个用于创建应用程序的工具包。还有其他旨在创建电脑游戏的库。然而,PyQt6和其他应用程序工具包可用于创建简单的游戏。


制作电脑游戏是提高编程技能的好方法。

The development

我们没有俄罗斯方块游戏的图像,我们使用PyQt6编程工具包中提供的绘图API绘制俄罗斯方块。每个电脑游戏的背后都有一个数学模型。俄罗斯方块也是如此。
游戏背后的一些想法:

  • 我们使用控件来创建游戏循环。QtCore.QBasicTimer
  • tetrominoes被画出来了。
  • 形状以正方形为基础移动(不是逐像素移动)。
  • 从数学上讲,棋盘是一个简单的数字列表。

该代码由四个类组成:、和。全班同学布置游戏。这是编写游戏逻辑的地方。该类包含所有俄罗斯方块的名称,该类包含俄罗斯方块代码。TetrisBoardTetrominoeShape

#!/usr/bin/python """ ZetCode PyQt6 tutorial This is a Tetris game clone. Author: Jan Bodnar Website: zetcode.com """ import random import sys from PyQt6.QtCore import Qt, QBasicTimer, pyqtSignal from PyQt6.QtGui import QPainter, QColor from PyQt6.QtWidgets import QMainWindow, QFrame, QApplication 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""" qr = self.frameGeometry() cp = self.screen().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) 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.FocusPolicy.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.Key_P: self.pause() return if self.isPaused: return elif key == Qt.Key.Key_Left.value: self.tryMove(self.curPiece, self.curX - 1, self.curY) elif key == Qt.Key.Key_Right.value: self.tryMove(self.curPiece, self.curX + 1, self.curY) elif key == Qt.Key.Key_Down.value: self.tryMove(self.curPiece.rotateRight(), self.curX, self.curY) elif key == Qt.Key.Key_Up.value: self.tryMove(self.curPiece.rotateLeft(), self.curX, self.curY) elif key == Qt.Key.Key_Space.value: self.dropDown() elif key == Qt.Key.Key_D.value: 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: NoShape = 0 ZShape = 1 SShape = 2 LineShape = 3 TShape = 4 SquareShape = 5 LShape = 6 MirroredLShape = 7 class Shape: 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 def main(): app = QApplication([]) tetris = Tetris() sys.exit(app.exec()) if __name__ == '__main__': main()

游戏被简化了一点,这样更容易理解。游戏启动后立即开始。我们可以按这个键暂停游戏。钥匙会让俄罗斯方块瞬间掉到底部。游戏以恒定速度进行,没有加速。分数是我们删除的行数。pSpace

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

创建该类的一个实例,并将其设置为应用程序的中心控件

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

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

self.tboard.start()

这条线启动了游戏。

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

使用创建自定义信号。当我们想在状态栏上写一条消息或分数时,会发出一个信号。

BoardWidth = 10 BoardHeight = 22 Speed = 300

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

Board'sBoardWidthBoardHeightSpeed

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

在该方法中,我们初始化了一些重要的变量。变量是一个从0到7的数字列表。它代表了各种形状的位置和板上形状的残留。

initBoardself.board

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

该方法确定给定块处的形状类型。

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

该板可以动态调整大小。因此,块的大小可能会发生变化。计算单个正方形的宽度(以像素为单位)并返回。是板的大小(以块为单位)。

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

绘画是按照这种方法进行的。负责PyQt6中的所有低级绘制。

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

下一步是绘制正在坠落的实际工件。

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

在该方法中,我们检查是否按下了按键。如果我们按向右箭头键,我们会尝试将工件向右移动。我们说尝试,因为这件作品可能无法移动。

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

箭头键将使下落块向左旋转。

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

钥匙会立即将掉落的碎片掉落到底部。

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

按下该键,该块将向下移动一个街区。它可以用来稍微加速碎片的下落

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

如果工件碰到底部,我们调用该方法。我们找出所有完整的行并将其删除。我们通过将当前整行上方的所有行向下移动一行来实现。请注意,我们颠倒了要删除的行的顺序。否则,它将无法正常工作。在我们的例子中,我们使用了一种天真的引力。这意味着这些碎片可能会漂浮在空隙之上。

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

在这种方法中,我们试图移动我们的形状。如果形状位于板的边缘或与其他部分相邻,我们将返回。否则,我们将当前的落锤放置在一个新的位置。

class Tetrominoe: 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)), ... ) ...

元组包含我们俄罗斯方块的所有可能坐标值。这是一个模板,所有部分都从中获取坐标值。

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

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

上图将有助于更深入地理解坐标值。例如,元组(0,-1)、(0,0)、(-1,0)和(-1,-1)表示Z形。该图说明了形状。

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

该方法将工件向左旋转。正方形不必旋转。这就是为什么我们简单地返回对当前对象的引用。将创建一个新工件,并将其坐标设置为旋转工件的坐标。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/30 3:35:31

互联网大厂都在哪些顶会上发论文?AI/ML/CV/NLP/推荐系统全解析

目录 一、AI 领域主流顶会全清单(CCF-A 类) 二、为什么 KDD / SIGIR / RecSys 都带 “ACM”? 🏢 通俗理解:ACM 就像一家“科技出版社集团” 举个例子 📌 其他主要主办方对比: 三、Workshop 是什么?能算正式成果吗? 🎯 一句话定义: 🧩 特点 vs. 主会议: 💡 为…

作者头像 李华
网站建设 2026/5/1 5:06:27

5步搞定!零网络环境下宝塔面板v7.7.0离线安装全攻略 [特殊字符]

5步搞定&#xff01;零网络环境下宝塔面板v7.7.0离线安装全攻略 &#x1f680; 【免费下载链接】btpanel-v7.7.0 宝塔v7.7.0官方原版备份 项目地址: https://gitcode.com/GitHub_Trending/btp/btpanel-v7.7.0 还在为服务器无法联网而发愁吗&#xff1f;想在内网环境快速…

作者头像 李华
网站建设 2026/5/1 4:59:52

VibeVoice:重新定义智能语音交互的边界与想象

在清晨的播客录制间里&#xff0c;一位创作者正通过AI语音助手与"虚拟嘉宾"进行深度对话——不同角色的声音切换流畅自然&#xff0c;情感表达细腻生动。这不再是科幻电影的场景&#xff0c;而是微软VibeVoice开源框架带来的现实变革。当传统语音合成技术还在为短文本…

作者头像 李华
网站建设 2026/4/29 0:17:01

MeshCentral:终极远程设备管理解决方案指南

MeshCentral&#xff1a;终极远程设备管理解决方案指南 【免费下载链接】MeshCentral A complete web-based remote monitoring and management web site. Once setup you can install agents and perform remote desktop session to devices on the local network or over the…

作者头像 李华
网站建设 2026/4/30 21:47:30

Bruno API测试工具终极指南:告别Postman的高效开源替代方案

Bruno API测试工具终极指南&#xff1a;告别Postman的高效开源替代方案 【免费下载链接】bruno 开源的API探索与测试集成开发环境&#xff08;作为Postman/Insomnia的轻量级替代方案&#xff09; 项目地址: https://gitcode.com/GitHub_Trending/br/bruno 还在为API测试…

作者头像 李华
网站建设 2026/4/30 18:46:58

Step-Audio 2 Mini:开源语音大模型如何重塑人机交互未来

Step-Audio 2 Mini&#xff1a;开源语音大模型如何重塑人机交互未来 【免费下载链接】Step-Audio-2-mini 项目地址: https://ai.gitcode.com/hf_mirrors/stepfun-ai/Step-Audio-2-mini 导语 2025年语音交互领域迎来技术突破——StepFun团队推出的开源模型Step-Audio 2…

作者头像 李华