revamp all of this code to make it a better game -- constants for block size and sprites
block_size = 8
grid_width = 15
grid_height = 13 -- adjusted grid height to fit the screen
green_sprite_idx = 2 -- sprite index for green block
gray_sprite_idx = 3 -- sprite index for gray blocks
golden_snowflake_sprite_idx = 4 -- sprite index for golden snowflakes
red_snowflake_sprite_idx = 5 -- sprite index for red snowflakes
rare_sprite_idx = 6 -- sprite index for rare block
special_sprite_idx = 7 -- sprite index for special block (appears infrequently)
large_sprite_idx = 8 -- sprite index for controllable large sprite

-- calculate ui and game offsets
ui_height = 8 -- height reserved for ui
game_offset_x = (128 - grid_width * block_size) / 2 -- center grid horizontally
game_offset_y = ui_height -- start grid beneath the ui
ui_margin = 4 -- margin for ui elements

-- player position (cursor)
player_x = 0
player_y = 0

-- grid to store blocks
blocks = {}
large_sprite = nil -- to store the position of the controllable large sprite
large_sprite_start_time = nil -- to store the start time of large sprite control

-- player score and game state
score = 0
time_limit = 60 * 5 -- 5 minutes in seconds
start_time = time()
game_over = false
game_won = false
final_time_str = "" -- to store final time string

-- scoring bubbles
scoring_bubbles = {}

-- bubble display time
bubble_display_time = 1.5 -- 1.5 seconds
points_per_second = 300 -- points rewarded per second for controlling the large sprite
large_sprite_active_time = 20 -- duration large sprite is controllable in seconds
last_score_time = 0 -- track the last time scored when controlling large sprite

-- initialize blocks
function init_blocks()
    for x=1, grid_width do
        blocks[x] = {}
        for y=1, grid_height do
            blocks[x][y] = 0
        end
    end
end

-- initialize game state
function _init()
    init_blocks()
end

-- update game state
function _update()
    if game_over or game_won then
        return
    end

    -- check time
    local elapsed_time = time() - start_time
    if elapsed_time > time_limit then
        if score < 8000 then
            game_over = true
        end
    end

    -- check win condition
    if score >= 8000 then
        game_won = true
        final_time_str = "final time: " .. to_minutes(time_limit - elapsed_time) -- capture final time
    end
  
    -- control logic for large sprite
    if large_sprite then
        local elapsed_control_time = time() - large_sprite_start_time
        if elapsed_control_time >= large_sprite_active_time then
            large_sprite = nil -- stop controlling the large sprite
        else
            -- generate points per second
            if time() - last_score_time >= 1 then
                score += points_per_second
                last_score_time = time()
            end
            
            -- move large sprite
            local moved = false
            if (btnp(0)) then large_sprite.x = max(0, large_sprite.x - block_size); moved = true end
            if (btnp(1)) then large_sprite.x = min(128 - block_size * 2, large_sprite.x + block_size); moved = true end
            if (btnp(2)) then large_sprite.y = max(ui_height, large_sprite.y - block_size); moved = true end
            if (btnp(3)) then large_sprite.y = min(128 - block_size * 2, large_sprite.y + block_size); moved = true end

            -- move sprites
            if moved then
                move_sprites_with_large_sprite()
            end

            -- update bubbles but do not do other updates when large sprite is active
            update_scoring_bubbles()
            return
        end
    end

    -- move cursor
    if (btnp(0)) then player_x = max(0, player_x - 1) end
    if (btnp(1)) then player_x = min(grid_width - 1, player_x + 1) end
    if (btnp(2)) then player_y = max(0, player_y - 1) end
    if (btnp(3)) then player_y = min(grid_height - 1, player_y + 1) end

    -- place block
    if (btnp(4)) then
        if blocks[player_x + 1][player_y + 1] == 0 then
            local rand = rnd()

            if rand < 0.01 then
                blocks[player_x + 1][player_y + 1] = special_sprite_idx -- special block with 1% chance
            elseif rand < 0.06 then
                blocks[player_x + 1][player_y + 1] = rare_sprite_idx -- rare block with 5% chance
            elseif rand < 0.31 then
                blocks[player_x + 1][player_y + 1] = green_sprite_idx -- green block with 25% chance
            else
                blocks[player_x + 1][player_y + 1] = gray_sprite_idx -- gray block with 69% chance
            end

            check_for_scoring()
        end
    end

    -- erase block
    if (btnp(5)) then
        if blocks[player_x + 1][player_y + 1] ~= 0 then
            blocks[player_x + 1][player_y + 1] = 0
        end
    end

    -- update scoring bubbles
    update_scoring_bubbles()
end

