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(&(&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