Day 3: Perfectly Spherical Houses in a Vacuum
Mix.install([
{:kino, "~> 0.12.3"}
])
Modules
defmodule Santa do
@moduledoc """
Functions for directing Santa and Robo-Santa within an
infinite 2D grid of houses
"""
defstruct x: 0, y: 0, visited: MapSet.new([{0, 0}])
@typedoc "A Santa struct"
@type t :: %__MODULE__{
x: integer(),
y: integer(),
visited: MapSet.t(location())
}
@typedoc "The directions Santa can move in"
@type direction :: :east | :south | :west | :north
@typedoc "The coordinates for a location"
@type location :: {integer(), integer()}
@doc """
Follow the directions provided for Santa.
When Santa moves, any newly visited locations are recorded under
the :visited field.
## Example
```elixir
iex> [:north, :north, :east]
iex> |> Santa.follow()
%Santa{x: 1, y: 2, visited: MapSet.new([{0, 0}, {0, 1}, {0, 2}, {1, 2}])}
```
"""
@spec follow(list(direction())) :: t()
def follow(directions) do
directions
|> Enum.reduce(%Santa{}, &move(&2, &1))
end
@doc """
Shares the directions with another Santa. A direction is given to the first
Santa, then the next direction is given to the second Santa, and so on...
## Example
```elixir
iex> [:north, :north, :east, :south]
iex> |> Santa.follow_with_helper()
[
%Santa{x: 1, y: 1, visited: MapSet.new([{0, 0}, {0, 1}, {1, 1}])},
%Santa{x: 0, y: 0, visited: MapSet.new([{0, 0}, {0, 1}])}
]
"""
@spec follow_with_helper(list(direction())) :: list(t())
def follow_with_helper(directions) do
directions
|> Enum.chunk_every(2)
|> Enum.reduce([%Santa{}, %Santa{}], fn
directions, santas -> Enum.zip_with(santas, directions, &move/2)
end)
end
@doc """
Returns the total number of unique locations visited by the santa(s)
including the start position of {0, 0}.
## Example
```elixir
iex> [:north, :north, :south, :south]
iex> |> Santa.follow()
iex> |> Santa.total_visited()
3
iex> [:north, :north, :south, :south]
iex> |> Santa.follow_with_helper()
iex> |> Santa.total_visited()
2
```
"""
@spec total_visited(t() | list(t())) :: pos_integer()
def total_visited(santas)
# single santa
def total_visited(santa) when is_map(santa) do
santa.visited
|> MapSet.size()
end
# a list of santas
def total_visited(santas) do
santas
|> Enum.map(&Map.get(&1, :visited))
|> Enum.reduce(&MapSet.union(&2, &1))
|> MapSet.size()
end
# update positon and locations of santa
defp move(santa, direction) do
{x, y} = next_location(santa, direction)
%{santa | x: x, y: y, visited: MapSet.put(santa.visited, {x, y})}
end
# determine coordinates of next location
defp next_location(santa, direction) do
%Santa{x: x, y: y} = santa
case direction do
:east -> {x + 1, y}
:south -> {x, y - 1}
:west -> {x - 1, y}
:north -> {x, y + 1}
end
end
end
defmodule Parser do
@moduledoc """
For parsing a string into a list of directions.
"""
@doc """
Parses a string into a list of directions.
## Example
```elixir
iex> Parser.parse("><^^v")
[:east, :west, :north, :north, :south]
```
"""
@spec parse(String.t()) :: list(Santa.direction())
def parse(str) do
str
|> String.graphemes()
|> Enum.map(&direction/1)
end
# parse direction
defp direction(str)
defp direction(">"), do: :east
defp direction("v"), do: :south
defp direction("<"), do: :west
defp direction("^"), do: :north
end
Input
input = Kino.Input.textarea("Please paste your puzzle input:")
Part 1
input
|> Kino.Input.read()
|> Parser.parse()
|> Santa.follow()
|> Santa.total_visited()
Part 2
input
|> Kino.Input.read()
|> Parser.parse()
|> Santa.follow_with_helper()
|> Santa.total_visited()