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

Advent of Code - Day 10

2023_day10.livemd

Advent of Code - Day 10

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

Introduction

–> Content

Puzzle

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

Parser

Code - Parser

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

Tests - Parser

ExUnit.start(autorun: false)

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

  @input1 """
  .....
  .S-7.
  .|.|.
  .L-J.
  .....
  """

  @expected1 [
    [".", ".", ".", ".", "."],
    [".", "S", "-", "7", "."],
    [".", "|", ".", "|", "."],
    [".", "L", "-", "J", "."],
    [".", ".", ".", ".", "."]
  ]

  describe "parse test" do
    test "input 1" do
      actual = parse(@input1)
      assert actual == @expected1
    end

    @input2 """
    .F----7F7F7F7F-7....
    .|F--7||||||||FJ....
    .||.FJ||||||||L7....
    FJL7L7LJLJ||LJ.L-7..
    L--J.L7...LJS7F-7L7.
    ....F-J..F7FJ|L7L7L7
    ....L7.F7||L7|.L7L7|
    .....|FJLJ|FJ|F7|.LJ
    ....FJL-7.||.||||...
    ....L---J.LJ.LJLJ...
    """

    @expected2 [
      [
        ".",
        "F",
        "-",
        "-",
        "-",
        "-",
        "7",
        "F",
        "7",
        "F",
        "7",
        "F",
        "7",
        "F",
        "-",
        "7",
        ".",
        ".",
        ".",
        "."
      ],
      [
        ".",
        "|",
        "F",
        "-",
        "-",
        "7",
        "|",
        "|",
        "|",
        "|",
        "|",
        "|",
        "|",
        "|",
        "F",
        "J",
        ".",
        ".",
        ".",
        "."
      ],
      [
        ".",
        "|",
        "|",
        ".",
        "F",
        "J",
        "|",
        "|",
        "|",
        "|",
        "|",
        "|",
        "|",
        "|",
        "L",
        "7",
        ".",
        ".",
        ".",
        "."
      ],
      [
        "F",
        "J",
        "L",
        "7",
        "L",
        "7",
        "L",
        "J",
        "L",
        "J",
        "|",
        "|",
        "L",
        "J",
        ".",
        "L",
        "-",
        "7",
        ".",
        "."
      ],
      [
        "L",
        "-",
        "-",
        "J",
        ".",
        "L",
        "7",
        ".",
        ".",
        ".",
        "L",
        "J",
        "S",
        "7",
        "F",
        "-",
        "7",
        "L",
        "7",
        "."
      ],
      [
        ".",
        ".",
        ".",
        ".",
        "F",
        "-",
        "J",
        ".",
        ".",
        "F",
        "7",
        "F",
        "J",
        "|",
        "L",
        "7",
        "L",
        "7",
        "L",
        "7"
      ],
      [
        ".",
        ".",
        ".",
        ".",
        "L",
        "7",
        ".",
        "F",
        "7",
        "|",
        "|",
        "L",
        "7",
        "|",
        ".",
        "L",
        "7",
        "L",
        "7",
        "|"
      ],
      [
        ".",
        ".",
        ".",
        ".",
        ".",
        "|",
        "F",
        "J",
        "L",
        "J",
        "|",
        "F",
        "J",
        "|",
        "F",
        "7",
        "|",
        ".",
        "L",
        "J"
      ],
      [
        ".",
        ".",
        ".",
        ".",
        "F",
        "J",
        "L",
        "-",
        "7",
        ".",
        "|",
        "|",
        ".",
        "|",
        "|",
        "|",
        "|",
        ".",
        ".",
        "."
      ],
      [
        ".",
        ".",
        ".",
        ".",
        "L",
        "-",
        "-",
        "-",
        "J",
        ".",
        "L",
        "J",
        ".",
        "L",
        "J",
        "L",
        "J",
        ".",
        ".",
        "."
      ]
    ]

    test "input 2" do
      actual = parse(@input2)
      assert actual == @expected2
    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(input_string) do
    graph = Parser.parse(input_string)

    coordinates_of_circuit(graph)
    |> distance_of_furthest_tile()
  end

  def coordinates_of_circuit(graph) do
    starting_coord = find_s_coord(graph)

    traverse_circuit(graph, starting_coord, starting_coord, [])
    |> Enum.reverse()
  end

  def distance_of_furthest_tile(coordinates) do
    (length(coordinates) / 2)
    |> Float.round()
  end

  def find_s_coord(graph) do
    Enum.find_value(graph |> Enum.with_index(), fn {row, row_i} ->
      Enum.find_value(row |> Enum.with_index(), fn {cell, col_i} ->
        if cell == "S", do: {row_i, col_i, cell}
      end)
    end)
  end

  def traverse_circuit(graph, starting_coord, current_coord, path_so_far) do
    if completed_circuit?(starting_coord, current_coord, path_so_far) do
      path_so_far
    else
      possible_next_coords = possible_next_coords(graph, current_coord)

      rejected_bit =
        possible_next_coords |> Enum.reject(fn coord -> coord == List.first(path_so_far) end)

      mapped_bit =
        rejected_bit
        |> Enum.map(fn next_coord ->
          traverse_circuit(graph, starting_coord, next_coord, [current_coord | path_so_far])
        end)

      mapped_bit |> hd()
    end
  end

  def possible_next_coords(graph, current_coord) do
    {current_y, current_x, _cell} = current_coord

    %{
      coord_east_of(graph, current_y, current_x) => east_viable?(graph, current_y, current_x),
      coord_south_of(graph, current_y, current_x) => south_viable?(graph, current_y, current_x),
      coord_west_of(graph, current_y, current_x) => west_viable?(graph, current_y, current_x),
      coord_north_of(graph, current_y, current_x) => north_viable?(graph, current_y, current_x)
    }
    |> Map.filter(fn {_k, v} -> v end)
    |> Map.keys()
  end

  defp east_viable?(graph, current_y, current_x) do
    pointing_east?(cell_at(graph, current_y, current_x)) &&
      pointing_west?(cell_east_of(graph, current_y, current_x))
  end

  defp south_viable?(graph, current_y, current_x) do
    pointing_south?(cell_at(graph, current_y, current_x)) &&
      pointing_north?(cell_south_of(graph, current_y, current_x))
  end

  defp west_viable?(graph, current_y, current_x) do
    pointing_west?(cell_at(graph, current_y, current_x)) &&
      pointing_east?(cell_west_of(graph, current_y, current_x))
  end

  defp north_viable?(graph, current_y, current_x) do
    pointing_north?(cell_at(graph, current_y, current_x)) &&
      pointing_south?(cell_north_of(graph, current_y, current_x))
  end

  defp coord_east_of(graph, current_y, current_x) do
    coord_at(graph, current_y, current_x + 1)
  end

  defp coord_south_of(graph, current_y, current_x) do
    coord_at(graph, current_y + 1, current_x)
  end

  defp coord_west_of(graph, current_y, current_x) do
    coord_at(graph, current_y, current_x - 1)
  end

  defp coord_north_of(graph, current_y, current_x) do
    coord_at(graph, current_y - 1, current_x)
  end

  defp cell_east_of(graph, current_y, current_x) do
    cell_at(graph, current_y, current_x + 1)
  end

  defp cell_south_of(graph, current_y, current_x) do
    cell_at(graph, current_y + 1, current_x)
  end

  defp cell_west_of(graph, current_y, current_x) do
    cell_at(graph, current_y, current_x - 1)
  end

  defp cell_north_of(graph, current_y, current_x) do
    cell_at(graph, current_y - 1, current_x)
  end

  defp coord_at(graph, y, x) do
    {y, x, cell_at(graph, y, x)}
  end

  defp cell_at(graph, y, x) do
    (Enum.at(graph, y) || []) |> Enum.at(x)
  end

  def pointing_east?(cell) do
    cell in ["S", "-", "L", "F"]
  end

  def pointing_south?(cell) do
    cell in ["S", "|", "7", "F"]
  end

  def pointing_west?(cell) do
    cell in ["S", "-", "J", "7"]
  end

  def pointing_north?(cell) do
    cell in ["S", "|", "L", "J"]
  end

  defp completed_circuit?(starting_coord, current_coord, path_so_far) do
    starting_coord == current_coord && List.last(path_so_far) == starting_coord
  end
end

Tests - Part 1

ExUnit.start(autorun: false)

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

  @input1 """
  .....
  .S-7.
  .|.|.
  .L-J.
  .....
  """
  @expected1 4

  test "simple example 1" do
    actual = run(@input1)
    assert actual == @expected1
  end

  describe "coordinates_of_circuit/1" do
    test "simple example" do
      input = [
        [".", ".", ".", ".", "."],
        [".", "S", "-", "7", "."],
        [".", "|", ".", "|", "."],
        [".", "L", "-", "J", "."],
        [".", ".", ".", ".", "."]
      ]

      assert coordinates_of_circuit(input) == [
               {1, 1, "S"},
               {1, 2, "-"},
               {1, 3, "7"},
               {2, 3, "|"},
               {3, 3, "J"},
               {3, 2, "-"},
               {3, 1, "L"},
               {2, 1, "|"}
             ]
    end
  end

  describe "distance_of_furthest_tile/1" do
    test "simple example" do
      input = [
        {1, 1, "S"},
        {1, 2, "-"},
        {1, 3, "7"},
        {2, 3, "|"},
        {3, 3, "J"},
        {3, 2, "-"},
        {3, 1, "L"},
        {2, 1, "|"}
      ]

      assert distance_of_furthest_tile(input) == 4
    end
  end

  describe "traverse_circuit/4" do
    test "returns if made it to the end" do
      input = [
        [".", ".", ".", "."],
        [".", "S", "7", "."],
        [".", "L", "J", "."],
        [".", ".", ".", "."]
      ]

      assert traverse_circuit(
               input,
               {1, 1, "S"},
               {1, 1, "S"},
               [{2, 1, "L"}, {2, 2, "J"}, {1, 2, "7"}, {1, 1, "S"}]
             ) ==
               [{2, 1, "L"}, {2, 2, "J"}, {1, 2, "7"}, {1, 1, "S"}]
    end

    test "add destination to the response when nearly at the end" do
      input = [
        [".", ".", ".", "."],
        [".", "S", "7", "."],
        [".", "L", "J", "."],
        [".", ".", ".", "."]
      ]

      assert traverse_circuit(
               input,
               {1, 1, "S"},
               {2, 1, "L"},
               [{2, 2, "J"}, {1, 2, "7"}, {1, 1, "S"}]
             ) ==
               [{2, 1, "L"}, {2, 2, "J"}, {1, 2, "7"}, {1, 1, "S"}]
    end

    test "full traversal" do
      input = [
        [".", ".", ".", ".", "."],
        [".", "S", "-", "7", "."],
        [".", "|", ".", "|", "."],
        [".", "L", "-", "J", "."],
        [".", ".", ".", ".", "."]
      ]

      assert traverse_circuit(input, {1, 1, "S"}, {1, 1, "S"}, []) == [
               {2, 1, "|"},
               {3, 1, "L"},
               {3, 2, "-"},
               {3, 3, "J"},
               {2, 3, "|"},
               {1, 3, "7"},
               {1, 2, "-"},
               {1, 1, "S"}
             ]
    end
  end

  describe "find_s_coord/1" do
    test "simple example" do
      input = [
        [".", ".", ".", ".", "."],
        [".", "S", "-", "7", "."],
        [".", "|", ".", "|", "."],
        [".", "L", "-", "J", "."],
        [".", ".", ".", ".", "."]
      ]

      assert find_s_coord(input) == {1, 1, "S"}
    end
  end

  describe "possible_next_coords/2" do
    test "when current coord is S, any adjacent coord pointing to current coord is possible" do
      input = [
        [".", "J", "."],
        ["|", "S", "-"],
        [".", "|", "."]
      ]

      assert possible_next_coords(input, {1, 1, "S"}) == [{1, 2, "-"}, {2, 1, "|"}]

      input2 = [
        [".", "7", "."],
        ["F", "S", "|"],
        [".", "F", "."]
      ]

      assert possible_next_coords(input2, {1, 1, "S"}) == [{0, 1, "7"}, {1, 0, "F"}]
    end

    def with_centre_eql(input, str) do
      List.replace_at(input, 1, Enum.at(input, 1) |> List.replace_at(1, str))
    end

    test "when current coord is not S, restrict to adjacent coordinates that current cell points to" do
      input = [
        [".", "F", "."],
        ["|", "X", "-"],
        [".", "|", "."]
      ]

      assert possible_next_coords(with_centre_eql(input, "|"), {1, 1, "|"}) == [
               {0, 1, "F"},
               {2, 1, "|"}
             ]

      assert possible_next_coords(with_centre_eql(input, "-"), {1, 1, "-"}) == [{1, 2, "-"}]

      assert possible_next_coords(with_centre_eql(input, "L"), {1, 1, "L"}) == [
               {0, 1, "F"},
               {1, 2, "-"}
             ]

      assert possible_next_coords(with_centre_eql(input, "J"), {1, 1, "J"}) == [{0, 1, "F"}]
      assert possible_next_coords(with_centre_eql(input, "7"), {1, 1, "7"}) == [{2, 1, "|"}]

      assert possible_next_coords(with_centre_eql(input, "F"), {1, 1, "F"}) == [
               {1, 2, "-"},
               {2, 1, "|"}
             ]
    end
  end
end

ExUnit.run()

Solution - Part 1

PartOne.solve(puzzle_input)
# Parser.parse(puzzle_input) |> Enum.map(fn list -> Enum.count(list) end) |> Enum.sum()

Part Two

Code - Part 2

defmodule PartTwo do
  require Integer, [:is_odd, :is_even]

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

  def run(input_string) do
    graph = Parser.parse(input_string)

    coordinates_of_circuit = PartOne.coordinates_of_circuit(graph)
    Enum.count(points_inside_circuit(graph, coordinates_of_circuit))
  end

  # https://en.wikipedia.org/wiki/Point_in_polygon
  def points_inside_circuit(graph, circuit) do
    circuit = [equivalent_of_s(circuit) | tl(circuit)]

    graph =
      Enum.map(graph, fn row ->
        Enum.map(row, fn cell ->
          if cell == "S" do
            {_y, _x, cell} = equivalent_of_s(circuit)
            cell
          else
            cell
          end
        end)
      end)

    Enum.map(height_range(graph), fn y ->
      # Note: Assumes a whole circuit is fully contained by graph,
      # and that being on the circuit does not count as being contained
      Enum.reduce(width_range(graph), {[], 0, nil}, fn x,
                                                       {coords, crossings, prev_crossing_cell} ->
        reduction_func(graph, circuit, {coords, crossings, prev_crossing_cell}, y, x)
      end)
      |> Tuple.to_list()
      |> hd()
      |> Enum.reverse()
    end)
    |> List.flatten()
  end

  def equivalent_of_s(circuit) do
    {ys, xs, _cell} = hd(circuit)
    {y1, x1, _cell} = Enum.at(circuit, 1)
    {y2, x2, _cell} = List.last(circuit)

    equivalent_cell =
      cond do
        # north and south
        Enum.any?([y1, y2], &amp;(&amp;1 > ys)) &amp;&amp; Enum.any?([y1, y2], &amp;(&amp;1 < ys)) -> "|"
        # east and west
        Enum.any?([x1, x2], &amp;(&amp;1 > xs)) &amp;&amp; Enum.any?([x1, x2], &amp;(&amp;1 < xs)) -> "-"
        # any to north and east
        Enum.any?([x1, x2], &amp;(&amp;1 > xs)) &amp;&amp; Enum.any?([y1, y2], &amp;(&amp;1 < ys)) -> "L"
        # any to north and west
        Enum.any?([x1, x2], &amp;(&amp;1 < xs)) &amp;&amp; Enum.any?([y1, y2], &amp;(&amp;1 < ys)) -> "J"
        # any to south and west
        Enum.any?([x1, x2], &amp;(&amp;1 < xs)) &amp;&amp; Enum.any?([y1, y2], &amp;(&amp;1 > ys)) -> "7"
        # any to south and east
        Enum.any?([x1, x2], &amp;(&amp;1 > xs)) &amp;&amp; Enum.any?([y1, y2], &amp;(&amp;1 > ys)) -> "F"
      end

    {ys, xs, equivalent_cell}
  end

  defp reduction_func(graph, circuit, {coords, crossings, prev_crossing_cell}, y, x) do
    row = Enum.at(graph, y)
    cell = Enum.at(row, x)
    coord = {y, x, cell}

    in_circuit = coord in circuit

    cond do
      # Staying outside track
      Integer.is_even(crossings) &amp;&amp; !in_circuit ->
        {coords, crossings, nil}

      # crossing | means going inside circuit
      Integer.is_even(crossings) &amp;&amp; in_circuit &amp;&amp; cell == "|" ->
        {coords, crossings + 1, nil}

      # started crossing track and continuing along - mark as "crossed" for now, see later if actually going inside
      Integer.is_even(crossings) &amp;&amp; in_circuit &amp;&amp; PartOne.pointing_east?(cell) &amp;&amp; cell != "-" ->
        {coords, crossings + 1, cell}

      # continuing along "-"
      Integer.is_odd(crossings) &amp;&amp; in_circuit &amp;&amp; cell == "-" ->
        {coords, crossings, prev_crossing_cell}

      # a) finishing crossing and the circuit going in same direction again   -> INSIDE now
      Integer.is_odd(crossings) &amp;&amp; in_circuit &amp;&amp; zigzag?(prev_crossing_cell, cell) ->
        {coords, crossings, nil}

      # b) finishing crossing and the circuit going in back in prev direction -> OUTSIDE still
      Integer.is_odd(crossings) &amp;&amp; in_circuit &amp;&amp; turnaround?(prev_crossing_cell, cell) ->
        {coords, crossings + 1, nil}

      # Staying inside track
      Integer.is_odd(crossings) &amp;&amp; !in_circuit ->
        {[coord | coords], crossings, nil}

      # crossing | means going outside circuit again
      Integer.is_odd(crossings) &amp;&amp; in_circuit &amp;&amp; cell == "|" ->
        {coords, crossings + 1, nil}

      # started crossing track and continuing along - mark as "crossed" for now, see later if actually going outside
      Integer.is_odd(crossings) &amp;&amp; in_circuit &amp;&amp; PartOne.pointing_east?(cell) ->
        {coords, crossings + 1, cell}

      # continuing along "-"
      Integer.is_even(crossings) &amp;&amp; in_circuit &amp;&amp; cell == "-" ->
        {coords, crossings, prev_crossing_cell}

      # a) finishing crossing and the circuit going in same direction again   -> OUTSIDE now
      Integer.is_even(crossings) &amp;&amp; in_circuit &amp;&amp; zigzag?(prev_crossing_cell, cell) ->
        {coords, crossings, nil}

      # b) finishing crossing and the circuit going in back in prev direction -> INSIDE still
      Integer.is_even(crossings) &amp;&amp; in_circuit &amp;&amp; turnaround?(prev_crossing_cell, cell) ->
        {coords, crossings + 1, nil}
    end
  end

  def zigzag?({_, _, first_cell}, {_, _, second_cell}), do: zigzag?(first_cell, second_cell)

  def zigzag?(first_cell, second_cell) do
    (PartOne.pointing_north?(first_cell) &amp;&amp; PartOne.pointing_south?(second_cell)) ||
      (PartOne.pointing_south?(first_cell) &amp;&amp; PartOne.pointing_north?(second_cell))
  end

  def turnaround?({_, _, first_cell}, {_, _, second_cell}),
    do: turnaround?(first_cell, second_cell)

  def turnaround?(first_cell, second_cell) do
    (PartOne.pointing_north?(first_cell) &amp;&amp; PartOne.pointing_north?(second_cell)) ||
      (PartOne.pointing_south?(first_cell) &amp;&amp; PartOne.pointing_south?(second_cell))
  end

  defp width_range(graph) do
    0..(width(graph) - 1)
  end

  defp height_range(graph) do
    0..(height(graph) - 1)
  end

  defp width(graph) do
    length(hd(graph))
  end

  defp height(graph) do
    length(graph)
  end
