Skip to content

Instantly share code, notes, and snippets.

@houhoulis
Created March 18, 2023 01:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save houhoulis/a17ec1c3c378cd0d453a7e454d18c4be to your computer and use it in GitHub Desktop.
Save houhoulis/a17ec1c3c378cd0d453a7e454d18c4be to your computer and use it in GitHub Desktop.
Snowflakes falling in terminal in Elixir
defmodule Flake do
defstruct position: {0, 0}, lifetime: 0
end
defmodule Snow do
@height System.shell("tput lines") |> elem(0) |> String.trim() |> String.to_integer()
@width System.shell("tput cols") |> elem(0) |> String.trim() |> String.to_integer()
@lifetime @height + 30
@sleep_duration 500
# Wind "speed" is really a probability that the flake will be carried one unit left or
# right per unit drop. So, maximum wind effect is falling at 45º. Would be nice to have
# an absolute wind (number of units sideways per unit drop).
# Default value is within -0.x..0.x, biased toward 0 rather than the extremes.
@wind_speed Enum.random([-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0]) *
Enum.random([-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0]) / 4.21
@flakes_per_row 5
def clear_screen, do: IO.write("\e\[2J")
def string_to_move_cursor(x, y), do: "\e\[#{y};#{x}H"
def move_cursor_top_left do
# IO.write("#{string_to_move_cursor(0, 0)}wind: #{@wind_speed}#{string_to_move_cursor(0, 0)}")
IO.write(
"#{string_to_move_cursor(0, 0)}#{DateTime.utc_now()}\nwind: #{Float.round(@wind_speed, 4)}#{string_to_move_cursor(0, 0)}"
)
end
def print_position(x, y) when x >= 0 and x < @width and y >= 0 and y < @height do
flake_string = if :rand.uniform() < 0.94, do: colorless_flake(), else: colorful_flake()
IO.write("#{string_to_move_cursor(x, y)}#{flake_string}")
move_cursor_top_left()
end
def print_position(_x, _y) do
end
def erase_position(x, y) when x >= 0 and x < @width and y >= 0 and y < @height do
IO.write("#{string_to_move_cursor(x, y)} ")
move_cursor_top_left()
end
def erase_position(_x, _y) do
end
def colorless_flake() do
# Grey is distinctive in some color configs and nigh-indistinguishable in others.
# Make 20% of flakes grey.
~w(* * * * \e[37m*\e[0m) |> Enum.random()
end
def colorful_flake() do
# red is too vibrant: "\e[31m*\e[0m"
~w(\e[32m*\e[0m \e[33m*\e[0m \e[34m*\e[0m \e[35m*\e[0m \e[36m*\e[0m) |> Enum.random()
end
def run do
clear_screen()
loop()
end
def loop(flakes \\ []) do
flakes = flakes ++ generate_flakes()
flakes =
flakes
|> Enum.map(&erased/1)
|> Enum.map(&moved/1)
|> Enum.reject(&out_of_scope/1)
|> Enum.map(&printed/1)
:timer.sleep(@sleep_duration)
loop(flakes)
end
def generate_flakes do
for _i <- 1..@flakes_per_row do
%Flake{position: {:rand.uniform(3 * @width) - @width - 1, 0}, lifetime: @lifetime}
end
end
# If lifetime is still at maximum, then the flake hasn't been drawn yet. No need to erase.
def erased(%Flake{lifetime: lifetime} = flake) when lifetime == @lifetime, do: flake
def erased(%Flake{position: {x, y}} = flake) do
erase_position(x, y)
flake
end
def moved(%Flake{} = flake) do
flake
|> horizontal_drift()
|> vertical_drift()
|> increment()
end
def horizontal_drift(%Flake{position: {_x, y}} = flake) when y >= @height - 1, do: flake
def horizontal_drift(%Flake{position: {x, y}} = flake) do
x =
x
|> random_horizontal_drift()
|> wind_drift()
%{flake | position: {x, y}}
end
def random_horizontal_drift(x) do
random_horizontal_factor = :rand.uniform()
x = if random_horizontal_factor < 0.1, do: x - 1, else: x
x = if random_horizontal_factor > 0.9, do: x + 1, else: x
x
end
def wind_drift(x) do
random_wind_factor = :rand.uniform()
x = if @wind_speed > 0 and random_wind_factor < @wind_speed, do: x + 1, else: x
x = if @wind_speed < 0 and random_wind_factor < -@wind_speed, do: x - 1, else: x
x
end
def vertical_drift(%Flake{position: {_x, y}} = flake) when y >= @height - 1, do: flake
def vertical_drift(%Flake{position: {x, y}} = flake) do
y = if :rand.uniform() < 0.1, do: y + 1, else: y
%{flake | position: {x, y}}
end
def increment(%Flake{position: {_x, y}, lifetime: lifetime} = flake) when y >= @height - 1 do
%{flake | lifetime: lifetime - 1}
end
def increment(%Flake{position: {x, y}, lifetime: lifetime} = flake) do
%{flake | position: {x, y + 1}, lifetime: lifetime - 1}
end
def out_of_scope(%Flake{position: {x, _y}, lifetime: lifetime})
when x < -@width or x >= 2 * @width or lifetime <= 0 do
true
end
def out_of_scope(_flake), do: false
def printed(flake) do
{x, y} = flake.position
print_position(x, y)
flake
end
end
Snow.run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment