[Advent Of Code 2022] Day 11: Monkey in the Middle [SPOILERS]

Had fun refactoring this!

Ruby Solution
class Monkey < Struct.new(:items, :worry_level, :throw_to)
  attr_reader :inspected_items

  def process(monkeys)
    @inspected_items ||= 0

    while items.any?
      adjusted_worry_level = worry_level.(items.shift)
      monkeys[throw_to.(adjusted_worry_level)].items.push(adjusted_worry_level)
      @inspected_items += 1
    end
  end
end

def monkey_business_level(rounds, adjust_worry_level)
  monkeys = File.read('inputs/11.txt',).split("\n\n").map(&:lines).map { |lines| lines.map { _1.scan(/[\w+*]+/) } }.map do |lines|
    items = lines[1][2..].map(&:to_i)
    worry_level = case lines[2][-2..]
      in ['*', 'old'] then -> { adjust_worry_level.(_1 * _1) }
      in ['+', value] then -> { adjust_worry_level.(_1 + value.to_i) }
      in ['*', value] then -> { adjust_worry_level.(_1 * value.to_i) }
    end
    throw_to = -> { (_1 % lines[3].last.to_i).zero? ? lines[4].last.to_i : lines[5].last.to_i }

    Monkey.new(items, worry_level, throw_to)
  end

  rounds.times { monkeys.each { _1.process(monkeys) } }
  monkeys.map(&:inspected_items).sort.last(2).inject(&:*)
end

a = monkey_business_level(20, -> { _1 / 3 })
b = monkey_business_level(10_000, -> { _1 % 9_699_690 })

require 'minitest/autorun'

describe 'day 11' do
  it 'part a' do assert_equal 88_208, a end
  it 'part b' do assert_equal 21_115_867_968, b end
end

I spent too much time trying to parse the input line by line until I chose to use a regular expression.

Python Solution
re_monkey = re.compile(
        r'Monkey \d+:\n'
        r'  Starting items: (\d+(?:, \d+)*)\n'
        r'  Operation: new = ((?:\bold\b|[ 0-9*+-])+)\n'
        r'  Test: divisible by (\d+)\n'
        r'    If true: throw to monkey (\d+)\n'
        r'    If false: throw to monkey (\d+)'
        )


@dataclass
class Monkey:
    """A monkey modifies, inspects and throws items."""
    items: list[int]
    operation: Callable[[int], int]
    divisor: int
    target_if_false: int
    target_if_true: int


def read_file(file: TextIO) -> list[Monkey]:
    """Read monkeys from the `file`."""
    data = file.read()
    res = []
    for mat in re_monkey.finditer(data):
        res.append(Monkey(
            items=[int(chunk) for chunk in mat.group(1).split(', ')],
            operation=eval(f'lambda old: {mat.group(2)}'),
            divisor=int(mat.group(3)),
            target_if_true=int(mat.group(4)),
            target_if_false=int(mat.group(5))
            ))
    return res


def monkey_business(
        monkeys: list[Monkey],
        rounds: int,
        reduce_stress_level: Callable[[int], int]
        ) -> int:
    """Calculate the level of monkey business after a number of rounds."""
    counter = collections.Counter()
    for _ in range(rounds):
        for i, monkey in enumerate(monkeys):
            counter[i] += len(monkey.items)
            for item in monkey.items:
                item = monkey.operation(item)
                item = reduce_stress_level(item)
                if item % monkey.divisor == 0:
                    target = monkey.target_if_true
                else:
                    target = monkey.target_if_false
                monkeys[target].items.append(item)
            monkey.items.clear()
    return product(count for idx, count in counter.most_common(2))


def part1(file: TextIO) -> int:
    """Solve the first part of the puzzle."""
    monkeys = read_file(file)
    return monkey_business(monkeys, 20, lambda item: item // 3)


def part2(file: TextIO) -> int:
    """Solve the second part of the puzzle."""
    monkeys = read_file(file)
    modulus = product(monkey.divisor for monkey in monkeys)
    return monkey_business(monkeys, 10_000, lambda item: item % modulus)

I originally parsed the input line-by-line with a str.startswith() but regex is nicer.

Someone pointed me at the parse module which looks phenomenal for this. Think format string or template string, but in reverse!

Code: Just Make It WorkFinal

Python RE Parser
MONKEY_RE = re.compile(r"""
Monkey (?P<id>\d):
  Starting items: (?P<items>(?:\d+, )*\d+)
  Operation: new = old (?P<operator>[+*]) (?P<operand>\d+|old)
  Test: divisible by (?P<test_num>\d+)
    If true: throw to monkey (?P<true>\d+)
    If false: throw to monkey (?P<false>\d+)
""".strip())

    def input_parser(self, puzzle_input: str) -> InputType:
        monkeys = []
        for block in puzzle_input.split("\n\n"):
            match = MONKEY_RE.match(block)
            if not match:
                raise ValueError(f"Block did not match regex. {block}")
            numbers = {k: int(v) for k, v in match.groupdict().items() if v.isdigit()}
            monkeys.append(Monkey(
                id=numbers["id"],
                items=[int(i) for i in match.group("items").split(", ")],
                operator={"+": operator.add, "*": operator.mul}[match.group("operator")],
                operand=None if match.group("operand") == "old" else numbers["operand"],
                test=numbers["test_num"],
                true=numbers["true"],
                false=numbers["false"],
                inspected=0,
            ))
        return monkeys
Python Solver
    def solver(self, monkeys: list[Monkey], rounds: int, div: bool) -> int:
        """Simulate rounds of monkeys inspecting and throwing items."""
        # Use the LCM to keep the item size low.
        lcm = math.lcm(*[m.test for m in monkeys])

        # Cycle through rounds and monkeys.
        for _ in range(rounds):
            for monkey in monkeys:
                # Track how many items the monkey inspected.
                monkey.inspected += len(monkey.items)
                # For each item, update values and throw it to another monkey.
                for item in monkey.items:
                    item = monkey.operator(item, monkey.operand or item)
                    if div:
                        item = item // 3
                    item %= lcm
                    # Throw the item to the next monkey.
                    next_monkey = monkey.true if (item % monkey.test == 0) else monkey.false
                    monkeys[next_monkey].items.append(item)
                monkey.items = []
        inspected = sorted(monkey.inspected for monkey in monkeys)
        return self.mult(inspected[-2:])

I did poorly on the leaderboard today because I tried to use a lambda to save the various test/operations as a function … but fell prey to the lambda late binding trap. This resulted in much head scratching, confusion and debugging.