1

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.

2 Answers2

1

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.

Michael Butscher
  • 10,028
  • 4
  • 24
  • 25
  • This implementation works for single rows, but when two or more rows are completed at once, consecutive or not, it throws a KeyError when trying `del locked_positions[(j, i)]`. I think this could work though, working to debug it. – JesseNoEyes Apr 12 '23 at 02:38
  • @JesseNoEyes 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. (updated my code accordingly). – Michael Butscher Apr 12 '23 at 08:53
0

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.

Kingsley
  • 14,398
  • 5
  • 31
  • 53
  • I like the angle you take on this implementation, but locked_positions will have to be updated somewhere. It is passed every iteration of the main game loop to draw the grid, so the pieces in the line that were deleted will be redrawn anyway. – JesseNoEyes Apr 12 '23 at 02:28