Advent of Code 2024 Day 17 – Chronospatial Computer

Advent of Code 2024 Day 17 – Chronospatial Computer

- 13 mins

Day 17: Chronospatial Computer

Part 1

Today’s problem involves a 3-bit computer that can execute a few simple instructions. It can run programs where each instruction is a single 3-bit number (0-7). The computer also has 3 registers that can hold integers of any size.

Each of the eight instructions has an opcode and they all take one argument (the operand). The instruction pointer starts at 0 and will increase by 2 after each instruction is processed, except for the jump instruction. The computer halts when it reaches the end of the program.

So the program 0,1,2,3 would execute the following instructions:

There are two types of operands, each instruction will specify which type it uses:

The instructions are as follows:

The input for today’s problem is the initial values of the three registers and a list of instructions. The goal is to find the output of the program.

For example:

Register A: 729
Register B: 0
Register C: 0

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

The above program should have the output 4,6,3,5,6,3,5,2,1,0.

My Solution

I need to:

  1. Parse the input
  2. Implement the instructions
  3. Execute the program and output the result

Parsing the input is straightforward.

with open("input.txt", "r", encoding="utf-8") as f:
	lines = f.readlines()
	registers = {}
	registers["A"] = int(lines[0].split(":")[1].strip())
	registers["B"] = int(lines[1].split(":")[1].strip())
	registers["C"] = int(lines[2].split(":")[1].strip())

	program = list(map(int, lines[4].split(":")[1].strip().split(",")))

For the registers, I’m storing the values in a dictionary. For the program, I’m storing the opcodes and operands in a list.

Implementing the instructions:

def adv(registers: Registers, operand: int):
    numerator = registers["A"]
    divisor = get_combo_value(registers, operand)
    registers["A"] = numerator // pow(2, divisor)


def bxl(registers: Registers, operand: int):
    registers["B"] = registers["B"] ^ operand


def bst(registers: Registers, operand: int):
    value = get_combo_value(registers, operand)
    registers["B"] = value % 8


def jnz(registers: Registers, operand: int) -> int:
    if registers["A"] == 0:
        return -1
    return operand


def bxc(registers: Registers, _):
    registers["B"] = registers["B"] ^ registers["C"]


def out(registers: Registers, operand: int) -> int:
    value = get_combo_value(registers, operand)
    return value % 8


def bdv(registers: Registers, operand: int):
    numerator = registers["A"]
    divisor = get_combo_value(registers, operand)
    registers["B"] = numerator // pow(2, divisor)


def cdv(registers: Registers, operand: int):
    numerator = registers["A"]
    divisor = get_combo_value(registers, operand)
    registers["C"] = numerator // pow(2, divisor)

Each instruction has the behavior described in the problem statement. I have a get_combo_value function to get the correct combo value as needed:

def get_combo_value(registers: Registers, operand: int) -> int:
    value = -1
    if operand in {0, 1, 2, 3}:
        value = operand
    match operand:
        case 4:
            value = registers["A"]
        case 5:
            value = registers["B"]
        case 6:
            value = registers["C"]
        case 7:
            raise ValueError("Invalid combo operand")
    return value

Executing the program:

def execute_instructions(registers: Registers, program: list[int]):
    instruction_pointer = 0
    outs = []
    instructions = {
        0: adv,
        1: bxl,
        2: bst,
        3: jnz,
        4: bxc,
        5: out,
        6: bdv,
        7: cdv,
    }

    while instruction_pointer < len(program):
        instruction = program[instruction_pointer]
        operand = program[instruction_pointer + 1]
        instruction_fn = instructions[instruction]

        if instruction == 3:
            res = jnz(registers, operand)
            if res != -1:
                instruction_pointer = res
                continue

        output = instruction_fn(registers, operand)
        if instruction == 5:
            outs.append(output)

        instruction_pointer += 2

    return outs

I’m iterating through the program, executing the instructions, and storing the output in a list. If the current instruction is a jump, I update the instruction pointer accordingly.

Finally, I output the result:

def main1():
    with open("input.txt", "r", encoding="utf-8") as f:
        lines = f.readlines()
        registers = {}
        registers["A"] = int(lines[0].split(":")[1].strip())
        registers["B"] = int(lines[1].split(":")[1].strip())
        registers["C"] = int(lines[2].split(":")[1].strip())

        program = list(map(int, lines[4].split(":")[1].strip().split(",")))

    outs = execute_instructions(registers, program)
    print("LOGF: outs", ",".join(map(str, outs)))

As always, I’ve only included the relevant parts of the code here, but to see my full solution, you can check out my Advent of Code GitHub repository.

Part 2

For part 2, it turns out that the original program is supposed to output another copy of the program! It does not correctly output the program, due to the value in register A being the wrong value. We need to find the lowest positive initial value for register A that will make the program output itself.

For example:

Register A: 2024
Register B: 0
Register C: 0

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

If register A is instead set to 117440, the program will output itself.

My Solution

This is actually a quine problem.

The good thing about this problem is that the out instruction always outputs values modulo 8. This means that:

We can use this property to build up the correct value for register A.

def find_quine(program: list[int], registers: Registers) -> int:
    queue = [(len(program) - 1, 0)]

    while queue:
        offset, value = queue.pop(0)
        for i in range(8):
            new_value = (value << 3) + i

            registers["A"] = new_value
            registers["B"] = 0
            registers["C"] = 0

            new_outs = execute_instructions(registers, program)

            if new_outs == program[offset:]:
                if offset == 0:
                    return new_value

                queue.append((offset - 1, new_value))

    return -1

I use a breadth-first search to find the smallest quine value. Explanation of the code:

Finally, I output the result:

def main2():
    with open("input.txt", "r", encoding="utf-8") as f:
        lines = f.readlines()
        registers = {}
        registers["A"] = int(lines[0].split(":")[1].strip())
        registers["B"] = int(lines[1].split(":")[1].strip())
        registers["C"] = int(lines[2].split(":")[1].strip())

        program = list(map(int, lines[4].split(":")[1].strip().split(",")))

    quine_value = find_quine(program, registers)
    print(f"LOGF: { quine_value = }")

Again, I’ve only included the relevant parts of the code here, check out my repository for the full solution.


That’s it for day 17 of Advent of Code 2024! I hope you enjoyed reading my solution and let’s see how the rest of the month goes!

Vinesh Benny

Vinesh Benny

A Software Engineer learning about different things in life and otherwise