Опубликован: 06.08.2013 | Уровень: для всех | Доступ: свободно
Лекция 7:

Tetromino

< Лекция 6 || Лекция 7: 12345 || Лекция 8 >

Checking for, and Removing, Complete Lines

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 decremented to the next highest row. The loop finally stops once y reaches -1.

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 jumps back to the beginning of the loop. Note that at no point when the line was being removed and the rows being pulled down that the y variable changed at all. So on the next iteration, the y variable is pointing to the same row as before.

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 decremented on line 433. Once the y variable has been decremented all the way to 0, the execution will exit the while loop.

Convert from Board Coordinates to Pixel Coordinates

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.

Drawing a Box on the Board or Elsewhere on the Screen

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

Drawing Everything to the Screen

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.

Drawing the Score and Level Text

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.

strDrawing a Piece on the Board or Elsewhere on the Screening

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 data structure that is passed to it. This function will be used to draw the falling piece and the "Next" piece. Since the piece data structure will contain all of the shape, position, rotation, and color information, nothing else besides the piece data structure needs to be passed to the function.

However, the "Next" piece is not drawn on the board. In this case, we ignore the position information stored inside the piece data structure and instead let the caller of the 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 overwrite those variables with the return values of convertToPixelCoords() call.

The nested for loops on line 489 and 490 will then call drawBox() for each box of the piece that needs to be drawn.

Drawing the "Next" Piece

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.

Summary

The Tetromino game (which is a clone of the more popular game, "Tetris") is pretty easy to explain to someone in English: "Blocks fall from the top of a board, and the player moves and rotates them so that they form complete lines. The complete lines disappear (giving the player points) and the lines above them move down. The game keeps going until the blocks fill up the entire board and the player loses."

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:

< Лекция 6 || Лекция 7: 12345 || Лекция 8 >
Юрий Макушин
Юрий Макушин
Россия, Москва, РЭА им. Плеханова, 2004