pythontetris

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


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 30x30 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.


Solution

  • 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.