Skip to content

Instantly share code, notes, and snippets.

@ejlangev
Last active December 28, 2021 21:18
Show Gist options
  • Save ejlangev/8f1a3a5387b8ed3ce8703aebd9e39630 to your computer and use it in GitHub Desktop.
Save ejlangev/8f1a3a5387b8ed3ce8703aebd9e39630 to your computer and use it in GitHub Desktop.
Advent Of Code 2021

Day 1

Part 1

defmodule AdventOfCode do
  def solve(input, prev \\ -1, acc \\ 0)
  def solve([], _, acc), do: acc
  def solve([hd | tl], -1, 0), do: solve(tl, hd, 0)
  def solve([hd | tl], prev, acc) do
    solve(tl, hd, acc + (if prev < hd, do: 1, else: 0))
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> Enum.map(&String.to_integer/1)
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  def solve(input, prev \\ -1, acc \\ 0)
  def solve([], _, acc), do: acc
  def solve([hd | tl], -1, 0), do: solve(tl, hd, 0)
  def solve([hd | tl], prev, acc) do
    solve(tl, hd, acc + (if prev < hd, do: 1, else: 0))
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> Enum.map(&String.to_integer/1)
  |> Enum.chunk_every(3, 1, :discard)
  |> Enum.map(&Enum.sum/1)
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 2

Part 1

defmodule AdventOfCode do
  def solve(input) do
    Enum.reduce(input, {0, 0}, fn val, {x, y} ->
      case val do
        "forward " <> num -> {x + String.to_integer(num), y}
        "up " <> num -> {x, y - String.to_integer(num)}
        "down " <> num -> {x, y + String.to_integer(num)}
      end
    end)
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> AdventOfCode.solve
  |> then(fn {x, y} -> IO.inspect(x * y, label: "Answer") end)
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  def solve(input) do
    Enum.reduce(input, {0, 0, 0}, fn val, {x, y, aim} ->
      case val do
        "forward " <> num -> {x + String.to_integer(num), y + (aim * String.to_integer(num)), aim}
        "up " <> num -> {x, y, aim - String.to_integer(num)}
        "down " <> num -> {x, y, aim + String.to_integer(num)}
      end
    end)
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> AdventOfCode.solve
  |> then(fn {x, y, _} -> IO.inspect(x * y, label: "Answer") end)
else
  _ -> IO.inspect("Failed to read input file")
end

Day 3

Part 1

use Bitwise

defmodule AdventOfCode do
  def solve(input) do
    total_length = length(input)
    number_of_places = String.length(Enum.at(input, 0))

    Enum.map(input, &(String.to_integer(&1, 2)))
    |> Enum.reduce(List.duplicate(0, number_of_places), fn val, results ->
      for i <- Range.new(0, number_of_places - 1) do
        Enum.at(results, i) + (if (val &&& (1 <<< i)) > 0, do: 1, else: 0)
      end
    end)
    |> Enum.with_index
    |> Enum.reduce(0, fn {elm, ind}, acc ->
      if elm > (total_length / 2), do: acc + (1 <<< ind), else: acc
    end)
    |> then(&({&1, bxor(trunc(:math.pow(2, number_of_places) - 1), &1)}))
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> AdventOfCode.solve
  |> then(fn {gamma, epsilon} -> IO.inspect(gamma * epsilon, label: "Answer") end)
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

use Bitwise

defmodule AdventOfCode do
  def solve(input, bit, comparator) do
    number_of_ones = Enum.reduce(input, 0, fn val, acc -> if (val &&& (1 <<< bit)) > 0, do: acc + 1, else: acc end)
    filter_ones? = comparator.(number_of_ones, length(input) / 2)
    Enum.filter(input, fn val -> if filter_ones?, do: (val &&& (1 <<< bit)) > 0, else: (val &&& (1 <<< bit)) == 0 end)
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> then(fn content -> {String.length(Enum.at(content, 0)), Enum.map(content, &(String.to_integer(&1, 2)))} end)
  |> then(fn {bit_length, basis} ->
    Enum.reduce(Range.new(bit_length - 1, 0), {basis, basis}, fn bit, {lesser, greater} ->
      {
        (if length(lesser) == 1, do: lesser, else: AdventOfCode.solve(lesser, bit, &Kernel.</2)),
        (if length(greater) == 1, do: greater, else: AdventOfCode.solve(greater, bit, &Kernel.>=/2))
      }
    end)
  end)
  |> Tuple.to_list
  |> List.flatten
  |> Enum.product
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 4

Part 1

defmodule AdventOfCode do
  def solve(numbers, boards) do
    max_position = length(numbers)
    number_map = Enum.with_index(numbers) |> Enum.into(%{})

    Enum.with_index(boards)
    |> Enum.map(fn { board, index } ->
      List.zip(board)
      |> Enum.map(&Tuple.to_list/1)
      |> then(&(board ++ &1))
      |> Enum.map(fn possibility ->
        Enum.map(possibility, &(Map.get(number_map, &1, max_position))) |> Enum.max
      end)
      |> Enum.min
      |> then(&({index, &1}))
    end)
    |> Enum.max_by(fn {_, res} -> res end)
    |> then(fn {index, position} ->
      used_number_map = Enum.take(numbers, position + 1) |> MapSet.new

      Enum.at(boards, index)
      |> List.flatten
      |> Enum.reject(&(MapSet.member?(used_number_map, &1)))
      |> Enum.sum
      |> then(&(&1 * Enum.at(numbers, position)))
    end)
  end

end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n\n", trim: true)
  |> then(fn [numbers | boards] ->
    {
      String.split(numbers, ",", trim: true) |> Enum.map(&String.to_integer/1),
      Enum.map(boards, fn board ->
        String.split(board, "\n", trim: true)
        |> Enum.map(fn r -> String.split(r, " ", trim: true) |> Enum.map(&String.to_integer/1) end)
      end)
    }
  end)
  |> then(fn {numbers, boards} -> AdventOfCode.solve(numbers, boards) end)
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  def solve(numbers, boards) do
    max_position = length(numbers)
    number_map = Enum.with_index(numbers) |> Enum.into(%{})

    Enum.with_index(boards)
    |> Enum.map(fn { board, index } ->
      List.zip(board)
      |> Enum.map(&Tuple.to_list/1)
      |> then(&(board ++ &1))
      |> Enum.map(fn possibility ->
        Enum.map(possibility, &(Map.get(number_map, &1, max_position))) |> Enum.max
      end)
      |> Enum.min
      |> then(&({index, &1}))
    end)
    |> Enum.max_by(fn {_, res} -> res end)
    |> then(fn {index, position} ->
      used_number_map = Enum.take(numbers, position + 1) |> MapSet.new

      Enum.at(boards, index)
      |> List.flatten
      |> Enum.reject(&(MapSet.member?(used_number_map, &1)))
      |> Enum.sum
      |> then(&(&1 * Enum.at(numbers, position)))
    end)
  end

end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n\n", trim: true)
  |> then(fn [numbers | boards] ->
    {
      String.split(numbers, ",", trim: true) |> Enum.map(&String.to_integer/1),
      Enum.map(boards, fn board ->
        String.split(board, "\n", trim: true)
        |> Enum.map(fn r -> String.split(r, " ", trim: true) |> Enum.map(&String.to_integer/1) end)
      end)
    }
  end)
  |> then(fn {numbers, boards} -> AdventOfCode.solve(numbers, boards) end)
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 5

Part 1

defmodule Point do
  defstruct [:x, :y]
end

defmodule AdventOfCode do
  def solve(segments) do
    Enum.filter(segments, fn [start, finish] -> start.x == finish.x || start.y == finish.y end)
    |> Enum.reduce(%{}, fn [start, finish], acc ->
      distance = Enum.max([abs(start.x - finish.x), abs(start.y - finish.y)])
      x_direction = if start.x > finish.x, do: -1, else: 1
      y_direction = if start.y > finish.y, do: -1, else: 1

      for i <- Range.new(0, distance), into: acc do
        x = if start.x == finish.x, do: start.x, else: start.x + (i * x_direction)
        y = if start.y == finish.y, do: start.y, else: start.y + (i * y_direction)
        {{x,y}, Map.get(acc, {x,y}, 0) + 1}
      end
    end)
    |> Enum.filter(fn {_, v} -> v >= 2 end)
    |> length
  end
end

defmodule Main do
  def main() do
    with {:ok, contents} <- File.read('advent-of-code-input.txt') do
      String.split(contents, "\n", trim: true)
      |> Enum.map(fn line ->
        String.split(line, " -> ", trim: true)
        |> Enum.map(fn point ->
          String.split(point, ",", trim: true)
          |> Enum.map(&String.to_integer/1)
          |> then(fn [x, y] -> %Point{x: x, y: y} end)
        end)
      end)
      |> AdventOfCode.solve
      |> IO.inspect(label: "Answer")
    else
      _ -> IO.inspect("Failed to read input file")
    end
  end
end

Main.main()

Part 2

defmodule Point do
  defstruct [:x, :y]
end

defmodule AdventOfCode do
  def solve(segments) do
    Enum.reduce(segments, %{}, fn [start, finish], acc ->
      distance = Enum.max([abs(start.x - finish.x), abs(start.y - finish.y)])
      x_direction = if start.x > finish.x, do: -1, else: 1
      y_direction = if start.y > finish.y, do: -1, else: 1

      for i <- Range.new(0, distance), into: acc do
        x = if start.x == finish.x, do: start.x, else: start.x + (i * x_direction)
        y = if start.y == finish.y, do: start.y, else: start.y + (i * y_direction)
        {{x,y}, Map.get(acc, {x,y}, 0) + 1}
      end
    end)
    |> Enum.filter(fn {_, v} -> v >= 2 end)
    |> length
  end
end

defmodule Main do
  def main() do
    with {:ok, contents} <- File.read('advent-of-code-input.txt') do
      String.split(contents, "\n", trim: true)
      |> Enum.map(fn line ->
        String.split(line, " -> ", trim: true)
        |> Enum.map(fn point ->
          String.split(point, ",", trim: true)
          |> Enum.map(&String.to_integer/1)
          |> then(fn [x, y] -> %Point{x: x, y: y} end)
        end)
      end)
      |> AdventOfCode.solve
      |> IO.inspect(label: "Answer")
    else
      _ -> IO.inspect("Failed to read input file")
    end
  end
