SEE: http://wiki.woodpecker.org.cn/moin/WoodpeckerIdxProj

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

  • 编写计算机游戏非常有挑战性,早晚有一天,一名程序员会希望编写一个计算机游戏。事实上,很多人因为玩游戏并希望创造自己的游戏而对编程产生兴趣的。编写计算机游戏可以大大的提高你的编程水平。

1.1. 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
      • 数学上背板是一个简单的数的列表

   1 #!/usr/bin/python
   2 # tetris.py
   3 import sys
   4 import random
   5 from PyQt4 import QtCore, QtGui
   6 class Tetris(QtGui.QMainWindow):
   7     def __init__(self):
   8         QtGui.QMainWindow.__init__(self)
   9         self.setGeometry(300, 300, 180, 380)
  10         self.setWindowTitle('Tetris')
  11         self.tetrisboard = Board(self)
  12         self.setCentralWidget(self.tetrisboard)
  13         self.statusbar = self.statusBar()
  14         self.connect(self.tetrisboard, QtCore.SIGNAL("messageToStatusbar(QString)"),
  15             self.statusbar, QtCore.SLOT("showMessage(QString)"))
  16         self.tetrisboard.start()
  17         self.center()
  18     def center(self):
  19         screen = QtGui.QDesktopWidget().screenGeometry()
  20         size =  self.geometry()
  21         self.move((screen.width()-size.width())/2,
  22             (screen.height()-size.height())/2)
  23 class Board(QtGui.QFrame):
  24     BoardWidth = 10
  25     BoardHeight = 22
  26     Speed = 300
  27     def __init__(self, parent):
  28         QtGui.QFrame.__init__(self, parent)
  29         self.timer = QtCore.QBasicTimer()
  30         self.isWaitingAfterLine = False
  31         self.curPiece = Shape()
  32         self.nextPiece = Shape()
  33         self.curX = 0
  34         self.curY = 0
  35         self.numLinesRemoved = 0
  36         self.board = []
  37         self.setFocusPolicy(QtCore.Qt.StrongFocus)
  38         self.isStarted = False
  39         self.isPaused = False
  40         self.clearBoard()
  41         self.nextPiece.setRandomShape()
  42     def shapeAt(self, x, y):
  43         return self.board[(y * Board.BoardWidth) + x]
  44     def setShapeAt(self, x, y, shape):
  45         self.board[(y * Board.BoardWidth) + x] = shape
  46     def squareWidth(self):
  47         return self.contentsRect().width() / Board.BoardWidth
  48     def squareHeight(self):
  49         return self.contentsRect().height() / Board.BoardHeight
  50     def start(self):
  51         if self.isPaused:
  52             return
  53         self.isStarted = True
  54         self.isWaitingAfterLine = False
  55         self.numLinesRemoved = 0
  56         self.clearBoard()
  57         self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"),
  58             str(self.numLinesRemoved))
  59         self.newPiece()
  60         self.timer.start(Board.Speed, self)
  61     def pause(self):
  62         if not self.isStarted:
  63             return
  64         self.isPaused = not self.isPaused
  65         if self.isPaused:
  66             self.timer.stop()
  67             self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), "paused")
  68         else:
  69             self.timer.start(Board.Speed, self)
  70             self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"),
  71                 str(self.numLinesRemoved))
  72         self.update()
  73     def paintEvent(self, event):
  74         painter = QtGui.QPainter(self)
  75         rect = self.contentsRect()
  76         boardTop = rect.bottom() - Board.BoardHeight * self.squareHeight()
  77         for i in range(Board.BoardHeight):
  78             for j in range(Board.BoardWidth):
  79                 shape = self.shapeAt(j, Board.BoardHeight - i - 1)
  80                 if shape != Tetrominoes.NoShape:
  81                     self.drawSquare(painter,
  82                         rect.left() + j * self.squareWidth(),
  83                         boardTop + i * self.squareHeight(), shape)
  84         if self.curPiece.shape() != Tetrominoes.NoShape:
  85             for i in range(4):
  86                 x = self.curX + self.curPiece.x(i)
  87                 y = self.curY - self.curPiece.y(i)
  88                 self.drawSquare(painter, rect.left() + x * self.squareWidth(),
  89                     boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
  90                     self.curPiece.shape())
  91     def keyPressEvent(self, event):
  92         if not self.isStarted or self.curPiece.shape() == Tetrominoes.NoShape:
  93             QtGui.QWidget.keyPressEvent(self, event)
  94             return
  95         key = event.key()
  96         if key == QtCore.Qt.Key_P:
  97             self.pause()
  98             return
  99         if self.isPaused:
 100             return
 101         elif key == QtCore.Qt.Key_Left:
 102             self.tryMove(self.curPiece, self.curX - 1, self.curY)
 103         elif key == QtCore.Qt.Key_Right:
 104             self.tryMove(self.curPiece, self.curX + 1, self.curY)
 105         elif key == QtCore.Qt.Key_Down:
 106             self.tryMove(self.curPiece.rotatedRight(), self.curX, self.curY)
 107         elif key == QtCore.Qt.Key_Up:
 108             self.tryMove(self.curPiece.rotatedLeft(), self.curX, self.curY)
 109         elif key == QtCore.Qt.Key_Space:
 110             self.dropDown()
 111         elif key == QtCore.Qt.Key_D:
 112             self.oneLineDown()
 113         else:
 114             QtGui.QWidget.keyPressEvent(self, event)
 115     def timerEvent(self, event):
 116         if event.timerId() == self.timer.timerId():
 117             if self.isWaitingAfterLine:
 118                 self.isWaitingAfterLine = False
 119                 self.newPiece()
 120             else:
 121                 self.oneLineDown()
 122         else:
 123             QtGui.QFrame.timerEvent(self, event)
 124     def clearBoard(self):
 125         for i in range(Board.BoardHeight * Board.BoardWidth):
 126             self.board.append(Tetrominoes.NoShape)
 127     def dropDown(self):
 128         newY = self.curY
 129         while newY > 0:
 130             if not self.tryMove(self.curPiece, self.curX, newY - 1):
 131                 break
 132             newY -= 1
 133         self.pieceDropped()
 134     def oneLineDown(self):
 135         if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
 136             self.pieceDropped()
 137     def pieceDropped(self):
 138         for i in range(4):
 139             x = self.curX + self.curPiece.x(i)
 140             y = self.curY - self.curPiece.y(i)
 141             self.setShapeAt(x, y, self.curPiece.shape())
 142         self.removeFullLines()
 143         if not self.isWaitingAfterLine:
 144             self.newPiece()
 145     def removeFullLines(self):
 146         numFullLines = 0
 147         rowsToRemove = []
 148         for i in range(Board.BoardHeight):
 149             n = 0
 150             for j in range(Board.BoardWidth):
 151                 if not self.shapeAt(j, i) == Tetrominoes.NoShape:
 152                     n = n + 1
 153             if n == 10:
 154                 rowsToRemove.append(i)
 155         rowsToRemove.reverse()
 156         for m in rowsToRemove:
 157             for k in range(m, Board.BoardHeight):
 158                 for l in range(Board.BoardWidth):
 159                     self.setShapeAt(l, k, self.shapeAt(l, k + 1))
 160         numFullLines = numFullLines + len(rowsToRemove)
 161         if numFullLines > 0:
 162             self.numLinesRemoved = self.numLinesRemoved + numFullLines
 163             self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"),
 164                 str(self.numLinesRemoved))
 165             self.isWaitingAfterLine = True
 166             self.curPiece.setShape(Tetrominoes.NoShape)
 167             self.update()
 168     def newPiece(self):
 169         self.curPiece = self.nextPiece
 170         self.nextPiece.setRandomShape()
 171         self.curX = Board.BoardWidth / 2 + 1
 172         self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
 173         if not self.tryMove(self.curPiece, self.curX, self.curY):
 174             self.curPiece.setShape(Tetrominoes.NoShape)
 175             self.timer.stop()
 176             self.isStarted = False
 177             self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), "Game over")
 178     def tryMove(self, newPiece, newX, newY):
 179         for i in range(4):
 180             x = newX + newPiece.x(i)
 181             y = newY - newPiece.y(i)
 182             if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
 183                 return False
 184             if self.shapeAt(x, y) != Tetrominoes.NoShape:
 185                 return False
 186         self.curPiece = newPiece
 187         self.curX = newX
 188         self.curY = newY
 189         self.update()
 190         return True
 191     def drawSquare(self, painter, x, y, shape):
 192         colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC,
 193                       0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]
 194         color = QtGui.QColor(colorTable[shape])
 195         painter.fillRect(x + 1, y + 1, self.squareWidth() - 2,
 196             self.squareHeight() - 2, color)
 197         painter.setPen(color.light())
 198         painter.drawLine(x, y + self.squareHeight() - 1, x, y)
 199         painter.drawLine(x, y, x + self.squareWidth() - 1, y)
 200         painter.setPen(color.dark())
 201         painter.drawLine(x + 1, y + self.squareHeight() - 1,
 202             x + self.squareWidth() - 1, y + self.squareHeight() - 1)
 203         painter.drawLine(x + self.squareWidth() - 1,
 204             y + self.squareHeight() - 1, x + self.squareWidth() - 1, y + 1)
 205 class Tetrominoes(object):
 206     NoShape = 0
 207     ZShape = 1
 208     SShape = 2
 209     LineShape = 3
 210     TShape = 4
 211     SquareShape = 5
 212     LShape = 6
 213     MirroredLShape = 7
 214 class Shape(object):
 215     coordsTable = (
 216         ((0, 0),     (0, 0),     (0, 0),     (0, 0)),
 217         ((0, -1),    (0, 0),     (-1, 0),    (-1, 1)),
 218         ((0, -1),    (0, 0),     (1, 0),     (1, 1)),
 219         ((0, -1),    (0, 0),     (0, 1),     (0, 2)),
 220         ((-1, 0),    (0, 0),     (1, 0),     (0, 1)),
 221         ((0, 0),     (1, 0),     (0, 1),     (1, 1)),
 222         ((-1, -1),   (0, -1),    (0, 0),     (0, 1)),
 223         ((1, -1),    (0, -1),    (0, 0),     (0, 1))
 224     )
 225     def __init__(self):
 226         self.coords = [[0,0] for i in range(4)]
 227         self.pieceShape = Tetrominoes.NoShape
 228         self.setShape(Tetrominoes.NoShape)
 229     def shape(self):
 230         return self.pieceShape
 231     def setShape(self, shape):
 232         table = Shape.coordsTable[shape]
 233         for i in range(4):
 234             for j in range(2):
 235                 self.coords[i][j] = table[i][j]
 236         self.pieceShape = shape
 237     def setRandomShape(self):
 238         self.setShape(random.randint(1, 7))
 239     def x(self, index):
 240         return self.coords[index][0]
 241     def y(self, index):
 242         return self.coords[index][1]
 243     def setX(self, index, x):
 244         self.coords[index][0] = x
 245     def setY(self, index, y):
 246         self.coords[index][1] = y
 247     def minX(self):
 248         m = self.coords[0][0]
 249         for i in range(4):
 250             m = min(m, self.coords[i][0])
 251         return m
 252     def maxX(self):
 253         m = self.coords[0][0]
 254         for i in range(4):
 255             m = max(m, self.coords[i][0])
 256         return m
 257     def minY(self):
 258         m = self.coords[0][1]
 259         for i in range(4):
 260             m = min(m, self.coords[i][1])
 261         return m
 262     def maxY(self):
 263         m = self.coords[0][1]
 264         for i in range(4):
 265             m = max(m, self.coords[i][1])
 266         return m
 267     def rotatedLeft(self):
 268         if self.pieceShape == Tetrominoes.SquareShape:
 269             return self
 270         result = Shape()
 271         result.pieceShape = self.pieceShape
 272         for i in range(4):
 273             result.setX(i, self.y(i))
 274             result.setY(i, -self.x(i))
 275         return result
 276     def rotatedRight(self):
 277         if self.pieceShape == Tetrominoes.SquareShape:
 278             return self
 279         result = Shape()
 280         result.pieceShape = self.pieceShape
 281         for i in range(4):
 282             result.setX(i, -self.y(i))
 283             result.setY(i, self.x(i))
 284         return result
 285 app = QtGui.QApplication(sys.argv)
 286 tetris = Tetris()
 287 tetris.show()
 288 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的数字列表。它表示不同的图形的位置和面板上剩余的图形。

   1  for j in range(Board.BoardWidth):
   2      shape = self.shapeAt(j, Board.BoardHeight - i - 1)
   3      if shape != Tetrominoes.NoShape:
   4          self.drawSquare(painter,
   5              rect.left() + j * self.squareWidth(),
   6              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()方法来访问它。

   1  if self.curPiece.shape() != Tetrominoes.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())

