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

Conway's Game of Life (CGL)

conways_game_of_life.livemd

Conway’s Game of Life (CGL)

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

import IEx.Helpers

Rules of CGL

  • CGL is a game played on a grid.
  • A grid is made up of cells.
  • The number of neighbors for a cell (include 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 appropriate and it will spring to life

CRC Pattern:

  • Construct-Reduce-Convert:
    • Construct is about getting data from the input
    • Reduce is about converting that data using the methods of the same module
    • Convert is about sending the output

Example: CRC Counter

defmodule Counter do
  # creating
  def new(string) do
    String.to_integer(string)
  end

  # reduce
  def add(counter, value \\ 1) do
    counter + value
  end

  # convert
  def show(counter) do
    "The answer is #{counter}"
  end
end
input = "42"

input
|> Counter.new()
|> Counter.add(1)
|> Counter.add(1)
|> Counter.add(-1)
|> Counter.show()

Plan

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

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
    alive_in_next_generation =
    cond do
      neighbor_count < 2 -> 
        false
      neighbor_count > 3 ->
        false
      neighbor_count == 2 ->
        cell.alive
      neighbor_count == 3 ->
        true
    end

    %{cell | alive: alive_in_next_generation}
  end

  def to_svg(cell) do
    x = cell.x * cell.width
    y = cell.y * cell.width
    """
    
    """
  end

  defp color(cell) do
    case cell.alive do
      true -> "green"
      false -> "gray"
    end
  end
end

Checking to_svg/1 function


box = 
  Cell.new([x: 10, y: 10])
  |> Cell.to_svg()

Using Kino library to draw the cell:

svg =
  """
  
  #{box}
  
  """  
  |> Kino.Image.new(:svg)

Checking evolve/1 function

An alive cell with 1 neightbor evolves to a death cell (alive == false)

Cell.new() |> Cell.evolve(1)

An alive cell with 4 neightbors evolves to a death cell (alive == false)

Cell.new() |> Cell.evolve(4)

An alive cell with 2 neightbors evolves keeping its state (alive == true)

Cell.new() |> Cell.evolve(2)

An alive cell with 3 neighbors evolves keeping its state (alive == true)

Cell.new() |> Cell.evolve(3)

A death cell with 1 neightbor evolves into a death cell (alive == false)

Cell.new([alive: false]) |> Cell.evolve(1)

A death cell with 4 neightbos evolves into a death cell (alive == false)

Cell.new([alive: false]) |> Cell.evolve(4)

A death cell with 2 neightbors evolves keeping its state (alive == false)

Cell.new([alive: false]) |> Cell.evolve(2)

A death cell with 3 neightbors evolves to an alive cell (alive == true)

Cell.new([alive: false]) |> Cell.evolve(3)

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 to_svg(grid) do
    cells = 
      for x <- 0..(grid.width-1), y <- 0..(grid.height-1) do
        Cell.to_svg(grid.cells[{x, y}])
      end 

    """
    
    #{cells}
    
    """  
  end

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

    %{grid | cells: cells}
  end

  def 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(&amp;(&amp;1.alive))
  end
end

Checking new/3 function

grid = Grid.new(3, 3, [{0, 1}, {1, 1}, {2, 1}])

Checking to_svg/1 function

grid
|> Grid.to_svg()

Using Kino library to draw the grid:

grid
|> Grid.to_svg()
|> Kino.Image.new(:svg)

Checking neighbor_count/2

Grid.neighbor_count(grid, Cell.new(x: 1, y: 1))

Checking evolve/2 function

grid
|> Grid.evolve
|> Grid.to_svg
|> Kino.Image.new(:svg)

Processes and State

Sending a message to the current process:

me = self()

# Sending a message to the current process. 
# The message will be enqueued in the Message queue of this process.
send(me, :hi)

# Shows information about this process
self()
|> i
# Receiving the message and processing it. In this case, processing is just returning it.
# Evaluating this will process the message that is already in the queue.
receive do
  m -> m
end

We could process the evolution of the Grid in a separate process. The idea is:

  • Spawn a separate process which will be responsible for evolving a grid.
  • We will communicate with this process sending a message.
defmodule GameOfLife do
  def spawn_process(width, height, cells) do
    grid = Grid.new(width, height, cells)

    # spawn the process and returns its id
    spawn(fn -> loop(grid) end)
  end

  @doc """
  Send the evolve message to pid.
  """
  def send_evolve(pid) do
    send(pid, :evolve)
  end

  @doc """
  Sends the show message to pdi.
  Receives the svg response and prints it.
  """
  def send_show(pid) do
    send(pid, {:show, self()})

    receive do
      svg -> 
        svg
        |> Kino.Image.new(:svg)
    end
  end


  # Creates an infinite loop that waits for messages to the process.
  defp loop(grid) do
    grid
    |> listen()
    |> loop()
  end 

  # Listen and process messages. Always returns a grid.
  defp listen(grid) do
    receive do
      :evolve -> 
        Grid.evolve(grid) 
    
      {:show, from_id} ->
        send(from_id, Grid.to_svg(grid))
        grid
    end
  end
end
gol_pid = GameOfLife.spawn_process(3, 3, [{0, 1}, {1, 1}, {2, 1}])

# Send message :evolve
GameOfLife.send_evolve(gol_pid)
GameOfLife.send_show(gol_pid)
GameOfLife.send_evolve(gol_pid)
GameOfLife.send_show(gol_pid)

GenServers

Genserver means Generic Server. It is a generic implementation on a server, bringing standard functions and macros that can be customized for each particular case.

defmodule LifeServer do
  use GenServer

  @impl true
  def init({width, height, cells}) do
    grid = Grid.new(width, height, cells)
    {:ok, grid}
  end

  @impl true
  @doc """
    handle_call is the function to implement if we want to reply back 
  """
  def handle_call(:show, _from, grid) do
    {:reply, Grid.to_svg(grid), grid}
  end

  @impl true
  def handle_cast(:evolve, grid) do
    {:noreply, Grid.evolve(grid)}
  end

  def start_link(args) do
    GenServer.start_link(__MODULE__, args)
  end

  def evolve(pid) do
    GenServer.cast(pid, :evolve)
    show(pid)
  end

  def show(pid) do
    GenServer.call(pid, :show)
    |> Kino.Image.new(:svg)
  end
end
# Start the server
{:ok, pid} = LifeServer.start_link({3, 3, [{0, 1}, {1, 1}, {2, 1}]})
LifeServer.show(pid)
LifeServer.evolve(pid)

The Fly in the Ointment

defmodu