end

Main.main()

Day 6

Part 1

defmodule AdventOfCode do
  def solve(fish, days) do
    Enum.reduce(Range.new(0, days - 1), fish, fn _, acc ->
      newborn_fish = Map.get(acc, 0, 0)
      Enum.reduce(1..8, acc, &(Map.put(&2, &1 - 1, Map.get(&2, &1, 0))))
      |> Map.put(8, newborn_fish)
      |> Map.update(6, newborn_fish, &(&1 + newborn_fish))
    end)
    |> Map.values
    |> Enum.sum
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split(",", trim: true)
  |> Enum.reduce(%{}, fn s, acc ->
    n = String.to_integer(s)
    Map.update(acc, n, 1, &(&1 + 1))
  end)
  |> AdventOfCode.solve(80)
  |> IO.inspect
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  def solve(fish, days) do
    Enum.reduce(Range.new(0, days - 1), fish, fn _, acc ->
      newborn_fish = Map.get(acc, 0, 0)
      Enum.reduce(1..8, acc, &(Map.put(&2, &1 - 1, Map.get(&2, &1, 0))))
      |> Map.put(8, newborn_fish)
      |> Map.update(6, newborn_fish, &(&1 + newborn_fish))
    end)
    |> Map.values
    |> Enum.sum
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split(",", trim: true)
  |> Enum.reduce(%{}, fn s, acc ->
    n = String.to_integer(s)
    Map.update(acc, n, 1, &(&1 + 1))
  end)
  |> AdventOfCode.solve(256)
  |> IO.inspect
else
  _ -> IO.inspect("Failed to read input file")
end

Day 7

Part 1

defmodule AdventOfCode do
  def solve(numbers) do
    Enum.map(Range.new(Enum.min(numbers), Enum.max(numbers)), fn i ->
      Enum.map(numbers, fn n -> abs(i - n) end) |> Enum.sum
    end)
    |> Enum.min
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split(",", trim: true)
  |> Enum.map(&String.to_integer/1)
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  def solve(numbers) do
    Enum.map(Range.new(Enum.min(numbers), Enum.max(numbers)), fn i ->
      Enum.map(numbers, fn n -> trunc((abs(i - n) * (abs(i - n) + 1)) / 2) end) |> Enum.sum
    end)
    |> Enum.min
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split(",", trim: true)
  |> Enum.map(&String.to_integer/1)
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 8

Part 1

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n", trim: true)
  |> Enum.map(fn line ->
    String.split(line, "|", trim: true)
    |> Enum.map(fn part ->
      String.split(part, " ", trim: true)
      |> Enum.map(&String.to_charlist/1)
    end)
  end)
  |> Enum.map(fn [_, outputs] ->
    Enum.count(outputs, &(length(&1) in [2,3,4,7]))
  end)
  |> Enum.sum
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  @correct_digits %{
    0 => 'abcefg',
    1 => 'cf',
    2 => 'acdeg',
    3 => 'acdfg',
    4 => 'bcdf',
    5 => 'abdfg',
    6 => 'abdefg',
    7 => 'acf',
    8 => 'abcdefg',
    9 => 'abcdfg'
  }

  @length_map %{
    2 => 1,
    3 => 7,
    4 => 4,
    7 => 8
  }

  def solve(inputs, outputs) do
    Enum.filter(inputs, &(length(&1) in Map.keys(@length_map)))
      |> Enum.reduce(%{}, fn parts, acc ->
        true_parts = MapSet.new(@correct_digits[@length_map[length(parts)]])

        Enum.reduce(parts, acc, fn part, map ->
          Map.update(map, part, true_parts, &(MapSet.intersection(&1, true_parts)))
        end)
      end)
      |> Enum.reduce([%{}], fn {letter, options}, acc ->
        Enum.flat_map(acc, fn val ->
          Enum.reject(options, &(&1 in Map.values(val)))
          |> Enum.map(&(Map.put(val, letter, &1)))
        end)
      end)
      |> build_output(outputs)
  end

  defp build_output(options, outputs) do
    inverted_digits = Enum.map(@correct_digits, fn {k, v} -> {v, k} end) |> Enum.into(%{})
    Enum.find_value(options, fn option ->
      Enum.map(outputs, fn output ->
        Enum.map(output, &(Map.get(option, &1))) |> Enum.sort
      end)
      |> Enum.map(&(Map.get(inverted_digits, Enum.sort(&1))))
      |> then(fn val -> if Enum.all?(val), do: val, else: nil end)
    end)
    |> Integer.undigits
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n", trim: true)
  |> Enum.map(fn line ->
    String.split(line, "|", trim: true)
    |> Enum.map(fn part ->
      String.split(part, " ", trim: true)
      |> Enum.map(&String.to_charlist/1)
    end)
  end)
  |> Enum.map(fn [inputs, outputs] -> AdventOfCode.solve(inputs, outputs) end)
  |> Enum.sum
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 9

Part 1

defmodule AdventOfCode do
  def solve(input) do
    Enum.zip(input)
      |> Enum.map(&Tuple.to_list/1)
      |> find_row_lowpoints
      |> MapSet.to_list
      |> Enum.reduce(MapSet.new, fn {i, j}, acc -> MapSet.put(acc, {j, i}) end)
      |> then(&(MapSet.intersection(&1, find_row_lowpoints(input))))
      |> Enum.map(fn {i, j} -> Enum.at(Enum.at(input, i), j) + 1 end)
      |> Enum.sum
  end

  defp find_row_lowpoints(input) do
    Enum.map(input, &Enum.with_index/1)
    |> Enum.with_index
    |> Enum.reduce(MapSet.new, fn {row, i}, outer_acc ->
      Enum.reduce(row, outer_acc, fn {val, j}, acc ->
        left = if j == 0, do: 10, else: elem(Enum.at(row, j - 1), 0)
        right = if j == (length(row) - 1), do: 10, else: elem(Enum.at(row, j + 1), 0)
        if val < left && val < right, do: MapSet.put(acc, {i, j}), else: acc
      end)
    end)
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n", trim: true)
  |> Enum.map(&(String.split(&1, "", trim: true) |> Enum.map(fn i -> String.to_integer(i) end)))
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  def solve(input) do
    Enum.with_index(input)
    |> Enum.reduce(MapSet.new, fn {row, i}, acc ->
      Enum.reduce(Enum.with_index(row), acc, fn {val, j}, i_acc ->
        if val == 9, do: i_acc, else: MapSet.put(i_acc, {i, j})
      end)
    end)
    |> Stream.unfold(fn
      %MapSet{map: map} when map_size(map) == 0 -> nil
      points ->
        MapSet.to_list(points)
        |> then(&({Enum.take(&1, 1), MapSet.new}))
        |> Stream.unfold(fn
          {[], _} -> nil
          {[{i, j} = hd | tl], used} ->
            if MapSet.member?(used, hd) do
              {nil, {tl, used}}
            else
              next_points = for n_i <- Range.new(i - 1, i + 1),
                n_j <- Range.new(j - 1, j + 1),
                n_i >= 0 and n_j >= 0,
                n_i < length(input) and n_j < length(List.first(input)),
                n_i != i or n_j != j,
                n_i == i or n_j == j,
                MapSet.member?(points, {n_i, n_j}),
                !MapSet.member?(used, {n_i, n_j}) do
                  {n_i, n_j}
              end
              {hd, {next_points ++ tl, MapSet.put(used, hd)}}
            end
        end)
        |> Enum.reject(&is_nil/1)
        |> then(&({&1, MapSet.difference(points, MapSet.new(&1))}))
    end)
    |> Enum.map(&length/1)
    |> Enum.sort(:desc)
    |> Enum.take(3)
    |> Enum.product

  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n", trim: true)
  |> Enum.map(&(String.split(&1, "", trim: true) |> Enum.map(fn i -> String.to_integer(i) end)))
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 10

Part 1

defmodule AdventOfCode do
  def solve(input) do
    Enum.map(input, fn line ->
      Stream.unfold({line, []}, fn
        nil -> nil
        {[], _} -> nil
        {[hd | tl], stack} when hd in ["[", "(", "<", "{"] -> {nil, {tl, [hd | stack]}}
        {[hd | tl], [s_hd | s_tl]} -> case {s_hd, hd} do
          {"(", ")"} -> {nil, {tl, s_tl}}
          {"{", "}"} -> {nil, {tl, s_tl}}
          {"[", "]"} -> {nil, {tl, s_tl}}
          {"<", ">"} -> {nil, {tl, s_tl}}
          _ -> {hd, nil}
        end
        {[hd | _], _} -> {hd, nil}
      end)
      |> Enum.reject(&is_nil/1)
    end)
    |> List.flatten
    |> Enum.frequencies
    |> Enum.reduce(0, fn {key, count}, acc ->
      acc + count * case key do
        ")" -> 3
        "]" -> 57
        "}" -> 1197
        ">" -> 25137
      end
    end)
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n", trim: true)
  |> Enum.map(&String.split(&1, "", trim: true))
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  def solve(input) do
    Enum.map(input, fn line ->
      Stream.unfold({line, []}, fn
        nil -> nil
        {[], stack} -> {stack, nil}
        {[hd | tl], stack} when hd in ["[", "(", "<", "{"] -> {nil, {tl, [hd | stack]}}
        {[hd | tl], [s_hd | s_tl]} -> case {s_hd, hd} do
          {"(", ")"} -> {nil, {tl, s_tl}}
          {"{", "}"} -> {nil, {tl, s_tl}}
          {"[", "]"} -> {nil, {tl, s_tl}}
          {"<", ">"} -> {nil, {tl, s_tl}}
          _ -> {[], nil}
        end
        _ -> {[], nil}
      end)
      |> Enum.reject(&is_nil/1)
    end)
    |> Enum.map(&List.first/1)
    |> Enum.map(fn line ->
      Enum.reduce(line, 0, fn c, acc ->
        acc * 5 + case c do
          "(" -> 1
          "[" -> 2
          "{" -> 3
          "<" -> 4
        end
      end)
    end)
    |> Enum.reject(&(&1 == 0))
    |> Enum.sort
    |> then(&(Enum.at(&1, div(length(&1), 2))))
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n", trim: true)
  |> Enum.map(&String.split(&1, "", trim: true))
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 11

