Skip to content

Commit 465da74

Browse files
committed
Day 17 solution
1 parent a087e70 commit 465da74

File tree

1 file changed

+218
-0
lines changed

1 file changed

+218
-0
lines changed

solutions/day17.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import re
2+
from typing import List, Tuple
3+
4+
from aoc.models.base import SolutionBase
5+
6+
7+
class ThreeBitComputer:
8+
"""Simulates a 3-bit computer with three registers and eight instructions.
9+
10+
This class implements a virtual machine that executes programs consisting of 3-bit
11+
instructions (0-7). The computer has three registers (A, B, C) and supports both
12+
literal and combo operands. Instructions include arithmetic operations, bitwise
13+
operations, jumps, and output generation.
14+
15+
Attributes:
16+
registers (dict): Dictionary storing values for registers A, B, and C
17+
instruction_pointer (int): Current position in the program
18+
output (list): List storing output values generated during program execution
19+
"""
20+
21+
def __init__(self, a: int = 0, b: int = 0, c: int = 0):
22+
"""Initialize the computer with specified register values.
23+
24+
Args:
25+
a (int, optional): Initial value for register A. Defaults to 0.
26+
b (int, optional): Initial value for register B. Defaults to 0.
27+
c (int, optional): Initial value for register C. Defaults to 0.
28+
"""
29+
self.registers = {"A": a, "B": b, "C": c}
30+
self.instruction_pointer = 0
31+
self.output = []
32+
33+
def get_combo_value(self, operand: int) -> int:
34+
"""Resolve the value of a combo operand based on its code.
35+
36+
Args:
37+
operand (int): The combo operand code (0-6)
38+
0-3: Return literal value
39+
4: Return value in register A
40+
5: Return value in register B
41+
6: Return value in register C
42+
43+
Returns:
44+
int: The resolved value of the combo operand
45+
46+
Raises:
47+
ValueError: If the operand is invalid (7 or out of range)
48+
"""
49+
if 0 <= operand <= 3:
50+
return operand
51+
52+
elif operand == 4:
53+
return self.registers["A"]
54+
55+
elif operand == 5:
56+
return self.registers["B"]
57+
58+
elif operand == 6:
59+
return self.registers["C"]
60+
61+
else:
62+
raise ValueError(f"Invalid combo operand: {operand}")
63+
64+
def execute_instruction(self, program: List[int]) -> bool:
65+
"""Execute the next instruction in the program.
66+
67+
Executes the instruction at the current instruction_pointer position.
68+
Supported instructions:
69+
0 (adv): Divide A by 2^(combo operand), store in A
70+
1 (bxl): XOR B with literal operand
71+
2 (bst): Store combo operand mod 8 in B
72+
3 (jnz): Jump to operand if A is non-zero
73+
4 (bxc): XOR B with C
74+
5 (out): Output combo operand mod 8
75+
6 (bdv): Divide A by 2^(combo operand), store in B
76+
7 (cdv): Divide A by 2^(combo operand), store in C
77+
78+
Args:
79+
program (List[int]): List of integers representing the program instructions
80+
81+
Returns:
82+
bool: False if program should halt, True if execution should continue
83+
"""
84+
if self.instruction_pointer >= len(program):
85+
return False
86+
87+
opcode = program[self.instruction_pointer]
88+
operand = program[self.instruction_pointer + 1]
89+
90+
if opcode == 0: # adv
91+
self.registers["A"] //= 1 << self.get_combo_value(operand)
92+
93+
elif opcode == 1: # bxl
94+
self.registers["B"] ^= operand
95+
96+
elif opcode == 2: # bst
97+
self.registers["B"] = self.get_combo_value(operand) % 8
98+
99+
elif opcode == 3: # jnz
100+
if self.registers["A"] != 0:
101+
self.instruction_pointer = operand
102+
return True
103+
104+
elif opcode == 4: # bxc
105+
self.registers["B"] ^= self.registers["C"]
106+
107+
elif opcode == 5: # out
108+
self.output.append(self.get_combo_value(operand) % 8)
109+
110+
elif opcode == 6: # bdv
111+
self.registers["B"] = self.registers["A"] // (1 << self.get_combo_value(operand))
112+
113+
elif opcode == 7: # cdv
114+
self.registers["C"] = self.registers["A"] // (1 << self.get_combo_value(operand))
115+
116+
self.instruction_pointer += 2
117+
return True
118+
119+
def run(self, program: List[int]) -> str:
120+
"""Run the entire program until completion.
121+
122+
Args:
123+
program (List[int]): List of integers representing the program instructions
124+
125+
Returns:
126+
str: Comma-separated string of output values generated during execution
127+
"""
128+
while self.execute_instruction(program):
129+
pass
130+
131+
return ",".join(map(str, self.output))
132+
133+
def check_output(self, program: List[int]) -> bool:
134+
"""Check if the program's output matches its own instructions.
135+
136+
Args:
137+
program (List[int]): List of integers representing the program instructions
138+
139+
Returns:
140+
bool: True if program output exactly matches the input program, False otherwise
141+
"""
142+
output_str = self.run(program)
143+
output_numbers = [int(x) for x in output_str.split(",")]
144+
return output_numbers == program
145+
146+
147+
class Solution(SolutionBase):
148+
"""Solution for Advent of Code 2024 - Day 17: Chronospatial Computer.
149+
150+
This class solves a puzzle involving a 3-bit computer simulation. The computer
151+
has three registers and eight instructions, processing programs of 3-bit numbers.
152+
Part 1 executes the program with given register values to produce output, while
153+
Part 2 finds the lowest value for register A that makes the program output itself.
154+
155+
Input format:
156+
- Multiple lines where:
157+
First three lines contain initial register values (A, B, C)
158+
One line contains the program as comma-separated 3-bit numbers
159+
160+
This class inherits from `SolutionBase` and provides methods to parse input,
161+
execute programs, and analyze program behavior.
162+
"""
163+
164+
def parse_data(self, data: List[str]) -> Tuple[int, int, int, List[int]]:
165+
"""Parse input data to extract register values and program instructions.
166+
167+
Args:
168+
data (List[str]): Input lines containing register values and program
169+
170+
Returns:
171+
Tuple containing:
172+
- int: Initial value for register A
173+
- int: Initial value for register B
174+
- int: Initial value for register C
175+
- List[int]: List of program instructions
176+
"""
177+
reg_a = int(re.search(r"Register A: (\d+)", data[0]).group(1))
178+
reg_b = int(re.search(r"Register B: (\d+)", data[1]).group(1))
179+
reg_c = int(re.search(r"Register C: (\d+)", data[2]).group(1))
180+
181+
program_line = next(line for line in data if line.startswith("Program:"))
182+
program = [int(x) for x in program_line.split(": ")[1].split(",")]
183+
184+
return reg_a, reg_b, reg_c, program
185+
186+
def part1(self, data: List[str]) -> str:
187+
"""Execute the program with given register values and return its output.
188+
189+
Args:
190+
data (List[str]): Input lines containing register values and program
191+
192+
Returns:
193+
str: Comma-separated string of values output by the program
194+
"""
195+
reg_a, reg_b, reg_c, program = self.parse_data(data)
196+
computer = ThreeBitComputer(reg_a, reg_b, reg_c)
197+
return computer.run(program)
198+
199+
def part2(self, data: List[str]) -> int:
200+
"""Find lowest positive value for register A that makes program output itself.
201+
202+
Args:
203+
data (List[str]): Input lines containing register values and program
204+
205+
Returns:
206+
int: Lowest positive value for register A that causes program to output
207+
a copy of its own instructions
208+
"""
209+
_, reg_b, reg_c, program = self.parse_data(data)
210+
211+
# Start from 1 as we need lowest positive value
212+
a = 1
213+
while True:
214+
computer = ThreeBitComputer(a, reg_b, reg_c)
215+
if computer.check_output(program):
216+
return a
217+
218+
a += 1

0 commit comments

Comments
 (0)