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(&height(ls, &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, &{&1, y}),
# right to left
Stream.map(side..x, &{&1, y}),
# top to bottom
Stream.map(0..y, &{x, &1}),
# bottom to top
Stream.map(side..y, &{x, &1})
]
|> Stream.map(fn ray ->
Stream.reject(ray, &(&1 == position))
end)
end
def inside_out_rays(%{side: side}, {x, y} = position) do
[
# to left
Stream.map(x..0, &{&1, y}),
# to right
Stream.map(x..side, &{&1, y}),
# to top
Stream.map(y..0, &{x, &1}),
# to bottom
Stream.map(y..side, &{x, &1})
]
|> Stream.map(fn ray ->
Stream.reject(ray, &(&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()