Part 1

defmodule AdventOfCode do
  def solve(input, steps, counter \\ 0)
  def solve(_, 0, counter), do: counter
  def solve(input, steps, counter) do
    Enum.with_index(input)
    |> Enum.reverse
    |> Enum.reduce({[], []}, fn {line, i}, {acc, flashes} ->
      {new_line, new_flashes} = Enum.reduce(Enum.reverse(Enum.with_index(line)), {[], flashes}, fn {n, j}, {i_acc, i_flashes} ->
        {[n + 1 | i_acc], (if n == 9, do: [{i, j} | i_flashes], else: i_flashes)}
      end)

      {[new_line | acc], new_flashes}
    end)
    |> Stream.unfold(fn
      nil -> nil
      {input, []} -> {input, nil}
      {input, [{i, j} | tl]} ->
        {new_input, new_flashes} = for n_i <- Range.new(i - 1, i + 1),
          n_j <- Range.new(j - 1, j + 1),
          n_i != i or n_j != j,
          n_i >= 0 and n_i < length(input),
          n_j >= 0 and n_j < length(Enum.at(input, 0)),
          reduce: {input, []} do
            {acc, new_flashes} ->
              n = Enum.at(Enum.at(acc, n_i), n_j)
              sublist = Enum.at(acc, n_i)
              {
                List.replace_at(acc, n_i, List.replace_at(sublist, n_j, n + 1)),
                (if n == 9, do: [{n_i, n_j} | new_flashes], else: new_flashes)
              }
          end
        {new_input, {new_input, tl ++ new_flashes}}
    end)
    |> Enum.to_list
    |> then(fn res ->
      new_input = Enum.map(List.last(res), fn line -> Enum.map(line, &(if &1 > 9, do: 0, else: &1)) end)
      solve(new_input, steps - 1, counter + length(res) - 1)
    end)
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n", trim: true)
  |> Enum.map(fn l -> String.split(l, "", trim: true) |> Enum.map(&String.to_integer/1) end)
  |> AdventOfCode.solve(100)
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  def solve(input, step \\ 1) do
    Enum.with_index(input)
    |> Enum.reverse
    |> Enum.reduce({[], []}, fn {line, i}, {acc, flashes} ->
      {new_line, new_flashes} = Enum.reduce(Enum.reverse(Enum.with_index(line)), {[], flashes}, fn {n, j}, {i_acc, i_flashes} ->
        {[n + 1 | i_acc], (if n == 9, do: [{i, j} | i_flashes], else: i_flashes)}
      end)

      {[new_line | acc], new_flashes}
    end)
    |> Stream.unfold(fn
      nil -> nil
      {input, []} -> {input, nil}
      {input, [{i, j} | tl]} ->
        {new_input, new_flashes} = for n_i <- Range.new(i - 1, i + 1),
          n_j <- Range.new(j - 1, j + 1),
          n_i != i or n_j != j,
          n_i >= 0 and n_i < length(input),
          n_j >= 0 and n_j < length(Enum.at(input, 0)),
          reduce: {input, []} do
            {acc, new_flashes} ->
              n = Enum.at(Enum.at(acc, n_i), n_j)
              sublist = Enum.at(acc, n_i)
              {
                List.replace_at(acc, n_i, List.replace_at(sublist, n_j, n + 1)),
                (if n == 9, do: [{n_i, n_j} | new_flashes], else: new_flashes)
              }
          end
        {new_input, {new_input, tl ++ new_flashes}}
    end)
    |> Enum.to_list
    |> then(fn res ->
      new_input = Enum.map(List.last(res), fn line -> Enum.map(line, &(if &1 > 9, do: 0, else: &1)) end)
      if (length(res) - 1) == Enum.sum(Enum.map(new_input, &length/1)) do
        step
      else
        solve(new_input, step + 1)
      end
    end)
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n", trim: true)
  |> Enum.map(fn l -> String.split(l, "", trim: true) |> Enum.map(&String.to_integer/1) end)
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 12

Part 1

defmodule AdventOfCode do
  defmodule Node do
    defstruct [:name, neighbors: []]

    def add_neighbor(%Node{neighbors: n} = base_node, name) do
      %{base_node | neighbors: [name | n]}
    end
  end

  def solve(input) do
    List.flatten(input)
    |> Enum.uniq
    |> Enum.map(&{&1, %Node{name: &1}})
    |> Enum.into(%{})
    |> then(fn nodes ->
      Enum.reduce(input, nodes, fn [start, stop], acc ->
        {start_node, stop_node} = {acc[start], acc[stop]}
        Map.merge(acc, %{
          start => Node.add_neighbor(start_node, stop),
          stop => Node.add_neighbor(stop_node, start)
        })
      end)
    end)
    |> then(&find_paths(&1, &1["start"], &1["end"]))
    |> length
  end

  defp find_paths(graph, start_node, end_node, path \\ [], visited \\ MapSet.new) do
    next_path = [start_node.name | path]
    next_nodes = MapSet.difference(MapSet.new(start_node.neighbors), visited)
    visited_set = if start_node.name == String.upcase(start_node.name) do
      visited
    else
      MapSet.put(visited, start_node.name)
    end

    if start_node.name == end_node.name do
      [next_path]
    else
      Enum.reduce(next_nodes, [], fn next_node, acc ->
        acc ++ find_paths(graph, graph[next_node], end_node, next_path, visited_set)
      end)
      |> Enum.reject(&length(&1) == 0)
    end
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n", trim: true)
  |> Enum.map(&String.split(&1, "-", trim: true))
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  defmodule Node do
    defstruct [:name, neighbors: []]

    def add_neighbor(%Node{neighbors: n} = base_node, name) do
      %{base_node | neighbors: [name | n]}
    end
  end

  def solve(input) do
    List.flatten(input)
    |> Enum.uniq
    |> Enum.map(&{&1, %Node{name: &1}})
    |> Enum.into(%{})
    |> then(fn nodes ->
      Enum.reduce(input, nodes, fn [start, stop], acc ->
        {start_node, stop_node} = {acc[start], acc[stop]}
        Map.merge(acc, %{
          start => Node.add_neighbor(start_node, stop),
          stop => Node.add_neighbor(stop_node, start)
        })
      end)
    end)
    |> then(&find_paths(&1, &1["start"], &1["end"]))
    |> length
  end

  defp find_paths(graph, start_node, end_node, path \\ [], visited \\ MapSet.new) do
    next_path = [start_node.name | path]
    contains_lowercase_duplicate = Enum.frequencies(next_path)
    |> Enum.any?(fn {k, v} ->
      String.upcase(k) != k and v > 1
    end)
    next_nodes = if contains_lowercase_duplicate do
      MapSet.difference(MapSet.new(start_node.neighbors), visited)
    else
      start_node.neighbors -- ["start"]
    end
    visited_set = if start_node.name == String.upcase(start_node.name) do
      visited
    else
      MapSet.put(visited, start_node.name)
    end

    if start_node.name == end_node.name do
      [next_path]
    else
      Enum.reduce(next_nodes, [], fn next_node, acc ->
        acc ++ find_paths(graph, graph[next_node], end_node, next_path, visited_set)
      end)
      |> Enum.reject(&length(&1) == 0)
    end
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n", trim: true)
  |> Enum.map(&String.split(&1, "-", trim: true))
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 13

Part 1

defmodule AdventOfCode do
  def solve(coords, folds) do
    Enum.reduce(folds, MapSet.new(coords), fn {let, val}, current_coords ->
      Enum.reduce(MapSet.to_list(current_coords), MapSet.new, fn {x, y}, new_coords ->
        cond do
          let == "x" and x > val -> MapSet.put(new_coords, {val - (x - val), y})
          let == "y" and y > val -> MapSet.put(new_coords, {x, val - (y - val)})
          true -> MapSet.put(new_coords, {x, y})
        end
      end)
    end)
    |> MapSet.size
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n\n", trim: true)
  |> Enum.map(&String.split(&1, "\n", trim: true))
  |> then(fn [coords, folds] ->
    {
      Enum.map(coords, fn l -> String.split(l, ",", trim: true) |> Enum.map(&String.to_integer/1) |> List.to_tuple end),
      Enum.map(folds, fn l ->
        String.replace(l, "fold along ", "")
        |> String.split("=", trim: true)
        |> then(fn [let, val] -> {let, String.to_integer(val)} end)
      end)
    }
  end)
  |> then(&AdventOfCode.solve(elem(&1, 0), Enum.take(elem(&1, 1), 1)))
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  def solve(coords, folds) do
    Enum.reduce(folds, MapSet.new(coords), fn {let, val}, current_coords ->
      Enum.reduce(MapSet.to_list(current_coords), MapSet.new, fn {x, y}, new_coords ->
        cond do
          let == "x" and x > val -> MapSet.put(new_coords, {val - (x - val), y})
          let == "y" and y > val -> MapSet.put(new_coords, {x, val - (y - val)})
          true -> MapSet.put(new_coords, {x, y})
        end
      end)
    end)
    |> then(&print_board(MapSet.to_list(&1)))
  end

  defp print_board(coords) do
    max_x = Enum.map(coords, &elem(&1, 0) + 1) |> Enum.max
    max_y = Enum.map(coords, &elem(&1, 1) + 1) |> Enum.max
    board = List.duplicate(List.duplicate(".", max_x), max_y)
    Enum.reduce(coords, board, fn {x, y}, acc ->
      List.replace_at(acc, y, List.replace_at(Enum.at(acc, y), x, "#"))
    end)
    |> Enum.map(&Enum.join(&1, ""))
    |> IO.inspect
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n\n", trim: true)
  |> Enum.map(&String.split(&1, "\n", trim: true))
  |> then(fn [coords, folds] ->
    {
      Enum.map(coords, fn l -> String.split(l, ",", trim: true) |> Enum.map(&String.to_integer/1) |> List.to_tuple end),
      Enum.map(folds, fn l ->
        String.replace(l, "fold along ", "")
        |> String.split("=", trim: true)
        |> then(fn [let, val] -> {let, String.to_integer(val)} end)
      end)
    }
  end)
  |> then(&AdventOfCode.solve(elem(&1, 0), elem(&1, 1)))
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 14