-- move other sprites with the large sprite
function move_sprites_with_large_sprite()
    local lx, ly = large_sprite.x, large_sprite.y
    local ls_col = flr((lx - game_offset_x) / block_size) + 1
    local ls_row = flr((ly - game_offset_y) / block_size) + 1

    for x=1, grid_width do
        for y=1, grid_height do
            if blocks[x][y] > 0 then
                -- calculate the effect of the large sprite
                local dx = x - ls_col
                local dy = y - ls_row

                if abs(dx) <= 1 and abs(dy) <= 1 then
                    local target_x = x + (dx > 0 and 1 or (dx < 0 and -1 or 0))
                    local target_y = y + (dy > 0 and 1 or (dy < 0 and -1 or 0))

                    -- ensure target stays within grid
                    if target_x >= 1 and target_x <= grid_width and 
                       target_y >= 1 and target_y <= grid_height and 
                       blocks[target_x][target_y] == 0 then

                        blocks[target_x][target_y] = blocks[x][y]
                        blocks[x][y] = 0
                    end
                end
            end
        end
    end
end

-- check for 3-in-a-row greens, rares, or specials to score points and play sound
function check_for_scoring()
    -- horizontal
    for y=1, grid_height do
        for x=1, grid_width-2 do
            if blocks[x][y] == green_sprite_idx and blocks[x+1][y] == green_sprite_idx and blocks[x+2][y] == green_sprite_idx then
                add_scoring_bubble(x, y, 300)
                sfx(2)
                blocks[x][y], blocks[x+1][y], blocks[x+2][y] = 0, 0, 0
            end
            if blocks[x][y] == rare_sprite_idx and blocks[x+1][y] == rare_sprite_idx and blocks[x+2][y] == rare_sprite_idx then
                add_scoring_bubble(x, y, 800)
                sfx(2)
                blocks[x][y], blocks[x+1][y], blocks[x+2][y] = 0, 0, 0
            end
            if blocks[x][y] == special_sprite_idx and blocks[x+1][y] == special_sprite_idx and blocks[x+2][y] == special_sprite_idx then
                add_scoring_bubble(x, y, 300)
                sfx(2)
                blocks[x][y], blocks[x+1][y], blocks[x+2][y] = 0, 0, 0
                -- create a controllable large sprite
                large_sprite = {x = game_offset_x + x * block_size, y = game_offset_y + y * block_size}
                large_sprite_start_time = time() -- record the start time for controlling the large sprite
                last_score_time = time() -- reset scoring time for large sprite
            end
        end
    end

    -- vertical
    for x=1, grid_width do
        for y=1, grid_height-2 do
            if blocks[x][y] == green_sprite_idx and blocks[x][y+1] == green_sprite_idx and blocks[x][y+2] == green_sprite_idx then
                add_scoring_bubble(x, y, 300)
                sfx(2)
                blocks[x][y], blocks[x][y+1], blocks[x][y+2] = 0, 0, 0
            end
            if blocks[x][y] == rare_sprite_idx and blocks[x][y+1] == rare_sprite_idx and blocks[x][y+2] == rare_sprite_idx then
                add_scoring_bubble(x, y, 800)
                sfx(2)
                blocks[x][y], blocks[x][y+1], blocks[x][y+2] = 0, 0, 0
            end
            if blocks[x][y] == special_sprite_idx and blocks[x][y+1] == special_sprite_idx and blocks[x][y+2] == special_sprite_idx then
                add_scoring_bubble(x, y, 300)
                sfx(2)
                blocks[x][y], blocks[x][y+1], blocks[x][y+2] = 0, 0, 0
                -- create a controllable large sprite
                large_sprite = {x = game_offset_x + x * block_size, y = game_offset_y + y * block_size}
                large_sprite_start_time = time() -- record the start time for controlling the large sprite
                last_score_time = time() -- reset scoring time for large sprite
            end
        end
    end

    -- diagonal (down-right)
    for x=1, grid_width-2 do
        for y=1, grid_height-2 do
            if blocks[x][y] == green_sprite_idx and blocks[x+1][y+1] == green_sprite_idx and blocks[x+2][y+2] == green_sprite_idx then
                add_scoring_bubble(x, y, 300)
                sfx(2)
                blocks[x][y], blocks[x+1][y+1], blocks[x+2][y+2] = 0, 0, 0
            end
            if blocks[x][y] == rare_sprite_idx and blocks[x+1][y+1] == rare_sprite_idx and blocks[x+2][y+2] == rare_sprite_idx then
                add_scoring_bubble(x, y, 800)
                sfx(2)
                blocks[x][y], blocks[x+1][y+1], blocks[x+2][y+2] = 0, 0, 0
            end
            if blocks[x][y] == special_sprite_idx and blocks[x+1][y+1] == special_sprite_idx and blocks[x+2][y+2] == special_sprite_idx then
                add_scoring_bubble(x, y, 300)
                sfx(2)
                blocks[x][y], blocks[x+1][y+1], blocks[x+2][y+2] = 0, 0, 0
                -- create a controllable large sprite
                large_sprite = {x = game_offset_x + x * block_size, y = game_offset_y + y * block_size}
                large_sprite_start_time = time() -- record the start time for controlling the large sprite
                last_score_time = time() -- reset scoring time for large sprite
            end
        end
    end

    -- diagonal (up-right)
    for x=1, grid_width-2 do
        for y=3, grid_height do
            if blocks[x][y] == green_sprite_idx and blocks[x+1][y-1] == green_sprite_idx and blocks[x+2][y-2] == green_sprite_idx then
                add_scoring_bubble(x, y, 300)
                sfx(2)
                blocks[x][y], blocks[x+1][y-1], blocks[x+2][y-2] = 0, 0, 0
            end
            if blocks[x][y] == rare_sprite_idx and blocks[x+1][y-1] == rare_sprite_idx and blocks[x+2][y-2] == rare_sprite_idx then
                add_scoring_bubble(x, y, 800)
                sfx(2)
                blocks[x][y], blocks[x+1][y-1], blocks[x+2][y-2] = 0, 0, 0
            end
            if blocks[x][y] == special_sprite_idx and blocks[x+1][y-1] == special_sprite_idx and blocks[x+2][y-2] == special_sprite_idx then
                add_scoring_bubble(x, y, 300)
                sfx(2)
                blocks[x][y], blocks[x+1][y-1], blocks[x+2][y-2] = 0, 0, 0
                -- create a controllable large sprite
                large_sprite = {x = game_offset_x + x * block_size, y = game_offset_y + y * block_size}
                large_sprite_start_time = time() -- record the start time for controlling the large sprite
                last_score_time = time() -- reset scoring time for large sprite
            end
        end
    end
