Day 10: Balance Bots

author: Harshvardhan Pandit

license: MIT

link to problem statement

You come upon a factory in which many robots are zooming around handing small microchips to each other.

Upon closer examination, you notice that each bot only proceeds when it has two microchips, and once it does, it gives each one to a different bot or puts it in a marked "output" bin. Sometimes, bots take microchips from "input" bins, too.

Inspecting one of the microchips, it seems like they each contain a single number; the bots must use some logic to decide what to do with each chip. You access the local control computer and download the bots' instructions (your puzzle input).

Some of the instructions specify that a specific-valued microchip should be given to a specific bot; the rest of the instructions indicate what a given bot should do with its lower-value or higher-value chip.

For example, consider the following instructions:

  • value 5 goes to bot 2
  • bot 2 gives low to bot 1 and high to bot 0
  • value 3 goes to bot 1
  • bot 1 gives low to output 1 and high to bot 0
  • bot 0 gives low to output 2 and high to output 0
  • value 2 goes to bot 2

Which leads to:

  • Initially, bot 1 starts with a value-3 chip, and bot 2 starts with a value-2 chip and a value-5 chip.
  • Because bot 2 has two microchips, it gives its lower one (2) to bot 1 and its higher one (5) to bot 0.
  • Then, bot 1 has two microchips; it puts the value-2 chip in output 1 and gives the value-3 chip to bot 0.
  • Finally, bot 0 has two microchips; it puts the 3 in output 2 and the 5 in output 0.

In the end, output bin 0 contains a value-5 microchip, output bin 1 contains a value-2 microchip, and output bin 2 contains a value-3 microchip. In this configuration, bot number 2 is responsible for comparing value-5 microchips with value-2 microchips.

Based on your instructions, what is the number of the bot that is responsible for comparing value-61 microchips with value-17 microchips?

Solution logic

Let us first understand the requirements.

  • There are bots, which have certain properties, and actions. This is a good use case for modeling them using class.
  • Each bot can handle two chips.
  • If at any time a bot has two chips, the bot follows the rules specified in the instructions to hand over one or both chips to some other bots, or to the output bin as specified.

The question is to find out which bot (number) is responsible for handling chips 61 and 17, which means that both chips are with the bot at the same time.

We model the bot class as:

Bot:

    attributes:
        number
        chips - 0, 1, 2
        handle chips:
            give low to <>
            give high to <>

    actions:
        take chip:
            add chip to chips
        handle chips:
            if there are two chips:
                resolve which is lower and which is higher
                give chips to bots/bins specified by the rules            

Reading in the rules line by line, we add bots to the bot-list, and check at each iteration if a bot has two chips, whether they are the specified chips, and if yes, we get the answer as the bot number. We maintain two lists, one for the bots, and the other for output bins. The bots will be a dictionary addressed by the bot's number whose keys will be the class object. The output bins be a dictionary whose keys will be lists for holding chips in that particular output bin.

Note: We always maintain the bot's chips in sorted order to

Reading in the input


In [1]:
with open('../inputs/day10.txt', 'r') as f:
    data = [line.strip() for line in f.readlines()]

Class Bot to hold the bot instances


In [2]:
class Bot:
    def __init__(self, number):
        self.number = number
        self.chips = []
        self.give_low = None
        self.give_high = None      
    
    def __repr__(self):
        return 'number: %s chips: %s low: %s high: %s' % (self.number, self.chips, self.give_low, self.give_high)

Creating the necessary data structures and regex patterns.

The bot instructions each contain a word specifying who the chip is from/going to and the chip number We use this information to create the instruction_type structure to hold the type and number of the instruction.


In [3]:
from collections import namedtuple
instruction_type = namedtuple('instruction_type', ('type', 'number'))

import re

bot_instruction = re.compile('(\w+) (\d+)')
input_instruction = re.compile('(\d+)')

bot_list = {}
output_bins = {}

Helper functions

The ensure... functions are there to help (or clear up the code) when accessing bots and output bins as they are encountered in the instructions. Another (elegant?) way to do this would be to wrap each of the calls to the dictionary in a try...except and catch IndexError for invalid accesses.


In [4]:
def ensure_bot_is_registered(bot_number):
    if bot_number not in bot_list:
        bot_list[bot_number] = Bot(bot_number)

        
