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

Day 08

2022/elixir/day08.livemd

Day 08

Mix.install([
  {:kino, "~> 0.8.0"},
  {:vega_lite, "~> 0.1.6"},
  {:kino_vega_lite, "~> 0.1.7"}
])

Puzzle Input

alias VegaLite, as: Vl
area = Kino.Input.textarea("Puzzle Input")
puzzle_input = Kino.Input.read(area)
example_input = """
30373
25512
65332
33549
35390
"""

Plotting

data =
  for {line, y} <-
        puzzle_input |> String.split("\n", trim: true) |> Stream.with_index(),
      {h, x} <-
        line
        |> String.codepoints()
        |> Stream.with_index() do
    %{"height" => h, "x" => x, "y" => y}
  end
Vl.new(title: "Forest height", width: 1800, height: 1800)
|> Vl.data_from_values(data)
|> Vl.mark(:circle)
|> Vl.encode_field(:x, "x",
  type: :quantitative,
  title: nil
)
|> Vl.encode_field(:y, "y",
  type: :quantitative,
  title: nil
)
|> Vl.encode_field(:color, "height",
  type: :quantitative,
  aggregate: :max
)
|> Vl.encode_field(:size, "height",
  type: :quantitative,
  aggregate: :max
)
|> Vl.config(view: [stroke: nil])

Common

defmodule Landscape do
  defstruct [:map, :side]

  def new(height_map) do
    {side, side} = height_map |> Map.keys() |> Enum.max()
    %Landscape{map: height_map, side: side}
  end

  def visible?(ls, position) do
    border_tree?(ls, position) || visible_from_outside?(ls, position)
  end

  def border_tree?(%{side: side}, {x, y}) do
    cond do
      x == side || y == side -> true
      x == 0 || y == 0 -> true
      :else -> false
    end
  end

  def visible_from_outside?(ls, position) do
    ls
    |> outside_in_rays(position)
    |> Enum.any?(fn ray ->
      Enum.all?(ray, fn candidate ->
        height(ls, candidate) < height(ls, position)
      end)
    end)
  end

  def height(ls, position) do
    Map.get(ls.map, position)
  end

  def scenic_score(ls, position) do
    self_height = height(ls, position)

    ls
    |> inside_out_rays(position)
    |> Stream.map(fn ray ->
      ray
      |> Stream.map(&amp;height(ls, &amp;1))
      |> Enum.reduce_while(0, fn tree_height, score ->
        cond do
          tree_height >= self_height -> {:halt, score + 1}
          tree_height < self_height -> {:cont, score + 1}
        end
      end)
    end)
    |> Enum.product()
  end

  def outside_in_rays(%{side: side}, {x, y} = position) do
    [
      # left to right 
      Stream.map(0..x, &amp;{&amp;1, y}),

      # right to left 
      Stream.map(side..x, &amp;{&amp;1, y}),

      # top to bottom
      Stream.map(0..y, &amp;{x, &amp;1}),

      # bottom to top
      Stream.map(side..y, &amp;{x, &amp;1})
    ]
    |> Stream.map(fn ray ->
      Stream.reject(ray, &amp;(&amp;1 == position))
    end)
  end

  def inside_out_rays(%{side: side}, {x, y} = position) do
    [
      # to left
      Stream.map(x..0, &amp;{&amp;1, y}),

      # to right
      Stream.map(x..side, &amp;{&amp;1, y}),

      # to top
      Stream.map(y..0, &amp;{x, &amp;1}),

      # to bottom
      Stream.map(y..side, &amp;{x, &amp;1})
    ]
    |> Stream.map(fn ray ->
      Stream.reject(ray, &amp;(&amp;1 == position))
    end)
  end
end
ExUnit.start(autorun: false)

defmodule LandspaceTests do
  use ExUnit.Case, async: true

  @map %{
    {0, 0} => 3,
    {0, 1} => 2,
    {0, 2} => 6,
    {0, 3} => 3,
    {0, 4} => 3,
    {1, 0} => 0,
    {1, 1} => 5,
    {1, 2} => 5,
    {1, 3} => 3,
    {1, 4} => 5,
    {2, 0} => 3,
    {2, 1} => 5,
    {2, 2} => 3,
    {2, 3} => 5,
    {2, 4} => 3,
    {3, 0} => 7,
    {3, 1} => 1,
    {3, 2} => 3,
    {3, 3} => 4,
    {3, 4} => 9,
    {4, 0} => 3,
    {4, 1} => 2,
    {4, 2} => 2,
    {4, 3} => 9,
    {4, 4} => 0
  }

  test "trees visible from sides" do
    ls = Landscape.new(@map)

    assert Landscape.visible?(ls, {0, 0})
    assert Landscape.visible?(ls, {0, 4})
    assert Landscape.visible?(ls, {4, 0})
    assert Landscape.visible?(ls, {4, 4})
  end

  test "trees visible inside" do
    ls = Landscape.new(@map)

    assert Landscape.visible?(ls, {1, 1})
    assert Landscape.visible?(ls, {1, 2})
    assert Landscape.visible?(ls, {2, 1})
    assert Landscape.visible?(ls, {3, 2})
  end

  test "scenic score" do
    ls = Landscape.new(@map)

    assert Landscape.scenic_score(ls, {2, 1}) == 4
    assert Landscape.scenic_score(ls, {2, 3}) == 8
  end
end

ExUnit.run()
forest =
  for {line, y} <-
        puzzle_input |> String.split("\n", trim: true) |> Stream.with_index(),
      {h, x} <-
        line
        |> String.codepoints()
        |> Stream.with_index() do
    %{height: h |> Integer.parse() |> elem(0), position: {x, y}}
  end
ls =
  forest
  |> Stream.map(fn %{height: h, position: p} -> {p, h} end)
  |> Map.new()
  |> Landscape.new()

Part One

forest
|> Stream.filter(fn tree ->
  position = Map.fetch!(tree, :position)
  Landscape.visible?(ls, position)
end)
|> Enum.count()

Part Two

forest
|> Stream.map(fn tree ->
  position = Map.fetch!(tree, :position)
  Landscape.scenic_score(ls, position)
end)
|> Enum.max()