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

Day 23

2022/day-23.livemd

Day 23

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

example_input =
  Kino.Input.textarea("example input:")
  |> Kino.render()

real_input = Kino.Input.textarea("real input:")

Common

read = fn input ->
  input
  |> Kino.Input.read()
end
defmodule Grid do
  defstruct [:map, :neighbors]

  def count_vacancies(grid) do
    %{map: map} = grid
    {xmin, xmax} = xs(map)
    {ymin, ymax} = ys(map)
    (xmax - xmin + 1) * (ymax - ymin + 1) - map_size(map)
  end

  def new(input) do
    map =
      for {row, x} <-
            input
            |> String.split("\n", trim: true)
            |> Enum.map(&amp;to_charlist/1)
            |> Enum.with_index(),
          {col, y} <- Enum.with_index(row),
          col == ?#,
          into: %{},
          do: {{x, y}, true}

    neighbors = [
      fn {x, y} -> [{x - 1, y}, {x - 1, y + 1}, {x - 1, y - 1}] end,
      fn {x, y} -> [{x + 1, y}, {x + 1, y + 1}, {x + 1, y - 1}] end,
      fn {x, y} -> [{x, y - 1}, {x - 1, y - 1}, {x + 1, y - 1}] end,
      fn {x, y} -> [{x, y + 1}, {x + 1, y + 1}, {x - 1, y + 1}] end
    ]

    struct!(__MODULE__, map: map, neighbors: neighbors)
  end

  def round(grid) do
    grid
    |> round_propose()
    |> round_check()
    |> round_finalize()
  end

  def round_check(grid) do
    map =
      for {coord, value} <- grid.map, reduce: %{} do
        checked ->
          case value do
            [_] -> put_in(checked, [coord], true)
            list -> Enum.reduce(list, checked, fn c, m -> put_in(m, [c], true) end)
          end
      end

    %{grid | map: map}
  end

  def round_finalize(grid) do
    %{neighbors: [head | neighbors]} = grid

    %{grid | neighbors: neighbors ++ [head]}
  end

  def round_propose(grid) do
    %{map: map, neighbors: neighbors} = grid

    map =
      for {coord, _} <- map, reduce: %{} do
        proposed ->
          cond do
            Enum.any?(neighbors, fn fun -> Enum.any?(fun.(coord), &amp;map[&amp;1]) end) ->
              case Enum.find(neighbors, fn fun -> not Enum.any?(fun.(coord), &amp;map[&amp;1]) end) do
                fun when is_function(fun) ->
                  [new_coord | _] = fun.(coord)
                  Map.update(proposed, new_coord, [coord], &amp;[coord | &amp;1])

                nil ->
                  put_in(proposed, [coord], [coord])
              end

            true ->
              put_in(proposed, [coord], [coord])
          end
      end

    %{grid | map: map}
  end

  def to_string(grid) do
    %{map: map} = grid
    {xmin, xmax} = xs(map)
    {ymin, ymax} = ys(map)

    for x <- xmin..xmax, into: [] do
      for y <- ymin..ymax do
        if map[{x, y}], do: ?#, else: ?.
      end
    end
    |> Enum.join("\n")
    |> Kernel.<>("\n")
  end

  defp xs(map), do: map |> Enum.map(fn {{x, _}, _} -> x end) |> Enum.min_max()
  defp ys(map), do: map |> Enum.map(fn {{_, y}, _} -> y end) |> Enum.min_max()
end

ExUnit.start(autorun: false)

defmodule GridTest do
  use ExUnit.Case, async: true

  setup do
    %{
      input: """
      .....
      ..##.
      ..#..
      .....
      ..##.
      .....
      """
    }
  end

  describe "count_vacancies/1" do
    test "creates data structure from input", %{input: input} do
      assert Grid.new(input) |> Grid.count_vacancies() == 3
    end
  end

  describe "new/1" do
    test "creates data structure from input", %{input: input} do
      assert Grid.new(input)
    end
  end

  describe "round/1" do
    test "moves elves according to rules", %{input: input} do
      assert Grid.new(input) |> Grid.round() |> Grid.to_string() == """
             ##
             ..
             #.
             .#
             #.
             """
    end

    test "elf movement after 2 rounds", %{input: input} do
      assert Grid.new(input) |> Grid.round() |> Grid.round() |> Grid.to_string() == """
             .##.
             #...
             ...#
             ....
             .#..
             """
    end
  end

  describe "to_string/1" do
    test "generates formatted output", %{input: input} do
      assert Grid.new(input) |> Grid.to_string() == """
             ##
             #.
             ..
             ##
             """
    end
  end
end

ExUnit.run()

Part 1

grid =
  real_input
  |> then(read)
  |> Grid.new()

1..10
|> Enum.reduce(grid, fn _, g -> Grid.round(g) end)
|> Grid.count_vacancies()

Part 2

real_input
|> then(read)
|> Grid.new()
|> then(&amp;{0, {%{map: nil}, &amp;1}})
|> Stream.iterate(fn {round, {_, grid}} -> {round + 1, {grid, Grid.round(grid)}} end)
|> Stream.drop_while(fn {_, {old, new}} -> old.map != new.map end)
|> Enum.take(1)
|> get_in([Access.at(0), Access.elem(0)])