## page was renamed from The Tetris game 俄罗斯方块游戏 ## page was renamed from zhArticleTemplate ##language:zh #pragma section-numbers on SEE: http://wiki.woodpecker.org.cn/moin/WoodpeckerIdxProj = The Tetris game in PyQt4 = '''PyQt4中的俄罗斯方块游戏''' ''Creating a computer game is very challenging. Sooner or later, a programmer will want to create a computer game one day. In fact, many people became interested in programming, because they played games and wanted to create their own. Creating a computer game will vastly help improving your programming skills.'' . 编写计算机游戏非常有挑战性,早晚有一天,一名程序员会希望编写一个计算机游戏。事实上,很多人因为玩游戏并希望创造自己的游戏而对编程产生兴趣的。编写计算机游戏可以大大的提高你的编程水平。 == Tetris == ''The tetris game is one of the most popular computer games ever created. The original game was designed and programmed by a russian programmer Alexey Pajitnov in 1985. Since then, tetris is available on almost every computer platform in lots of variations. Even my mobile phone has a modified version of the tetris game.'' . 俄罗斯方块游戏是现有的最受欢迎的计算机游戏之一。游戏最初由一名俄罗斯程序员Alexey Pajitnov于1985年设计并编写。从那时开始,俄罗斯方块游戏的众多变种出现在几乎每种计算机平台上。甚至我的移动电话也有一个改版的俄罗斯方块游戏。 ''Tetris is called a falling block puzzle game. In this game, we have seven different shapes called tetrominoes. S-shape, Z-shape, T-shape, L-shape, Line-shape, MirroredL-shape and a Square-shape. Each of these shapes is formed with four squares. The shapes are falling down the board. The object of the tetris game is to move and rotate the shapes, so that they fit as much as possible. If we manage to form a row, the row is destroyed and we score. We play the tetris game until we top out.'' . 俄罗斯方块也叫作掉落方块解迷游戏。在这个游戏中,我们有七种不同的形状叫做tetrominoes.S形,Z形,T形,L形,线形,反L形和方块。每个形状由四个小方块组成。形状从顶板上落下来。俄罗斯方块游戏的目标是移动并旋转形状以便将他们尽可能的组合起来。如果我们控制填充满了一行,这一行就会消失,并且我们的得分。直到方块顶到顶部游戏结束。 Tetrominoes Figure: Tetrominoes ''PyQt4 is a toolkit designed to create applications. There are other libraries which are targeted at creating computer games. Nevertheless, PyQt4 and other application toolkits can be used to create games.'' . PyQt4被设计成用来编写程序的工具包。有其他的库是专门设计用来编写计算机游戏的。尽管如此,PyQt4和其他应用程序工具包也可以被用来编写游戏。 ''The following example is a modified version of the tetris game, available with PyQt4 installation files.'' . 下面的例子是俄罗斯方块游戏的改版,随PyQt4的安装文件而存在。 The development ''We do not have images for our tetris game, we draw the tetrominoes using the drawing API available in the PyQt4 programming toolkit. Behind every computer game, there is a mathematical model. So it is in tetris.'' . 我们没有为我们的俄罗斯方块使用图片。我们通过PyQt4编程工具包中的绘图API来绘制方块。每个计算机游戏中,都有数学模型,俄罗斯方块游戏也是。 ''Some ideas behind the game.'' . 游戏内部的设计。 * We use QtCore.QBasicTimer() to create a game cycle . 我们用QtCore.QBasicTimer()来创建一个游戏循环 * The tetrominoes are drawn . 绘制俄罗斯方块 * The shapes move on a square by square basis (not pixel by pixel) . 图形一块一块的移动而不是一个像素一个像素的移动。 * Mathematically a board is a simple list of numbers . 数学上背板是一个简单的数的列表 {{{ #!python #!/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) 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) 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_()) }}} ''I have simplified the game a bit, so that it is easier to understand. The game starts immediately, after it is launched. We can pause the game by pressing the p key. The space key will drop the tetris piece immediately to the bottom. The game goes at constant speed, no acceleration is implemented. The score is the number of lines, that we have removed.'' . 我们对游戏作一些简化,以便于理解。游戏在启动后立刻开始。我们可以通过按'p'键暂停游戏。空格键将使俄罗斯方块立刻落到底部。游戏使用固定的速度,没有实现加速。游戏的分数是我们已经消掉的行数。 {{{ self.statusbar = self.statusBar() self.connect(self.tetrisboard, QtCore.SIGNAL("messageToStatusbar(QString)"), self.statusbar, QtCore.SLOT("showMessage(QString)")) }}} ''We create a statusbar, where we will display messages. We will display three possible messages. The number of lines alredy removed. The paused message and the game over message.'' . 我们创建一个状态栏用来显示信息。我们将显示三种可能的信息,已经消掉的行数,暂停的消息和游戏结束的消息。 {{{ ... self.curX = 0 self.curY = 0 self.numLinesRemoved = 0 self.board = [] ... }}} ''Before we start the game cycle, we initialize some important variables. The self.board variable is a list of numbers from 0 ... 7. It represents the position of various shapes and remains of the shapes on the board.'' . 在我们开始游戏之前,我们初始化一些重要的变量。self.board变量四从0到7的数字列表。它表示不同的图形的位置和面板上剩余的图形。 {{{ #!python 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) }}} ''The painting of the game is divided into two steps. In the first step, we draw all the shapes, or remains of the shapes, that have been dropped to the bottom of the board. All the squares are rememberd in the self.board list variable. We access it using the shapeAt() method.'' . 游戏的显示分成两步。第一步,我们绘制所有的图形,或已经掉落在底部的剩余的图形。所有的方块被保存在self.board列表变量中。我们通过使用shapeAt()方法来访问它。 {{{ #!python 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()) }}} ''The next step is drawing of the actual piece, that is falling down.'' . 下一步是绘制正在掉落的当前块。 {{{ #!python 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) }}} ''In the keyPressEvent we check for pressed keys. If we press the right arrow key, we try to move the piece to the right. We say try, because the piece might not be able to move.'' . 在keyPressEvent我们检查按下的按键。如果我们按下了右方向键,我们就试着向右移动块。试着是因为块可能无法移动。 {{{ #!python 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 }}} ''In the tryMove() method we try to move our shapes. If the shape is at the edge of the board or is adjacent to some other piece, we return false. Otherwise we place the current falling piece to a new position.'' . 在tryMove()方法中,我们尽力来移动我们的块,如果块在背板的边缘或者靠在其他的块上,我们返回假,否则我们将当前块放置在新的位置。 {{{ #!python 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) }}} ''In the timer event, we either create a new piece, after the previous one was dropped to the bottom, or we move a falling piece one line down.'' . 时间事件中,我们或者在上一个方块到达底部后创建一个新方块,或者将下落的方块向下移动一行。 {{{ #!python 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)) ... }}} ''If the piece hits the bottom, we call the removeFullLines() method. First we find out all full lines. And we remove them. We do it by moving all lines above the current full line to be removed one line down. Notice, that we reverse the order of the lines to be removed. Otherwise, it would not work correctly. In our case we use a naive gravity. This means, that the pieces may be floating above empty gaps.'' . 如果方块到达了底部,我们调用removeFullLines()方法。首先我们找出所有的满行,然后我们移去他们,通过向下移动当前添满的行上的所有行来完成。注意,我们反转将要消去的行的顺序,否则它会工作不正常。这种情况我们使用简单的引力,这意味着块会浮动在缺口上面。 {{{ #!python 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") }}} ''The newPiece() method creates randomly a new tetris piece. If the piece cannot go into it's initial position, the game is over.'' . newPiece()方法随机生成一个新的俄罗斯方块。如果方块无法进入它的初始位置,游戏结束。 ''The Shape class saves information about the tetris piece.'' . Shape类保存方块的信息。 {{{ self.coords = [[0,0] for i in range(4)] }}} ''Upon creation we create an empty coordinates list. The list will save the coordinates of the tetris piece. For example, these tuples (0, -1), (0, 0), (1, 0), (1, 1) represent a rotated S-shape. The following diagram illustrates the shape.'' . 在生成之前,我们创建一个空的坐标列表,这个列表将会保存俄罗斯方块的坐标,例如这些元组(0, -1), (0, 0), (1, 0), (1, 1)表示一个S形,以下的图形说明了形状。 Coordinates Figure: Coordinates ''When we draw the current falling piece, we draw it at self.curX, self.curY position. Then we look at the coordinates table and draw all the four squares.'' . 当我们绘制当前掉落的块时,我们在self.curX,self.curY位置绘制。然后我们查找坐标表并绘制所有的四个方块。 Tetris Figure: Tetris = 交流 = ::-- [[zhuyj]] [<>] <>