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

Advent of Code - Day 16

2023_day16.livemd

Advent of Code - Day 16

Mix.install([
  {:kino_aoc, "~> 0.1"}
])

Introduction

–> Content

Puzzle

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2023", "16", System.fetch_env!("LB_AOC_SESSION"))

Parser

Code - Parser

defmodule Parser do
  def parse(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.map(fn line -> String.split(line, "", trim: true) end)
  end
end

Tests - Parser

ExUnit.start(autorun: false)

defmodule ParserTest do
  use ExUnit.Case, async: true
  import Parser

  @input """
  .|...\\....
  |.-.\\.....
  .....|-...
  ........|.
  ..........
  .........\\
  ..../.\\\\..
  .-.-/..|..
  .|....-|.\\
  ..//.|....
  """

  @expected [
    [".", "|", ".", ".", ".", "\\", ".", ".", ".", "."],
    ["|", ".", "-", ".", "\\", ".", ".", ".", ".", "."],
    [".", ".", ".", ".", ".", "|", "-", ".", ".", "."],
    [".", ".", ".", ".", ".", ".", ".", ".", "|", "."],
    [".", ".", ".", ".", ".", ".", ".", ".", ".", "."],
    [".", ".", ".", ".", ".", ".", ".", ".", ".", "\\"],
    [".", ".", ".", ".", "/", ".", "\\", "\\", ".", "."],
    [".", "-", ".", "-", "/", ".", ".", "|", ".", "."],
    [".", "|", ".", ".", ".", ".", "-", "|", ".", "\\"],
    [".", ".", "/", "/", ".", "|", ".", ".", ".", "."]
  ]

  describe "parse/1" do
    test "simple example" do
      assert parse(@input) == @expected
    end
  end
end

ExUnit.run()

Part One

Code - Part 1

defmodule PartOne do
  def solve(input) do
    IO.puts("--- Part One ---")
    IO.puts("Result: #{run(input)}")
  end

  def run(tiles_string) when is_bitstring(tiles_string), do: run(Parser.parse(tiles_string))

  def run(tiles) do
    trace_beams(tiles)
    |> Enum.count()
  end

  def trace_beams(tiles),
    do: trace_beams(tiles, [{0, 0, :right}])

  def trace_beams(tiles, beams),
    do: trace_beams(tiles, beams, MapSet.new())

  def trace_beams(_tiles, [], res),
    do: Enum.map(res, fn {beam_i, beam_j, _} -> {beam_i, beam_j} end) |> Enum.uniq()

  def trace_beams(tiles, beams, res) do
    res = MapSet.union(res, MapSet.new(beams))

    next_beams =
      Enum.map(beams, fn {beam_i, beam_j, beam_dir} ->
        cell = tiles |> Enum.at(beam_i) |> Enum.at(beam_j)

        case cell do
          "." ->
            pass_through(tiles, {beam_i, beam_j, beam_dir})

          "\\" ->
            reflect(tiles, {beam_i, beam_j, beam_dir}, "\\")

          "/" ->
            reflect(tiles, {beam_i, beam_j, beam_dir}, "/")

          "|" ->
            if beam_dir in [:left, :right] do
              [
                pass_through(tiles, {beam_i, beam_j, :down}),
                pass_through(tiles, {beam_i, beam_j, :up})
              ]
            else
              pass_through(tiles, {beam_i, beam_j, beam_dir})
            end

          "-" ->
            if beam_dir in [:down, :up] do
              [
                pass_through(tiles, {beam_i, beam_j, :left}),
                pass_through(tiles, {beam_i, beam_j, :right})
              ]
            else
              pass_through(tiles, {beam_i, beam_j, beam_dir})
            end

          nil ->
            nil
        end
      end)
      |> List.flatten()
      |> Enum.reject(fn v -> v == nil || MapSet.member?(res, v) end)

    trace_beams(tiles, next_beams, res)
  end

  defp pass_through(tiles, {beam_i, beam_j, beam_dir}) do
    {new_beam_i, new_beam_j} =
      case beam_dir do
        :right -> {beam_i, beam_j + 1}
        :left -> {beam_i, beam_j - 1}
        :down -> {beam_i + 1, beam_j}
        :up -> {beam_i - 1, beam_j}
      end

    if min(new_beam_i, new_beam_j) < 0 ||
         new_beam_i >= length(tiles) ||
         new_beam_j >= length(hd(tiles)) do
      nil
    else
      {new_beam_i, new_beam_j, beam_dir}
    end
  end

  defp reflect(tiles, {beam_i, beam_j, :right}, "/"),
    do: pass_through(tiles, {beam_i, beam_j, :up})

  defp reflect(tiles, {beam_i, beam_j, :left}, "/"),
    do: pass_through(tiles, {beam_i, beam_j, :down})

  defp reflect(tiles, {beam_i, beam_j, :down}, "/"),
    do: pass_through(tiles, {beam_i, beam_j, :left})

  defp reflect(tiles, {beam_i, beam_j, :up}, "/"),
    do: pass_through(tiles, {beam_i, beam_j, :right})

  defp reflect(tiles, {beam_i, beam_j, :right}, "\\"),
    do: pass_through(tiles, {beam_i, beam_j, :down})

  defp reflect(tiles, {beam_i, beam_j, :left}, "\\"),
    do: pass_through(tiles, {beam_i, beam_j, :up})

  defp reflect(tiles, {beam_i, beam_j, :down}, "\\"),
    do: pass_through(tiles, {beam_i, beam_j, :right})

  defp reflect(tiles, {beam_i, beam_j, :up}, "\\"),
    do: pass_through(tiles, {beam_i, beam_j, :left})

  def place_on_map(tiles, coords), do: place_on_map(tiles, coords, "#")
  def place_on_map(tiles, nil, symbol), do: place_on_map(tiles, [], symbol)

  def place_on_map(tiles, coords, "#") do
    Enum.map(0..(length(tiles) - 1), fn i ->
      Enum.map(0..(length(hd(tiles)) - 1), fn j ->
        if {i, j} in coords, do: "#", else: "."
      end)
      |> Enum.join()
    end)
  end
end

Tests - Part 1

ExUnit.start(autorun: false)

defmodule PartOneTest do
  use ExUnit.Case, async: true
  import PartOne

  @raw_input """
  .|...\\....
  |.-.\\.....
  .....|-...
  ........|.
  ..........
  .........\\
  ..../.\\\\..
  .-.-/..|..
  .|....-|.\\
  ..//.|....
  """

  @input Parser.parse(@raw_input)

  describe "run/1" do
    test "main example" do
      assert run(@raw_input) == 46
    end
  end

  describe "trace_beams/2" do
    test "main example" do
      assert place_on_map(@input, trace_beams(@input)) == [
               "######....",
               ".#...#....",
               ".#...#####",
               ".#...##...",
               ".#...##...",
               ".#...##...",
               ".#..####..",
               "########..",
               ".#######..",
               ".#...#.#.."
             ]
    end
  end
end

ExUnit.run()

Solution - Part 1

PartOne.solve(puzzle_input)

Part Two

Code - Part 2

defmodule PartTwo do
  import PartOne, except: [run: 1]

  def solve(input) do
    IO.puts("--- Part Two ---")
    IO.puts("Result: #{run(input)}")
  end

  def run(tiles_string) when is_bitstring(tiles_string), do: run(Parser.parse(tiles_string))

  def run(tiles) do
    # left and right side entering
    (Enum.map(0..(length(tiles) - 1), fn entering_row ->
       [{entering_row, 0, :right}, {entering_row, length(hd(tiles)) - 1, :left}]
     end) ++
       Enum.map(0..(length(hd(tiles)) - 1), fn entering_column ->
         # top and bottom side entering
         [{0, entering_column, :down}, {length(tiles) - 1, entering_column, :up}]
       end))
    |> List.flatten()
    |> Enum.map(fn starting_point ->
      trace_beams(tiles, [starting_point])
      |> Enum.count()
    end)
    |> Enum.max()
  end
end

Tests - Part 2

ExUnit.start(autorun: false)

defmodule PartTwoTest do
  use ExUnit.Case, async: true
  import PartTwo

  @raw_input """
  .|...\\....
  |.-.\\.....
  .....|-...
  ........|.
  ..........
  .........\\
  ..../.\\\\..
  .-.-/..|..
  .|....-|.\\
  ..//.|....
  """

  @input Parser.parse(@raw_input)

  describe "run/1" do
    test "main example" do
      assert run(@raw_input) == 51
    end
  end
end

ExUnit.run()

Solution - Part 2

PartTwo.solve(puzzle_input)
# 8674