随笔 - 41, 文章 - 8, 评论 - 8, 引用 - 0
数据加载中……

[Python][PyQt4]PyQt4 中的俄罗斯方块

PyQt4 中的俄罗斯方块

创建一个电脑游戏是很具挑战性的。迟早程序员想创建一个电脑游戏。事实上,很多人对编程感兴趣,是因为它们玩了游戏,然后想创建自己的游戏。创建一个游戏会提升你的编程技能。

俄罗斯方块

俄罗斯方块从创建开始就成为很流行的游戏之一。游戏最开始是由俄罗斯程序员 Alexey Pajitnov 设计和编写的。从那时开始几乎在所有的计算机平台运行。甚至我的手机就有一个修改版的俄罗斯方块。

这个游戏是落下方块。我们可以有七种不同的形状,称为 tetromino 。S形状,Z形状,T形状,L形状,行,镜像L形状,方块形状。而这些形状又由四个方块构成。这些形状会掉到板上。这个游戏的目标是移动和旋转这些形状,所以可以把他们填满。如果我们填满了一行,这行就被销毁然后我们得分。游戏在方块填到顶部就结束了。

_images/tetrominoes.png

PyQt4 是可以用于创建应用的。也有很多其它库可以用于游戏。尽管如此,PyQt4 仍可以创建游戏。

下面的例子是一个改版的俄罗斯方块,在 PyQt4 安装的目录下。

开发

我们不准备使用图片,而是使用 PyQt4 的 API 。在每个计算机游戏背后,都有一个数学模型。俄罗斯方块也是。

一些想法。

  • 使用 QtCore.QBasicTimer 创建游戏的一轮
  • 这些方块是往下掉的
  • 这些形状一格格移动
  • 游戏盘数学上是简单的列表
#!/usr/bin/python
# tetris.py
import sys
import random
from PyQt4 import QtCore, QtGui
class Tetris(QtGui.QMainWindow):
    def __init__(self):
        #QtGui.QMainWindow.__init__(self)
        super(Tetris, self).__init__()
        self.setGeometry(300, 300, 180, 380)
        self.setWindowTitle('Tetris')
        self.tetrisboard = Board(self)
        self.setCentralWidget(self.tetrisboard)
        self.statusbar = self.statusBar()
        self.connect(self.tetrisboard, QtCore.SIGNAL("messageToStatusbar(QString)"),
            self.statusbar, QtCore.SLOT("showMessage(QString)"))
        self.tetrisboard.start()
        self.center()
    def center(self):
        screen = QtGui.QDesktopWidget().screenGeometry()
        size =  self.geometry()
        self.move((screen.width()-size.width())/2,
            (screen.height()-size.height())/2)
