neolateral

programming, drawing, photograpy etc.

The Coding Train channel on YouTube is my favourite programming channel. I love watching the programming challenges, and sometimes I follow along and try it myself too.

I watched the "Coding Challenge 180: Falling Sand", and found it very interesting. I decided to follow along and make it myself. I used LÖVE 2D because I really prefer coding in a lua+love over p5.js.

Without further ado, here's a screengrab from the final simulation in love2d. And the code follows below.

Demo

Falling Sand Simulation in Love2D

Code

To run the program, place both conf.lua and main.lua in a new folder, and then go to the folder on the command line and run love2d in the current folder using the command love ..

conf.lua

--- conf.lua: Config for the love2d game.
--
-- date: 26/2/2024
-- author: Abhishek Mishra

-- canvas size
local canvasWidth = 400
local canvasHeight = 400

function love.conf(t)
    -- set the window title
    t.window.title = "Falling Sand Simulation"

    -- set the window size
    t.window.width = canvasWidth
    t.window.height = canvasHeight

    -- disable unused modules for performance
    t.modules.joystick = false
    t.modules.physics = false
    t.modules.touch = false
end

main.lua

--- main.lua: A falling sand simulation in LÖVE
-- This is heavily based on the video by The Coding Train Coding Challenge #180:
-- Falling Sand at https://www.youtube.com/watch?v=L4u7Zy_b868
--
-- Here's how this works:
-- * When the user clicks and drags the mouse, the cells under the mouse are
--  filled with sand.
-- * The sand starts falling down.
-- * If the cell below is empty, the sand falls down.
-- * If the cell below is filled, the sand falls to the left or right
--  with a 50% probability.
-- * If there is no space to fall, the sand stays in place.
--
-- date: 26/2/2024
-- author: Abhishek Mishra

-- define square grid size
local gridW = 3

-- grid and its dimensions
local grid, nextGrid, gridRows, gridCols

-- dragging flag
local dragging = false

-- colour hue value
local hue = 0

-- seed the random number generator
math.randomseed(os.time())

--- create a 2d grid, with all the cells set to 0
--
-- @param cols: number of columns
-- @param rows: number of rows
-- @return: a 2d grid
local function createGrid(cols, rows)
    local g = {}
    for x = 1, cols do
        g[x] = {}
        for y = 1, rows do
            g[x][y] = 0
        end
    end
    return g
end

--- check if the cell column is within the column range
-- @param x: column number
-- @return: true if the column is within the range, false otherwise
local function colInGrid(x)
    return x > 0 and x <= gridCols
end

--- check if the cell row is within the row range
-- @param y: row number
-- @return: true if the row is within the range, false otherwise
local function rowInGrid(y)
    return y > 0 and y <= gridRows
end

--- love.load: Called once at the start of the simulation
function love.load()
    -- get the canvas size
    local cw = love.graphics.getWidth()
    local ch = love.graphics.getHeight()

    -- Number of rows/cols in the grid
    gridRows = cw / gridW
    gridCols = ch / gridW

    -- Create the grid
    grid = createGrid(gridCols, gridRows)
end

--- love.update: Called every frame, updates the simulation
function love.update()
    -- Create a new grid
    nextGrid = createGrid(gridCols, gridRows)

    -- If dragging, fill the cell under the mouse
    if dragging then
        local mouseCol, mouseRow = love.mouse.getPosition()
        mouseCol = math.floor(mouseCol / gridW) + 1
        mouseRow = math.floor(mouseRow / gridW) + 1

        -- lets have the mouse drag drow sand in a 5x5 matrix
        -- but each cell in the matrix has 75% chance of being filled
        local matrix = 3
        local extent = math.floor(matrix / 2)
        for x = -extent, extent do
            for y = -extent, extent do
                -- ensure the cell is within the grid
                if colInGrid(mouseCol + x) and rowInGrid(mouseRow + x) then
                    -- fill the cell with 75% probability
                    if math.random() > 0.25 then
                        nextGrid[mouseCol + x][mouseRow + y] = hue
                    end
                end
            end
        end
    end

    -- Loop through the grid
    for x = 1, gridCols do
        for y = 1, gridRows do
            local state = grid[x][y]

            -- If the cell is filled
            if state > 0 then
                local below = grid[x][y + 1]

                local belowA = -1; local belowB = -1

                -- choose a random direction, a value either -1 or 1
                local direction = (math.random(0, 1) - 0.5) * 2

                -- we have belowA direction available only if x + direction is
                -- within the grid
                if colInGrid(x + direction) then
                    belowA = grid[x + direction][y + 1]
                end

                -- we have belowB direction available only if x - direction is
                -- within the grid
                if colInGrid(x - direction) then
                    belowB = grid[x - direction][y + 1]
                end

                -- If the cell below is empty
                if below == 0 then
                    -- Move the cell down
                    nextGrid[x][y + 1] = state
                    nextGrid[x][y] = 0
                elseif belowA == 0 then
                    -- Move the cell down and to the A direction
                    nextGrid[x + direction][y + 1] = state
                elseif belowB == 0 then
                    -- Move the cell down and to the B direction
                    nextGrid[x - direction][y + 1] = state
                else
                    nextGrid[x][y] = state
                end
            end
        end
    end

    -- Update the grid
    grid = nextGrid

    -- increment the hue value
    hue = (hue + 1) % 360
end

--- love.draw: Called every frame, draws the simulation
function love.draw()
    -- fill the background with black
    love.graphics.setBackgroundColor(0, 0, 0)

    -- Draw the grid
    for x = 1, gridRows do
        for y = 1, gridCols do
            if grid[x][y] > 0 then
                -- Set color to purple for filled cells
                love.graphics.setColor(HSV(grid[x][y] / 360, 1, 1))
                -- Draw a 1x1 rectangle for each cell
                love.graphics.rectangle("fill", (x - 1) * gridW,
                    (y - 1) * gridW, gridW, gridW)
            end
            -- remove draing the grid for performance
            -- -- Set stroke color to white
            -- love.graphics.setColor(255, 255, 255)
            -- -- Draw a rectangle with stroke, gridW x gridW
            -- love.graphics.rectangle("line", (x - 1) * gridW,
            --     (y - 1) * gridW, gridW, gridW)
        end
    end
end

--- mouse drag to fill cells
function love.mousepressed(x, y, button)
    --- if mouse is pressed set dragging to true
    if button == 1 and dragging == false then
        dragging = true
    end
end

--- mouse release to stop filling cells
function love.mousereleased(x, y, button)
    --- if mouse is released set dragging to false
    if button == 1 and dragging == true then
        dragging = false
    end
end

-- escape to exit
function love.keypressed(key)
    if key == "escape" then
        love.event.quit()
    end
end

--- Converts HSV to RGB. (input and output range: 0 - 1)
-- see https://love2d.org/wiki/HSV_color
function HSV(h, s, v)
    if s <= 0 then return v,v,v end
    h = h*6
    local c = v*s
    local x = (1-math.abs((h%2)-1))*c
    local m,r,g,b = (v-c), 0, 0, 0
    if h < 1 then
        r, g, b = c, x, 0
    elseif h < 2 then
        r, g, b = x, c, 0
    elseif h < 3 then
        r, g, b = 0, c, x
    elseif h < 4 then
        r, g, b = 0, x, c
    elseif h < 5 then
        r, g, b = x, 0, c
    else
        r, g, b = c, 0, x
    end
    return r+m, g+m, b+m
end