Part 1 & 2

defmodule AdventOfCode do
  def solve(basis, rules, steps) do
    Enum.chunk_every(basis, 2, 1, :discard)
    |> Enum.reduce({%{}, %{}}, fn chunk, {memo, res} ->
      {result_freqs, new_memo} = expand(chunk, rules, steps, memo)

      Map.merge(res, result_freqs, fn _, v1, v2 -> v1 + v2 end)
      |> Map.update(List.last(chunk), 0, &(&1 - 1))
      |> then(&{new_memo, &1})
    end)
    |> then(fn {_, res} ->
      Map.update(res, List.last(basis), 0, &(&1 + 1))
    end)
    |> Map.values
    |> Enum.min_max
    |> then(&elem(&1, 1) - elem(&1, 0))
  end

  def expand([first, last], _, 0, memo) when first == last, do: {%{first => 2}, memo}
  def expand([first, last], _, 0, memo), do: {%{first => 1, last => 1}, memo}
  def expand([first, last] = basis, rules, steps, memo) do
    basis_key = "#{first}#{last}"
    inner_letter = rules[basis_key]
    cond do
      Map.has_key?(memo, "#{basis_key}-#{steps}") -> {Map.get(memo, "#{basis_key}-#{steps}"), memo}
      Map.has_key?(rules, basis_key) ->
        {left_res, left_memo} = expand([first, inner_letter], rules, steps - 1, memo)
        {right_res, new_memo} = expand([inner_letter, last], rules, steps - 1, Map.put(left_memo, "#{first}#{inner_letter}-#{steps - 1}", left_res))

        Map.merge(%{inner_letter => -1}, left_res, fn _, v1, v2 -> v1 + v2 end)
        |> Map.merge(right_res, fn _, v1, v2 -> v1 + v2 end)
        |> then(&{&1, Map.put(new_memo, "#{inner_letter}#{last}-#{steps - 1}", right_res)})
      first == last -> {%{first => 2}, memo}
      true -> {%{first => 1, last => 1}, memo}
    end
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n\n", trim: true)
  |> Enum.map(&String.split(&1, "\n", trim: true))
  |> then(fn [[basis], rules] ->
    {
      String.graphemes(basis),
      Enum.map(rules, fn l ->
        String.split(l, " -> ", trim: true)
        |> then(&List.to_tuple/1)
      end)
        |> Enum.into(%{})
    }
  end)
  |> then(&AdventOfCode.solve(elem(&1, 0), elem(&1, 1), 40))
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 15

Part 1

defmodule AdventOfCode do
  @effective_infinity 999999999999999

  def solve(board) do
    max_x = length(Enum.at(board, 0)) - 1
    max_y = length(board) - 1

    distance_map = for i <- Range.new(0, max_x),
      j <- Range.new(0, max_y), into: %{} do
        {{i, j}, @effective_infinity}
      end
      |> Map.put({0,0}, 0)


    find_shortest_path(board, [{0, 0}], MapSet.new, distance_map)
    |> Map.get({max_x, max_y})
  end

  def find_shortest_path(board, [{c_x, c_y} | tail], used, distance_map) do
    possible_next_points = for i <- Range.new(c_x - 1, c_x + 1),
      j <- Range.new(c_y - 1, c_y + 1),
      i >= 0 and j >= 0,
      i < length(Enum.at(board, 0)) and j < length(board),
      i == c_x or j == c_y,
      i != c_x or j != c_y,
      !MapSet.member?(used, {i, j}) do
        {i, j}
      end

    Enum.reduce(possible_next_points, distance_map, fn {x, y}, acc ->
      existing_cost = Map.get(acc, {x, y})
      current_path_cost = Map.get(acc, {c_x, c_y})
      edge_cost = Enum.at(Enum.at(board, x), y)

      if existing_cost > (current_path_cost + edge_cost) do
        Map.put(acc, {x, y}, current_path_cost + edge_cost)
      else
        acc
      end
    end)
    |> then(fn new_distance_map ->
      Enum.uniq(possible_next_points ++ tail)
      |> Enum.sort_by(&Map.get(new_distance_map, &1))
      |> then(&{&1, new_distance_map})
    end)
    |> then(fn
      {[], ndm} -> ndm
      {points, ndm} -> find_shortest_path(board, points, MapSet.put(used, List.first(points)), ndm)
    end)
  end

end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n", trim: true)
  |> Enum.map(fn l -> String.graphemes(l) |> Enum.map(&String.to_integer/1) end)
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  @effective_infinity 999999999999999

  def solve(board) do
    max_x = length(Enum.at(board, 0)) - 1
    max_y = length(board) - 1

    distance_map = for i <- Range.new(0, max_x),
      j <- Range.new(0, max_y), into: %{} do
        {{i, j}, @effective_infinity}
      end
      |> Map.put({0,0}, 0)


    find_shortest_path(board, [{0, 0}], MapSet.new, distance_map)
    |> Map.get({max_x, max_y})
  end

  def find_shortest_path(board, [{c_x, c_y} | tail], used, distance_map) do
    possible_next_points = for i <- Range.new(c_x - 1, c_x + 1),
      j <- Range.new(c_y - 1, c_y + 1),
      i >= 0 and j >= 0,
      i < length(Enum.at(board, 0)) and j < length(board),
      i == c_x or j == c_y,
      i != c_x or j != c_y,
      !MapSet.member?(used, {i, j}) do
        {i, j}
      end

    Enum.reduce(possible_next_points, distance_map, fn {x, y}, acc ->
      existing_cost = Map.get(acc, {x, y})
      current_path_cost = Map.get(acc, {c_x, c_y})
      edge_cost = Enum.at(Enum.at(board, x), y)

      if existing_cost > (current_path_cost + edge_cost) do
        Map.put(acc, {x, y}, current_path_cost + edge_cost)
      else
        acc
      end
    end)
    |> then(fn new_distance_map ->
      Enum.uniq(possible_next_points ++ tail)
      |> Enum.sort_by(&Map.get(new_distance_map, &1))
      |> then(&{&1, new_distance_map})
    end)
    |> then(fn
      {[], ndm} -> ndm
      {points, ndm} -> find_shortest_path(board, points, MapSet.put(used, List.first(points)), ndm)
    end)
  end

end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n", trim: true)
  |> Enum.map(fn l -> String.graphemes(l) |> Enum.map(&String.to_integer/1) end)
  |> then(fn board ->
    Enum.reduce(0..4, [], fn y, outer_acc ->
      outer_acc ++ Enum.reduce(0..4, List.duplicate([], length(board)), fn x, inner_acc ->
        Enum.map(Enum.with_index(board), fn {row, i} ->
          Enum.at(inner_acc, i) ++ Enum.map(row, &rem(&1 + x + y, 10) + div(&1 + x + y, 10))
        end)
      end)
    end)
  end)
  |> AdventOfCode.solve
  |> IO.inspect
else
  _ -> IO.inspect("Failed to read input file")
end

Day 16

Part 1

defmodule AdventOfCode do
  def solve(<<version::3, id::3, remainder::bitstring>>) do
    if id == 4 do
      Stream.unfold(remainder, fn
        nil -> nil
        <<0::size(1), _::size(4), _::bitstring>> -> {5, nil}
        <<1::size(1), _::size(4), rest::bitstring>> -> {5, rest}
      end)
      |> Enum.sum
      |> then(&{version, &1 + 6})
    else
      case remainder do
        <<0::size(1), remaining_length::size(15), rest::bitstring>> ->
          Stream.unfold({remaining_length, rest}, fn
            {0, _} -> nil
            {n, bits} ->
              {version, bits_used} = solve(bits)
              <<_::size(bits_used), remaining_bits::bitstring>> = bits
              {{version, bits_used}, {n - bits_used, remaining_bits}}
          end)
          |> Enum.reduce({0, 16}, fn {v1, v2}, {a1, a2} -> {v1 + a1, v2 + a2} end)
        <<1::size(1), subpackets::size(11), other_bits::bitstring>> ->
          Enum.reduce(Range.new(subpackets, 1, -1), {other_bits, 12, 0}, fn _, {bits, bits_used, version_sum} ->
            {version, used} = solve(bits)
            <<_::size(used), next_bits::bitstring>> = bits
            {next_bits, bits_used + used, version_sum + version}
          end)
          |> then(fn {_, bits_used, version} -> {version, bits_used} end)
      end
      |> then(fn {v, b} -> {v + version, 6 + b} end)
    end
  end

end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n", trim: true)
  |> then(fn [num] ->
    len = 4 * String.length(num)
    <<String.to_integer(num, 16)::size(len)>>
  end)
  |> AdventOfCode.solve
  |> then(&elem(&1, 0))
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

use Bitwise

defmodule AdventOfCode do
  def solve(<<_::3, id::3, remainder::bitstring>>) do
    if id == 4 do
      Stream.unfold(remainder, fn
        nil -> nil
        <<0::size(1), val::size(4), _::bitstring>> -> {val, nil}
        <<1::size(1), val::size(4), rest::bitstring>> -> {val, rest}
      end)
      |> Enum.with_index
      |> then(fn values ->
        Enum.reduce(values, 0, fn {val, i}, acc ->
          acc + (val <<< (4 * (length(values) - i - 1)))
        end)
        |> then(&{&1, 6 + (5 * length(values))})
      end)
    else
      {inputs, bits_used} = case remainder do
        <<0::size(1), remaining_length::size(15), rest::bitstring>> ->
          Stream.unfold({remaining_length, rest}, fn
            {0, _} -> nil
            {n, bits} ->
              {answer, bits_used} = solve(bits)
              <<_::size(bits_used), remaining_bits::bitstring>> = bits
              {answer, {n - bits_used, remaining_bits}}
          end)
          |> Enum.to_list
          |> then(&{&1, 16 + remaining_length})
        <<1::size(1), subpackets::size(11), other_bits::bitstring>> ->
          Enum.reduce(Range.new(subpackets, 1, -1), {other_bits, 12, []}, fn _, {bits, bits_used, answer_acc} ->
            {answer, used} = solve(bits)
            <<_::size(used), next_bits::bitstring>> = bits
            {next_bits, bits_used + used, [answer | answer_acc]}
          end)
          |> then(fn {_, bits_used, answer_acc} -> {Enum.reverse(answer_acc), bits_used} end)
      end

      case id do
        0 -> Enum.sum(inputs)
        1 -> Enum.product(inputs)
        2 -> Enum.min(inputs)
        3 -> Enum.max(inputs)
        5 -> if Enum.at(inputs, 0) > Enum.at(inputs, 1), do: 1, else: 0
        6 -> if Enum.at(inputs, 0) < Enum.at(inputs, 1), do: 1, else: 0
        7 -> if Enum.at(inputs, 0) == Enum.at(inputs, 1), do: 1, else: 0
      end
      |> then(fn res -> {res, 6 + bits_used} end)
    end
  end

