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

Conway's Game of Life

life.livemd

Conway’s Game of Life

Mix.install([
  {:kino, "~> 0.14.1"}
])

import IEx.Helpers

Rules of CGL

  • Life is a game played on a grid
  • Grids are made up of cells
  • The number of neighbors for a cell (includes above/below/left/right and corners)
    • If a cell has fewer than 2 neighbors, it will die of loneliness
    • If a cell has more than 3 neighbors, it will die of overcrowding
    • If a cell has 2 neighbors, it will stay the same.
    • If a cell has 3 neighbors, conditions are perfect and it will spring to life

Plan

  • Cell (create: new, reduce: evolve, convert: show, data: x, y, width, alive)
  • Board (create: new, reduce: evolve, convert: show, data: width, height, cells)
nil

Cell

defmodule Cell do
  defstruct [x: 0, y: 0, width: 10, alive: true]

  def new(opts \\ []) do
    __struct__(opts)
  end

  def evolve(cell, neighbor_count) do
    next_cell_state = 
      cond do
        neighbor_count < 2 -> 
          false
        neighbor_count > 3 -> 
          false
        neighbor_count == 2 -> 
          cell.alive
        neighbor_count == 3 -> 
          true
      end
    %{cell| alive: next_cell_state}
  end

  def show(cell) do
    x = cell.x * cell.width
    y = cell.y * cell.width
    
    """
    
    """
  end
  defp color(cell) do
    case cell.alive do
      true -> "green"
      false -> "black"
    end
  end
end
box = Cell.new([x: 2, y: 8])
|> Cell.show()

svg = 
  """
  
    #{box}
  
  """
|> Kino.Image.new(:svg)
Cell.new([x: 10, y: 10, alive: false]) |> Cell.evolve(2)

Grid

defmodule Grid do
  defstruct [width: 10, height: 10, cells: %{}]

  def new(width, height, live_cell_locations) do
    cells = 
      for x <- 0..(width-1), y <- 0..(height-1), into: %{} do
        {{x, y}, Cell.new([x: x, y: y, alive: {x, y} in live_cell_locations])}
      end
    %__MODULE__{width: width, height: height, cells: cells}
  end

  def evolve(grid) do
    cells = 
      for x <- 0..(grid.width-1), y <- 0..(grid.height-1), into: %{} do
        old_cell = grid.cells[{x, y}]
        new_cell = Cell.evolve(old_cell, neighbor_count(grid, old_cell))
        {{x, y}, new_cell}
      end

    %{grid | cells: cells}
  end

  defp neighbor_count(grid, cell) do
    for x <- (cell.x - 1)..(cell.x + 1), 
      y <- (cell.y - 1)..(cell.y + 1), {x, y} != {cell.x, cell.y} do
      Map.get(grid.cells, {x, y}, Cell.new([alive: false]))
    end
    |> Enum.count(fn x -> x.alive end)
  end

  def show(grid) do
    cells = 
      for x <- 0..(grid.width-1), y <- 0..(grid.height-1) do
        Cell.show(grid.cells[{x, y}])
      end
    """
    
      #{cells}
    
    """
  end
end
grid = Grid.new(3, 3, [{0, 1}, {1, 1}, {2, 1}]) 

grid
|> Grid.evolve
|> Grid.evolve
|> Grid.evolve
|> Grid.evolve
|> Grid.show
|> Kino.Image.new(:svg)
# Grid.neighbor_count(grid, grid.cells[{1, 1}])

Processes and State

me = self()

send(me, :hi)

self()
|> i
receive do
  m -> m
end
defmodule Life do


  def start(height, width, cells) do
    grid = Grid.new(height, width, cells)
    spawn(fn -> loop(grid) end)
  end

  def loop(grid) do
    grid
    |> listen()
    |> loop()
  end
  
  def listen(grid) do
    receive do
      :evolve -> 
        Grid.evolve(grid)
      {:show, from_pid} -> 
        
        send(from_pid, Grid.show(grid))
        grid
    end
  end

  def evolve(pid) do
    send(pid, :evolve)
  end

  def show(pid) do
    send(pid, {:show, self()})
    receive do
      m -> m
    end
    |> Kino.Image.new(:svg)
  end
end
life = Life.start(3, 3, [{0, 1}, {1, 1}, {2, 1}])
send(life, {:show, self()})
receive do 
  m -> m
end
|> Kino.Image.new(:svg)
Life.evolve(life)
Life.show(life)

GenServers

defmodule LifeServer do
  use GenServer

  # API
  def start_link({name, _h, _w, _cells}=args) do
    GenServer.start_link(__MODULE__, args, name: name)
  end

  def evolve(pid \\ :life) do
    GenServer.call(pid, :evolve)
    |> Kino.Image.new(:svg)
  end

  def boom(pid \\ :life) do
    GenServer.cast(pid, :boom)
  end

  # callbacks
  def init({_name, h, w, cells}) do
    grid = Grid.new(h, w, cells)
    {:ok, grid}
  end

  def handle_cast(:boom, _state) do
    raise "boom"
  end

  def handle_call(:evolve, _from, grid) do
    new_grid = Grid.evolve(grid)
    {:reply, Grid.show(new_grid), new_grid}
  end

  # functions
  def child_spec({name, _, _, _}=attrs) do
    %{id: name, start: {LifeServer, :start_link, [attrs]}}
  end
end
LifeServer.boom(:life)
init_values = {:life, 3, 3, [{0, 1}, {1, 1}, {2, 1}]}
init_values2 = {:la_vida_loca, 3, 3, [{1, 0}, {1, 1}, {1, 2}]}
childspecs = [{LifeServer, init_values}, {LifeServer, init_values2}]

Enum.each(childspecs, &amp;Kino.start_child!/1)
 [GenServer.whereis(:life), GenServer.whereis(:la_vida_loca)]
GenServer.call(:life, :evolve) |> Kino.Image.new(:svg)
LifeServer.evolve(:la_vida_loca)
LifeServer.child_spec({:life, 3, 3, [{0, 1}, {1, 1}, {2, 1}]})
# bad things man
LifeServer.boom()

The Fly in the Ointment

i LifeServer
i :sys
:sys.get_state :life

Lifecycle