Learning Python with Advent of Code Walkthroughs

Dazbo's Advent of Code solutions, written in Python

Leonardo's Monorail

Advent of Code 2016 - Day 12

Day 12: Leonardo's Monorail

Useful Links

Concepts and Packages Demonstrated

Introspection

Problem Intro

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:

Part 1

After executing the instructions, what value is left in register a?

My strategy is to create a Computer class that simulates the assembunny processor.

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}")

Part 2

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}")

Results

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})