end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.split("\n", trim: true)
  |> then(fn [num] ->
    len = 4 * String.length(num)
    <<String.to_integer(num, 16)::size(len)>>
  end)
  |> AdventOfCode.solve
  |> then(&elem(&1, 0))
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 17

Part 1

defmodule AdventOfCode do
  @max_step 500

  def solve(x_range, y_range) do
    x_max = Enum.max(x_range)
    y_min = Enum.min(y_range)
    possible_x_values = Enum.reduce(x_max..1, [], fn n, acc ->
      positions = Enum.scan(n..1, &(&1 + &2))
      min_index = Enum.find_index(positions, &(&1 in x_range))
      max_index = Enum.find_index(positions, &(&1 > x_max)) || @max_step
      if min_index, do: [{n, min_index + 1, max_index} | acc], else: acc
    end)
    |> Enum.to_list

    {_, min_steps, max_steps} = Enum.min_by(possible_x_values, &elem(&1, 0))

    Stream.unfold(y_min, fn
      nil -> nil
      n ->
        positions = Enum.map(min_steps..max_steps, &Enum.sum(Range.new(n, n - &1)))
        y_index = Enum.find_index(positions, &(&1 in y_range))
        cond do
          !is_nil(y_index) -> {n, n + 1}
          Enum.min(positions) > Enum.max(y_range) -> {nil, nil}
          true -> {nil, n+1}
        end
    end)
    |> Enum.reject(&is_nil/1)
    |> Enum.max
    |> then(&div(&1 * (&1+1), 2))
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.trim("target area: ")
  |> String.split(", ", trim: true)
  |> Enum.map(fn part ->
    String.slice(part, 2, String.length(part) - 2)
    |> String.split("..")
    |> Enum.map(&String.to_integer/1)
    |> then(fn [l, u] -> Range.new(l, u) end)
  end)
  |> then(fn [x, y] -> AdventOfCode.solve(x, y) end)
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  @max_step 1000

  def solve(x_range, y_range) do
    x_max = Enum.max(x_range)
    y_min = Enum.min(y_range)
    possible_x_values = Enum.reduce(x_max..1, [], fn n, acc ->
      positions = Enum.scan(n..1, &(&1 + &2))
      min_index = Enum.find_index(positions, &(&1 in x_range))
      max_index = Enum.find_index(positions, &(&1 > x_max)) || @max_step
      if min_index, do: [{n, min_index + 1, max_index} | acc], else: acc
    end)
    |> Enum.to_list

    Enum.flat_map(possible_x_values, fn {x_vel, min_steps, max_steps} ->
      Enum.reduce_while(y_min..@max_step, [], fn y_vel, acc ->
        positions = Enum.map(min_steps..max_steps, &Enum.sum(Range.new(y_vel, y_vel - (&1-1))))

        cond do
          Enum.any?(positions, &(&1 in y_range)) -> {:cont, [{x_vel, y_vel} | acc]}
          Enum.min(positions) > Enum.max(y_range) -> {:halt, acc}
          true -> {:cont, acc}
        end
      end)
    end)
    |> length
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.trim(contents)
  |> String.trim("target area: ")
  |> String.split(", ", trim: true)
  |> Enum.map(fn part ->
    String.slice(part, 2, String.length(part) - 2)
    |> String.split("..")
    |> Enum.map(&String.to_integer/1)
    |> then(fn [l, u] -> Range.new(l, u) end)
  end)
  |> then(fn [x, y] -> AdventOfCode.solve(x, y) end)
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 18

Part 1

use Bitwise

defmodule AdventOfCode do
  def solve(input) do
    Enum.reduce(input, fn elm, acc ->
      Stream.unfold({:explode, [acc, elm]}, fn
        nil -> nil
        {:explode, current} -> case explode(current) do
          {0, _, _, res} -> {nil, {:split, res}}
          {_, _, _, res} -> {nil, {:explode, res}}
        end
        {:split, current} -> case split(current) do
          {true, res} -> {nil, {:explode, res}}
          {_, res} -> {res, nil}
        end
      end)
      |> Enum.at(-1)
    end)
    |> magnitude
  end

  defp explode(input, depth \\ 0)
  defp explode(a, _) when is_number(a), do: {0, nil, nil, a}
  defp explode([a, b], 4) when is_number(a) and is_number(b), do: {7, a, b, 0}
  defp explode([a, b], _) when is_number(a) and is_number(b), do: {0, a, b, [a, b]}
  defp explode([a, b], depth) do
    with {:a, {0, _, _, a}} <- {:a, explode(a, depth + 1)},
         {:b, {0, _, _, b}} <- {:b, explode(b, depth + 1)} do
      {0, nil, nil, [a, b]}
    else
      {:a, {n, v1, v2, a}} when (n &&& 4) > 0 -> {n - 4, v1, v2, [a, add_to_outermost(b, :left, v2)]}
      {:a, {n, v1, v2, a}} -> {n, v1, v2, [a, b]}
      {:b, {n, v1, v2, b}} when (n &&& 2) > 0 -> {n - 2, v1, v2, [add_to_outermost(a, :right, v1), b]}
      {:b, {n, v1, v2, b}} -> {n, v1, v2, [a, b]}
    end
  end

  defp split(n) when is_number(n) and n >= 10, do: {true, [div(n, 2), n - div(n, 2)]}
  defp split(n) when is_number(n), do: {false, n}
  defp split([a, b]) do
    with {:a, {false, a}} <- {:a, split(a)},
         {:b, {false, b}} <- {:b, split(b)} do
      {false, [a, b]}
    else
      {:a, {_, a}} -> {true, [a, b]}
      {:b, {_, b}} -> {true, [a, b]}
    end
  end

  defp magnitude(n) when is_number(n), do: n
  defp magnitude([a, b]), do: 3 * magnitude(a) + 2 * magnitude(b)

  defp add_to_outermost(a, _, val) when is_number(a), do: a + val
  defp add_to_outermost([a, b], :left, val), do: [add_to_outermost(a, :left, val), b]
  defp add_to_outermost([a, b], :right, val), do: [a, add_to_outermost(b, :right, val)]
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> Enum.map(&Code.eval_string(&1) |> elem(0))
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

use Bitwise

defmodule AdventOfCode do
  def solve(input) do
    Enum.flat_map(Enum.with_index(input), fn {outer_elm, outer_i} ->
      for {inner_elm, inner_i} <- Enum.with_index(input), outer_i != inner_i do
        [outer_elm, inner_elm]
      end
    end)
    |> Enum.map(fn entry ->
      Stream.unfold({:explode, entry}, fn
        nil -> nil
        {:explode, current} -> case explode(current) do
          {0, _, _, res} -> {nil, {:split, res}}
          {_, _, _, res} -> {nil, {:explode, res}}
        end
        {:split, current} -> case split(current) do
          {true, res} -> {nil, {:explode, res}}
          {_, res} -> {res, nil}
        end
      end)
      |> Enum.at(-1)
      |> magnitude
    end)
    |> Enum.max
  end

  defp explode(input, depth \\ 0)
  defp explode(a, _) when is_number(a), do: {0, nil, nil, a}
  defp explode([a, b], 4) when is_number(a) and is_number(b), do: {7, a, b, 0}
  defp explode([a, b], _) when is_number(a) and is_number(b), do: {0, a, b, [a, b]}
  defp explode([a, b], depth) do
    with {:a, {0, _, _, a}} <- {:a, explode(a, depth + 1)},
         {:b, {0, _, _, b}} <- {:b, explode(b, depth + 1)} do
      {0, nil, nil, [a, b]}
    else
      {:a, {n, v1, v2, a}} when (n &&& 4) > 0 -> {n - 4, v1, v2, [a, add_to_outermost(b, :left, v2)]}
      {:a, {n, v1, v2, a}} -> {n, v1, v2, [a, b]}
      {:b, {n, v1, v2, b}} when (n &&& 2) > 0 -> {n - 2, v1, v2, [add_to_outermost(a, :right, v1), b]}
      {:b, {n, v1, v2, b}} -> {n, v1, v2, [a, b]}
    end
  end

  defp split(n) when is_number(n) and n >= 10, do: {true, [div(n, 2), n - div(n, 2)]}
  defp split(n) when is_number(n), do: {false, n}
  defp split([a, b]) do
    with {:a, {false, a}} <- {:a, split(a)},
         {:b, {false, b}} <- {:b, split(b)} do
      {false, [a, b]}
    else
      {:a, {_, a}} -> {true, [a, b]}
      {:b, {_, b}} -> {true, [a, b]}
    end
  end

  defp magnitude(n) when is_number(n), do: n
  defp magnitude([a, b]), do: 3 * magnitude(a) + 2 * magnitude(b)

  defp add_to_outermost(a, _, val) when is_number(a), do: a + val
  defp add_to_outermost([a, b], :left, val), do: [add_to_outermost(a, :left, val), b]
  defp add_to_outermost([a, b], :right, val), do: [a, add_to_outermost(b, :right, val)]
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> Enum.map(&Code.eval_string(&1) |> elem(0))
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 19

Part 1

