Skip to content

Instantly share code, notes, and snippets.

@tallakt
Last active August 29, 2015 14:08
Show Gist options
  • Save tallakt/c7bfa6766f3731d2f35f to your computer and use it in GitHub Desktop.
Save tallakt/c7bfa6766f3731d2f35f to your computer and use it in GitHub Desktop.
Conways Game of Life in Elixir
#!/usr/bin/env elixir
# https://elixirquiz.github.io/2014-11-01-game-of-life.html
defmodule Conway do
defstruct lookup: HashDict.new, rows: 0, cols: 0
def run_file(filename) do
case load_file(filename) do
{:ok, initial_state} ->
run initial_state
{:error, reason} ->
IO.puts reason
end
end
defp load_file(filename) do
lines = File.stream!(filename) |> Enum.take(41)
case check_number_of_input_lines(lines) do
{:ok, row_count} ->
stripped = Enum.map(lines, fn s -> String.replace s, ~r/\r?\n$/, "" end)
case check_all_lines_good_length(stripped) do
{:ok, column_count} ->
case check_line_contents(stripped) do
:ok ->
lookup = build_lookup_table_from_strings stripped
{:ok, %Conway{lookup: lookup, rows: row_count, cols: column_count}}
err ->
err
end
err ->
err
end
err ->
err
end
end
defp build_lookup_table_from_strings(lines) do
lines
|> Enum.with_index
|> Enum.reduce(HashDict.new, fn {row, row_number}, acc ->
row
|> String.codepoints
|> Enum.with_index
|> Enum.reduce(acc, fn {char, column_number}, inner_acc ->
if char == "#" do
Dict.put inner_acc, {row_number, column_number}, :alive
else
inner_acc
end
end)
end)
end
defp check_number_of_input_lines(lines) do
case Enum.count(lines) do
n when n in 5..40 ->
{:ok, n}
_ ->
{:error, "Number of lines in input must be between 5 and 40"}
end
end
defp check_all_lines_good_length(lines) do
length = lines |> Enum.at(0) |> String.length
cond do
Enum.any?(lines, &(String.length(&1) != length)) ->
{:error, "The lines in the file are not all same length" }
length in 5..40 ->
{:ok, length}
true ->
{:error, "Lines must be of length 4 to 40"}
end
end
defp check_line_contents(lines) do
if lines |> Enum.all?(fn s -> String.match? s, ~r/^[ #]+$/ end) do
:ok
else
{:error, "Lines must consist of spaces and #"}
end
end
def run(initial_state) do
states = Stream.unfold initial_state, fn acc -> {acc, next_step(acc)} end
timer = Stream.timer(500) |> Stream.cycle
Stream.zip(states, timer)
|> Enum.each(fn {s, _} -> print(s) end)
end
def next_step(conway) do
new_lookup =
conway
|> grid
|> Enum.reduce(HashDict.new, fn {row, col}, acc ->
update_single_grid_cell(conway, row, col, acc)
end)
%{conway | lookup: new_lookup}
end
defp update_single_grid_cell(conway, row, col, acc) do
live_neighbors =
neighbors_for(row, col)
|> Enum.map(fn neighbor -> Dict.get(conway.lookup, neighbor) end)
|> Enum.count(fn x -> x end)
#require IEx
#if Dict.get(conway.lookup, {row, col}), do: IEx.pry
case live_neighbors do
x when x in 0..1 -> # too few, dies
acc
x when x == 2 -> # continue living
Dict.put acc, {row, col}, Dict.get(conway.lookup, {row, col})
x when x == 3 -> # reproduction / continue living
Dict.put acc, {row, col}, :alive
_ -> # overpopulation
acc
end
end
defp neighbors_for(row, col) do
rr = row - 1 .. row + 1
cc = col - 1 .. col + 1
for a <- rr, b <- cc, {a, b} != {row, col}, do: {a, b}
end
defp grid(conway) do
for r <- 1..conway.rows, c <- 1..conway.cols, do: {r, c}
end
def print(conway) do
IO.ANSI.clear |> IO.puts
IO.ANSI.home |> IO.puts
(1..conway.rows) |> Enum.each(fn row -> print_line conway, row end)
end
defp print_line(conway, row) do
(1..conway.cols)
|> Enum.map(fn col -> if Dict.get(conway.lookup, {row, col}), do: "#", else: " " end)
|> Enum.join
|> IO.puts
end
end
Conway.run_file hd(System.argv)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment