Tetromino
Finding the Bottom
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 holding down an arrow key when they hit the space key.
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 function call returns 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 hit the bottom of the board.
Moving by Holding Down the Key
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 left arrow key? (The same for line 233 where 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 hold down the arrow key without letting up on it, then the 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 hit the arrow keys to get the falling piece to move over multiple spaces on the board. Instead, they can just hold down an arrow key and the piece will keep moving over until they let up on the key. When that happens, the code on lines 216 to 221 will set the moving variable to False and the condition on line 265 will be False. That is what stops the falling piece from sliding over more.
To demonstrate why the 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 infinite loop, so in order to terminate it, press Ctrl-C. The output of this program will look something like this:
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 start time. This third number is the number of seconds that have elapsed since the begin = time.time() line of code was executed.
If this number is greater than 4, the code will start printing "PASSED WAIT TIME!" instead of "Not yet...". This is how our game program can know if a certain amount of time has passed since a line of code was run.
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).
Letting the Piece "Naturally" Fall
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 data structure (so that future pieces can land on it), and the 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).
Drawing Everything on the Screen
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 Surface appear on the actual computer screen, and the tick() method call adds a slight pause so the game doesn’t run too fast.
makeTextObjs(), A Shortcut Function for Making Text
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 shortcut. Given the text, Font object, and a Color object, it calls render() for us and returns the Surface and Rect object for this text. This just saves us from typing out the code to create the Surface and Rect object each time we need them.
The Same Old terminate() Function
307. def terminate(): 308. pygame.quit() 309. sys.exit()
The terminate() function works the same as in the previous game programs.
Waiting for a Key Press Event with the checkForKeyPress() Function
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 event queue. It ignores any KEYDOWN events (KEYDOWN was specified to pygame.event.get() only to clear those events out of the event queue).
If there were no KEYUP events in the event queue, then the function returns None.