def ensure_bin_is_registered(bin_number):
    if bin_number not in output_bins:
        output_bins[bin_number] = []

Actions for bots - giving out chips

Each bot will only act when it has two chips, and it will give those two chips according to the rules specified. The function returns True if the specified action was performed, False otherwise.


In [5]:
def bot_chip_handler(bot_number):
    # retrieve the bot
    bot = bot_list[bot_number]
    
    # check if it has two chips
    if len(bot.chips) < 2:
        return False

    # check if it has the specified chip
    if 61 in bot.chips and 17 in bot.chips:
        print('answer', bot.number)
    
    # check if it has no instructions, which means no actions
    if bot.give_low is None and bot.give_high is None:
        return False
    
    # give low chip
    if bot.give_low.type == 'bot':
        # give chip to bot
        bot_list[bot.give_low.number].chips.append(bot.chips[0])
        bot_list[bot.give_low.number].chips.sort()
    else:
        # give chip to output bin
        output_bins[bot.give_low.number].append(bot.chips[0])

    # give high chip
    if bot.give_high.type == 'bot':
        # give chip to bot
        bot_list[bot.give_high.number].chips.append(bot.chips[1])
        bot_list[bot.give_high.number].chips.sort()
    else:
        # give chip to output bin
        output_bins[bot.give_high.number].append(bot.chips[1])

    # remove chips from bot
    bot.chips = []
    # return action succesfull
    return True

main

Here we take each in the given instructions, identify whether the line is an instruction for the bot, or an action where bots are given/take chips from the input bin. The instruction lines start with bot and the action lines start with value.

For bot instructions, the appropriate bot is retrieved from list, its rules are added to it.

For actions, the bot is retrieved and the chip is added to it. The chip handler function is then run to determine what to do with the bot.

At the end of each iteration, we go from each both in increasing order by running the chip handler, and if any bot gives out chip to another bot, we start again, since the bot can give chips to other bots with a number lesser than theirs.


In [6]:
for line in data:
    if line.startswith('bot'):
        # this is an instruction for the bot
        # get the tokens from the line
        main_bot, give_low, give_high = [
            # the first is a word, the second is the bot/bin number
            instruction_type(token[0].strip(), int(token[1]))
            for token in bot_instruction.findall(line)]
        
        # retrieve the bot
        ensure_bot_is_registered(main_bot.number)
        main_bot = bot_list[main_bot.number]
        
        # handle low/high chip rules
        # and whether they are for bots or bins
        if give_low.type == 'bot':
            ensure_bot_is_registered(give_low.number)
        else:
            ensure_bin_is_registered(give_low.number)
        main_bot.give_low = give_low
        if give_high.type == 'bot':
            ensure_bot_is_registered(give_high.number)
        else:
            ensure_bin_is_registered(give_high.number)
        main_bot.give_high = give_high
        
    else:
        # this is an action where bots are given chips from the input bin
        # there are only two values - the chip value and the bot number
        chip_number, bot_number = map(int, input_instruction.findall(line))
        ensure_bot_is_registered(bot_number)
        bot = bot_list[bot_number]
        bot.chips.append(chip_number)
        bot.chips.sort()
        # run handler on this bot since it could now have two chips
        bot_chip_handler(bot.number)
    
    # run handlers on all bots in an increasing order
    # this is an arbitary choice, it is not specified in the input
    condition = True
    while condition:
        condition = False
        for bot_number in sorted(bot_list.keys()):
            returned_flag = bot_chip_handler(bot_number)
            if returned_flag == True:
                condition = True
                break


answer 86

Part Two

What do you get if you multiply together the values of one chip in each of outputs 0, 1, and 2?

Solution logic

We need to multiple one of the values in output bins 0, 1, and 2. This means either that they contain only one value or that we have to choose only the first one. In any case, we access only the first element in each bin.

reduce(mul, [output_bins[i][0] for i in range(3)])

This is the functional way to write -

n = 1
for i in range 0 to 2 (3 - 1 or i < 3)
    n *= output[i][0] 

In [7]:
from functools import reduce
from operator import mul
print('answer', reduce(mul, [output_bins[i][0] for i in range(3)]))


answer 22847

== END ==