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

Advent of Code 2023

2023/day14.livemd

Advent of Code 2023

Mix.install([
  {:req, "~> 0.3.2"}
])

Day 14

input =
  "https://adventofcode.com/2023/day/14/input"
  |> Req.get!(headers: [cookie: "session=#{System.get_env("AOC_COOKIE")}"])
  |> Map.get(:body)
sample = """
O....#....
O.OO#....#
.....##...
OO.#O....O
.O.....O#.
O.#..O.#.#
..O..#O..O
.......O..
#....###..
#OO..#....
"""
defmodule A do
  def parse(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.map(&String.split(&1, "", trim: true))
  end

  def transpose(list_of_lists) do
    list_of_lists
    |> List.zip()
    |> Enum.map(&Tuple.to_list/1)
  end

  def rotate(list_of_lists) do
    list_of_lists
    |> Enum.reverse()
    |> transpose()
  end

  def roll(tiles) do
    tiles
    |> Enum.map(fn tiles ->
      tiles
      |> Enum.reduce({0, 0, []}, fn tile, {rocks, spaces, collected} ->
        case tile do
          "O" ->
            {rocks + 1, spaces, collected}

          "." ->
            {rocks, spaces + 1, collected}

          "#" ->
            collected =
              collected ++ List.duplicate("O", rocks) ++ List.duplicate(".", spaces) ++ ["#"]

            {0, 0, collected}
        end
      end)
      |> then(fn
        {0, 0, collected} ->
          collected

        {rocks, spaces, collected} ->
          collected ++ List.duplicate("O", rocks) ++ List.duplicate(".", spaces)
      end)
    end)
  end

  def measure_load(tiles) do
    tiles
    |> Enum.reverse()
    |> Enum.with_index(1)
    |> Enum.map(fn {row, i} ->
      row
      |> Enum.filter(&(&1 == "O"))
      |> Enum.count()
      |> Kernel.*(i)
    end)
    |> Enum.sum()
  end

  def part1(input) do
    input
    |> parse()
    |> transpose()
    |> roll()
    |> transpose()
    |> measure_load()
  end

  def part2(input) do
    input
    |> parse()
    |> then(fn tiles ->
      0..1_000_000_000
      |> Enum.take(1000)
      |> Enum.reduce({tiles, Map.new(), Map.new(), nil}, fn i, {tiles, seen, loads, start} ->
        hash = tiles |> Enum.map(&Enum.join(&1)) |> Enum.join()

        loads = Map.put(loads, i, measure_load(tiles))

        "#{i} (#{measure_load(tiles)}) = #{hash}"
        |> IO.inspect()

        start =
          if Map.get(seen, hash) == 2 && start == nil do
            IO.inspect(i, label: "cycle start")
            i
          else
            start
          end

        if Map.get(seen, hash) == 3 do
          IO.inspect(start, label: "cycle start")
          IO.inspect(i, label: "cycle end")

          period =
            (i - start)
            |> IO.inspect(label: "period")

          k =
            (rem(1_000_000_000 - start, period) + start)
            |> IO.inspect(label: "key")

          Map.get(loads, k)
          |> IO.inspect(label: "winner")

          raise "on the 3rd round"
        end

        {cycle(tiles), Map.update(seen, hash, 1, &(&1 + 1)), loads, start}
      end)
    end)
  end

  def cycle(tiles) do
    tiles
    |> roll_north()
    |> roll_west()
    |> roll_south()
    |> roll_east()
  end

  def roll_north(tiles) do
    tiles
    |> transpose()
    |> roll()
    |> transpose()
  end

  def roll_west(tiles) do
    tiles
    |> rotate()
    |> transpose()
    |> roll()
    |> transpose()
    |> rotate()
    |> rotate()
    |> rotate()
  end

  def roll_south(tiles) do
    tiles
    |> rotate()
    |> rotate()
    |> transpose()
    |> roll()
    |> transpose()
    |> rotate()
    |> rotate()
  end

  def roll_east(tiles) do
    tiles
    |> rotate()
    |> rotate()
    |> rotate()
    |> transpose()
    |> roll()
    |> transpose()
    |> rotate()
  end
end

Part 1

input
|> A.part1()

Part 2

input
|> A.part2()