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.
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.
- 游戏内部的设计。
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的数字列表。它表示不同的图形的位置和面板上剩余的图形。
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()方法来访问它。
The next step is drawing of the actual piece, that is falling down.
- 下一步是绘制正在掉落的当前块。
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()方法中,我们尽力来移动我们的块,如果块在背板的边缘或者靠在其他的块上,我们返回假,否则我们将当前块放置在新的位置。
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]
Contents