"""俄罗斯方块
author: wolfstar
last edited: 2018年1月"""
importsys, randomfrom PyQt5.QtWidgets importQMainWindow, QFrame, QDesktopWidget, QApplicationfrom PyQt5.QtCore importQt, QBasicTimer, pyqtSignalfrom PyQt5.QtGui importQPainter, QColor#主界面类
classTetris(QMainWindow):def __init__(self):
super().__init__()
self.initUI()definitUI(self):#Board创建一个面板类的实例,并设置应用程序的核心部件。
self.tboard =Board(self)
self.setCentralWidget(self.tboard)#创建一个状态栏将显示消息。我们将显示三种可能的消息:已删除的行数,停顿了一下消息,或游戏结束的消息。msg2Statusbar是一个自定义的信号,在Board 中实现类。showMessage()是一个内置的方法,在状态栏显示一条消息。
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()#窗体居中显示方法
defcenter(self):
screen=QDesktopWidget().screenGeometry()
size=self.geometry()
self.move((screen.width()- size.width()) / 2,
(screen.height()- size.height()) / 2)#创建面板类
classBoard(QFrame):#创建一个自定义的信号。当我们想写一个信息或状态栏的分数的时候,msg2Statusbar发出一个信号
msg2Statusbar =pyqtSignal(str)#这些都是Board的类变量。BoardWidth和BoardHeight定义的块的大小。Speed定义了游戏的速度。每个300 ms将开始一个新游戏循环。
BoardWidth = 10BoardHeight= 22Speed= 300
def __init__(self, parent):
super().__init__(parent)
self.initBoard()#我们在initBoard()方法初始化一些重要的变量。board变量是一个从0到7的数字列表。它代表了面板上各种形状和位置。
definitBoard(self):
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()#shapeAt()方法确定在给定形状块的类型。
defshapeAt(self, x, y):return self.board[(y * Board.BoardWidth) +x]defsetShapeAt(self, x, y, shape):
self.board[(y* Board.BoardWidth) + x] =shape#Board可以动态地调整大小。因此,块的大小可能会有所改变。squareWidth()计算单一方块像素的宽度并返回它。Board.BoardWidth方块板的大小。
defsquareWidth(self):return self.contentsRect().width() //Board.BoardWidthdefsquareHeight(self):return self.contentsRect().height() //Board.BoardHeight#游戏开始方法
defstart(self):ifself.isPaused:returnself.isStarted=True
self.isWaitingAfterLine=False
self.numLinesRemoved=0
self.clearBoard()
self.msg2Statusbar.emit(str(self.numLinesRemoved))
self.newPiece()
self.timer.start(Board.Speed, self)#游戏暂停方法
defpause(self):if notself.isStarted:returnself.isPaused= notself.isPausedifself.isPaused:
self.timer.stop()
self.msg2Statusbar.emit("paused")else:
self.timer.start(Board.Speed, self)
self.msg2Statusbar.emit(str(self.numLinesRemoved))
self.update()#窗体绘图
defpaintEvent(self, event):
painter=QPainter(self)#根据窗体舞台大小计算顶部
rect =self.contentsRect()
boardTop= rect.bottom() - Board.BoardHeight *self.squareHeight()#游戏的绘制分为两个步骤,第一步,绘制所有方块,这些方块都要保存在底部列表中。列表通过shapeAt() 方法来添加方块。
for i inrange(Board.BoardHeight):for j inrange(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())defkeyPressEvent(self, event):if not self.isStarted or self.curPiece.shape() ==Tetrominoe.NoShape:
super(Board, self).keyPressEvent(event)returnkey=event.key()if key ==Qt.Key_P:
self.pause()return
ifself.isPaused:return
#keyPressEvent()方法检查按下键。当按右箭头键,我们试图向右移动一块。我们使用tyrMove,因为可能无法移动。
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()#按下D键,可以加速下降。
elif key ==Qt.Key_D:
self.oneLineDown()else:
super(Board, self).keyPressEvent(event)#计时器事件,当我们前一个方块降到底部后,创建一个新的方块。
deftimerEvent(self, event):if event.timerId() ==self.timer.timerId():ifself.isWaitingAfterLine:
self.isWaitingAfterLine=False
self.newPiece()else:
self.oneLineDown()else:
super(Board, self).timerEvent(event)#clearBoard()方法通过设置Tetrominoe.NoShape清除面板
defclearBoard(self):for i in range(Board.BoardHeight *Board.BoardWidth):
self.board.append(Tetrominoe.NoShape)defdropDown(self):
newY=self.curYwhile newY >0:if not self.tryMove(self.curPiece, self.curX, newY - 1):breaknewY-= 1self.pieceDropped()defoneLineDown(self):if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
self.pieceDropped()defpieceDropped(self):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 notself.isWaitingAfterLine:
self.newPiece()#如果到达底部,会调用removeFullLines()方法。我们会检查所有完整的线条然后删除它们。然后移动所有行高于当前删除整行一行。请注意,我们反的顺序行被删除。否则,就会出错。
defremoveFullLines(self):
numFullLines=0
rowsToRemove=[]for i inrange(Board.BoardHeight):
n=0for j inrange(Board.BoardWidth):if not self.shapeAt(j, i) ==Tetrominoe.NoShape:
n= n + 1
if n == 10:
rowsToRemove.append(i)
rowsToRemove.reverse()for m inrowsToRemove:for k inrange(m, Board.BoardHeight):for l inrange(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()#通过newPiece()方法创建一个新的方块,如果不能进入它的初始位置,游戏就结束了。
defnewPiece(self):
self.curPiece=Shape()
self.curPiece.setRandomShape()
self.curX= Board.BoardWidth // 2 + 1self.curY= Board.BoardHeight - 1 +self.curPiece.minY()if notself.tryMove(self.curPiece, self.curX, self.curY):
self.curPiece.setShape(Tetrominoe.NoShape)
self.timer.stop()
self.isStarted=False
self.msg2Statusbar.emit("Game over")#使用tryMove()方法尝试移动方块。如果方块的边缘已经接触到面板边缘或者不能移动,我们返回False。否则我们当前块下降到一个新的位置。
deftryMove(self, newPiece, newX, newY):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:returnFalseif self.shapeAt(x, y) !=Tetrominoe.NoShape:returnFalse
self.curPiece=newPiece
self.curX=newX
self.curY=newY
self.update()returnTruedefdrawSquare(self, painter, x, y, 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)#Tetrominoe类包含所有可能的形状。NoShape空形状。
classTetrominoe(object):
NoShape=0
ZShape= 1SShape= 2LineShape= 3TShape= 4SquareShape= 5LShape= 6MirroredLShape= 7
#Shape 类保存方块信息
classShape(object):#coordsTable 元组包含所有可能的俄罗斯方块的坐标值。这是一个模板的所有块坐标值。
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)defshape(self):returnself.pieceShapedefsetShape(self, shape):
table=Shape.coordsTable[shape]for i in range(4):for j in range(2):
self.coords[i][j]=table[i][j]
self.pieceShape=shapedefsetRandomShape(self):
self.setShape(random.randint(1, 7))defx(self, index):returnself.coords[index][0]defy(self, index):return self.coords[index][1]defsetX(self, index, x):
self.coords[index][0]=xdefsetY(self, index, y):
self.coords[index][1] =ydefminX(self):
m=self.coords[0][0]for i in range(4):
m=min(m, self.coords[i][0])returnmdefmaxX(self):
m=self.coords[0][0]for i in range(4):
m=max(m, self.coords[i][0])returnmdefminY(self):
m= self.coords[0][1]for i in range(4):
m= min(m, self.coords[i][1])returnmdefmaxY(self):
m= self.coords[0][1]for i in range(4):
m= max(m, self.coords[i][1])returnm#rotateLeft() 向左旋转方块。如果方块本身不能被旋转,我们就返回当前对象的应用。否则就创建一个新的块及其坐标设置为的旋转。
defrotateLeft(self):if self.pieceShape ==Tetrominoe.SquareShape:returnself
result=Shape()
result.pieceShape=self.pieceShapefor i in range(4):
result.setX(i, self.y(i))
result.setY(i,-self.x(i))returnresultdefrotateRight(self):if self.pieceShape ==Tetrominoe.SquareShape:returnself
result=Shape()
result.pieceShape=self.pieceShapefor i in range(4):
result.setX(i,-self.y(i))
result.setY(i, self.x(i))returnresultif __name__ == '__main__':
app=QApplication([])
tetris=Tetris()
sys.exit(app.exec_())