class Board(QtGui.QFrame):
    BoardWidth = 10
    BoardHeight = 22
    Speed = 300
    def __init__(self, parent):
        #QtGui.QFrame.__init__(self, parent)
        super(Board, self).__init__(parent)
        self.timer = QtCore.QBasicTimer()
        self.isWaitingAfterLine = False
        self.curPiece = Shape()
        self.nextPiece = Shape()
        self.curX = 0
        self.curY = 0
        self.numLinesRemoved = 0
        self.board = []
        self.setFocusPolicy(QtCore.Qt.StrongFocus)
        self.isStarted = False
        self.isPaused = False
        self.clearBoard()
        self.nextPiece.setRandomShape()
    def shapeAt(self, x, y):
        return self.board[(y * Board.BoardWidth) + x]
    def setShapeAt(self, x, y, shape):
        self.board[(y * Board.BoardWidth) + x] = shape
    def squareWidth(self):
        return self.contentsRect().width() / Board.BoardWidth
    def squareHeight(self):
        return self.contentsRect().height() / Board.BoardHeight
    def start(self):
        if self.isPaused:
            return
        self.isStarted = True
        self.isWaitingAfterLine = False
        self.numLinesRemoved = 0
        self.clearBoard()
        self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"),
            str(self.numLinesRemoved))
        self.newPiece()
        self.timer.start(Board.Speed, self)
    def pause(self):
        if not self.isStarted:
            return
        self.isPaused = not self.isPaused
        if self.isPaused:
            self.timer.stop()
            self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), "paused")
        else:
            self.timer.start(Board.Speed, self)
            self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"),
                str(self.numLinesRemoved))
        self.update()
    def paintEvent(self, event):
        painter = QtGui.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 != Tetrominoes.NoShape:
                    self.drawSquare(painter,
                        rect.left() + j * self.squareWidth(),
                        boardTop + i * self.squareHeight(), shape)
        if self.curPiece.shape() != Tetrominoes.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):
        if not self.isStarted or self.curPiece.shape() == Tetrominoes.NoShape:
            QtGui.QWidget.keyPressEvent(self, event)
            return
        key = event.key()
        if key == QtCore.Qt.Key_P:
            self.pause()
            return
        if self.isPaused:
            return
        elif key == QtCore.Qt.Key_Left:
            self.tryMove(self.curPiece, self.curX - 1, self.curY)
        elif key == QtCore.Qt.Key_Right:
            self.tryMove(self.curPiece, self.curX + 1, self.curY)
        elif key == QtCore.Qt.Key_Down:
            self.tryMove(self.curPiece.rotatedRight(), self.curX, self.curY)
        elif key == QtCore.Qt.Key_Up:
            self.tryMove(self.curPiece.rotatedLeft(), self.curX, self.curY)
        elif key == QtCore.Qt.Key_Space:
            self.dropDown()
        elif key == QtCore.Qt.Key_D:
            self.oneLineDown()
        else:
            QtGui.QWidget.keyPressEvent(self, event)
    def timerEvent(self, event):
        if event.timerId() == self.timer.timerId():
            if self.isWaitingAfterLine:
                self.isWaitingAfterLine = False
                self.newPiece()
            else:
                self.oneLineDown()
        else:
            QtGui.QFrame.timerEvent(self, event)
    def clearBoard(self):
        for i in range(Board.BoardHeight * Board.BoardWidth):
            self.board.append(Tetrominoes.NoShape)
    def dropDown(self):
        newY = self.curY
        while newY > 0:
            if not self.tryMove(self.curPiece, self.curX, newY - 1):
                break
            newY -= 1
        self.pieceDropped()
    def oneLineDown(self):
        if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
            self.pieceDropped()
    def pieceDropped(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 not self.isWaitingAfterLine:
            self.newPiece()
    def removeFullLines(self):
        numFullLines = 0
        rowsToRemove = []
        for i in range(Board.BoardHeight):
            n = 0
            for j in range(Board.BoardWidth):
                if not self.shapeAt(j, i) == Tetrominoes.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.emit(QtCore.SIGNAL("messageToStatusbar(QString)"),
                str(self.numLinesRemoved))
            self.isWaitingAfterLine = True
            self.curPiece.setShape(Tetrominoes.NoShape)
            self.update()
    def newPiece(self):
        self.curPiece = self.nextPiece
        self.nextPiece.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(Tetrominoes.NoShape)
            self.timer.stop()
            self.isStarted = False
            self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), "Game over")
    def tryMove(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:
                return False
            if self.shapeAt(x, y) != Tetrominoes.NoShape:
                return False
        self.curPiece = newPiece
        self.curX = newX
        self.curY = newY
        self.update()
        return True
    def drawSquare(self, painter, x, y, shape):
        colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC,
                      0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]
        color = QtGui.QColor(colorTable[shape])
        painter.fillRect(x + 1, y + 1, self.squareWidth() - 2,
            self.squareHeight() - 2, color)
        painter.setPen(color.light())
        painter.drawLine(x, y + self.squareHeight() - 1, x, y)
        painter.drawLine(x, y, x + self.squareWidth() - 1, y)
        painter.setPen(color.dark())
        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 Tetrominoes(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 = Tetrominoes.NoShape
        self.setShape(Tetrominoes.NoShape)
    def shape(self):
        return self.pieceShape
    def setShape(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 = shape
    def setRandomShape(self):
        self.setShape(random.randint(1, 7))
    def x(self, index):
        return self.coords[index][0]
    def y(self, index):
        return self.coords[index][1]
    def setX(self, index, x):
        self.coords[index][0] = x
    def setY(self, index, y):
        self.coords[index][1] = y
    def minX(self):
        m = self.coords[0][0]
        for i in range(4):
            m = min(m, self.coords[i][0])
        return m
    def maxX(self):
        m = self.coords[0][0]
        for i in range(4):
            m = max(m, self.coords[i][0])
        return m
    def minY(self):
        m = self.coords[0][1]
        for i in range(4):
            m = min(m, self.coords[i][1])
        return m
    def maxY(self):
        m = self.coords[0][1]
        for i in range(4):
            m = max(m, self.coords[i][1])
        return m
    def rotatedLeft(self):
        if self.pieceShape == Tetrominoes.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 rotatedRight(self):
        if self.pieceShape == Tetrominoes.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
app = QtGui.QApplication(sys.argv)
tetris = Tetris()
tetris.show()
sys.exit(app.exec_())

我稍微简化了此游戏,所以理解起来比较容易。游戏在载入后就立即开始了。我们可以按 'p' 来暂停游戏。而空格可以让形状立即掉到底部。游戏以恒速进行,并没有实现加速。分数是我们移掉的行数。

self.statusbar = self.statusBar()
self.connect(self.tetrisboard, QtCore.SIGNAL("messageToStatusbar(QString)"),
    self.statusbar, QtCore.SLOT("showMessage(QString)"))

我们创建了状态栏,用于显示消息。我们将显示三种可能的消息。移除的行数,暂停消息,结束消息。

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

在游戏开始前,我们先初始化一些重要的变量。变量 self.board 是一组 0 到 7 的列表。它代表这些形状的位置,以及剩余的形状。

for j in range(Board.BoardWidth):
    shape = self.shapeAt(j, Board.BoardHeight - i - 1)
    if shape != Tetrominoes.NoShape:
        self.drawSquare(painter,
            rect.left() + j * self.squareWidth(),
            boardTop + i * self.squareHeight(), shape)

游戏的绘制被分为两部分。第一步,我们绘制所有的形状,或剩余的形状,这些是已经掉落的。所有的都记在 self.board 中。我们使用 shapeAt() 方法访问它。

if self.curPiece.shape() != Tetrominoes.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 == QtCore.Qt.Key_Left:
    self.tryMove(self.curPiece, self.curX - 1, self.curY)
elif key == QtCore.Qt.Key_Right:
    self.tryMove(self.curPiece, self.curX + 1, self.curY)

keyPressEvent 中,我们检查按键。如果我们按向右的键,那么那块就会往右移。我们说 try,是因为可能不能移。

def tryMove(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:
            return False
        if self.shapeAt(x, y) != Tetrominoes.NoShape:
            return False
    self.curPiece = newPiece
    self.curX = newX
    self.curY = newY
    self.update()
    return True

在这个方法中,我们尝试移动形状。如果到了画板边缘或是碰到其它,我们就返回 False 。否则,我们就放置到新的位置。

def timerEvent(self, event):
    if event.timerId() == self.timer.timerId():
        if self.isWaitingAfterLine:
            self.isWaitingAfterLine = False
            self.newPiece()
        else:
            self.oneLineDown()
    else:
        QtGui.QFrame.timerEvent(self, event)

在计时器事件中,我们或者是创建一个新的方块,或是让方块下落。

def removeFullLines(self):
    numFullLines = 0
    rowsToRemove = []
    for i in range(Board.BoardHeight):
        n = 0
        for j in range(Board.BoardWidth):
            if not self.shageAt(j, i) == Tetrominoes.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))
...

如果那块碰到底部,我们就调用 removeFullLines() 方法。首先,我们找到所有的行数。然后我们移除他们。我们要把满了的行数移除。注意,我们把要移除的列表进行反转。否则,它可能不能正确移除。我们没有使用重力效应。这就意味着某些方块会悬浮。

def newPiece(self):
    self.curPiece = self.nextPiece
    self.nextPiece.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(Tetrominoes.NoShape)
        self.timer.stop()
        self.isStarted = False
        self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), "Game Over")

newPiece() 方法随机创建一个方块。如果这个方块不能动了,游戏就结束了。

Shape 类保存了每个方块的形状。

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

在创建时,我们创建了一个空的坐标列表。这个列表保存每个方块的坐标。比如,这些元组 (0,-1),(0,0),(-1,0),(-1,-1) 代表旋转的 S 形状。下面的图表指示了这个形状。

_images/coordinates.png

当我们绘制方块时,我们就把它画在 self.curXself.curY 上。然后我们把四个方块画起来。


这是一个用 PyQt4 写的俄罗斯方块。

posted on 2012-02-12 10:18 mirguest 阅读(1729) 评论(0)  编辑 收藏 引用 所属分类: Python


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理