Dazbo's Advent of Code solutions, written in Python
We’re introduced to “assembunny”, a simplified assembly language. We need to build a computer that can execute assembunny code.
The input data looks like this:
cpy 1 a
cpy 1 b
cpy 26 d
jnz c 2
jnz 1 5
cpy 7 c
inc d
dec c
jnz c -2
cpy a c
inc a
dec b
jnz b -2
cpy c b
dec d
jnz d -6
cpy 16 c
cpy 12 d
inc a
dec d
jnz d -2
dec c
jnz c -5
The assembunny code includes four registers (a, b, c, and d), which are initialized to 0. The instructions are:
cpy x y
: copies x
(either an integer or the value of a register) into register y
.inc x
: increases the value of register x
by one.dec x
: decreases the value of register x
by one.jnz x y
: jumps to an instruction y
away (positive means forward, negative means backward), but only if x
(a register or a number) is not zero.After executing the instructions, what value is left in register a
?
My strategy is to create a Computer
class that simulates the assembunny processor.
Computer
class will have attributes for the four registers (a
, b
, c
, d
), an instruction pointer (_ip
), and a list to store the program’s instructions.run_program
method will parse the input instructions and execute them in a loop until the instruction pointer is outside the bounds of the program.__getattribute__
to dynamically call the appropriate method (e.g., _op_cpy
, _op_inc
) based on the instruction name. This is a form of introspection.Here’s the Computer
class:
class Computer():
""" Stores a set of registers, which each store an int value.
Processes instructions in a supplied program,
using the instruction pointer to determine which instuction is next.
"""
def __init__(self) -> None:
self._registers = {
'a': 0,
'b': 0,
'c': 0,
'd': 0
}
self._ip = 0 # instruction pointer
self._instructions = [] # list of instructions in the format [instr, [parms]]
@property
def registers(self):
return self._registers
def set_register(self, reg, value):
if reg not in self._registers:
raise KeyError(f"No such register '{reg}'")
self._registers[reg] = value
def run_program(self, instructions_input: list):
for line in instructions_input:
instr_parts = line.split()
instr = instr_parts[0]
instr_parms = instr_parts[1:]
self._instructions.append([instr, instr_parms])
while self._ip < len(self._instructions):
self._execute_instruction(self._instructions[self._ip])
def _execute_instruction(self, instr_and_parms:list):
instr = instr_and_parms[0]
instr_parms = instr_and_parms[1]
try:
self.__getattribute__(f"_op_{instr}")(instr_parms)
except AttributeError as err:
raise AttributeError(f"Bad instruction {instr} at {self._ip}") from err
if instr != "jnz":
self._ip += 1
def int_or_reg_val(self, x) -> int:
if x in self._registers:
return self._registers[x]
else:
return int(x)
def _op_cpy(self, instr_parms:list):
src, dst = instr_parms
self._registers[dst] = self.int_or_reg_val(src)
def _op_inc(self, instr_params:list):
self._registers[instr_params[0]] += 1
def _op_dec(self, instr_params:list):
self._registers[instr_params[0]] -= 1
def _op_jnz(self, instr_params:list):
reg_or_val, jump_val = instr_params
if str.isalpha(jump_val):
assert jump_val in self._registers, jump_val + " must be a register."
jump_val = self._registers[jump_val]
if reg_or_val in self._registers and self._registers[reg_or_val] != 0:
self._ip += int(jump_val)
elif str.isnumeric(reg_or_val) and int(reg_or_val) != 0:
self._ip += int(jump_val)
else:
self._ip += 1
def __repr__(self):
return f"{self.__class__.__name__}{self._registers}"
The main
function for Part 1 is straightforward:
def main():
input_file = os.path.join(SCRIPT_DIR, INPUT_FILE)
with open(input_file, mode="rt") as f:
data = f.read().splitlines()
computer = Computer()
computer.run_program(data)
logger.info(f"Part 1: {computer}")
If you instead initialize register c
to 1
, what value is now left in register a
?
This is a simple change. We just need to create a new Computer
instance, set register c
to 1, and run the program again.
computer = Computer()
computer.set_register('c', 1)
computer.run_program(data)
logger.info(f"Part 2: {computer}")
Here’s the final code and output:
import logging
import os
import time
SCRIPT_DIR = os.path.dirname(__file__)
INPUT_FILE = "input/input.txt"
SAMPLE_INPUT_FILE = "input/sample_input.txt"
logging.basicConfig(level=logging.DEBUG,
format="%(asctime)s.%(msecs)03d:%(levelname)s:%(name)s:\t%(message)s",
datefmt='%Y-%m-%d %H:%M:%S')
logger = logging.getLogger("Assembunny")
logger.setLevel(logging.INFO)
# (Computer class from above)
def main():
input_file = os.path.join(SCRIPT_DIR, INPUT_FILE)
with open(input_file, mode="rt") as f:
data = f.read().splitlines()
computer = Computer()
computer.run_program(data)
logger.info(f"Part 1: {computer}")
computer = Computer()
computer.set_register('c', 1)
computer.run_program(data)
logger.info(f"Part 2: {computer}")
if __name__ == "__main__":
t1 = time.perf_counter()
main()
t2 = time.perf_counter()
print(f"Execution time: {t2 - t1:0.4f} seconds")
And the output:
Part 1: Computer({'a': 42, 'b': 0, 'c': 0, 'd': 0})
Part 2: Computer({'a': 42, 'b': 0, 'c': 1, 'd': 0})