(рис 7.1)
Tetromino is a Tetris clone. Differently shaped blocks (each made up of four boxes) fall from the
top of the screen, and the player must guide them down to form complete rows that have no
In this chapter, I have come up with a set of terms for the different things in the
(рис 7.2)
S_SHAPE_TEMPLATE or
J_SHAPE_TEMPLATE.This
You will also need the
1. # Tetromino (a Tetris clone) 2. # By Al Sweigart al@inventwithpython.com 3. # http://inventwithpython.com/pygame 4. # Creative Commons BY-NC-SA 3.0 US 5. 6. import random, time, pygame, sys 7. from pygame.locals import * 8. 9. FPS = 25 10. WINDOWWIDTH = 640 11. WINDOWHEIGHT = 480 12. BOXSIZE = 20 13. BOARDWIDTH = 10 14. BOARDHEIGHT = 20 15. BLANK = '.'
These are the constants used by our Tetromino game. Each box is a square that is 20 pixels wide
and high. The board itself is 10 boxes wide and 20 boxes tall. The BLANK constant will be used
as a value to represent
17. MOVESIDEWAYSFREQ = 0.15 18. MOVEDOWNFREQ = 0.1
Every time the player pushes the left or right arrow key down, the falling piece should move one
box over to the left or right, respectively. However, the player can also MOVESIDEWAYSFREQ constant will set it so
that every 0.15 seconds that passes with the left or right arrow key held down, the piece will
move another space over.
The MOVEDOWNFREQ constant is the same thing except it tells how frequently the piece drops by
one box while the player has the down arrow key held down.
20. XMARGIN = int((WINDOWWIDTH - BOARDWIDTH * BOXSIZE) / 2) 21. TOPMARGIN = WINDOWHEIGHT - (BOARDHEIGHT * BOXSIZE) - 5
The program needs to calculate how many pixels are to the left and right side of the board to use
later in the program. WINDOWWIDTH is the total number of pixels wide the entire window is. The
board is BOARDWIDTH boxes wide and each box is BOXSIZE pixels wide. If we BOXSIZE pixels from this for each of the boxes wide in the board (which is BOARDWIDTH *BOXSIZE), we’ll have the size of the margin to the left and right of the board. If we XMARGIN for either the left-side or right-side margin.
We can calculate the size of the space between the top of the board and the top of the window in a
similar manner. The board will be drawn 5 pixels above the bottom of the window, so 5 is
subtracted from topmargin to account for this.
(рис 7.3)
23. # R G B 24. WHITE = (255, 255, 255) 25. GRAY = (185, 185, 185) 26. BLACK = ( 0, 0, 0) 27. RED = (155, 0, 0) 28. LIGHTRED = (175, 20, 20) 29. GREEN = ( 0, 155, 0) 30. LIGHTGREEN = ( 20, 175, 20) 31. BLUE = ( 0, 0, 155) 32. LIGHTBLUE = ( 20, 20, 175) 33. YELLOW = (155, 155, 0) 34. LIGHTYELLOW = (175, 175, 20) 35. 36. BORDERCOLOR = BLUE 37. BGCOLOR = BLACK 38. TEXTCOLOR = WHITE 39. TEXTSHADOWCOLOR = GRAY 40. COLORS = ( BLUE, GREEN, RED, YELLOW) 41. LIGHTCOLORS = (LIGHTBLUE, LIGHTGREEN, LIGHTRED, LIGHTYELLOW) 42. assert len(COLORS) == len(LIGHTCOLORS) # each color must have light color
The pieces will come in four colors: blue, green, red, and yellow. When we draw the boxes
though, there will be a thin highlight on the box in a lighter color. So this means we need to create
light blue, light green, light red, and light yellow colors as well.
Each of these four colors will be stored in COLORS (for the normal colors) and
LIGHTCOLORS (for the lighter colors).
44. TEMPLATEWIDTH = 5 45. TEMPLATEHEIGHT = 5 46. 47. S_SHAPE_TEMPLATE = [['.....', 48. '.....', 49. '..OO.', 50. '.OO..', 51. '.....'], 52. ['.....', 53. '..O..', 54. '..OO.', 55. '...O.', 56. '.....']] 57. 58. Z_SHAPE_TEMPLATE = [['.....', 59. '.....', 60. '.OO..', 61. '..OO.', 62. '.....'], 63. ['.....', 64. '..O..', 65. '.OO..', 66. '.O...', 67. '.....']] 68. 69. I_SHAPE_TEMPLATE = [['..O..', 70. '..O..', 71. '..O..', 72. '..O..', 73. '.....'], 74. ['.....', 75. '.....', 76. 'OOOO.', 77. '.....', 78. '.....']] 79. 80. O_SHAPE_TEMPLATE = [['.....', 81. '.....', 82. '.OO..', 83. '.OO..', 84. '.....']] 85. 86. J_SHAPE_TEMPLATE = [['.....', 87. '.O...', 88. '.OOO.', 89. '.....', 90. '.....'], 91. ['.....', 92. '..OO.', 93. '..O..', 94. '..O..', 95. '.....'], 96. ['.....', 97. '.....', 98. '.OOO.', 99. '...O.', 100. '.....'], 101. ['.....', 102. '..O..', 103. '..O..', 104. '.OO..', 105. '.....']] 106. 107. L_SHAPE_TEMPLATE = [['.....', 108. '...O.', 109. '.OOO.', 110. '.....', 111. '.....'], 112. ['.....', 113. '..O..', 114. '..O..', 115. '..OO.', 116. '.....'], 117. ['.....', 118. '.....', 119. '.OOO.', 120. '.O...', 121. '.....'], 122. ['.....', 123. '.OO..', 124. '..O..', 125. '..O..', 126. '.....']] 127. 128. T_SHAPE_TEMPLATE = [['.....', 129. '..O..', 130. '.OOO.', 131. '.....', 132. '.....'], 133. ['.....', 134. '..O..', 135. '..OO.', 136. '..O..', 137. '.....'], 138. ['.....', 139. '.....', 140. '.OOO.', 141. '..O..', 142. '.....'], 143. ['.....', 144. '..O..', 145. '.OO..', 146. '..O..', 147. '.....']]
Our
['.....', '.....', '..OO.', '.OO..', '.....']
We will write the rest of our code so that it interprets a list of strings like the one above to represent a shape where the periods are empty spaces and the O’s are boxes, like this:
(рис 7.4)
You can see that this list is
spam = ['hello', 3.14, 'world', 42, 10, 'fuzz'] eggs = ['hello', 3.14, 'world' , 42, 10, 'fuzz']
Though, of eggs list would be much more .
Normally, splitting a line of code across multiple lines in the file editor would require putting a \
character at the end of the line. The \ tells Python, "This code continues onto the next line." (This
isValidMove() function.)
We will make "template" S_SHAPE_TEMPLATE. This way,
len(S_SHAPE_TEMPLATE) will represent how many possible rotations there are for the S
shape, and S_SHAPE_TEMPLATE[0] will represent the S shape’s first possible rotation. Lines
47 to 147 will create "template"
Imagine that each possible piece in a tiny 5 x 5 board of empty space, with some of the spaces on
the board filled in with boxes. The following expressions that use S_SHAPE_TEMPLATE[0]
are True:
S_SHAPE_TEMPLATE[0][2][2] == 'O' S_SHAPE_TEMPLATE[0][2][3] == 'O' S_SHAPE_TEMPLATE[0][3][1] == 'O' S_SHAPE_TEMPLATE[0][3][2] == 'O'
If we represented this shape on paper, it would look something like this:
(рис 7.5)
This is how we can represent things like Tetromino pieces as Python values such as strings and
lists. The TEMPLATEWIDTH and TEMPLATEHEIGHT constants simply set how large each row
and column for each shape’s rotation should be (The templates will always be 5x5).
149. SHAPES = {'S': S_SHAPE_TEMPLATE,
150. 'Z': Z_SHAPE_TEMPLATE,
151. 'J': J_SHAPE_TEMPLATE,
152. 'L': L_SHAPE_TEMPLATE,
153. 'I': I_SHAPE_TEMPLATE,
154. 'O': O_SHAPE_TEMPLATE,
155. 'T': T_SHAPE_TEMPLATE}
The SHAPES variable will be a dictionary that stores all of the different templates. Because each
template has all the possible rotations of a single shape, this means that the SHAPES variable
contains all possible rotations of every possible shape. This will be the
158. def main():
159. global FPSCLOCK, DISPLAYSURF, BASICFONT, BIGFONT
160. pygame.init()
161. FPSCLOCK = pygame.time.Clock()
162. DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
163. BASICFONT = pygame.font.Font('freesansbold.ttf', 18)
164. BIGFONT = pygame.font.Font('freesansbold.ttf', 100)
165. pygame.display.set_caption('Tetromino')
166.
167. showTextScreen('Tetromino')
The main() function handles creating some more global constants and showing the start screen
that appears when the program is run.
168. while True: # game loop
169. if random.randint(0, 1) == 0:
170. pygame.mixer.music.load('tetrisb.mid')
171. else:
172. pygame.mixer.music.load('tetrisc.mid')
173. pygame.mixer.music.play(-1, 0.0)
174. runGame()
175. pygame.mixer.music.stop()
176. showTextScreen('Game Over')
The code for the actual game is all in runGame(). The main() function here simply randomly
decides what tetrisb.mid or tetrisc.mid runGame() to begin the game. When the player loses, runGame() will return
to main(), which then stops the
When the player presses a key, the showTextScreen() function that displays the game over
screen will return. The game loop will loop back to the beginning at line 169 and start another
game.
179. def runGame(): 180. # setup variables for the start of the game 181. board = getBlankBoard() 182. lastMoveDownTime = time.time() 183. lastMoveSidewaysTime = time.time() 184. lastFallTime = time.time() 185. movingDown = False # note: there is no movingUp variable 186. movingLeft = False 187. movingRight = False 188. score = 0 189. level, fallFreq = calculateLevelAndFallFreq(score) 190. 191. fallingPiece = getNewPiece() 192. nextPiece = getNewPiece()
Before the game begins and pieces start falling, we need to initialize some variables to their start-
of-game values. On line 191 the fallingPiece variable will be set to the currently falling
piece that can be rotated by the player. On line 192 the nextPiece variable will be set to the
piece that shows up in the "Next" part of the screen so that player knows what piece is coming up
after setting the falling piece.
194. while True: # main game loop 195. if fallingPiece == None: 196. # No falling piece in play, so start a new piece at the top 197. fallingPiece = nextPiece 198. nextPiece = getNewPiece() 199. lastFallTime = time.time() # reset lastFallTime 200. 201. if not isValidPosition(board, fallingPiece): 202. return # can't fit a new piece on the board, so game over 203. 204. checkForQuit()
The main game loop that starts on line 194 handles all of the code for the main part of the game
when pieces are falling to the bottom. The fallingPiece variable is set to None after the
falling piece has landed. This means that the piece in nextPiece should be copied to the
fallingPiece variable, and a random new piece should be put into the nextPiece variable.
A new piece can be generated from the getNewPiece() function. The lastFallTime
variable is also reset to the current time so that the piece will fall in however many seconds is in
fallFreq.
The pieces that getNewPiece() are positioned a little bit above the board, usually with part of
the piece already on the board. But if this is an invalid position because the board is already filled
up there (in which case the isValidPosition() call on line 201 will return False), then
we know that the board is full and the player should lose the game. When this happens, the
runGame() function returns.
205. for event in pygame.event.get(): # event handling loop 206. if event.type == KEYUP:
The
207. if (event.key == K_p):
208. # Pausing the game
209. DISPLAYSURF.fill(BGCOLOR)
210. pygame.mixer.music.stop()
211. showTextScreen('Paused') # pause until a key press
212. pygame.mixer.music.play(-1, 0.0)
213. lastFallTime = time.time()
214. lastMoveDownTime = time.time()
215. lastMoveSidewaysTime = time.time()
If the player has pressed the P key, then the game should pause. We need to hide the board from
the player (otherwise the player could
The code blanks out the display DISPLAYSURF.fill(BGCOLOR) and
stops the music. The showTextScreen() function is called to display the "Paused" text and
wait for the player to press a key to continue.
Once the player has pressed a key, showTextScreen() will return. Line 212 will restart the
lastFallTime, lastMoveDownTime and lastMoveSidewaysTime
variables should all be reset to the current time (which is done on lines 213 to 215).
216. elif (event.key == K_LEFT or event.key == K_a): 217. movingLeft = False 218. elif (event.key == K_RIGHT or event.key == K_d): 219. movingRight = False 220. elif (event.key == K_DOWN or event.key == K_s): 221. movingDown = False
Letting up on one of the arrow keys (or the WASD keys) will set the movingLeft,
movingRight, or movingDown variables back to False, indicating that the player no longer
wants to move the piece in those directions. The code later will handle what to do based on the
Boolean values inside these "moving" variables. Note that the up arrow and W keys are used for
rotating the piece, not moving the piece up. This is why there is no movingUp variable.
223. elif event.type == KEYDOWN: 224. # moving the block sideways 225. if (event.key == K_LEFT or event.key == K_a) and isValidPosition(board, fallingPiece, adjX=-1): 226. fallingPiece['x'] -= 1 227. movingLeft = True 228. movingRight = False 229. lastMoveSidewaysTime = time.time()
When the isValidPosition()), then we should change the position
to one space to the left by fallingPiece['x'] by 1. The
isValidPosition() function has adjX and adjY. Normally the
isValidPosition() function checks the position of the data provided by the piece object
that is passed for the second parameter. However, sometimes we don’t want to check where the
piece is currently located, but rather a few spaces over from that position.
If we pass -1 for the adjX (a short name for "adjusted X"), then it doesn’t check the validity of
the position in the piece’s adjX would check one space to the right. There is also
an adjY optional adjY checks one space above where the piece is
adjY would check three spaces down from
where the piece is.
The movingLeft variable is set to True, and just to make sure the falling piece won’t move
both left and right, the movingRight variable is set to False on line 228. The
lastMoveSidewaysTime variable will be updated to the current time on line 229.
These variables are set so that the player can just movingLeft variable is set to True, the program can know that the left
arrow key (or A key) has been pressed and not yet let go. And if 0.15 seconds (the number stored
in MOVESIDEWAYSFREQ) has passed since the time stored in lastMoveSidewaysTime,
then it is time for the program to move the falling piece to the left again.
The lastMoveSidewaysTime works just like how the lastClickTime variable did in the
Simulate chapter.
231. elif (event.key == K_RIGHT or event.key == K_d) and isValidPosition(board, fallingPiece, adjX=1): 232. fallingPiece['x'] += 1 233. movingRight = True 234. movingLeft = False 235. lastMoveSidewaysTime = time.time()
The code on lines 231 to 235 is almost identical to lines 225 to 229, except that it handles moving the falling piece to the right when the right arrow key (or D key) has been pressed.
237. # rotating the block (if there is room to rotate) 238. elif (event.key == K_UP or event.key == K_w): 239. fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(SHAPES[fallingPiece['shape']])
The up arrow key (or W key) will rotate the falling piece to its next rotation. All the code has to
do is increment the 'rotation' key’s value in the fallingPiece dictionary by 1.
However, if incrementing the 'rotation' key’s value makes it larger than the total number of
rotations, then "modding" by the total number of possible rotations for that shape (which is what
len(SHAPES[fallingPiece['shape']]) is) then it will "roll over" to 0.
Here’s an example of this modding with the J shape, which has 4 possible rotations:
>>> 0 % 4 0 >>> 1 % 4 1 >>> 2 % 4 2 >>> 3 % 4 3 >>> 5 % 4 1 >>> 6 % 4 2 >>> 7 % 4 3 >>> 8 % 4 0 >>>
240. if not isValidPosition(board, fallingPiece): 241. fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(SHAPES[fallingPiece['shape']])
If the new rotated position is not valid because it fallingPiece['rotation']. We can also mod it by
len(SHAPES[fallingPiece['shape']]) so that if the new value is -1, the modding
will change it back to the last rotation in the list. Here’s an example of modding a negative
number:
>>> -1 % 4 3
242. elif (event.key == K_q): # rotate the other direction 243. fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(SHAPES[fallingPiece['shape']]) 244. if not isValidPosition(board, fallingPiece): 245. fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(SHAPES[fallingPiece['shape']])
Lines 242 to 245 do the same thing 238 to 241, except they handle the case where the player has
pressed the Q key which rotates the piece in the opposite direction. In this case, we 1
from fallingPiece['rotation'] (which is done on line 243) instead of adding 1.
247. # making the block fall faster with the down key 248. elif (event.key == K_DOWN or event.key == K_s): 249. movingDown = True 250. if isValidPosition(board, fallingPiece, adjY=1): 251. fallingPiece['y'] += 1 252. lastMoveDownTime = time.time()
If the down arrow or S key is pressed down, then the player wants the piece to fall faster than
normal. Line 251 moves the piece down one space on the board (but only if it is a valid space).
The movingDown variable is set to True and lastMoveDownTime is reset to the current
time. These variables will be checked later so that the piece keeps falling at the faster rate as long
as the down arrow or S key is held down.
254. # move the current block all the way down 255. elif event.key == K_SPACE: 256. movingDown = False 257. movingLeft = False 258. movingRight = False 259. for i in range(1, BOARDHEIGHT): 260. if not isValidPosition(board, fallingPiece, adjY=i): 261. break 262. fallingPiece['y'] += i - 1
When the player presses the space key the falling piece will immediately drop down as far as it can go on the board and land. The program first needs to find out how many spaces the piece can move until it lands.
Lines 256 to 258 will set all the moving variables to False (which makes the code in later parts
of the programming think that the user has let up on any arrow keys that were held down). This is
done because this code will move the piece to the absolute bottom and begin falling the next
piece, and we don’t want to surprise the player by having those pieces immediately start moving
just because they were
To find the farthest that the piece can fall, we should first call isValidPosition() and pass
the integer 1 for the adjY parameter. If isValidPosition() returns False, we know that
the piece cannot fall any further and is already at the bottom. If isValidPosition() returns
True, then we know that it can fall 1 space down.
In that case, we should call isValidPosition() with adjY set to 2. If it returns True
again, we will call isValidPosition() with adjY set to 3, and so on. This is what the for
loop on line 259 handles: calling isValidPosition() with increasing integer values to pass
for adjY until the False. At that point, we know that the value in i is one
space more past the bottom. This is why line 262 increases fallingPiece['y'] by i - 1
instead of i.
Also note that the second parameter to range() on line 259’s for statement is set to
BOARDHEIGHT because this is the maximum amount that the piece could fall before it must
264. # handle moving the block because of user input 265. if (movingLeft or movingRight) and time.time() - lastMoveSidewaysTime > MOVESIDEWAYSFREQ: 266. if movingLeft and isValidPosition(board, fallingPiece, adjX=- 1): 267. fallingPiece['x'] -= 1 268. elif movingRight and isValidPosition(board, fallingPiece, adjX=1): 269. fallingPiece['x'] += 1 270. lastMoveSidewaysTime = time.time()
Remember that on line 227 the movingLeft variable was set to True if the player pressed
down on the movingRight was set to True if
the player pressed down on the right arrow key.) The moving variables were set back to False if
the user let up on these keys also (see line 217 and 219).
What also happened when the player pressed down on the left or right arrow key was that the
lastMoveSidewaysTime variable was set to the current time (which was the return value of
time.time()). If the player continued to movingLeft or movingRight variable would still be set to True.
If the user held down on the key for longer than 0.15 seconds (the value stored in
MOVESIDEWAYSFREQ is the float 0.15) then the expression time.time() -
lastMoveSidewaysTime > MOVESIDEWAYSFREQ would evaluate to True. Line 265’s
condition is True if the user has both held down the arrow key and 0.15 seconds has passed, and
in that case we should move the falling piece to the left or right even though the user hasn’t
pressed the arrow key again.
This is very useful because it would become tiresome for the player to repeatedly False and the condition on
line 265 will be False. That is what stops the falling piece from
To time.time() - lastMoveSidewaysTime >
MOVESIDEWAYSFREQ returns True after the number of seconds in MOVESIDEWAYSFREQ has
passed, run this short program:
import time
WAITTIME = 4
begin = time.time()
while True:
now = time.time()
message = '%s, %s, %s' % (begin, now, (now - begin))
if now - begin > WAITTIME:
print(message + ' PASSED WAIT TIME!')
else:
print(message + ' Not yet...')
time.sleep(0.2)
This program has an
1322106392.2, 1322106392.2, 0.0 Not yet... 1322106392.2, 1322106392.42, 0.219000101089 Not yet... 1322106392.2, 1322106392.65, 0.449000120163 Not yet... 1322106392.2, 1322106392.88, 0.680999994278 Not yet... 1322106392.2, 1322106393.11, 0.910000085831 Not yet... 1322106392.2, 1322106393.34, 1.1400001049 Not yet... 1322106392.2, 1322106393.57, 1.3710000515 Not yet... 1322106392.2, 1322106393.83, 1.6360001564 Not yet... 1322106392.2, 1322106394.05, 1.85199999809 Not yet... 1322106392.2, 1322106394.28, 2.08000016212 Not yet... 1322106392.2, 1322106394.51, 2.30900001526 Not yet... 1322106392.2, 1322106394.74, 2.54100012779 Not yet... 1322106392.2, 1322106394.97, 2.76999998093 Not yet... 1322106392.2, 1322106395.2, 2.99800014496 Not yet... 1322106392.2, 1322106395.42, 3.22699999809 Not yet... 1322106392.2, 1322106395.65, 3.45600008965 Not yet... 1322106392.2, 1322106395.89, 3.69200015068 Not yet... 1322106392.2, 1322106396.12, 3.92100000381 Not yet... 1322106392.2, 1322106396.35, 4.14899992943 PASSED WAIT TIME! 1322106392.2, 1322106396.58, 4.3789999485 PASSED WAIT TIME! 1322106392.2, 1322106396.81, 4.60700011253 PASSED WAIT TIME! 1322106392.2, 1322106397.04, 4.83700013161 PASSED WAIT TIME! 1322106392.2, 1322106397.26, 5.06500005722 PASSED WAIT TIME! Traceback (most recent call last): File "C:\timetest.py", line 13, in <module> time.sleep(0.2) KeyboardInterrupt
The first number on each line of output is the return value of time.time() when the program
first started (and this value never changes). The second number is the latest return value from
time.time() (this value keeps getting updated on each iteration of the loop). And the third
number is the current time minus the begin = time.time() line of code was executed.
If this number is greater than 4, the code will start printing "PASSED
In our Tetromino program, the time.time() – lastMoveSidewaysTime expression will
evaluate to the number of seconds that has elapsed since the last time
lastMoveSidewaysTime was set to the current time. If this value is greater than the value in
MOVESIDEWAYSFREQ, we know it is time for the code to move the falling piece over one more
space.
Don’t forget to update lastMoveSidewaysTime to the current time again! This is what we
do on line 270.
272. if movingDown and time.time() - lastMoveDownTime > MOVEDOWNFREQ and isValidPosition(board, fallingPiece, adjY=1): 273. fallingPiece['y'] += 1 274. lastMoveDownTime = time.time()
Lines 272 to 274 do almost the same thing as lines 265 to 270 do except for moving the falling
piece down. This has a separate move variable (movingDown) and "last time" variable
(lastMoveDownTime) as well as a different "move frequency" variable (MOVEDOWNFREQ).
276. # let the piece fall if it is time to fall 277. if time.time() - lastFallTime > fallFreq: 278. # see if the piece has landed 279. if not isValidPosition(board, fallingPiece, adjY=1): 280. # falling piece has landed, set it on the board 281. addToBoard(board, fallingPiece) 282. score += removeCompleteLines(board) 283. level, fallFreq = calculateLevelAndFallFreq(score) 284. fallingPiece = None 285. else: 286. # piece did not land, just move the block down 287. fallingPiece['y'] += 1 288. lastFallTime = time.time()
The rate that the piece is naturally moving down (that is, falling) is tracked by the
lastFallTime variable. If enough time has elapsed since the falling piece last fell down one
space, lines 279 to 288 will handle dropping the piece by one space.
If the condition on line 279 is True, then the piece has landed. The call to addToBoard() will
make the piece part of the board rcodeoveCompleteLines() call will handle erasing any complete lines on the board and
pulling the boxes down. The rcodeoveCompleteLines() function also returns an integer
value of how many lines were removed, so we add this number to the score.
Because the score may have changed, we call the calculateLevelAndFallFreq()
function to update the current level and frequency that the pieces fall. And finally, we set the
fallingPiece variable to None to indicate that the next piece should become the new falling
piece, and a random new piece should be generated for the new next piece (That is done on lines
195 to 199 at the beginning of the game loop).
If the piece has not landed, we simply set its Y position down one space (on line 287) and reset
lastFallTime to the current time (on line 288).
290. # drawing everything on the screen 291. DISPLAYSURF.fill(BGCOLOR) 292. drawBoard(board) 293. drawStatus(score, level) 294. drawNextPiece(nextPiece) 295. if fallingPiece != None: 296. drawPiece(fallingPiece) 297. 298. pygame.display.update() 299. FPSCLOCK.tick(FPS)
Now that the game loop has handled all events and updated the game state, the game loop just
needs to draw the game state to the screen. Most of the drawing is handled by other functions, so
the game loop code just needs to call those functions. Then the call to
pygame.display.update() makes the display
302. def makeTextObjs(text, font, color): 303. surf = font.render(text, True, color) 304. return surf, surf.get_rect()
The makeTextObjs() function just provides us with a render() for us and returns the
307. def terminate(): 308. pygame.quit() 309. sys.exit()
The terminate() function works the same as in the previous
312. def checkForKeyPress(): 313. # Go through event queue looking for a KEYUP event. 314. # Grab KEYDOWN events to remove them from the event queue. 315. checkForQuit() 316. 317. for event in pygame.event.get([KEYDOWN, KEYUP]): 318. if event.type == KEYDOWN: 319. continue 320. return event.key 321. return None
The checkForKeyPress() function works almost the same as it did in the Wormy game.
First it calls checkForQuit() to handle any QUIT events (or KEYUP events specifically for
the Esc key) and terminates the program if there are any. Then it pulls out all the KEYUP and
KEYDOWN events from the KEYDOWN events (KEYDOWN was
specified to pygame.event.get() only to clear those events out of the
If there were no KEYUP events in the None.
324. def showTextScreen(text):
325. # This function displays large text in the
326. # center of the screen until a key is pressed.
327. # Draw the text drop shadow
328. titleSurf, titleRect = makeTextObjs(text, BIGFONT, TEXTSHADOWCOLOR)
329. titleRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2))
330. DISPLAYSURF.blit(titleSurf, titleRect)
331.
332. # Draw the text
333. titleSurf, titleRect = makeTextObjs(text, BIGFONT, TEXTCOLOR)
334. titleRect.center = (int(WINDOWWIDTH / 2) - 3, int(WINDOWHEIGHT / 2) -
3)
335. DISPLAYSURF.blit(titleSurf, titleRect)
336.
337. # Draw the additional "Press a key to play." text.
338. pressKeySurf, pressKeyRect = makeTextObjs('Press a key to play.',
BASICFONT, TEXTCOLOR)
339. pressKeyRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2) +
100)
340. DISPLAYSURF.blit(pressKeySurf, pressKeyRect)
Instead of separate functions for the start screen and game over screens, we will create one
showTextScreen(). The showTextScreen() function will draw
whatever text we pass for the text parameter. Also, the text "Press a key to play." will be
displayed in addition.
Notice that lines 328 to 330 draw the text in a darker shadow color first, and then lines 333 to 335
draw the same text again, except offset by 3 pixels to the left and 3 pixels
The showTextScreen() will be used for the start screen, the game over screen, and also for a
pause screen (The pause screen is
342. while checkForKeyPress() == None: 343. pygame.display.update() 344. FPSCLOCK.tick()
We want the text to pygame.display.update() and FPSCLOCK. until
checkForKeyPress() returns a value other than None. This happens when the user presses
a key.
347. def checkForQuit(): 348. for event in pygame.event.get(QUIT): # get all the QUIT events 349. terminate() # terminate if any QUIT events are present 350. for event in pygame.event.get(KEYUP): # get all the KEYUP events 351. if event.key == K_ESCAPE: 352. terminate() # terminate if the KEYUP event was for the Esc key 353. pygame.event.post(event) # put the other KEYUP event objects back
The checkForQuit() function can be called to handle any events that will cause the program
to terminate. This happens if there are any QUIT events in the KEYUP event of the Esc key. The player should be able to press the
Esc key at any time to quit the program.
Because the pygame.event.get() call on line 350 pulls out all of the KEYUP events
(including events for keys other than the Esc key), if the event is not for the Esc key, we want to
put it back into the pygame.event.post() function.
356. def calculateLevelAndFallFreq(score): 357. # Based on the score, return the level the player is on and 358. # how many seconds pass until a falling piece falls one space. 359. level = int(score / 10) + 1 360. fallFreq = 0.27 - (level * 0.02) 361. return level, fallFreq
Every time the player completes a line, their score will increase by one point. Every ten points, the game goes up a level and the pieces start falling down faster. Both the level and the falling frequency can be calculated from the score that is passed to this function.
To calculate the level, we use the int() function to round down the score int() call will round it down to 0. The + 1 part of
the code is there because we want the first level to be level 1, not level 0. When the score reaches
10, then int(10 / 10) will evaluate to 1, and the + 1 will make the level 2. Here is a
(рис 7.6)
To calculate the falling frequency, we start with a level * 0.02 part of the
We can also make a
(рис 7.7)
You can see that at level 14, the falling frequency will be less than 0. This won’t cause any bugs
with our code, because line 277 just checks that the True and the piece will fall on every iteration of
the game loop. From level 14 and beyond, the piece cannot fall any faster.
If the is set at 25, this means that at reaching level 14, the falling piece will fall 25 spaces a
second. Considering that the board is only 20 spaces tall, that means the player will have less than
a second to set each piece!
If you want the pieces to start (if you can see what I mean) falling faster at a slower rate, you can
change the calculateLevelAndFallFreq() uses. For example, let’s say
line 360 was this:
360. fallFreq = 0.27 - (level * 0.01)
In the above case, the pieces would only fall 0.01 seconds faster on each level rather than 0.02
seconds faster. The
(рис 7.8)
As you can see, with this new calculateLevelAndFallFreq().
363. def getNewPiece():
364. # return a random new piece in a random rotation and color
365. shape = random.choice(list(SHAPES.keys()))
366. newPiece = {'shape': shape,
367. 'rotation': random.randint(0, len(SHAPES[shape]) - 1),
368. 'x': int(BOARDWIDTH / 2) - int(TEMPLATEWIDTH / 2),
369. 'y': -2, # start it above the board (i.e. less than 0)
370. 'color': random.randint(0, len(COLORS)-1)}
371. return newPiece
The getNewPiece() SHAPES.keys()) on line 365. The keys() dictionary method returns a
value of the data type "dict_keys", which must be converted to a list value with the list()
function before being passed to random.choice(). This is because the random.choice()
function only accepts list values for its parameter. The random.choice() function then
randomly returns the value of an item from the list.
The piece 'shape', 'rotation',
'x', 'y' and 'color'.
The value for the 'rotation' key is a random integer between 0 to one less than however
many possible rotations there are for that shape. The number of rotations for a shape can be found
from the expression len(SHAPES[shape]).
Notice that we don’t store the list of string values (like the ones store in the constants like
S_SHAPE_TEMPLATE) in each piece PIECES constant.
The 'x' key’s value is always set to the middle of the board (also accounting for the width of the
pieces themselves, which is found from our TEMPLATEWIDTH constant). The 'y' key’s value is
always set to -2 to place it slightly above the board. (The top row of the board is row 0.)
Since the COLORS constant is a COLORS (
Once all of the values in the newPiece dictionary are set, the getNewPiece() function
returns newPiece.
374. def addToBoard(board, piece): 375. # fill in the board based on piece's location, shape, and rotation 376. for x in range(TEMPLATEWIDTH): 377. for y in range(TEMPLATEHEIGHT): 378. if SHAPES[piece['shape']][piece['rotation']][y][x] != BLANK: 379. board[x + piece['x']][y + piece['y']] = piece['color']
The board addToBoard() function does is takes a piece
The for loops on lines 376 and 377 go through every space in the piece
382. def getBlankBoard(): 383. # create and return a new blank board data structure 384. board = [] 385. for i in range(BOARDWIDTH): 386. board.append([BLANK] * BOARDHEIGHT) 387. return board
The BLANK, then it is an empty space. If the value is an integer, then it represents
a box that is the color that the integer indexes in the COLORS constant list. That is, 0 is blue, 1 is
green, 2 is red, and 3 is yellow.
In order to create a blank board, list replication is used to create the lists of BLANK values which
represents a column. This is done on line 386. One of these lists is created for each of the
columns in the board (this is what the for loop on line 385 does).
390. def isOnBoard(x, y): 391. return x >= 0 and x < BOARDWIDTH and y < BOARDHEIGHT
The isOnBoard() is a simple function which checks that the XY coordinates that are passed
represent valid values that exist on the board. As long as both the XY coordinates are not less 0
or greater than or equal to the BOARDWIDTH and BOARDHEIGHT constants, then the function
returns True.
394. def isValidPosition(board, piece, adjX=0, adjY=0): 395. # Return True if the piece is within the board and not colliding 396. for x in range(TEMPLATEWIDTH): 397. for y in range(TEMPLATEHEIGHT): 398. isAboveBoard = y + piece['y'] + adjY < 0 399. if isAboveBoard or SHAPES[piece['shape']][piece['rotation']][y][x] == BLANK: 400. continue
The isValidPosition() function is given a board True if all the boxes in the piece are both on the board and not
(рис 7.9)
On the left board, the falling piece’s (that is, the top left corner of the falling piece’s) XY
coordinates are (2, 3) on the board. But the boxes inside the falling piece’s
On the left board, the falling piece’s boxes are at the following "piece" coordinates:
(2, 2) (3, 2) (1, 3) (2, 3)
When we add the (2, 3) coordinate (the piece’s coordinates on the board) to these coordinates, it looks like this:
(2 + 2, 2 + 3) (3 + 2, 2 + 3) (1 + 2, 3 + 3) (2 + 2, 3 + 3)
After adding the (2, 3) coordinate the boxes are at the following "board" coordinates:
(4, 5) (5, 5) (3, 6) (4, 6)
And now that we can figure out where the falling piece’s boxes are as board coordinates, we can
see if they overlap with the landed boxes that are already on the board. The for loops on
lines 396 and 397 go through each of the possible coordinates on the falling piece.
We want to check if a box of the falling piece is either off of the board or isAboveBoard
that is set to True if the box on the falling piece at the coordinates pointed to be x and y is above
the board. Otherwise it is set to False.
The if statement on line 399 checks if the space on the piece is above the board or is blank. If
either of those is True, then the code executes a continue statement and goes to the next
iteration. (Note that the end of line 399 has [y][x] instead of [x][y]. This is because the
coordinates in the PIECES
401. if not isOnBoard(x + piece['x'] + adjX, y + piece['y'] + adjY): 402. return False 403. if board[x + piece['x'] + adjX][y + piece['y'] + adjY] != BLANK: 404. return False 405. return True
The if statement on line 401 checks that the piece’s box is not located on the board. The if
statement on line 403 checks that the True, then the isValidPosition() function will return False.
Notice that these if statements also adjust the coordinates for the adjX and adjY parameters
that were passed in to the function.
If the code goes through the for loop and hasn’t found a False, then the
position of the piece must be valid and so the function returns True on line 405.
407. def isCompleteLine(board, y): 408. # Return True if the line filled with boxes with no gaps. 409. for x in range(BOARDWIDTH): 410. if board[x][y] == BLANK: 411. return False 412. return True
The isCompleteLine does a simple check at the row specified by the y parameter. A row on
the board is considered to be "complete" when every space is filled by a box. The for loop on
line 409 goes through each space in the row. If a space is blank (which is caused by it having the
same value as the BLANK constant), then the function return False.
415. def removeCompleteLines(board): 416. # Remove any completed lines on the board, move everything above them down, and return the number of complete lines. 417. numLinesRemoved = 0 418. y = BOARDHEIGHT - 1 # start y at the bottom of the board 419. while y >= 0:
The rcodeoveCompleteLines() function will find any complete lines in the passed board data
structure, remove the lines, and then shift all the boxes on the board above that line down one
row. The function will return the number of lines that were removed (which is tracked by the
numLinesRcodeoved variable) so that this can be added to the score.
The way this function works is by running in a loop starting on line 419 with the y variable
starting at the lowest row (which is BOARDHEIGHT - 1). Whenever the row specified by y is
not complete, y will be
420. if isCompleteLine(board, y): 421. # Remove the line and pull boxes down by one line. 422. for pullDownY in range(y, 0, -1): 423. for x in range(BOARDWIDTH): 424. board[x][pullDownY] = board[x][pullDownY-1] 425. # Set very top line to blank. 426. for x in range(BOARDWIDTH): 427. board[x][0] = BLANK 428. numLinesRemoved += 1 429. # Note on the next iteration of the loop, y is the same. 430. # This is so that if the line that was pulled down is also 431. # complete, it will be removed. 432. else: 433. y -= 1 # move on to check next row up 434. return numLinesRemoved
The isCompleteLine() function will return True if the line that y is referring to is
complete. In that case, the program needs to copy the values of each row above the removed line
to the next lowest line. This is what the for loop on line 422 does (which is why its call to the
range() function begins at y, rather than 0. Also note that it uses the three argument form of
range(), so that the list it returns starts at y, ends at 0, and after each iteration "increases" by -
1.)
Let’s look at the following example. To save space, only the top five rows of the board are shown. Row 3 is a complete line, which means that all the rows above it (row 2, 1, and 0) must be "pulled down". First, row 2 is copied down to row 3. The board on the right shows what the board will look like after this is done:
(рис 7.10)
This "pulling down" is really just copying the higher row’s values to the row below it on line 424. After row 2 is copied to row 3, then row 1 is copied to row 2 followed by row 0 copied to row 1:
(рис 7.11)
Row 0 (the row at the very top) doesn’t have a row above it to copy values down. But row 0
doesn’t need a row copied to it, it just needs all the spaces set to BLANK. This is what lines 426
and 427 do. After that, the board will have changed from the board shown below on the left to the
board shown below on the right:
(рис 7.12)
After the complete line is removed, the execution reaches the end of the while loop that started
on line 419, so the execution
This is needed because if there were two complete lines, then the second complete line would
have been pulled down and would also have to be removed. The code will then remove this
complete line, and then go to the next iteration. It is only when there is not a completed line that
the y variable is while loop.
437. def convertToPixelCoords(boxx, boxy): 438. # Convert the given xy coordinates of the board to xy 439. # coordinates of the location on the screen. 440. return (XMARGIN + (boxx * BOXSIZE)), (TOPMARGIN + (boxy * BOXSIZE))
This helper function converts the board’s box coordinates to pixel coordinates. This function works the same way to the other "convert coordinates" functions used in the previous game programs.
443. def drawBox(boxx, boxy, color, pixelx=None, pixely=None): 444. # draw a single box (each tetromino piece has four boxes) 445. # at xy coordinates on the board. Or, if pixelx pixely 446. # are specified, draw to the pixel coordinates stored in 447. # pixelx pixely (this is used for the "Next" piece). 448. if color == BLANK: 449. return 450. if pixelx == None and pixely == None: 451. pixelx, pixely = convertToPixelCoords(boxx, boxy) 452. pygame.draw.rect(DISPLAYSURF, COLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 1, BOXSIZE - 1)) 453. pygame.draw.rect(DISPLAYSURF, LIGHTCOLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 4, BOXSIZE - 4))
The drawBox() function draws a single box on the screen. The function can receive boxx and
boxy parameters for board coordinates where the box should be drawn. However, if the pixelx
and pixely parameters are specified, then these pixel coordinates will override the boxx and
boxy parameters. The pixelx and pixely parameters are used to draw the boxes of the
"Next" piece, which is not on the board.
If the pixelx and pixely parameters are not set, then they will be set to None by default
when the function first begins. Then the if statement on line 450 will None values
with the return values from convertToPixelCoords(). This call gets the pixel coordinates
of the board coordinates specified by boxx and boxy.
The code won’t fill the entire box’s space with color. To have a black outline in between the
boxes of a piece, the left and top parameters in the pygame.draw.rect() call have + 1
added to them and a - 1 is added to the width and height parameters. In order to draw the
highlighted box, first the box is drawn with the darker color on line 452. Then, a slightly smaller
box is drawn on top of the darker box on line 453.
456. def drawBoard(board): 457. # draw the border around the board 458. pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (XMARGIN - 3, TOPMARGIN - 7, (BOARDWIDTH * BOXSIZE) + 8, (BOARDHEIGHT * BOXSIZE) + 8), 5) 459. 460. # fill the background of the board 461. pygame.draw.rect(DISPLAYSURF, BGCOLOR, (XMARGIN, TOPMARGIN, BOXSIZE * BOARDWIDTH, BOXSIZE * BOARDHEIGHT)) 462. # draw the individual boxes on the board 463. for x in range(BOARDWIDTH): 464. for y in range(BOARDHEIGHT): 465. drawBox(x, y, board[x][y])
The drawBoard() function is responsible for calling the drawing functions for the board’s
border and all the boxes on the board. First the board’s border is drawn on DISPLAYSURF,
followed by the background color of the board. Then a call to drawBox() is made for each
space on the board. The drawBox() function is smart enough to leave out the box if
board[x][y] is set to BLANK.
468. def drawStatus(score, level):
469. # draw the score text
470. scoreSurf = BASICFONT.render('Score: %s' % score, True, TEXTCOLOR)
471. scoreRect = scoreSurf.get_rect()
472. scoreRect.topleft = (WINDOWWIDTH - 150, 20)
473. DISPLAYSURF.blit(scoreSurf, scoreRect)
474.
475. # draw the level text
476. levelSurf = BASICFONT.render('Level: %s' % level, True, TEXTCOLOR)
477. levelRect = levelSurf.get_rect()
478. levelRect.topleft = (WINDOWWIDTH - 150, 50)
479. DISPLAYSURF.blit(levelSurf, levelRect)
The drawStatus() function is responsible for rendering the text for the "Score:" and "Level:"
information that appears in the upper right of the corner of the screen.
482. def drawPiece(piece, pixelx=None, pixely=None): 483. shapeToDraw = SHAPES[piece['shape']][piece['rotation']] 484. if pixelx == None and pixely == None: 485. # if pixelx pixely hasn't been specified, use the location stored in the piece data structure 486. pixelx, pixely = convertToPixelCoords(piece['x'], piece['y']) 487. 488. # draw each of the blocks that make up the piece 489. for x in range(TEMPLATEWIDTH): 490. for y in range(TEMPLATEHEIGHT): 491. if shapeToDraw[y][x] != BLANK: 492. drawBox(None, None, piece['color'], pixelx + (x * BOXSIZE), pixely + (y * BOXSIZE))
The drawPiece() function will draw the boxes of a piece according to the piece
However, the "Next" piece is not drawn on the board. In this case, we ignore the position
information stored inside the piece drawPiece()
function pass in arguments for the optional pixelx and pixely parameters to specify where
exactly on the window the piece should be drawn.
If no pixelx and pixely arguments are passed in, then lines 484 and 486 will convertToPixelCoords() call.
The for loops on line 489 and 490 will then call drawBox() for each box of the piece
that needs to be drawn.
495. def drawNextPiece(piece):
496. # draw the "next" text
497. nextSurf = BASICFONT.render('Next:', True, TEXTCOLOR)
498. nextRect = nextSurf.get_rect()
499. nextRect.topleft = (WINDOWWIDTH - 120, 80)
500. DISPLAYSURF.blit(nextSurf, nextRect)
501. # draw the "next" piece
502. drawPiece(piece, pixelx=WINDOWWIDTH-120, pixely=100)
503.
504.
505. if __name__ == '__main__':
506. main()
The drawNextPiece() draws the "Next" piece in the upper right corner of the screen. It does
this by calling the drawPiece() function and passing in arguments for drawPiece()’s
pixelx and pixely parameters.
That’s the last function. Line 505 and 506 are run after all the function definitions have been
executed, and then the main() function is called to begin the main part of the program.
The Tetromino game (which is a clone of the more popular game, "Tetris") is pretty
Explaining it in plain English is one thing, but when we have to tell a computer exactly what to do there are many details we have to fill in. The original Tetris game was designed and programmed one person, Alex Pajitnov, in the Soviet Union in 1984. The game is simple, fun, and addictive. It is one of the most popular video games ever made, and has sold 100 million copies with many people creating their own clones and variations of it.
And it was all created by one person who knew how to program.
With the right idea and some programming knowledge you can create incredibly fun games. And with some practice, you will be able to turn your game ideas into real programs that might become as popular as Tetris!
For additional programming practice, you can download buggy versions of Tetromino from http://invpy.com/buggy/tetromino and try to figure out how to fix the bugs.
There are also variations of the Tetromino game on the book’s website. "Pentomino" is a version of this game with pieces made up of five boxes. There is also "Tetromino for Idiots", where all of the pieces are made up of just one box.
(рис 7.13)
These variations can be downloaded from:
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.