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.

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.

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.

   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.

 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.

   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.

   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.

   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.

   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.

   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.

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

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

TheTetrisGame (last edited 2009-12-25 07:12:35 by localhost)