Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Game of Life

game_of_life.livemd

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(&amp;Kino.Frame.render(frame, &amp;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, &amp;animate(&amp;1, frame, millis))
    |> Stream.take_while(fn scene -> Enum.any?(scene.points, &amp;inside?(scene, &amp;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)