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

Advent of Code 2024

2024/day17.livemd

Advent of Code 2024

aoc_helpers_path =
  __ENV__.file
  |> String.split("#")
  |> hd()
  |> Path.dirname()
  |> then(fn dir ->
    [dir, "..", "aoc_helpers"]
  end)
  |> Path.join()

Mix.install([
  {:aoc_helpers, path: aoc_helpers_path}
])

Day 17

import AocHelpers

Kino.configure(inspect: [charlists: :as_lists])

input = download_puzzle(2024, 17, cookie: System.get_env("AOC_COOKIE"))

sample = """
Register A: 729
Register B: 0
Register C: 0

Program: 0,1,5,4,3,0
"""

sample2 = """
Register A: 2024
Register B: 0
Register C: 0

Program: 0,3,5,4,3,0
"""

Kino.nothing()
defmodule Day17 do
  def parse(input) do
    [registers, program] =
      input
      |> blocks()

    [a, b, c] =
      registers
      |> lines()
      |> Enum.map(&(&1 |> String.split(": ") |> Enum.at(1)))
      |> map_ints()

    p =
      program
      |> String.split(": ")
      |> Enum.at(1)
      |> String.split(",")
      |> map_ints()

    {a, b, c, p}
  end

  def run_program({a, b, c, p}) do
    # let's convert the program to a map for easier access since elixir
    # doesn't have an array/vector like structure
    p = p |> Enum.with_index(fn element, index -> {index, element} end) |> Enum.into(Map.new())

    cpu = %{
      a: a,
      b: b,
      c: c,
      ip: 0,
      program: p,
      output: []
    }

    run(cpu)
    |> Map.get(:output)
    |> Enum.reverse()
    |> Enum.join(",")
  end

  def run(cpu) do
    # {cpu.a, cpu.b, cpu.c, cpu.ip} |> IO.inspect()

    case op(cpu) do
      nil ->
        cpu

      op ->
        cpu
        |> execute(op)
        |> run()
    end
  end

  def execute(cpu, 0) do
    # adv
    cpu
    |> Map.put(:a, div(cpu.a, Integer.pow(2, combo(cpu))))
    |> incr_op()
  end

  def execute(cpu, 1) do
    # bxl
    cpu
    |> Map.put(:b, Bitwise.bxor(cpu.b, operand(cpu)))
    |> incr_op()
  end

  def execute(cpu, 2) do
    # bst
    cpu
    |> Map.put(:b, Integer.mod(combo(cpu), 8))
    |> incr_op()
  end

  def execute(cpu, 3) do
    # jnz
    ip =
      if cpu.a == 0 do
        cpu.ip + 2
      else
        operand(cpu)
      end

    cpu
    |> set_ip(ip)
  end

  def execute(cpu, 4) do
    # bxc
    cpu
    |> Map.put(:b, Bitwise.bxor(cpu.b, cpu.c))
    |> incr_op()
  end

  def execute(cpu, 5) do
    # out
    cpu
    |> Map.update!(:output, &[Integer.mod(combo(cpu), 8) | &1])
    |> incr_op()
  end

  def execute(cpu, 6) do
    # bdv
    cpu
    |> Map.put(:b, div(cpu.a, Integer.pow(2, combo(cpu))))
    |> incr_op()
  end

  def execute(cpu, 7) do
    # cdv
    cpu
    |> Map.put(:c, div(cpu.a, Integer.pow(2, combo(cpu))))
    |> incr_op()
  end

  def op(cpu), do: Map.get(cpu.program, cpu.ip)
  def operand(cpu), do: Map.get(cpu.program, cpu.ip + 1)
  def incr_op(%{ip: ip} = cpu), do: cpu |> set_ip(ip + 2)
  def set_ip(cpu, ip), do: cpu |> Map.put(:ip, ip)

  def combo(cpu) do
    case operand(cpu) do
      n when n in 0..3 -> n
      4 -> cpu.a
      5 -> cpu.b
      6 -> cpu.c
    end
  end

  def combo(n, _) when n in 0..3, do: n
  def combo(4, cpu), do: cpu.a
  def combo(5, cpu), do: cpu.b
  def combo(6, cpu), do: cpu.c
end
import Day17

Part 1

input
|> parse()
|> run_program()

Part 2

{_a, b, c, p} =
  input
  |> parse()

target = p |> Enum.join(",")

# program divides a by 8 each "loop"
# program is 16 ints
# lower bound = 8^15 = 35_184_372_088_832
# upper bound = 8^16 - 1 = 281_474_976_710_655
# brute force is 246_290_604_621_823 program runs

# turns out we can find each number output, then multiply a by 8 as we find each digit
# until we match them all
# run_program({3, b, c, p})
# run_program({Bitwise.<<<(3, 3), b, c, p})

p
|> Enum.reduce(0, fn _, a ->
  Stream.iterate(1, &amp;(&amp;1 + 1))
  |> Enum.reduce_while(a * 8, fn _, a ->
    out = run_program({a, b, c, p})

    if String.ends_with?(target, out) do
      {:halt, a}
    else
      {:cont, a + 1}
    end
  end)
end)