defmodule AdventOfCode do
  def solve(input) do
    [basis | others] = generate_candidates(input)

    Stream.unfold({MapSet.new([basis.name]), %{basis.name => %{position: {0,0,0}, beacons: MapSet.new(basis.beacons)}}}, fn
      nil -> nil
      {%{map: m}, tbm} when map_size(m) == length(input) -> {tbm, nil}
      {used_scanners, transformed_beacon_map} ->
        [unknown, known] = Enum.reject(others, &MapSet.member?(used_scanners, &1.name))
          |> Enum.find_value(fn scanner ->
            known_candidate = Enum.find(scanner.candidates, &MapSet.member?(used_scanners, &1.name))
            if known_candidate, do: [scanner, known_candidate], else: nil
          end)

        known_beacons = Map.fetch!(transformed_beacon_map, known.name).beacons

        Enum.map(unknown.beacons, &generate_rotations/1)
        |> Enum.zip
        |> Enum.map(&Tuple.to_list/1)
        |> Enum.find_value(fn points ->
          foreach_pair(MapSet.to_list(known_beacons), points, &Enum.find_value/2, fn {k_x, k_y, k_z}, {x, y, z} ->
            {t_x, t_y, t_z} = {x - k_x, y - k_y, z - k_z}
            transformed_points = MapSet.new(Enum.map(points, fn {p_x, p_y, p_z} -> {p_x - t_x, p_y - t_y, p_z - t_z} end))

            MapSet.intersection(known_beacons, transformed_points)
            |> MapSet.size
            |> then(&if &1 >= 12, do: {{t_x, t_y, t_z}, transformed_points}, else: nil)
          end)
        end)
        |> then(fn {{t_x, t_y, t_z}, points} ->
          {nil, {MapSet.put(used_scanners, unknown.name), Map.put(transformed_beacon_map, unknown.name, %{position: {-t_x, -t_y, -t_z}, beacons: points})}}
        end)
    end)
    |> Enum.at(-1)
    |> Enum.reduce(MapSet.new, fn {_, v}, acc -> MapSet.union(acc, v.beacons) end)
    |> MapSet.size
  end

  defp generate_candidates(input) do
    scanners = Enum.map(input, fn scanner ->
      foreach_pair(scanner.beacons, scanner.beacons, &Enum.map/2, &distance/2)
      |> List.flatten
      |> Enum.reject(&(&1 == 0))
      |> MapSet.new
      |> then(&Map.put(scanner, :distances, &1))
    end)

    Enum.map(scanners, fn s1 ->
      Enum.filter(scanners, fn s2 -> s1.name != s2.name and MapSet.size(MapSet.intersection(s1.distances, s2.distances)) >= 66 end)
      |> then(&Map.put(s1, :candidates, &1))
    end)
  end

  defp distance({x1, y1, z1}, {x2, y2, z2}), do: abs(x1 - x2) + abs(y1 - y2) + abs(z1 - z2)

  defp foreach_pair(set1, set2, enum, func) do
    enum.(set1, fn s1 -> enum.(set2, &func.(s1, &1)) end)
  end

  defp generate_rotations(p) do
    roll = fn {a, b, c} -> {a, c, -b} end
    turn = fn {a, b, c} -> {-b, a, c} end

    Enum.scan(0..23, p, fn i, acc ->
      acc
      |> then(&(if i == 12, do: roll.(turn.(roll.(&1))), else: &1))
      |> then(&(if rem(i, 4) == 0, do: roll.(&1), else: turn.(&1)))
    end)
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n\n", trim: true)
  |> Enum.map(fn scanner ->
    [name | coords] = String.split(scanner, "\n", trim: true)
    Enum.map(coords, fn l -> String.split(l, ",", trim: true) |> Enum.map(&String.to_integer/1) |> List.to_tuple end)
    |> then(&%{name: name, beacons: &1})
  end)
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  def solve(input) do
    [basis | others] = generate_candidates(input)

    Stream.unfold({MapSet.new([basis.name]), %{basis.name => %{position: {0,0,0}, beacons: MapSet.new(basis.beacons)}}}, fn
      nil -> nil
      {%{map: m}, tbm} when map_size(m) == length(input) -> {tbm, nil}
      {used_scanners, transformed_beacon_map} ->
        [unknown, known] = Enum.reject(others, &MapSet.member?(used_scanners, &1.name))
          |> Enum.find_value(fn scanner ->
            known_candidate = Enum.find(scanner.candidates, &MapSet.member?(used_scanners, &1.name))
            if known_candidate, do: [scanner, known_candidate], else: nil
          end)

        known_beacons = Map.fetch!(transformed_beacon_map, known.name).beacons

        Enum.map(unknown.beacons, &generate_rotations/1)
        |> Enum.zip
        |> Enum.map(&Tuple.to_list/1)
        |> Enum.find_value(fn points ->
          foreach_pair(MapSet.to_list(known_beacons), points, &Enum.find_value/2, fn {k_x, k_y, k_z}, {x, y, z} ->
            {t_x, t_y, t_z} = {x - k_x, y - k_y, z - k_z}
            transformed_points = MapSet.new(Enum.map(points, fn {p_x, p_y, p_z} -> {p_x - t_x, p_y - t_y, p_z - t_z} end))

            MapSet.intersection(known_beacons, transformed_points)
            |> MapSet.size
            |> then(&if &1 >= 12, do: {{t_x, t_y, t_z}, transformed_points}, else: nil)
          end)
        end)
        |> then(fn {{t_x, t_y, t_z}, points} ->
          {nil, {MapSet.put(used_scanners, unknown.name), Map.put(transformed_beacon_map, unknown.name, %{position: {-t_x, -t_y, -t_z}, beacons: points})}}
        end)
    end)
    |> Enum.at(-1)
    |> Map.values
    |> Enum.map(&(&1.position))
    |> then(fn vals -> foreach_pair(vals, vals, &Enum.map/2, &distance/2) end)
    |> List.flatten
    |> Enum.max
  end

  defp generate_candidates(input) do
    scanners = Enum.map(input, fn scanner ->
      foreach_pair(scanner.beacons, scanner.beacons, &Enum.map/2, &distance/2)
      |> List.flatten
      |> Enum.reject(&(&1 == 0))
      |> MapSet.new
      |> then(&Map.put(scanner, :distances, &1))
    end)

    Enum.map(scanners, fn s1 ->
      Enum.filter(scanners, fn s2 -> s1.name != s2.name and MapSet.size(MapSet.intersection(s1.distances, s2.distances)) >= 66 end)
      |> then(&Map.put(s1, :candidates, &1))
    end)
  end

  defp distance({x1, y1, z1}, {x2, y2, z2}), do: abs(x1 - x2) + abs(y1 - y2) + abs(z1 - z2)

  defp foreach_pair(set1, set2, enum, func) do
    enum.(set1, fn s1 -> enum.(set2, &func.(s1, &1)) end)
  end

  defp generate_rotations(p) do
    roll = fn {a, b, c} -> {a, c, -b} end
    turn = fn {a, b, c} -> {-b, a, c} end

    Enum.scan(0..23, p, fn i, acc ->
      acc
      |> then(&(if i == 12, do: roll.(turn.(roll.(&1))), else: &1))
      |> then(&(if rem(i, 4) == 0, do: roll.(&1), else: turn.(&1)))
    end)
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n\n", trim: true)
  |> Enum.map(fn scanner ->
    [name | coords] = String.split(scanner, "\n", trim: true)
    Enum.map(coords, fn l -> String.split(l, ",", trim: true) |> Enum.map(&String.to_integer/1) |> List.to_tuple end)
    |> then(&%{name: name, beacons: &1})
  end)
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 20

Part 1

defmodule AdventOfCode do
  def solve(input, runs, border \\ ".")
  def solve({_, image}, 0, _), do: List.flatten(image) |> Enum.count(&(&1 == "#"))
  def solve({algo_map, image}, runs, border) do
    width = length(Enum.at(image, 0))
    top_border_row = List.duplicate(border, width + 4)

    Enum.map(image, &([border, border] ++ &1 ++ [border, border]))
    |> then(&(List.duplicate(top_border_row, 2) ++ &1 ++ List.duplicate(top_border_row, 2)))
    |> Enum.map(&Enum.chunk_every(&1, 3, 1, :discard))
    |> Enum.zip
    |> Enum.map(&Tuple.to_list(&1) |> Enum.chunk_every(3, 1, :discard))
    |> Enum.zip
    |> Enum.map(&Tuple.to_list/1)
    |> Enum.reduce([], fn row, acc ->
      Enum.map(row, fn entry ->
        List.flatten(entry)
        |> Enum.map(&(if &1 == ".", do: 0, else: 1))
        |> Integer.undigits(2)
        |> then(&Map.fetch!(algo_map, &1))
      end)
      |> then(&[&1 | acc])
    end)
    |> Enum.reverse
    |> then(&solve({algo_map, &1}, runs - 1, (if border == ".", do: algo_map[0], else: algo_map[511])))
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n\n", trim: true)
  |> then(fn [algo, image] ->
    algo_map = String.split(algo, "", trim: true)
    |> Enum.with_index
    |> Enum.into(%{}, fn {k, v} -> {v, k} end)

    input_image = String.split(image, "\n", trim: true)
    |> Enum.map(&String.graphemes/1)

    {algo_map, input_image}
  end)
  |> AdventOfCode.solve(2)
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  def solve(input, runs, border \\ ".")
  def solve({_, image}, 0, _), do: List.flatten(image) |> Enum.count(&(&1 == "#"))
  def solve({algo_map, image}, runs, border) do
    top_border_row = List.duplicate(border, length(Enum.at(image, 0)) + 4)

    Enum.map(image, &([border, border] ++ &1 ++ [border, border]))
    |> then(&(List.duplicate(top_border_row, 2) ++ &1 ++ List.duplicate(top_border_row, 2)))
    |> Enum.map(&Enum.chunk_every(&1, 3, 1, :discard))
    |> Enum.zip
    |> Enum.map(&Tuple.to_list(&1) |> Enum.chunk_every(3, 1, :discard))
    |> Enum.zip
    |> Enum.map(&Tuple.to_list/1)
    |> Enum.reduce([], fn row, acc ->
      Enum.map(row, fn entry ->
        List.flatten(entry)
        |> Enum.map(&(if &1 == ".", do: 0, else: 1))
        |> Integer.undigits(2)
        |> then(&Map.fetch!(algo_map, &1))
      end)
      |> then(&[&1 | acc])
    end)
    |> Enum.reverse
    |> then(&solve({algo_map, &1}, runs - 1, (if border == ".", do: algo_map[0], else: algo_map[511])))
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n\n", trim: true)
  |> then(fn [algo, image] ->
    input_image = String.split(image, "\n", trim: true) |> Enum.map(&String.graphemes/1)
    algo_map = for {v, i} <- Enum.with_index(String.graphemes(algo)), into: %{} do
      {i, v}
    end

    {algo_map, input_image}
  end)
  |> AdventOfCode.solve(50)
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 21

