AdventOfCode/2024/day17.exs

146 lines
4.1 KiB
Elixir
Raw Permalink Normal View History

#!/usr/bin/env elixir
defmodule Day17 do
defmodule Computer do
defstruct a: 0, b: 0, c: 0, output: [], program: []
end
def part1(computer) do
process(computer.program, computer)
|> Map.fetch!(:output)
|> Enum.reverse()
|> Enum.join(",")
end
def process([], computer), do: computer
# adv div(A, 2^ combo operand) -> A
def process([0, operand | program], computer) do
value = div(computer.a, 2 ** combo(operand, computer))
computer = %Computer{computer | a: value}
process(program, computer)
end
# bxl B XOR literal operand -> B
def process([1, operand | program], computer) do
value = Bitwise.bxor(computer.b, operand)
computer = %Computer{computer | b: value}
process(program, computer)
end
# bst combo operand mod 8 -> B (only 3 lowest bits)
def process([2, operand | program], computer) do
value = rem(combo(operand, computer), 8)
computer = %Computer{computer | b: value}
process(program, computer)
end
# jnz noting if A is 0, otherwise set instruction to literal operand - don't inc by 2 if jump
def process([3, operand | program], computer) do
if computer.a == 0 do
process(program, computer)
else
program = Enum.drop(computer.program, operand)
process(program, computer)
end
end
# bxc B XOR C -> B (ignore operand)
def process([4, _operand | program], computer) do
value = Bitwise.bxor(computer.b, computer.c)
computer = %Computer{computer | b: value}
process(program, computer)
end
# out combo operand mod 8 + output (CSV)
def process([5, operand | program], computer) do
value = rem(combo(operand, computer), 8)
computer = %Computer{computer | output: [value | computer.output]}
process(program, computer)
end
# bdv same as adv but store in B
def process([6, operand | program], computer) do
value = div(computer.a, 2 ** combo(operand, computer))
computer = %Computer{computer | b: value}
process(program, computer)
end
# cdv same as adv but store in C
def process([7, operand | program], computer) do
value = div(computer.a, 2 ** combo(operand, computer))
computer = %Computer{computer | c: value}
process(program, computer)
end
def combo(literal, _computer) when literal in [0, 1, 2, 3], do: literal
def combo(4, computer), do: computer.a
def combo(5, computer), do: computer.b
def combo(6, computer), do: computer.c
def part2(computer) do
# TODO runs forever. Probably need to detect loops?
search_for_self(computer)
end
def search_for_self(computer, a \\ 0) do
computer = %Computer{computer | a: a}
output = process(computer.program, computer).output |> Enum.reverse()
if output == computer.program do
a
else
search_for_self(computer, a + 1)
end
end
def input do
with [input_filename] <- System.argv(),
{:ok, input} <- File.read(input_filename) do
"Register A: " <> input = input
{a, input} = Integer.parse(input)
"\nRegister B: " <> input = input
{b, input} = Integer.parse(input)
"\nRegister C: " <> input = input
{c, input} = Integer.parse(input)
"\n\nProgram: " <> input = input
program = input |> String.split([",", "\n"], trim: true) |> Enum.map(&String.to_integer/1)
%Computer{a: a, b: b, c: c, program: program}
else
_ -> :error
end
end
#######################
# HERE BE BOILERPLATE #
#######################
def run do
case input() do
:error -> print_usage()
input -> run_parts_with_timer(input)
end
end
defp run_parts_with_timer(input) do
run_with_timer(1, fn -> part1(input) end)
run_with_timer(2, fn -> part2(input) end)
end
defp run_with_timer(part, fun) do
{time, result} = :timer.tc(fun)
IO.puts("Part #{part} (completed in #{format_time(time)}):\n")
IO.puts("#{inspect(result)}\n")
end
defp format_time(μsec) when μsec < 1_000, do: "#{μsec}μs"
defp format_time(μsec) when μsec < 1_000_000, do: "#{μsec / 1000}ms"
defp format_time(μsec), do: "#{μsec / 1_000_000}s"
defp print_usage do
IO.puts("Usage: elixir day17.exs input_filename")
end
end
Day17.run()