Skip to content

Instantly share code, notes, and snippets.

@SebastianPoell
Last active August 20, 2024 10:08
Show Gist options
  • Save SebastianPoell/dbe5d82ac20b31fe3148d78b78c90560 to your computer and use it in GitHub Desktop.
Save SebastianPoell/dbe5d82ac20b31fe3148d78b78c90560 to your computer and use it in GitHub Desktop.
pile_of_sand.rb
# This script implements a simple cellular automaton to simulate sand,
# inspired by the game "Noita". View introduce a grid which holds
# boolean values (true means there is sand). Then, we define three
# simple rules how those cells evolve. Finally, we advance this grid
# over and over to get the simulation.
#
class PileOfSand
attr_reader :width, :height
def initialize(width:, height:)
@width, @height = width, height
# The grid (col, row) contains boolean values and a cell gets set
# to true if it is sand.
@grid = Array.new(width) { Array.new(height) }
end
# Sets the spawner position to generate a grain of sand
def start_spawn(x, y)
@spawner = { x: x, y: y }
end
# Removes the spawner position
def stop_spawn
@spawner = nil
end
# Advances the simulation of the cellular automaton.
# Each cell gets iterated and decides which cells to set/unset.
def advance
# Spawn new grain of sand if spawner is set
@grid[@spawner[:x]][@spawner[:y]] = true if @spawner
# Iterate blocks (col, row) and compute the next state of the grid.
# Please note, that for rows we are iterating from the bottom up to
# achieve a more realistic simulation.
@grid.size.times do |x|
@grid[x].size.downto(0) do |y|
# Do nothing for non-sand and bottom row
next if !@grid[x][y] || y == @grid[x].size - 1
# If nothing is below, move there
if !@grid[x][y+1]
@grid[x][y+1] = true
@grid[x][y] = false
# If nothing is below/left, move there
elsif x > 0 && !@grid[x-1][y+1]
@grid[x-1][y+1] = true
@grid[x][y] = false
# If nothing is below/right, move there
elsif x < @grid.size - 1 && !@grid[x+1][y+1]
@grid[x+1][y+1] = true
@grid[x][y] = false
end
end
end
# Return advanced grid
@grid
end
end
@SebastianPoell
Copy link
Author

Use it like that:

pile = PileOfSand.new(width: 60, height: 40)
grid = pile.advance

Or use it with ruby2d:

Screen.Recording.2022-05-14.at.16.48.25.mov
# Use ruby2d library for drawing
require "ruby2d"

# Set the window size
# Recommended to set it in ratio to the grid
set width: 600, height: 400

# Compute block size to convert between pixels and grid
BLOCK_SIZE_X = (get :width) / pile.width
BLOCK_SIZE_Y = (get :height) / pile.height

# Register event handler to set sand spawner
Ruby2D::Window::on :mouse_down do |event|
  pile.start_spawn(event.x / BLOCK_SIZE_X, event.y / BLOCK_SIZE_Y)
end

# Register event handler to remove sand spawner
Ruby2D::Window::on :mouse_up do |event|
  pile.stop_spawn
end

Ruby2D::Window::update do

  # Clear window
  clear

  # Advance simulation
  grid = pile.advance

  # Draw grid. Note, that this does not necessarily have to use rectangles
  grid.size.times do |x|
    grid[x].size.times do |y|
      next unless grid[x][y]
      Rectangle.new(
        x: x * BLOCK_SIZE_X, y: y * BLOCK_SIZE_Y,
        width: BLOCK_SIZE_X, height: BLOCK_SIZE_Y,
        color: "#C2B280"
      )
    end
  end
end

# Render window
Ruby2D::Window::show

@tducsai
Copy link

tducsai commented May 18, 2022

Very cool 👍 ⏳

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment