Powered by AppSignal & Oban Pro

Day 8: Treetop Treehouse

2022/day-08.livemd

Day 8: Treetop Treehouse

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

Day 8

Sample Input

30373
25512
65332
33549
35390
sample_input = Kino.Input.textarea("Paste Sample Input")
real_input = Kino.Input.textarea("Paste Real Input")
defmodule Grid do
  defstruct rows: %{}

  def from_string(raw_input) do
    rows =
      raw_input
      |> String.split("\n")
      |> Enum.with_index()
      |> Enum.into(%{}, fn {value, index} -> {index, value} end)

    %__MODULE__{rows: rows}
  end

  def read(%__MODULE__{} = grid, {row, col}) do
    read(grid, row, col)
  end

  def read(%__MODULE__{rows: rows}, row, col) when is_integer(row) and is_integer(col) do
    rows
    |> Map.get(row)
    |> read(col)
  end

  def read(row_chars, col) when is_binary(row_chars) and is_integer(col) do
    <<_pre::binary-size(col), value::binary-size(1), _rest::binary>> = row_chars
    value
  end

  def row_count(%__MODULE__{rows: rows}), do: Enum.count(rows)

  def col_count(%__MODULE__{rows: %{0 => row}}), do: String.length(row)

  def to_string(%__MODULE__{} = grid) do
    to_string(grid, fn val, _row, _col -> val end)
  end

  def to_string(%__MODULE__{rows: rows} = grid, transform) do
    count = row_count(grid)

    0..(count - 1)
    |> Enum.map(fn row_num -> {row_num, Map.get(rows, row_num)} end)
    |> Enum.map(fn {row_num, row} ->
      row
      |> String.codepoints()
      |> Enum.with_index()
      |> Enum.map(fn {value, col_num} -> transform.(value, row_num, col_num) end)
      |> Enum.join()
    end)
    |> Enum.join("\n")
  end
end
lines_of_sight = fn grid ->
  row_size = Grid.row_count(grid)
  col_size = Grid.col_count(grid)

  for row <- 0..(row_size - 1) do
    for col <- 0..(col_size - 1), do: {row, col}
  end ++
    for row <- (row_size - 1)..0 do
      for col <- (col_size - 1)..0, do: {row, col}
    end ++
    for col <- 0..(col_size - 1) do
      for row <- 0..(row_size - 1), do: {row, col}
    end ++
    for col <- (col_size - 1)..0 do
      for row <- (row_size - 1)..0, do: {row, col}
    end
end

visible_trees_in_line = fn line, grid ->
  {visible, _max} =
    Enum.reduce(line, {MapSet.new(), -1}, fn {row, col}, {visible, max} ->
      value = Grid.read(grid, row, col)

      if value > max do
        {MapSet.put(visible, {row, col}), value}
      else
        {visible, max}
      end
    end)

  visible
end

visible_trees = fn input ->
  grid =
    input
    |> Kino.Input.read()
    |> Grid.from_string()

  grid
  |> lines_of_sight.()
  |> Enum.reduce(MapSet.new(), fn line, visible ->
    MapSet.union(visible, visible_trees_in_line.(line, grid))
  end)
end

show_visible_trees = fn input ->
  visible = visible_trees.(input)

  input
  |> Kino.Input.read()
  |> Grid.from_string()
  |> Grid.to_string(fn _val, row, col ->
    if MapSet.member?(visible, {row, col}), do: "*", else: " "
  end)
  |> IO.puts()
end

sample_input |> visible_trees.() |> Enum.count()
real_input |> visible_trees.() |> Enum.count()
show_visible_trees.(real_input)
defmodule ScenicScores do
  def max_scenic_score(grid) do
    row_count = Grid.row_count(grid)
    col_count = Grid.col_count(grid)

    for row <- 0..(row_count - 1), col <- 0..(col_count - 1), reduce: 0 do
      acc ->
        score = scenic_score(grid, {row, col})
        max(score, acc)
    end
  end

  def scenic_score(grid, vantage) do
    [:north, :south, :east, :west]
    |> Enum.map(fn direction -> trees_in_view(grid, vantage, direction) end)
    |> Enum.reduce(1, &(&1 * &2))
  end

  def trees_in_view(grid, {row, col}, direction) do
    vantage_height = Grid.read(grid, {row, col})
    size = {Grid.row_count(grid), Grid.col_count(grid)}

    Enum.reduce_while(0..100, {{row, col}, 0}, fn _, {cursor, count} ->
      case next(cursor, size, direction) do
        nil ->
          {:halt, count}

        neighbor ->
          height = Grid.read(grid, neighbor)

          cond do
            height >= vantage_height -> {:halt, count + 1}
            height < vantage_height -> {:cont, {neighbor, count + 1}}
          end
      end
    end)
  end

  def next({0, _col}, _, :north), do: nil
  def next({row, _col}, {row_count, _}, :south) when row_count <= row + 1, do: nil
  def next({_row, col}, {_, col_count}, :east) when col_count <= col + 1, do: nil
  def next({_row, 0}, _, :west), do: nil
  def next({row, col}, _, :north), do: {row - 1, col}
  def next({row, col}, _, :south), do: {row + 1, col}
  def next({row, col}, _, :east), do: {row, col + 1}
  def next({row, col}, _, :west), do: {row, col - 1}
end
max_score = fn input ->
  input
  |> Kino.Input.read()
  |> Grid.from_string()
  |> ScenicScores.max_scenic_score()
end

max_score.(sample_input)
max_score.(real_input)