end

-- add a scoring bubble
function add_scoring_bubble(x, y, points)
    score += points
    local bx = game_offset_x + (x - 1) * block_size
    local by = game_offset_y + (y - 1) * block_size
    add(scoring_bubbles, {x=bx, y=by, points=points, time=time()})
end

-- update scoring bubbles
function update_scoring_bubbles()
    for bubble in all(scoring_bubbles) do
        if time() - bubble.time > bubble_display_time then
            del(scoring_bubbles, bubble)
        end
    end
end

-- draw the game
function _draw()
    cls()

    if game_over then
        print_centered("game over", 64, 64, 8)
        scatter_red_snowflakes() -- show red snowflakes
        return
    elseif game_won then
        print_centered("you win", 64, 64, 10)
        print_centered(final_time_str, 64, 76, 7) -- display captured final time below
        scatter_snowflakes()
        return
    end

    -- prepare score and time strings
    local score_str = "score: "..tostr(score)
    local time_str = "time: "..to_minutes(time_limit - (time() - start_time))
    
    -- calculate positions for score and time
    local ui_total_text = score_str.."  "..time_str
    local ui_x = 128 - ui_margin - (#ui_total_text * 4)

    -- draw score and time side by side in the top right corner
    print(score_str, ui_x, ui_margin, 7)
    print(time_str, ui_x + (#score_str * 4) + 8, ui_margin, 7)

    -- draw game grid below the ui
    for x=1, grid_width do
        for y=1, grid_height do
            local bx = game_offset_x + (x - 1) * block_size
            local by = game_offset_y + (y - 1) * block_size
            if blocks[x][y] > 0 then
                spr(blocks[x][y], bx, by)
            end
        end
    end

    -- draw large sprite if it exists
    if large_sprite then
        spr(large_sprite_idx, large_sprite.x, large_sprite.y, 2, 2) -- large sprite with a scale factor of 2x2
        
        -- display the text "froggin' fantastic" at the bottom of the screen
        print_centered("froggin' fantastic", 64, 120, 11)
    end

    -- draw scoring bubbles
    for bubble in all(scoring_bubbles) do
        print("+"..bubble.points, bubble.x, bubble.y, 10)
    end

    -- draw cursor (outline of the currently selected block)
    local cx = game_offset_x + player_x * block_size
    local cy = game_offset_y + player_y * block_size
    rect(cx, cy, cx + block_size - 1, cy + block_size - 1, 7)
end

-- print centered text
function print_centered(text, x, y, color)
    local length = #text * 4
    print(text, x - length / 2, y, color)
end

-- convert seconds to a mm:ss format
function to_minutes(sec)
    return flr(sec/60)..":"..sub("0"..flr(sec%60), -2)
end

-- scatter golden snowflakes using sprite 004
function scatter_snowflakes()
    for i = 1, 50 do
        -- randomize x and y positions across the screen
        local x = rnd(128 - block_size)
        local y = rnd(128 - block_size)
        spr(golden_snowflake_sprite_idx, x, y)
    end
end

-- scatter slow-moving red snowflakes using sprite 005
function scatter_red_snowflakes()
    for i = 1, 50 do
        -- randomize x and y positions and move them slowly
        local x = rnd(128 - block_size)
        local y = rnd(128 - block_size)
        spr(red_snowflake_sprite_idx, x, y)
    end
end