Game of Life
Section
Mix.install([
{:kino, "~> 0.5.0"}
])
defmodule Life do
@type point() :: {integer(), integer()}
@type points() :: [point()]
@type point_set() :: MapSet.t(point())
@type point_count() :: %{point() => non_neg_integer()}
@spec evolve(points()) :: points()
def evolve(alive) do
alive_set = MapSet.new(alive)
alive
|> Enum.reduce(%{}, &increment_neighbours/2)
|> Enum.filter(&alive?(&1, alive_set))
|> Enum.map(fn {point, _} -> point end)
end
@spec alive?({point(), non_neg_integer()}, point_set()) :: boolean()
def alive?({p, c}, alive_set), do: c == 3 || (c == 2 && MapSet.member?(alive_set, p))
@spec increment_neighbours(point(), point_count()) :: point_count()
def increment_neighbours({x, y}, map) do
[{0, 1}, {1, 1}, {1, 0}, {1, -1}, {0, -1}, {-1, -1}, {-1, 0}, {-1, 1}]
|> Enum.map(fn {x_d, y_d} -> {x + x_d, y + y_d} end)
|> Enum.reduce(map, &Map.update(&2, &1, 1, fn n -> n + 1 end))
end
end
defmodule Life.T do
def translate(points, x_d, y_d), do: points |> Enum.map(fn {x, y} -> {x + x_d, y + y_d} end)
def flip(points), do: points |> Enum.map(fn {x, y} -> {y, x} end)
def rotate(points), do: points |> Enum.map(fn {x, y} -> {y, -x} end)
@spec translate_to_origin(Life.points()) :: Life.points()
def translate_to_origin(points) do
[x_min, y_min] = points |> Enum.unzip() |> Tuple.to_list() |> Enum.map(&Enum.min/1)
Enum.map(points, fn {x, y} -> {x - x_min, y - y_min} end)
end
end
defmodule Life.Test do
defp oscillator?(original, current, f, 0), do: f.(original) == f.(current)
defp oscillator?(original, current, f, n),
do: oscillator?(original, Life.evolve(current), f, n - 1)
@spec oscillator?(Life.points(), non_neg_integer()) :: boolean()
def oscillator?(original, n), do: oscillator?(original, original, &Enum.sort/1, n)
@spec still_life?(Life.points()) :: boolean()
def still_life?(original), do: oscillator?(original, 1)
@spec spaceship?(Life.points(), non_neg_integer()) :: boolean()
def spaceship?(original, n),
do: oscillator?(original, original, &Enum.sort(Life.T.translate_to_origin(&1)), n)
end
defmodule Life.Pattern do
def block, do: [{0, 0}, {0, 1}, {1, 1}, {1, 0}]
def beehive, do: [{0, 1}, {1, 0}, {1, 2}, {2, 0}, {2, 2}, {3, 1}]
def loaf, do: [{0, 2}, {1, 1}, {1, 3}, {2, 0}, {2, 3}, {3, 1}, {3, 2}]
def boat, do: [{0, 1}, {0, 2}, {1, 0}, {1, 2}, {2, 1}]
def tub, do: [{0, 1}, {1, 0}, {1, 2}, {2, 1}]
def blinker, do: [{0, 0}, {0, 1}, {0, 2}]
def toad, do: [{0, 0}, {1, 0}, {2, 0}, {1, 1}, {2, 1}, {3, 1}]
def beacon, do: [{0, 2}, {0, 3}, {1, 3}, {2, 0}, {3, 0}, {3, 1}]
def glider, do: [{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 2}]
def small_spaceship,
do: [{0, 1}, {0, 3}, {1, 0}, {2, 0}, {3, 0}, {3, 3}, {4, 0}, {4, 1}, {4, 2}]
def medium_spaceship,
do: [
{0, 1},
{0, 2},
{1, 1},
{1, 2},
{1, 3},
{2, 1},
{2, 2},
{2, 3},
{3, 0},
{3, 2},
{3, 3},
{4, 0},
{4, 1},
{4, 2},
{5, 1}
]
def large_spaceship,
do: [
{0, 1},
{0, 3},
{1, 4},
{2, 0},
{2, 4},
{3, 0},
{3, 4},
{4, 4},
{5, 1},
{5, 4},
{6, 2},
{6, 3},
{6, 4}
]
end
ExUnit.start(autorun: false)
defmodule MyTest do
use ExUnit.Case, async: true
test "block is still_life?" do
assert Life.Test.still_life?(Life.Pattern.block())
end
test "beehive is still_life?" do
assert Life.Test.still_life?(Life.Pattern.beehive())
end
test "loaf is still_life?" do
assert Life.Test.still_life?(Life.Pattern.loaf())
end
test "boat is still_life?" do
assert Life.Test.still_life?(Life.Pattern.boat())
end
test "tub is still_life?" do
assert Life.Test.still_life?(Life.Pattern.tub())
end
test "blinker is oscillator? of cycle 2" do
assert Life.Test.oscillator?(Life.Pattern.blinker(), 2)
end
test "toad is oscillator? of cycle 2" do
assert Life.Test.oscillator?(Life.Pattern.toad(), 2)
end
test "beacon is oscillator? of cycle 2" do
assert Life.Test.oscillator?(Life.Pattern.beacon(), 2)
end
test "glider is spaceship? of cycle 4" do
assert Life.Test.spaceship?(Life.Pattern.glider(), 4)
end
test "small_spaceship is spaceship? of cycle 4" do
assert Life.Test.spaceship?(Life.Pattern.small_spaceship(), 4)
end
test "medium_spaceship is spaceship? of cycle 4" do
assert Life.Test.spaceship?(Life.Pattern.medium_spaceship(), 4)
end
test "large_spaceship is spaceship? of cycle 4" do
assert Life.Test.spaceship?(Life.Pattern.large_spaceship(), 4)
end
end
ExUnit.run()
defmodule Life.Scene do
@moduledoc """
The scene for the game objects.
"""
@size 1
@color "black"
@background "yellow"
defstruct [:width, :height, :points]
def point_to_svg({x, y}) do
rect(x, y, @size, @size, @color)
end
def rect(x, y, width, height, fill) do
""
end
def to_svg(%Life.Scene{width: width, height: height, points: points}) do
svgs = Enum.map(points, &point_to_svg/1)
"""
#{[rect(0, 0, width, height, @background) | svgs]}
"""
end
def inside?(scene, {x, y}) do
x >= 0 and y >= 0 and x < scene.width and y < scene.height
end
def step(scene = %Life.Scene{points: points}) do
%Life.Scene{scene | points: Life.evolve(points)}
end
def display(scene, frame) do
scene
|> Life.Scene.to_svg()
|> Kino.Image.new(:svg)
|> then(&Kino.Frame.render(frame, &1))
end
def animate(scene, frame, millis) do
Process.sleep(millis)
scene = Life.Scene.step(scene)
display(scene, frame)
scene
end
def animation(frame, scene, millis) do
Stream.iterate(scene, &animate(&1, frame, millis))
|> Stream.take_while(fn scene -> Enum.any?(scene.points, &inside?(scene, &1)) end)
|> Stream.run()
end
end
alias Life.T
alias Life.Pattern
frame = Kino.Frame.new() |> Kino.render()
points =
[
Pattern.block(),
Pattern.beehive() |> T.translate(5, 5),
Pattern.loaf() |> T.translate(10, 10),
Pattern.boat() |> T.translate(15, 15),
Pattern.tub() |> T.translate(20, 20),
Pattern.blinker() |> T.translate(25, 25),
Pattern.toad() |> T.translate(30, 30),
Pattern.beacon() |> T.translate(35, 35),
Pattern.glider() |> T.translate(40, 40),
Pattern.small_spaceship() |> T.translate(50, 50),
Pattern.medium_spaceship() |> T.translate(60, 60),
Pattern.large_spaceship() |> T.translate(70, 70)
]
|> List.flatten()
scene = %Life.Scene{width: 100, height: 100, points: points}
Life.Scene.animation(frame, scene, 80)