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

Advent of Code - Day 14

2023_day14.livemd

Advent of Code - Day 14

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

Introduction

–> Content

Puzzle

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2023", "14", 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 row -> String.split(row, "", trim: true) end)
  end
end

Tests - Parser

ExUnit.start(autorun: false)

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

  @input """
  O....#....
  O.OO#....#
  .....##...
  OO.#O....O
  .O.....O#.
  O.#..O.#.#
  ..O..#O..O
  .......O..
  #....###..
  #OO..#....
  """

  @expected [
    ["O", ".", ".", ".", ".", "#", ".", ".", ".", "."],
    ["O", ".", "O", "O", "#", ".", ".", ".", ".", "#"],
    [".", ".", ".", ".", ".", "#", "#", ".", ".", "."],
    ["O", "O", ".", "#", "O", ".", ".", ".", ".", "O"],
    [".", "O", ".", ".", ".", ".", ".", "O", "#", "."],
    ["O", ".", "#", ".", ".", "O", ".", "#", ".", "#"],
    [".", ".", "O", ".", ".", "#", "O", ".", ".", "O"],
    [".", ".", ".", ".", ".", ".", ".", "O", ".", "."],
    ["#", ".", ".", ".", ".", "#", "#", "#", ".", "."],
    ["#", "O", "O", ".", ".", "#", ".", ".", ".", "."]
  ]

  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(input_string) when is_bitstring(input_string), do: run(Parser.parse(input_string))

  def run(input) do
    total_load(tilt_north(input))
  end

  def tilt_north(input) do
    # IO.inspect(0..(length(input) - 1), label: :row_range)
    # IO.inspect(0..((input |> hd() |> length()) - 1), label: :col_range)
    Enum.reduce(0..(length(input) - 1), input, fn row_i, output ->
      Enum.reduce(0..((output |> hd() |> length()) - 1), output, fn col_i, output ->
        # IO.inspect({row_i, col_i}, label: :coord)
        roll_piece_north(output, row_i, col_i)
      end)
    end)
  end

  def total_load(input) do
    number_of_rows = length(input)

    input
    |> Enum.with_index(fn row, index -> {row, number_of_rows - index} end)
    |> Enum.reduce(0, fn {row, load_factor}, res ->
      Enum.count(row, fn elem -> elem == "O" end) * load_factor + res
    end)
  end

  defp roll_piece_north(input, row_idx, col_idx) do
    if piece_at(input, row_idx, col_idx) != "O" do
      input
    else
      start_point = {row_idx, col_idx}

      end_point = {
        Enum.find(row_idx..0, fn i ->
          # IO.inspect({i, {i - 1, col_idx}, piece_at(input, i - 1, col_idx)}, label: :finding_coord_to_move_to)
          i == 0 || piece_at(input, i - 1, col_idx) != "."
        end),
        col_idx
      }

      # IO.inspect({input, start_point, end_point}, label: :input_to_move_piece)
      move_piece(input, start_point, end_point)
    end
  end

  def move_piece(input, {start_row_idx, start_col_idx}, {end_row_idx, end_col_idx}) do
    res =
      input
      |> List.update_at(start_row_idx, fn row ->
        # IO.inspect({row, start_col_idx}, label: :start_row_to_update)
        List.update_at(row, start_col_idx, fn "O" -> "." end)
      end)
      |> List.update_at(end_row_idx, fn row ->
        # IO.inspect({row, end_col_idx}, label: :end_row_to_update)
        List.update_at(row, end_col_idx, fn "." -> "O" end)
      end)

    # IO.inspect(res, label: :move_piece_Res)
  end

  def piece_at(input, row_idx, col_idx) do
    input |> Enum.at(row_idx) |> Enum.at(col_idx)
  end
end

Tests - Part 1

ExUnit.start(autorun: false)

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

  @raw_input """
  O....#....
  O.OO#....#
  .....##...
  OO.#O....O
  .O.....O#.
  O.#..O.#.#
  ..O..#O..O
  .......O..
  #....###..
  #OO..#....
  """

  @input Parser.parse(@raw_input)

  @expected_tilt [
    ["O", "O", "O", "O", ".", "#", ".", "O", ".", "."],
    ["O", "O", ".", ".", "#", ".", ".", ".", ".", "#"],
    ["O", "O", ".", ".", "O", "#", "#", ".", ".", "O"],
    ["O", ".", ".", "#", ".", "O", "O", ".", ".", "."],
    [".", ".", ".", ".", ".", ".", ".", ".", "#", "."],
    [".", ".", "#", ".", ".", ".", ".", "#", ".", "#"],
    [".", ".", "O", ".", ".", "#", ".", "O", ".", "O"],
    [".", ".", "O", ".", ".", ".", ".", ".", ".", "."],
    ["#", ".", ".", ".", ".", "#", "#", "#", ".", "."],
    ["#", ".", ".", ".", ".", "#", ".", ".", ".", "."]
  ]

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

  describe "tilt_north/1" do
    test "main example" do
      assert tilt_north(@input) == @expected_tilt
    end
  end

  describe "total_load/1" do
    test "main example" do
      assert total_load(@expected_tilt) == 136
    end
  end
