Day 18

We've got a somewhat similar problem to the one about dancing. Here we have a set of sounds that are going to be assigned different frequency. We have a notion of a sound being 'played' which sort of serves as a marker in the program. The sounds can also be 'recovered' which reverts them back to their most recent played value. This again looks like I'm going to come up with a function or few that mimic the moves and then let it run. I then expect the second part to ask me about 1 trillion iterations or something and I'll have to come up with a trick.

Start by loading up the instructions and also creating some data structures to hold current and most recent data:

In [1]:
import string

instructions = open('/Users/Sven/py_files/aoc_2017/d18_input.txt').readlines()
instructions = [inst.replace('\n', '') for inst in instructions]
In [2]:
# dictionaries seem good for this kind of data:
letters = string.ascii_letters
zeroes = [0 for i in range(26)]
played = []
current = dict(zip(letters, zeroes))

Now we'll go ahead and make a function to handle the instructions... I guess it's probably better to just use a single one isntead of many

In [3]:
def read_inst(play, curr, i):
    # get the type of move, target, and value
    inst = instructions[i].split()
    inst_t, inst_x = inst[0], inst[1] 
    if len(inst) == 3:
        inst_y = inst[2]
        # if the instructions reference one of the registers, get that value
        # otherwise, turn to int:
        if inst_y in letters:
            inst_y = current[inst_y]
        else:
            inst_y = int(inst_y)
    # note that we haven't determined if inst_x is int or str
    
    # Use this to determine if we've recovered something or not:
    rcv = 0
    
    # So I guess we gotta just go through all the cases now... I looked up switches
    # in Python but they don't seem that much better than a longass ifelse here
    if inst_t == 'snd':
        play = [curr[inst_x]]
    elif inst_t == 'set':
        curr[inst_x] = inst_y
    elif inst_t == 'add':
        curr[inst_x] += inst_y
    elif inst_t == 'mul':
        curr[inst_x] *= inst_y
    elif inst_t == 'mod':
        curr[inst_x] %= inst_y
    elif inst_t == 'rcv':
        if curr[inst_x] > 0:
            print(play)
            rcv = 1
    elif inst_t == 'jgz':
        if curr[inst_x] != 0:
            i += inst_y
    
    # now increment i if we need to:
    if inst_t != 'jgz' or curr[inst_x] == 0:
        i += 1
    return(play,  curr, i, rcv)

Now we keep iterating the function until we receive a nonzero value for rcv

In [4]:
i = 0
play = played
curr = current
rcv = 0
while rcv == 0:
    play, curr, i, rcv = read_inst(play, curr, i)
[8600]

Dang, that multiple return value is much simpler than in R. I'm not sure I really like how I used it as 4 return values seems like a bit much but we'll consider using that in the future.

Part 2:

We've been informed that this is not actually a question about sounds, it's about sending and receiving values between two simultaneous executions of the program. The snd and rcv keywords send and receive a value from one side to the other. The two programs have different starting registries. Each program runs the set of instructions in order however they will sit and wait to receive instructions if none are present in the queue for them. We wish to run the program until either both sides are waiting to receive or the program runs its course.

Alright, I'm going to try a new approach using class definitions which is a little uncomfortable for me but let's try. I'm much more comfortable with the functional programming paradigm but I should learn how to work with this perspective.

First a class definition for the computers:

In [5]:
class program:
    
    # Now we make the initialization function which gives it the basic properties
    def __init__(self, name, init_reg):
        letters = string.ascii_letters
        zeroes = [init_reg for i in range(26)]
        # name
        self.name = name
        # Registry
        self.reg = dict(zip(letters, zeroes))
        # Incomming queue
        self.queue = []
        # location within the instructions
        self.i = 0
        # Is it stalled?
        self.stalled = 0
        # How many sent?
        self.n_sent = 0
        
    # Method to receive data, we'll ignore None as inputs so we can
    # just have the method receive that when the other program is not sending
    def add_to_queue(self, val):
        if val is not None:
            self.queue.append(val)
            self.stalled = 0
        
    # Method to read instrutions
    def read_instructions(self):
                
        # check if we're beyond the bounds of the instructions
        if self.i >= len(instructions) or self.i < 0:
            self.stalled = 1
            return(None)
        
        # get the type of move, target, and value
        inst = instructions[self.i].split()
        #if self.name == 'a':
        #    print(inst)
        inst_t, inst_x = inst[0], inst[1] 
        if len(inst) == 3:
            inst_y = inst[2]
            # if the instructions reference one of the registers, get that value
            # otherwise, turn to int:
            if inst_y in string.ascii_letters:
                inst_y = self.reg[inst_y]
            else:
                inst_y = int(inst_y)
            # note that we haven't determined if inst_x is int or str
               
        # If we need to return something:
        ret = None
        
        # So I guess we gotta just go through all the cases now... I looked up switches
        # in Python but they don't seem that much better than a longass ifelse here
        if inst_t == 'snd':
            ret = self.reg[inst_x]
            self.n_sent += 1
        elif inst_t == 'set':
            self.reg[inst_x] = inst_y
        elif inst_t == 'add':
            self.reg[inst_x] += inst_y
        elif inst_t == 'mul':
            self.reg[inst_x] *= inst_y
        elif inst_t == 'mod':
            self.reg[inst_x] %= inst_y
        elif inst_t == 'rcv':
            # if there's items in the queue, use them
            if len(self.queue) > 0:
                self.reg[inst_x] = self.queue.pop(0)
                self.stalled = 0
            # Otherwise, set the stall flag, don't increment i
            else:
                self.stalled = 1
                return(None)
        elif inst_t == 'jgz':
            # Deal with cases of inst_x being an integer:
            if inst_x in self.reg:
                chk = self.reg[inst_x]
            else:
                chk = int(inst_x)
            if chk > 0:
                self.i += inst_y

        # now increment i if we need to:
        if inst_t != 'jgz' or chk <= 0:
            self.i += 1
        return(ret)
    
# Initialize our programs.
progA = program('a', 0)
progB = program('b', 1)    

Now we'll let the programs go while at least one program is not stalled:

In [6]:
while True:
    while progA.stalled == 0:
        progB.add_to_queue(progA.read_instructions())
    while progB.stalled == 0:
        progA.add_to_queue(progB.read_instructions())
    if progA.stalled != 0 and progB.stalled != 0:
        break
progB.n_sent
Out[6]:
7239

Well I don't know if this was an extremely ugly or nice solution but I'm somewhat proud of it. It seems like the basic implementation of classes in python is not that difficult. I'm sure we can make it so in the future though!