Advent of Code 2020 in Elixir - Day 11

Seating System

defmodule Aoc2020.Day11 do
  @moduledoc "Seating System"

  def run(), do: :timer.tc(fn -> part_2() end)

  def part_1() do
    parse_input()
    |> find_static_layout(&neighbour_seat_state/2)
    |> count_occupied_seats()
  end

  def part_2() do
    parse_input()
    |> find_static_layout(&visible_seat_state/2)
    |> count_occupied_seats()
  end

  def test_input do
    "L.LL.LL.LL\nLLLLLLL.LL\nL.L.L..L..\nLLLL.LL.LL\nL.LL.LL.LL\nL.LLLLL.LL\n..L.L.....\nLLLLLLLLLL\nL.LLLLLL.L\nL.LLLLL.LL"
  end

  def parse_input() do
    # test_input()
    File.read!("priv/inputs/2020/day11.txt")
    |> String.trim()
    |> String.split("\n")
    |> Enum.map(fn row ->
      String.codepoints(row)
      |> Enum.map(fn cell -> parse_value(cell) end)
      |> :array.from_list()
    end)
    |> :array.from_list()
  end

  def parse_value(str) do
    case str do
      "." -> :floor
      "L" -> :empty
      "#" -> :taken
    end
  end

  def get_cell(grid, x, y) do
    :array.get(x, :array.get(y, grid))
  end

  def get_neighbours(grid, x, y) do
    grid_height = :array.size(grid)
    grid_width = :array.size(:array.get(0, grid))

    neighbour_locations =
      for x2 <- (x - 1)..(x + 1),
          y2 <- (y - 1)..(y + 1),
          !(x2 == x && y2 == y),
          x2 >= 0 && x2 < grid_width,
          y2 >= 0 && y2 < grid_height,
          do: {x2, y2}

    Enum.map(neighbour_locations, fn {x, y} -> get_cell(grid, x, y) end)
  end

  def find_static_layout(grid, strategy) do
    next_state = evolve_seating(grid, strategy)

    if next_state == grid do
      grid
    else
      find_static_layout(next_state, strategy)
    end
  end

  def evolve_seating(grid, strategy) do
    :array.map(
      fn y, row ->
        :array.map(
          fn x, cell ->
            apply(strategy, [grid, {x, y, cell}])
          end,
          row
        )
      end,
      grid
    )
  end

  def neighbour_seat_state(grid, {_, _, :floor} = cell), do: :floor

  def neighbour_seat_state(grid, {x, y, :empty} = cell) do
    if count_occupied_neighbours(grid, cell) == 0, do: :taken, else: :empty
  end

  def neighbour_seat_state(grid, {x, y, :taken} = cell) do
    if count_occupied_neighbours(grid, cell) >= 4, do: :empty, else: :taken
  end

  def count_occupied_neighbours(grid, {x, y, _}) do
    get_neighbours(grid, x, y)
    |> Enum.filter(&(&1 == :taken))
    |> length()
  end

  def count_occupied_seats(grid) do
    :array.foldl(
      fn _, row, count ->
        count +
          :array.foldl(
            fn _, cell, count ->
              count + if cell == :taken, do: 1, else: 0
            end,
            0,
            row
          )
      end,
      0,
      grid
    )
  end

  # part 2
  def look_direction(grid, {x, y}, {xv, yv} = direction) do
    grid_height = :array.size(grid)
    grid_width = :array.size(:array.get(0, grid))
    x2 = x + xv
    y2 = y + yv

    if x2 >= 0 && x2 < grid_width && y2 >= 0 && y2 < grid_height do
      case get_cell(grid, x2, y2) do
        :taken -> :taken
        :empty -> :empty
        _ -> look_direction(grid, {x2, y2}, direction)
      end
    else
      :empty
    end
  end

  def count_visibly_occupied(grid, {x, y, _}) do
    directions = for xv <- [-1, 0, 1], yv <- [-1, 0, 1], {xv, yv} != {0, 0}, do: {xv, yv}

    Enum.map(directions, fn direction -> look_direction(grid, {x, y}, direction) end)
    |> Enum.filter(&(&1 == :taken))
    |> length()
  end

  def visible_seat_state(grid, {_, _, :floor} = cell), do: :floor

  def visible_seat_state(grid, {x, y, :empty} = cell) do
    if count_visibly_occupied(grid, cell) == 0, do: :taken, else: :empty
  end

  def visible_seat_state(grid, {x, y, :taken} = cell) do
    if count_visibly_occupied(grid, cell) >= 5, do: :empty, else: :taken
  end

  def render_to_string(grid) do
    IO.puts(
      :array.foldl(
        fn _, row, acc ->
          acc ++
            :array.foldl(
              fn _, cell, acc2 ->
                case cell do
                  :empty -> acc2 ++ 'L'
                  :taken -> acc2 ++ '#'
                  :floor -> acc2 ++ '.'
                  _ -> '?'
                end
              end,
              '',
              row
            ) ++ '\n'
        end,
        '',
        grid
      )
    )
  end
end