end

Tests - Part 2

ExUnit.start(autorun: false)

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

  @input1 """
  ...........
  .S-------7.
  .|F-----7|.
  .||.....||.
  .||.....||.
  .|L-7.F-J|.
  .|..|.|..|.
  .L--J.L--J.
  ...........
  """

  @input2 """
  .F----7F7F7F7F-7....
  .|F--7||||||||FJ....
  .||.FJ||||||||L7....
  FJL7L7LJLJ||LJ.L-7..
  L--J.L7...LJS7F-7L7.
  ....F-J..F7FJ|L7L7L7
  ....L7.F7||L7|.L7L7|
  .....|FJLJ|FJ|F7|.LJ
  ....FJL-7.||.||||...
  ....L---J.LJ.LJLJ...
  """

  describe "run" do
    test "example 1" do
      actual = run(@input1)
      assert actual == 4
    end

    test "example 2" do
      actual = run(@input2)
      assert actual == 8
    end
  end

  describe "points_inside_circuit/2" do
    @graph1 [
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."],
      [".", "S", "-", "-", "-", "-", "-", "-", "-", "7", "."],
      [".", "|", "F", "-", "-", "-", "-", "-", "7", "|", "."],
      [".", "|", "|", ".", ".", ".", ".", ".", "|", "|", "."],
      [".", "|", "|", ".", ".", ".", ".", ".", "|", "|", "."],
      [".", "|", "L", "-", "7", ".", "F", "-", "J", "|", "."],
      [".", "|", ".", ".", "|", ".", "|", ".", ".", "|", "."],
      [".", "L", "-", "-", "J", ".", "L", "-", "-", "J", "."],
      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."]
    ]

    @circuit1 [
      {1, 1, "S"},
      {1, 2, "-"},
      {1, 3, "-"},
      {1, 4, "-"},
      {1, 5, "-"},
      {1, 6, "-"},
      {1, 7, "-"},
      {1, 8, "-"},
      {1, 9, "7"},
      {2, 9, "|"},
      {3, 9, "|"},
      {4, 9, "|"},
      {5, 9, "|"},
      {6, 9, "|"},
      {7, 9, "J"},
      {7, 8, "-"},
      {7, 7, "-"},
      {7, 6, "L"},
      {6, 6, "|"},
      {5, 6, "F"},
      {5, 7, "-"},
      {5, 8, "J"},
      {4, 8, "|"},
      {3, 8, "|"},
      {2, 8, "7"},
      {2, 7, "-"},
      {2, 6, "-"},
      {2, 5, "-"},
      {2, 4, "-"},
      {2, 3, "-"},
      {2, 2, "F"},
      {3, 2, "|"},
      {4, 2, "|"},
      {5, 2, "L"},
      {5, 3, "-"},
      {5, 4, "7"},
      {6, 4, "|"},
      {7, 4, "J"},
      {7, 3, "-"},
      {7, 2, "-"},
      {7, 1, "L"},
      {6, 1, "|"},
      {5, 1, "|"},
      {4, 1, "|"},
      {3, 1, "|"},
      {2, 1, "|"}
    ]

    test "finds coordinates inside circuit simple example" do
      assert points_inside_circuit(@graph1, @circuit1) == [
               {6, 2, "."},
               {6, 3, "."},
               {6, 7, "."},
               {6, 8, "."}
             ]
    end

    test "finds coordinates inside circuit complex example" do
      graph = Parser.parse(@input2)
      circuit = PartOne.coordinates_of_circuit(graph)

      assert points_inside_circuit(graph, circuit) == [
               {3, 14, "."},
               {4, 7, "."},
               {4, 8, "."},
               {4, 9, "."},
               {5, 7, "."},
               {5, 8, "."},
               {6, 6, "."},
               {6, 14, "."}
             ]
    end
  end

  describe "equivalent_of_s" do
    @graph1 [
      [".", ".", ".", ".", "."],
      [".", "S", "-", "7", "-"],
      [".", "|", "F", "|", "-"],
      [".", "|", "|", "|", "."],
      [".", "L", "-", "J", "."],
      [".", "|", "L", "-", "7"]
    ]

    @circuit1 [
      {1, 1, "S"},
      {1, 2, "-"},
      {1, 3, "7"},
      {2, 3, "|"},
      {3, 3, "|"},
      {4, 3, "J"},
      {4, 2, "-"},
      {4, 1, "L"},
      {3, 1, "|"},
      {2, 1, "|"}
    ]

    test "simple example" do
      assert PartOne.coordinates_of_circuit(@graph1) == @circuit1
      assert equivalent_of_s(@circuit1) == {1, 1, "F"}
    end
  end

  describe "zigzag?/2" do
    test "false examples" do
      assert zigzag?("F", "7") == false
      assert zigzag?("L", "J") == false
    end

    test "true examples" do
      assert zigzag?("F", "J") == true
      assert zigzag?("L", "7") == true
    end

    test "works with coordinate inputs" do
      assert zigzag?({1, 1, "F"}, {1, 9, "7"}) == false
    end
  end

  describe "turnaround?/2" do
    test "true examples" do
      assert turnaround?("F", "7") == true
      assert turnaround?("L", "J") == true
    end

    test "false examples" do
      assert turnaround?("F", "J") == false
      assert turnaround?("L", "7") == false
    end

    test "works with coordinate inputs" do
      assert turnaround?({1, 1, "F"}, {1, 9, "7"}) == true
    end
  end
end

ExUnit.run()

Solution - Part 2

PartTwo.solve(puzzle_input)