The next step is drawing of the actual piece, that is falling down.

  • 下一步是绘制正在掉落的当前块。

   1  elif key == QtCore.Qt.Key_Left:
   2      self.tryMove(self.curPiece, self.curX - 1, self.curY)
   3  elif key == QtCore.Qt.Key_Right:
   4      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我们检查按下的按键。如果我们按下了右方向键,我们就试着向右移动块。试着是因为块可能无法移动。

   1  def tryMove(self, newPiece, newX, newY):
   2      for i in range(4):
   3          x = newX + newPiece.x(i)
   4          y = newY - newPiece.y(i)
   5          if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
   6              return False
   7          if self.shapeAt(x, y) != Tetrominoes.NoShape:
   8              return False
   9      self.curPiece = newPiece
  10      self.curX = newX
  11      self.curY = newY
  12      self.update()
  13      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()方法中,我们尽力来移动我们的块,如果块在背板的边缘或者靠在其他的块上,我们返回假,否则我们将当前块放置在新的位置。

   1  def timerEvent(self, event):
   2      if event.timerId() == self.timer.timerId():
   3          if self.isWaitingAfterLine:
   4              self.isWaitingAfterLine = False
   5              self.newPiece()
   6          else:
   7              self.oneLineDown()
   8      else:
   9          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.

  • 时间事件中,我们或者在上一个方块到达底部后创建一个新方块,或者将下落的方块向下移动一行。

   1  def removeFullLines(self):
   2      numFullLines = 0
   3      rowsToRemove = []
   4      for i in range(Board.BoardHeight):
   5          n = 0
   6          for j in range(Board.BoardWidth):
   7              if not self.shapeAt(j, i) == Tetrominoes.NoShape:
   8                  n = n + 1
   9          if n == 10:
  10              rowsToRemove.append(i)
  11              rowsToRemove.reverse()
  12          for m in rowsToRemove:
  13              for k in range(m, Board.BoardHeight):
  14                  for l in range(Board.BoardWidth):
  15                      self.setShapeAt(l, k, self.shapeAt(l, k + 1))
  16  ...

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()方法。首先我们找出所有的满行,然后我们移去他们,通过向下移动当前添满的行上的所有行来完成。注意,我们反转将要消去的行的顺序,否则它会工作不正常。这种情况我们使用简单的引力,这意味着块会浮动在缺口上面。

   1  def newPiece(self):
   2      self.curPiece = self.nextPiece
   3      self.nextPiece.setRandomShape()
   4      self.curX = Board.BoardWidth / 2 + 1
   5      self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
   6      if not self.tryMove(self.curPiece, self.curX, self.curY):
   7          self.curPiece.setShape(Tetrominoes.NoShape)
   8          self.timer.stop()
   9          self.isStarted = False
  10          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

2. 交流

::-- zhuyj [2008-12-22 08:32:52]

The_Tetris_game_俄罗斯方块游戏 (last edited 2009-12-25 07:09:36 by localhost)