Created
March 18, 2023 01:34
-
-
Save houhoulis/a17ec1c3c378cd0d453a7e454d18c4be to your computer and use it in GitHub Desktop.
Snowflakes falling in terminal in Elixir
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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