Advent of Code 2020 in Elixir - Day 17

Conway Cubes

defmodule Aoc2020.Day17 do
  @moduledoc "Conway Cubes"

  def run() do
    initial_state = parse_input()

    run_cycles(initial_state, 6)
    |> MapSet.size()
  end

  def parse_input() do
    File.read!("priv/inputs/2020/day17.txt")
    |> String.trim()
    |> String.split("\n")
    |> Enum.with_index()
    |> Enum.flat_map(fn {row, y} ->
      String.codepoints(row)
      |> Enum.with_index()
      |> Enum.filter(fn {v, _} -> v == "#" end)
      |> Enum.map(fn {_, x} -> {x, y, 0} end)
    end)
    |> MapSet.new()
  end

  def run_cycles(state, 0), do: state

  def run_cycles(state, remaining) do
    run_cycles(next_state(state), remaining - 1)
  end

  def next_state(current_active) do
    active_neighbourhood(current_active)
    |> Enum.filter(fn cube ->
      is_active = Enum.member?(current_active, cube)

      case {is_active, active_neighbour_count(current_active, cube)} do
        {true, c} when c in [2, 3] -> true
        {false, 3} -> true
        _ -> false
      end
    end)
    |> MapSet.new()
  end

  def active_neighbourhood(current_active) do
    Enum.reduce(current_active, MapSet.new(), fn cube, neighbourhood ->
      MapSet.union(neighbourhood, neighbours(cube))
    end)
  end

  def neighbours({x, y, z}) do
    ns =
      for x1 <- (x - 1)..(x + 1),
          y1 <- (y - 1)..(y + 1),
          z1 <- (z - 1)..(z + 1),
          {x1, y1, z1} != {x, y, z},
          do: {x1, y1, z1}

    MapSet.new(ns)
  end

  def active_neighbour_count(current_active, cube) do
    MapSet.size(MapSet.intersection(current_active, neighbours(cube)))
  end
end