Part 1

defmodule AdventOfCode do
  def solve(input) do
    score_stream = Stream.cycle(1..10)

    Stream.cycle(1..100)
    |> Stream.chunk_every(3)
    |> then(fn stream ->
      Enum.with_index(input)
      |> Enum.map(fn {start, i} -> {start, Stream.drop(stream, i) |> Stream.take_every(2)} end)
    end)
    |> Enum.map(fn {start, stream} ->
      Stream.transform(stream, {0, start}, fn roll, {score, position} ->
        if score >= 1000 do
          {:halt, {score, position}}
        else
          new_position = Enum.at(score_stream, Enum.sum(roll) + position - 1)
          new_score = score + new_position
          {[new_score], {new_score, new_position}}
        end
      end)
      |> Enum.to_list
    end)
    |> Enum.with_index
    |> Enum.map(fn {r, i} -> {i, i + length(r), r} end)
    |> then(fn results ->
      {winning_position, total_turns, _} = Enum.min_by(results, &elem(&1, 1))
      rolls_and_scores = Enum.map(results, fn {i, _, r} ->
        player_turns = if i <= winning_position, do: total_turns, else: total_turns - 1
        {player_turns, Enum.at(r, player_turns - 1)}
      end)

      3 * Enum.sum(Enum.map(rolls_and_scores, &elem(&1, 0))) * elem(Enum.min_by(rolls_and_scores, &elem(&1, 1)), 1)
    end)
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> Enum.map(&String.split(&1, " ", trim: true) |> Enum.at(-1) |> String.to_integer)
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  @winning_score 21

  def solve([p1, p2]) do
    initial_state = {{0, p1}, {0, p2}}

    permutations(Enum.to_list(1..3), 3)
    |> Enum.map(&Enum.sum/1)
    |> Enum.frequencies
    |> then(&build_map(initial_state, &1, %{}))
    |> Map.fetch!(initial_state)
    |> Tuple.to_list
    |> Enum.max
  end

  defp build_map({{s, _}, _} = state, _, acc) when s >= @winning_score, do: Map.put(acc, state, {1, 0})
  defp build_map({_, {s, _}} = state, _, acc) when s >= @winning_score, do: Map.put(acc, state, {0, 1})
  defp build_map({{s1, p1}, state2} = game_state, roll_map, acc) do
    if !Map.has_key?(acc, game_state) do
      score_stream = Stream.cycle(1..10)
      Enum.map(Map.keys(roll_map), fn roll ->
        new_position = Enum.at(score_stream, roll + p1 - 1)
        {roll, {state2, {new_position + s1, new_position}}}
      end)
      |> Enum.reduce({{0, 0}, acc}, fn {roll, state}, {{p1, p2}, next_acc} ->
        next_map = build_map(state, roll_map, next_acc)
        {p2_next, p1_next} = Map.fetch!(next_map, state)
        roll_multiplier = Map.fetch!(roll_map, roll)
        {{p1 + p1_next * roll_multiplier, p2 + p2_next * roll_multiplier}, next_map}
      end)
      |> then(fn {res, next_map} -> Map.put(next_map, game_state, res) end)
    else
      acc
    end
  end

  defp permutations(l, i) when i == 0 or length(l) == 0, do: [[]]
  defp permutations(l, i) do
    for x <- l, y <- permutations(l, i - 1), do: [x|y]
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> Enum.map(&String.split(&1, " ", trim: true) |> Enum.at(-1) |> String.to_integer)
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 22

Part 1

defmodule AdventOfCode do
  def solve(input) do
    constraint = Range.new(-50, 50)
    Enum.reject(input, fn {_, {x, y, z}} -> Enum.any?([x,y,z], &Range.disjoint?(constraint, &1)) end)
    |> Enum.reduce(MapSet.new, fn {type, {x_r, y_r, z_r}}, acc ->
      for x <- x_r, y <- y_r, z <- z_r, reduce: acc do
        n -> if type == "on", do: MapSet.put(n, {x,y,z}), else: MapSet.delete(n, {x,y,z})
      end
    end)
    |> MapSet.size
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> Enum.map(&String.split(&1, " ", trim: true))
  |> Enum.map(fn [type, contents] ->
    String.split(contents, ",", trim: true)
    |> Enum.map(&String.split(&1, ".."))
    |> List.flatten
    |> Enum.map(&String.replace(&1, ~r/[^0-9\-]/, "") |> String.to_integer)
    |> Enum.chunk_every(2)
    |> Enum.map(fn [a, b] -> Range.new(a, b) end)
    |> then(&{type, List.to_tuple(&1)})
  end)
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  def solve(input) do
    Enum.with_index(input)
    |> Enum.filter(fn {r, _} -> elem(r, 0) == "on" end)
    |> Enum.map(fn {r, i} ->
      Enum.drop(input, i + 1)
      |> Enum.filter(&(overlaps?(elem(r, 1), elem(&1, 1))))
      |> Enum.map(&elem(&1, 1))
      |> then(&{elem(r, 1), &1})
    end)
    |> Enum.map(fn {start, remove} ->
      Enum.reduce(remove, [start], fn range, acc ->
        Enum.flat_map(acc, &remove_intersection(&1, range))
      end)
    end)
    |> List.flatten
    |> Enum.map(fn r -> Tuple.to_list(r) |> Enum.map(&Range.size/1) |> Enum.product end)
    |> Enum.sum
  end

  def remove_intersection({x_a, y_a, _} = a, b) do
    if overlaps?(a, b) do
      res = run_on_all(a, b, &find_range_overlap/2)
      [x_s, y_s, z_s] = run_on_all(a, List.to_tuple(res), &remove_range_intersection/2)

      Enum.with_index([x_s, y_s])
      |> Enum.map(fn {v, i} -> [elem(b, i) | v] end)
      |> then(&(&1 ++ [[Enum.at(res, 2)]]))
      |> Enum.reduce([{}], fn l, acc ->
        Enum.flat_map(l, fn entry -> Enum.map(acc, &Tuple.append(&1, entry)) end)
      end)
      |> then(fn r -> r ++ Enum.map(z_s, &{x_a, y_a, &1}) end)
      |> Enum.map(fn r -> run_on_all(r, a, &find_range_overlap/2) |> List.to_tuple end)
      |> Enum.reject(&(&1 == b or fully_covered?(&1, b)))
    else
      [a]
    end
  end

  def overlaps?(a, b), do: !Enum.any?(run_on_all(a, b, &Range.disjoint?/2))

  def fully_covered?(a, b) do
    Enum.map([a, b], fn elm -> Tuple.to_list(elm) |> Enum.map(&Enum.min_max/1) end)
    |> Enum.zip
    |> Enum.all?(fn {{a1, a2}, {b1, b2}} -> b1 <= a1 and b2 >= a2 end)
  end

  def run_on_all(a, b, func), do: Enum.map([a, b], &Tuple.to_list/1) |> Enum.zip |> Enum.map(fn {x, y} -> func.(x, y) end)

  def find_range_overlap(r1, r2) do
    {min_1, max_1} = Enum.min_max(r1)
    {min_2, max_2} = Enum.min_max(r2)
    Range.new(Enum.max([min_1, min_2]), Enum.min([max_1, max_2]))
  end

  def remove_range_intersection(r1, r2) do
    {min_1, max_1} = Enum.min_max(r1)
    {min_2, max_2} = Enum.min_max(r2)

    cond do
      min_1 == min_2 and max_1 == max_2 -> []
      min_1 < min_2 and max_1 > max_2 -> [Range.new(min_1, min_2 - 1), Range.new(max_2 + 1, max_1)]
      min_1 < min_2 -> [Range.new(min_1, min_2 - 1)]
      true -> [Range.new(max_2 + 1, max_1)]
    end
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> Enum.map(&String.split(&1, " ", trim: true))
  |> Enum.map(fn [type, contents] ->
    String.split(contents, ",", trim: true)
    |> Enum.map(&String.split(&1, ".."))
    |> List.flatten
    |> Enum.map(&String.replace(&1, ~r/[^0-9\-]/, "") |> String.to_integer)
    |> Enum.chunk_every(2)
    |> Enum.map(fn [a, b] -> Range.new(a, b) end)
    |> then(&{type, List.to_tuple(&1)})
  end)
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 23

Parts 1 & 2

defmodule AdventOfCode do
  @effective_infinity 99999999999999999999999
  @movement_multiplier %{?A => 1, ?B => 10, ?C => 100, ?D => 1000}
  @room_to_index %{?A => 2, ?B => 4, ?C => 6, ?D => 8}

  def solve(input, room_size, acc \\ %{}) do
    if Map.has_key?(acc, input) do
      {Map.get(acc, input), [], acc}
    else
      possible_moves = next_moves(input, room_size)
      winning_move = Enum.find(possible_moves, fn {_, {_, rooms}} -> complete?(rooms, room_size) end)

      if winning_move do
        {move, board} = winning_move
        {elem(move, 0), [board], Map.put(acc, board, elem(move, 0))}
      else
        non_looping_moves = Enum.reject(possible_moves, &(Map.has_key?(acc, elem(&1, 1)) and is_nil(Map.get(acc, elem(&1, 1)))))

        if length(non_looping_moves) == 0 do
          {@effective_infinity, [], Map.put(acc, input, @effective_infinity)}
        else
          Enum.reduce(non_looping_moves, {[], acc}, fn {move, board}, {res, next_acc} ->
            {cost, moves, result_acc} = solve(board, room_size, next_acc)
            {[{move, board, {cost, moves, result_acc}} | res], result_acc}
          end)
          |> then(fn {results, next_acc} ->
            {move, board, {cost, all_boards, _}} = Enum.min_by(results, fn {move, _, {cost, _, _}} ->
              elem(move, 0) + cost
            end)
            total_cost = if cost >= @effective_infinity, do: @effective_infinity, else: elem(move, 0) + cost
            {total_cost, [board | all_boards], Map.put(next_acc, input, total_cost)}
          end)
        end
      end
    end
  end

  def next_moves({hallway, rooms}, room_size) do
    room_indexes = Map.values(@room_to_index)

    open_hallway_ranges = Enum.with_index(hallway)
    |> Enum.reduce({-1, []}, fn {v, i}, {left, acc} ->
      cond do
        left == -1 and v == ?. -> {i, acc}
        left == -1 -> {-1, acc}
        i == (length(hallway) - 1) -> {nil, [Range.new(left, i) | acc]}
        v != ?. -> {-1, [Range.new(left, i - 1) | acc]}
        true -> {left, acc}
      end
    end)
    |> then(&Enum.reverse(elem(&1, 1)))
    room_to_hallway_or_room_moves = Enum.with_index(rooms)
    |> Enum.filter(fn {r, i} ->
      length(r) > 0 and Enum.any?(r, &(&1 != (?A + i)))
    end)
    |> Enum.flat_map(fn {[hd | tl] = room, i} ->
      index_outside_room = 2 + 2 * i
      if Enum.at(hallway, index_outside_room) != ?. do
        []
      else
        start_point = {index_outside_room, room_size - length(room) + 1}
        target_room_index = abs(?A - hd)
        target_room = Enum.at(rooms, target_room_index)
        target_room_hallway_index = 2 + 2 * target_room_index
        target_all_correct = Enum.all?(target_room, &(&1 == hd))

        Enum.filter(open_hallway_ranges, &((index_outside_room + 1) in &1 or (index_outside_room - 1) in &1))
        |> Enum.uniq
        |> Enum.flat_map(fn range ->
          if length(target_room) < room_size and target_room_hallway_index in range and target_all_correct do
            end_point = {target_room_hallway_index, room_size - length(target_room)}
            distance = room_size - length(room) + 1 + abs(target_room_hallway_index - index_outside_room) + room_size - length(target_room)
            next_state = {hallway, List.replace_at(List.replace_at(rooms, i, tl), target_room_index, [hd | target_room])}
            [{{distance * @movement_multiplier[hd], hd, start_point, end_point}, next_state}]
          else
            Enum.reject(range, &(&1 in room_indexes))
            |> Enum.map(fn destination ->
              #IO.inspect(destination, label: "Destination")
              end_point = {destination, 0}
              distance = manhattan_distance(start_point, end_point)
              next_state = {List.replace_at(hallway, destination, hd), List.replace_at(rooms, i, tl)}

              {{distance * @movement_multiplier[hd], hd, start_point, end_point}, next_state}
            end)
          end
        end)
      end
    end)

    hallway_to_room_moves = Enum.with_index(hallway)
    |> Enum.filter(fn {v, _} -> v != ?. and Enum.all?(Enum.at(rooms, abs(?A - v)), &(&1 == v)) end)
    |> Enum.filter(fn {v, i} ->
      Enum.any?(open_hallway_ranges, &(((i+1) in &1 or (i-1) in &1) and @room_to_index[v] in &1))
    end)
    |> Enum.map(fn {v, i} ->
      target_room = Enum.at(rooms, abs(?A - v))
      start_point = {i, 0}
      end_point = {@room_to_index[v], room_size - length(target_room)}
      distance = manhattan_distance(start_point, end_point)
      next_state = {List.replace_at(hallway, i, ?.), List.replace_at(rooms, abs(?A - v), [v | target_room])}

      {{distance * @movement_multiplier[v], v, start_point, end_point}, next_state}
    end)

    room_to_hallway_or_room_moves ++ hallway_to_room_moves
  end

  def complete?(rooms, room_size), do: Enum.all?(Enum.with_index(rooms), fn {r, i} -> length(r) == room_size && Enum.all?(r, &(&1 == (?A + i))) end)

  def manhattan_distance({x1, y1}, {x2, y2}), do: abs(x2 - x1) + abs(y2 - y1)
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> then(&Enum.slice(&1, 1, length(&1) - 2))
  |> then(fn [hallway | rooms] ->
    Enum.map(rooms, &String.trim(&1) |> String.split("#", trim: true))
    |> Enum.zip
    |> Enum.map(&(Tuple.to_list(&1) |> List.to_charlist))
    |> then(fn r -> {Enum.filter(String.to_charlist(hallway), &(&1 == ?.)), r} end)
  end)
  |> AdventOfCode.solve(4)
  |> elem(0)
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 24

Part 1

defmodule AdventOfCode do
  def solve(instruction_sets) do
    Enum.with_index(instruction_sets)
    |> Enum.reduce({[], []}, fn {instruction_set, i}, {res, acc} ->
      is_pop = Enum.any?(instruction_set, &(&1 == {"div", "z", 26}))

      if is_pop do
        {_, _, val} = Enum.at(instruction_set, 5)
        [var, init] = hd(acc)

        [left, right, digit] = if (init + val) < 0 do
          [var, i, abs(init + val)]
        else
          [i, var, init + val]
        end

        {[{left, 9}, {right, 9 - digit} | res], tl(acc)}
      else
        {_, _, val} = Enum.at(instruction_set, 15)
        {res, [[i, val] | acc]}
      end
    end)
    |> elem(0)
    |> Enum.sort_by(&elem(&1, 0))
    |> Enum.map(&elem(&1, 1))
    |> Enum.join("")
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> Enum.map(fn line ->
    String.split(line, " ", trim: true)
    |> Enum.map(&(if Regex.match?(~r/[\-0-9]+/, &1), do: String.to_integer(&1), else: &1))
    |> List.to_tuple
  end)
  |> Enum.chunk_by(fn x -> elem(x, 0) == "inp" end)
  |> Enum.chunk_every(2, 2, :discard)
  |> Enum.map(&List.flatten/1)
  |> AdventOfCode.solve
  #|> AdventOfCode.find_dependencies_and_outputs
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Part 2

defmodule AdventOfCode do
  def solve(instruction_sets) do
    Enum.with_index(instruction_sets)
    |> Enum.reduce({[], []}, fn {instruction_set, i}, {res, acc} ->
      is_pop = Enum.any?(instruction_set, &(&1 == {"div", "z", 26}))

      if is_pop do
        {_, _, val} = Enum.at(instruction_set, 5)
        [var, init] = hd(acc)

        [left, right, digit] = if (init + val) < 0 do
          [var, i, abs(init + val)]
        else
          [i, var, init + val]
        end

        {[{left, 1 + digit}, {right, 1} | res], tl(acc)}
      else
        {_, _, val} = Enum.at(instruction_set, 15)
        {res, [[i, val] | acc]}
      end
    end)
    |> elem(0)
    |> Enum.sort_by(&elem(&1, 0))
    |> Enum.map(&elem(&1, 1))
    |> Enum.join("")
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> Enum.map(fn line ->
    String.split(line, " ", trim: true)
    |> Enum.map(&(if Regex.match?(~r/[\-0-9]+/, &1), do: String.to_integer(&1), else: &1))
    |> List.to_tuple
  end)
  |> Enum.chunk_by(fn x -> elem(x, 0) == "inp" end)
  |> Enum.chunk_every(2, 2, :discard)
  |> Enum.map(&List.flatten/1)
  |> AdventOfCode.solve
  #|> AdventOfCode.find_dependencies_and_outputs
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end

Day 25

Parts 1 & 2

defmodule AdventOfCode do
  def solve(input) do
    width = length(Enum.at(input, 0))
    height = length(input)

    Enum.with_index(input)
    |> Enum.reduce(%{}, fn {row, y}, acc ->
      Enum.reduce(Enum.with_index(row), acc, fn {val, x}, inner_acc ->
        if val != ".", do: Map.put(inner_acc, {x, y}, val), else: inner_acc
      end)
    end)
    |> Stream.unfold(fn
      nil -> nil
      map ->
        {east, south} = Enum.group_by(map, &elem(&1, 1)) |> then(fn x -> {Enum.into(x[">"], %{}), Enum.into(x["v"], %{})} end)
        {east_moves, east_stationary} = Map.keys(east)
          |> Enum.group_by(fn {x, y} -> Map.has_key?(map, {rem(x+1, width), y}) end)
          |> then(&{MapSet.new(Enum.map(Map.get(&1, false, []), fn {x, y} -> {rem(x+1, width), y} end)), MapSet.new(Map.get(&1, true, []))})

        {south_moves, south_stationary} = Map.keys(south)
          |> Enum.group_by(fn {x, y} ->
            result_key = {x, rem(y + 1, height)}
            Map.has_key?(south, result_key) or MapSet.member?(east_moves, result_key) or MapSet.member?(east_stationary, result_key)
          end)
          |> then(&{MapSet.new(Enum.map(Map.get(&1, false, []), fn {x, y} -> {x, rem(y+1, height)} end)), MapSet.new(Map.get(&1, true, []))})

        if (MapSet.size(east_moves) + MapSet.size(south_moves)) == 0 do
          {1, nil}
        else
          east_result = Enum.reduce(MapSet.union(east_moves, east_stationary), %{}, &Map.put(&2, &1, ">"))
          south_result = Enum.reduce(MapSet.union(south_moves, south_stationary), %{}, &Map.put(&2, &1, "v"))
          {1, Map.merge(east_result, south_result)}
        end
    end)
    |> Enum.sum
  end
end

with {:ok, contents} <- File.read('advent-of-code-input.txt') do
  String.split(contents, "\n", trim: true)
  |> Enum.map(&String.graphemes/1)
  |> AdventOfCode.solve
  |> IO.inspect(label: "Answer")
else
  _ -> IO.inspect("Failed to read input file")
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment