Problem with eventual corrupted state in tetris

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
Post Reply
sinplea
Prole
Posts: 2
Joined: Sun Aug 18, 2024 5:47 pm

Problem with eventual corrupted state in tetris

Post by sinplea »

The Problem:
In my Tetris clone, after some amount of play time, my board state will become corrupted so that when a Tetromino is placed, all the tiles in that Tetromino's columns will act as if they are filled despite no direct calls to fill the tiles. Once the bug occurs, any new Tetromino placed will cause the bug to repeat. (In that columns will continue to fill until game over) Note the state shift below between two pieces being locked. The right most column is suddenly completely filled even though we can see that we only locked a Tetromino made from 4 tiles.

Code: Select all

[Locking] X: 2, Y: 10
[Locking] X: 3, Y: 10
[Locking] X: 4, Y: 10
[Locking] X: 4, Y: 11
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0
0 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 0
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
Gameboard W: 10, H: 20


[Locking] X: 9, Y: 8
[Locking] X: 10, Y: 8
[Locking] X: 10, Y: 9
[Locking] X: 10, Y: 10
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1
0 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 1
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
Gameboard W: 10, H: 20
For some more info. This function is what locks the tetromino. It's fired when the piece can no longer fall.

Code: Select all

-- In tetromino.lua
local function lockTetromino(tetromino, gameBoard)
    for y, row in ipairs(tetromino.blueprint) do
        for x, value in ipairs(row) do
            if value == 1 then
                local boardX = tetromino.x + x - 1
                local boardY = tetromino.y + y - 1

                print("[Locking] X: " .. boardX .. ", Y: " .. boardY)

                if boardX >= 1 and boardX <= gameBoard.width and boardY >= 1 and boardY <= gameBoard.height then
                    gameBoard.data[boardY][boardX] = { filled = true, color = tetromino.color }
                end
            end
        end
    end

    tetromino.isLocked = true
end
This function is responsible for clearing rows as they fill, and it is called whenever a piece is locked to the game board

Code: Select all

function gameBoard:clearRows()
    local clearable = {}

    -- Identify full rows starting from the bottom
    for y = self.height, 1, -1 do
        local isFull = true

        for x = 1, self.width do
            if not self.data[y][x].filled then
                isFull = false
                break
            end
        end

        if isFull then
            table.insert(clearable, y)
        end
    end

    local clearCount = 0
    -- Clear rows and shift everything down
    for _, rowIndex in ipairs(clearable) do
        local adjustedIndex = rowIndex + clearCount
        for i = adjustedIndex, 2, -1 do
            self.data[i] = self.data[i - 1]
        end

        -- Clear the top row after shifting everything down
        for j = 1, self.width do
            self.data[1][j] = { filled = false, color = nil }  -- Clear the top row correctly
        end

        clearCount = clearCount + 1
    end

    return #clearable
end
And that function is only called once a lock is detected from the active tetromino piece in my game state:

Code: Select all

function game:draw()
    gameBoard:draw()

    if ActivePiece and not ActivePiece.isLocked then
        ActivePiece:draw(gameBoard)
    end
end

function game:update(dt, state)
    -- Tetromino:update returns true if piece locked
    local hasControl = true
    if ActivePiece:update(dt, gameBoard, state) then
        hasControl = false
        gameBoard:clearRows()

        gameBoard:print()
        print("Gameboard W: " .. gameBoard.width .. ", H: " .. gameBoard.height)
        print("\n")

        local r = love.math.random(1, 7)
        ActivePiece = tetromino:new(r, math.floor(gameBoard.width / 2))
        hasControl = true
    end

    if hasControl then
        if love.keyboard.isDown("a") then
            ActivePiece:move(dt, -1, gameBoard)
        elseif love.keyboard.isDown("d") then
            ActivePiece:move(dt, 1, gameBoard)
        elseif love.keyboard.isDown("s") then
            ActivePiece:increaseDropSpeed()
        end
    end
end
The Tetromino itself only draws itself when it is the active tetromino. Otherwise, the board handles drawing the already locked pieces.

Code: Select all

-- from tetromino.lua
function Tetromino:draw(gameBoard)
    love.graphics.setColor(table.unpack(self.color))

    for y, row in ipairs(self.blueprint) do
        for x, val in ipairs(row) do
            if val == 1 then
                -- Calculate the actual position on the screen
                local drawX = gameBoard.offsetX + (self.x + (x - 2)) * gameBoard.tileSize
                local drawY = gameBoard.offsetY + (self.y + (y - 2)) * gameBoard.tileSize

                love.graphics.rectangle("fill", drawX, drawY, gameBoard.tileSize, gameBoard.tileSize)
            end
        end
    end
end

--from game_board.lua
function gameBoard:draw()
    -- First: draw the background grid
    love.graphics.setColor(1, 1, 1)

    for y = 1, self.height do
        for x = 1, self.width do
            -- Draw the grid lines. Reduce x and y offset by 1 for correct alignment
            love.graphics.rectangle(
                "line",
                self.offsetX + (x - 1) * self.tileSize,
                self.offsetY + (y - 1) * self.tileSize,
                self.tileSize,
                self.tileSize
            )

            -- Draw the locked Tetromino blocks if filled
            if self.data[y][x].filled then
                local drawX = self.offsetX + (x - 1) * self.tileSize
                local drawY = self.offsetY + (y - 1) * self.tileSize

                love.graphics.setColor(table.unpack(self.data[y][x].color))
                love.graphics.rectangle("fill", drawX, drawY, self.tileSize, self.tileSize)

                love.graphics.setColor(1, 1, 1)
            end
        end
    end
end
I'm really confused by this bug. Am I missing something obvious? The game works fine for awhile, but then out of the the blue this bug occurs. It doesn't seem to involved the clearRows function as that function seems to correct the corrupted board state until a new piece is added. This is my first post and only my third (simple) love2d game, so lua and love is new to me. I wanted to ask you all if you have ever experienced issues when updating 2D arrays, working in tile based systems like this, or if there is some Lua quirks I should be aware of. Also any debugging tips would be appreciated, as of right now the only strategy I really I have for this is print statements.
Attachments
Tetris.love
(5.24 KiB) Downloaded 110 times
User avatar
pgimeno
Party member
Posts: 3686
Joined: Sun Oct 18, 2015 2:58 pm

Re: Problem with eventual corrupted state in tetris

Post by pgimeno »

Welcome to the forums.

The problem is in gameBoard:clearRows(). You're basically shifting all rows down, but you're not creating a new empty one on the top. The first time you delete a row, row 2 becomes the same table as row 1. On subsequent times, that reference keeps propagating down, until all rows contain the same table. So, after enough rows have been cleared, when you change a cell in a high enough row, that cell is visible in all rows above it as they all are the same table (and therefore the same row).

The problem is solved by adding this line between the loop that shifts the line and the loop that clears the cells:

Code: Select all

        self.data[1] = {}
sinplea
Prole
Posts: 2
Joined: Sun Aug 18, 2024 5:47 pm

Re: Problem with eventual corrupted state in tetris

Post by sinplea »

pgimeno wrote: Sun Aug 18, 2024 10:21 pm Welcome to the forums.

The problem is in gameBoard:clearRows(). You're basically shifting all rows down, but you're not creating a new empty one on the top. The first time you delete a row, row 2 becomes the same table as row 1. On subsequent times, that reference keeps propagating down, until all rows contain the same table. So, after enough rows have been cleared, when you change a cell in a high enough row, that cell is visible in all rows above it as they all are the same table (and therefore the same row).

The problem is solved by adding this line between the loop that shifts the line and the loop that clears the cells:

Code: Select all

        self.data[1] = {}
This is 100% correct. Thank you so much! This line does indeed fix the problem, and I have now learned a valuable lesson on how table references work in Lua. Thanks again. Seriously, you rock!
Post Reply

Who is online

Users browsing this forum: Bing [Bot], Google [Bot], Semrush [Bot] and 3 guests