含有章节索引的中文 文章模板

::-- ["zhuyj"] [DateTime(2008-12-22T08:31:17Z)] TableOfContents

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.

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

The following example is a modified version of the tetris game, available with PyQt4 installation files.

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

Some ideas behind the game.

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

 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.

Toggle line numbers
   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.

Toggle line numbers
   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.

Toggle line numbers
   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.

Toggle line numbers
   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 
  10      self.curPiece = newPiece
  11      self.curX = newX
  12      self.curY = newY
  13      self.update()
  14      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.

Toggle line numbers
   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.

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

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.

Toggle line numbers
   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 
   7      if not self.tryMove(self.curPiece, self.curX, self.curY):
   8          self.curPiece.setShape(Tetrominoes.NoShape)
   9          self.timer.stop()
  10          self.isStarted = False
  11          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.

The Shape class saves information about the tetris piece.

 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.

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.

Tetris Figure: Tetris

2. 交流

PageComment2