Puzzle #10

Β 22nd July 2023 at 2:40pm

First part

It took me a while to understand the problem, and it becomes substantially easier upon realising the instructions are not in chronological order. I've divided the problem in smaller tasks, and am first taking care of the input.

Input parsing

I'm still trying to do it functionally. Naturally I thought of parsing the whole input, and separate the attribution of sole values

value 43 goes to bot 90

from the exchanges of values between bots, and bots and outputs.

bot 100 gives low to bot 101 and high to bot 31

bot 148 gives low to output 19 and high to bot 156

These attributions and exchanges will be stored in two separate lists.

In the first list, objects are of the form (value, bot), meaning the value to be attributed to the bot.

In the second list, objects are of the form (bot, [b/o]number, [b/o]number), where bot stands for the bot handing out values, and each element is a string whose first character is a subject (o for output or b for bot) andΒ the rest is the corresponding number; eg: ('34', 'o12', 'b111') stands for bot 34 gives low to output 12 and high to bot 111.

This is the code so far:


import re

VALUE_REGEX = r"\b\d+\b"
BOT_REGEX = r"""bot (\d+) gives low to \b(\w)\w* (\d+) and high to \b(\w)\w* (\d+)"""


def input_parser(
        attrs: list,
        exchgs: list,
        input_lines: list[str]
        ):
    if not input_lines:
        return attrs, exchgs
    else:
        line = input_lines[0]
    # value 43 goes to bot 90
    if 'value' in line:
        value, bot = re.findall(VALUE_REGEX, line)
        return input_parser(attrs + [(value, bot)],
                            exchgs,
                            input_lines[1:])
    # bot 200 gives low to bot 174 and high to bot 87
    # bot 148 gives low to output 19 and high to bot 156
    else:
        bot, l_sbj, low, h_sbj, high = re.findall(BOT_REGEX, line)[0]
        return input_parser(attrs,
                            exchgs + [(bot, l_sbj + low, h_sbj + high)],
                            input_lines[1:])

Now I can move onto giving out the initial values – the attributions.

Attributions

I'm thinking of having a dictionary whose keys are bot IDs and values are empty tuples; as attributions are made, these tuples are substituted by a new tuple with the attribution (we won't ever do mutation of data). Through inspection and some python tricks, I know bot 17 will start with two values (some has to, otherwise there would never be any action).

def make_attributions(
        bots: dict,
        attrs: list[tuple]
        ) -> dict:
    def produce_new_attribution(
            old: dict,
            key: int,
            value: int
            ) -> dict:
        new = copy(old)
        new.update({key: old[key] + (value,)})
        return new
    if not attrs:
        return bots
    else:
        value, bot = attrs[0]
        return make_attributions(
                produce_new_attribution(bots, 'b' + bot, value),
                attrs[1:]
                )

The attributions are done without any major hurdles. Moving onto the exchanges.

Exchanges

Also by inspection, I noticed that bots will only do one exchange, if any. This makes the exercise a little simpler.

To make an exchange, a bot must have two values. So, at any given state of the bot collective (the bots dictionary), whenever a bot holds two values, it can be matched to its instruction.

def make_exchanges(
        bots: dict,
        exchgs: list,
        bot_pairs: list,
        ) -> dict:
    def produce_new_exchange(
            old: dict,
            exch: tuple
            ) -> dict:
        bot, taking_low, taking_high = exch
        value_low, value_high = sorted(list(map(int, old['b' + bot])))
        if (value_low, value_high) == (17, 61):
            print(f'the bot is {bot}')
        new = copy(old)
        new.update({'b' + bot: tuple()})
        new.update({taking_low: old[taking_low] + (value_low,)})
        new.update({taking_high: old[taking_high] + (value_high,)})
        return new
    if not exchgs:
        return bots, bot_pairs
    else:
        # get any bot liable for exchanging
        bot = list(filter(lambda bot: len(bots[bot]) == 2, bots))[0]
        # get the corresponding exchange
        match = list(filter(lambda ex: ex[0] == bot[1:], exchgs))[0]
        return make_exchanges(
                produce_new_exchange(bots, match),
                [exch for exch in exchgs if exch != match],
                bot_pairs + [[match[0],
                             match[1][1:],
                             match[2][1:]]]
                )


with open("input") as file_input:
    INPUT = file_input.read().splitlines()

attrs, exchgs = input_parser([], [], INPUT)

attributed = make_attributions(
        defaultdict(lambda: tuple()),
        attrs
        )

exchanged = make_exchanges(
        attributed,
        exchgs,
        []
        )

This finishes Part 1. In hindsight, the problem was not so complicated. I'm glad I made the decision to separate bot and output early on, which came in handy for Part 2.


Part 2

print(reduce(lambda x, y: int(x) * int(y),
       [exchanged[0]['o0'][0], exchanged[0]['o1'][0], exchanged[0]['o2'][0]]))

The syntax is a little messy due to single value tuples, but the result is correct!