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

Day 3: Perfectly Spherical Houses in a Vacuum

2015/3.livemd

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(&amp;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()