How can I make sure all my rows shift down after clearing in Tetris clone?

Question:

I’m making a Tetris clone in Python and am having difficulty ironing out my function to clear completed rows. While it works in most cases, when multiple, non-consecutive rows are completed by the same block, some of them will not be shifted down as many rows in the grid as expected. This leaves empty rows at the bottom of the grid.

For example, if "0" is a placed block and "." is an empty space:

.......
.......
..00...
000000.
00.000.
000000.

dropping a line piece on the right side, I would expect two lines to be cleared and the grid to now look like:

.......
.......
.......
.......
..00..0
00.0000

Instead, the result I end up with is:

.......
.......
.......
..00..0
00.0000
.......

with the rows only being shifted 1 row down instead of the expected 2 rows.

Here is my current implementation of the clear_rows function responsible for handling the clearing and shifting:

def clear_rows(grid, locked_positions):
    cleared_rows = 0
    for i in range(len(grid) -1, -1, -1):
        row = grid[i]
        if BLACK not in row and WHITE not in row:
            cleared_rows += 1
            index = i
            for j in range(len(row)):
                del locked_positions[(j, i)]

    if cleared_rows > 0:
        pg.mixer.Sound.play(LINE_SOUND)
        for key in sorted(list(locked_positions), key=lambda x: x[1])[::-1]:
            x, y = key
            if y < index:
                new_key = (x, y + cleared_rows)
                locked_positions[new_key] = locked_positions.pop(key)
                
    if cleared_rows == 4:
        pg.mixer.Sound.play(NUT)

    return cleared_rows

grid is a list of 20 lists of 10 tuples, each representing the color of one 30×30 pixel square in the play area of the Tetris game. Each block is (0, 0, 0) by default unless it is instructed to draw them otherwise according to the locked_positions.

locked_positions is a dictionary that is passed to the grid when it gets drawn containing keys that are the (x, y) positions of pieces that have previously landed, and values that are the RGB of those blocks.

Asked By: JesseNoEyes

||

Answers:

Iteratively look for complete rows, then remove them one at a time.
I don’t think we need to consider locked rows – the row-check takes care of whether it’s empty or full.

Since the loop is progressing in reverse-order, it’s simple to just use del() on the row to remove. The iteration will be ok (because it’s reverse).

For every row you remove, add a new blank row at the top. If the grid is reversed, these can just be appended, and then re-reverse the grid to make them at the "top".

def clearRows( grid ):
    ROW_WIDTH    = 10
    EMPTY_ROW    = [ BLACK for i in range( ROW_WIDTH ) ]
    cleared_rows =  0

    for i in range( len(grid) -1, -1, -1 ):           # starting from last row
        row = grid[i]
        if ( BLACK not in row and WHITE not in row ): # if full row
            del( grid[ i ] )                          # remove the row
            cleared_rows += 1

    # reverse the grid so we can append empty rows easily
    grid.reverse()
    for i in range( cleared_rows ):               # For every row removed ~
        grid.append( EMPTY_ROW[:] )               # Add '........' (copy)
    grid.reverse()

    # Play some sounds if rows went away
    if ( cleared_rows > 0 ):
        pg.mixer.Sound.play(LINE_SOUND)

        if cleared_rows == 4:
            pg.mixer.Sound.play(NUT)

    return cleared_rows

Interestingly, you could just model the game exactly like the grid of text-lines in the example description. Just use ‘.’ for empty, and ‘0’ to ‘9’ as colour indicators (or whatever). Then the screen-drawing code can interpret the grid of strings at painting time.

Answered By: Kingsley

If multiple rows must be removed, locked_positions must be adjusted for each of them (or once in a rather complicated way). The code should therefore roughly look like (untested):

def clear_rows(grid, locked_positions):
    cleared_rows = 0
    for i in range(len(grid)) #was: range(len(grid) -1, -1, -1):
        row = grid[i]
        if BLACK not in row and WHITE not in row:
            cleared_rows += 1
            index = i
            for j in range(len(row)):
                del locked_positions[(j, i)]

            for key in sorted(list(locked_positions), key=lambda x: x[1])[::-1]:
                x, y = key
                if y < index:
                    new_key = (x, y + 1)
                    locked_positions[new_key] = locked_positions.pop(key)

    if cleared_rows > 0:
        pg.mixer.Sound.play(LINE_SOUND)
        
                
    if cleared_rows == 4:
        pg.mixer.Sound.play(NUT)

    return cleared_rows

Mainly the third for-loop was moved into the first and only cares about the currently removed row.

Update: The for i loop should run top-down because with bottom-up for multiple consecutive rows the row with the same index would have to be processed two or more times.

Answered By: Michael Butscher
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.