end

ExUnit.run()

Solution - Part 1

PartOne.solve(puzzle_input)

Part Two

Code - Part 2

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

  def run(graph), do: run(graph, 1_000_000_000)

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

  def run(graph, iterations) do
    run(graph, iterations, %{}, nil, false)
    |> PartOne.total_load()
  end

  def run(graph, 1, _, _, _), do: graph
  # def run(graph, iterations_remaining, tally, nil, doubles_found) do

  def run(graph, iterations_remaining, tally, cycle_length, doubles_found) do
    # IO.inspect({tally |> Map.values, cycle_length, iterations_remaining, PartOne.total_load(graph)}, label: :tally_beginning)

    cond do
      !cycle_length && 2 in Map.values(tally) && 3 not in Map.values(tally) ->
        run(graph, iterations_remaining, tally, 1, true)

      cycle_length && 3 in Map.values(tally) ->
        run(
          graph,
          iterations_remaining - div(iterations_remaining, cycle_length) * cycle_length + 1,
          %{},
          nil,
          true
        )

      true ->
        res = spin_cycle(graph)

        new_tally = Map.update(tally, res, 1, fn count -> count + 1 end)

        cycle_length =
          cond do
            cycle_length && Enum.max(Map.values(new_tally)) == 3 -> cycle_length
            cycle_length -> cycle_length + 1
            true -> cycle_length
          end

        # IO.inspect({new_tally |> Map.values, cycle_length, iterations_remaining, PartOne.total_load(res)}, label: :new_tally)

        run(res, iterations_remaining - 1, new_tally, cycle_length, doubles_found)
    end
  end

  def spin_cycle(graph) do
    graph
    |> tilt_north()
    |> tilt_west()
    |> tilt_south()
    |> tilt_east()
  end

  def tilt_north(graph) do
    Enum.reduce(0..(number_of_rows(graph) - 1), graph, fn row_i, graph ->
      Enum.reduce(0..(number_of_cols(graph) - 1), graph, fn col_i, graph ->
        roll_piece_north(graph, row_i, col_i)
      end)
    end)
  end

  defp roll_piece_north(graph, row_idx, col_idx) do
    if piece_at(graph, row_idx, col_idx) != "O" do
      graph
    else
      start_point = {row_idx, col_idx}

      end_point = {
        Enum.find(row_idx..0, fn i ->
          i == 0 || piece_at(graph, i - 1, col_idx) != "."
        end),
        col_idx
      }

      move_piece(graph, start_point, end_point)
    end
  end

  defp tilt_west(graph) do
    Enum.reduce(0..(number_of_rows(graph) - 1), graph, fn row_i, graph ->
      Enum.reduce(0..(number_of_cols(graph) - 1), graph, fn col_i, graph ->
        roll_piece_west(graph, row_i, col_i)
      end)
    end)
  end

  defp roll_piece_west(graph, row_idx, col_idx) do
    if piece_at(graph, row_idx, col_idx) != "O" do
      graph
    else
      start_point = {row_idx, col_idx}

      end_point = {
        row_idx,
        Enum.find(col_idx..0, fn i ->
          i == 0 || piece_at(graph, row_idx, i - 1) != "."
        end)
      }

      move_piece(graph, start_point, end_point)
    end
  end

  defp tilt_south(graph) do
    Enum.reduce((number_of_rows(graph) - 1)..0, graph, fn row_i, graph ->
      Enum.reduce(0..(number_of_cols(graph) - 1), graph, fn col_i, graph ->
        roll_piece_south(graph, row_i, col_i)
      end)
    end)
  end

  defp roll_piece_south(graph, row_idx, col_idx) do
    if piece_at(graph, row_idx, col_idx) != "O" do
      graph
    else
      start_point = {row_idx, col_idx}

      end_point = {
        Enum.find(row_idx..(number_of_rows(graph) - 1), fn i ->
          i == number_of_rows(graph) - 1 || piece_at(graph, i + 1, col_idx) != "."
        end),
        col_idx
      }

      move_piece(graph, start_point, end_point)
    end
  end

  defp tilt_east(graph) do
    Enum.reduce(0..(number_of_rows(graph) - 1), graph, fn row_i, graph ->
      Enum.reduce((number_of_cols(graph) - 1)..0, graph, fn col_i, graph ->
        roll_piece_east(graph, row_i, col_i)
      end)
    end)
  end

  defp roll_piece_east(graph, row_idx, col_idx) do
    if piece_at(graph, row_idx, col_idx) != "O" do
      graph
    else
      start_point = {row_idx, col_idx}

      end_point = {
        row_idx,
        Enum.find(col_idx..(number_of_cols(graph) - 1), fn i ->
          i == number_of_cols(graph) || piece_at(graph, row_idx, i + 1) != "."
        end)
      }

      move_piece(graph, start_point, end_point)
    end
  end

  defp move_piece(graph, {start_row_idx, start_col_idx}, {end_row_idx, end_col_idx}) do
    graph
    |> List.update_at(start_row_idx, fn row ->
      List.update_at(row, start_col_idx, fn "O" -> "." end)
    end)
    |> List.update_at(end_row_idx, fn row ->
      List.update_at(row, end_col_idx, fn "." -> "O" end)
    end)
  end

  defp piece_at(graph, row_idx, col_idx) do
    graph |> Enum.at(row_idx) |> Enum.at(col_idx)
  end

  defp number_of_cols(graph) do
    if value = Process.get(:number_of_cols) do
      value
    else
      value = graph |> hd() |> length()
      Process.put(:number_of_cols, value)
      value
    end
  end

  defp number_of_rows(graph) do
    if value = Process.get(:number_of_rows) do
      value
    else
      value = graph |> length()
      Process.put(:number_of_rows, value)
      value
    end
  end
end

Tests - Part 2

ExUnit.start(autorun: false)

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

  @raw_input """
  O....#....
  O.OO#....#
  .....##...
  OO.#O....O
  .O.....O#.
  O.#..O.#.#
  ..O..#O..O
  .......O..
  #....###..
  #OO..#....
  """

  # @input Parser.parse(@raw_input)

  # @after_1_cycle [
  #   [".", ".", ".", ".", ".", "#", ".", ".", ".", "."],
  #   [".", ".", ".", ".", "#", ".", ".", ".", "O", "#"],
  #   [".", ".", ".", "O", "O", "#", "#", ".", ".", "."],
  #   [".", "O", "O", "#", ".", ".", ".", ".", ".", "."],
  #   [".", ".", ".", ".", ".", "O", "O", "O", "#", "."],
  #   [".", "O", "#", ".", ".", ".", "O", "#", ".", "#"],
  #   [".", ".", ".", ".", "O", "#", ".", ".", ".", "."],
  #   [".", ".", ".", ".", ".", ".", "O", "O", "O", "O"],
  #   ["#", ".", ".", ".", "O", "#", "#", "#", ".", "."],
  #   ["#", ".", ".", "O", "O", "#", ".", ".", ".", "."]
  # ]

  # @after_2_cycles [
  #   [".", ".", ".", ".", ".", "#", ".", ".", ".", "."],
  #   [".", ".", ".", ".", "#", ".", ".", ".", "O", "#"],
  #   [".", ".", ".", ".", ".", "#", "#", ".", ".", "."],
  #   [".", ".", "O", "#", ".", ".", ".", ".", ".", "."],
  #   [".", ".", ".", ".", ".", "O", "O", "O", "#", "."],
  #   [".", "O", "#", ".", ".", ".", "O", "#", ".", "#"],
  #   [".", ".", ".", ".", "O", "#", ".", ".", ".", "O"],
  #   [".", ".", ".", ".", ".", ".", ".", "O", "O", "O"],
  #   ["#", ".", ".", "O", "O", "#", "#", "#", ".", "."],
  #   ["#", ".", "O", "O", "O", "#", ".", ".", ".", "O"]
  # ]

  # @after_3_cycles [
  #   [".", ".", ".", ".", ".", "#", ".", ".", ".", "."],
  #   [".", ".", ".", ".", "#", ".", ".", ".", "O", "#"],
  #   [".", ".", ".", ".", ".", "#", "#", ".", ".", "."],
  #   [".", ".", "O", "#", ".", ".", ".", ".", ".", "."],
  #   [".", ".", ".", ".", ".", "O", "O", "O", "#", "."],
  #   [".", "O", "#", ".", ".", ".", "O", "#", ".", "#"],
  #   [".", ".", ".", ".", "O", "#", ".", ".", ".", "O"],
  #   [".", ".", ".", ".", ".", ".", ".", "O", "O", "O"],
  #   ["#", ".", ".", ".", "O", "#", "#", "#", ".", "O"],
  #   ["#", ".", "O", "O", "O", "#", ".", ".", ".", "O"]
  # ]

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

  # describe "spin_cycle/1" do
  #   test "main example" do
  #     assert spin_cycle(@input) == @after_1_cycle
  #     assert spin_cycle(@after_1_cycle) == @after_2_cycles
  #     assert spin_cycle(@after_2_cycles) == @after_3_cycles  
  #     # res = spin_cycle(@after_3_cycles);
  #     # # Enum.map(res, fn row -> IO.puts(Enum.join(row)) end); IO.puts("")
  #     # res = spin_cycle(res);
  #     # # Enum.map(res, fn row -> IO.puts(Enum.join(row)) end); IO.puts("")
  #     # res = spin_cycle(res);
  #     # # Enum.map(res, fn row -> IO.puts(Enum.join(row)) end); IO.puts("")
  #     # res = spin_cycle(res);
  #     # # Enum.map(res, fn row -> IO.puts(Enum.join(row)) end); IO.puts("")
  #     # res = spin_cycle(res);
  #     # # Enum.map(res, fn row -> IO.puts(Enum.join(row)) end); IO.puts("")

  #   end
  # end
end

ExUnit.run()

Solution - Part 2

PartTwo.solve(puzzle_input)