2022-12-11 12:58:35 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import dataclasses
|
2022-12-11 13:29:21 +01:00
|
|
|
from math import lcm
|
2022-12-11 12:58:35 +01:00
|
|
|
|
|
|
|
|
|
|
|
def main(filename: str, expected_part_1: int = None, expected_part_2: int = None):
|
|
|
|
print(f"\n+ Running on {filename}")
|
|
|
|
with open(filename) as f:
|
|
|
|
data = f.read().strip().split("\n\n")
|
|
|
|
|
2022-12-11 13:29:21 +01:00
|
|
|
data_1 = parse_data(data)
|
|
|
|
solution_part_1 = solve_part_1(data_1)
|
2022-12-11 12:58:35 +01:00
|
|
|
|
|
|
|
print(f"1. Found {solution_part_1}")
|
|
|
|
if expected_part_1:
|
|
|
|
assert expected_part_1 == solution_part_1
|
|
|
|
|
2022-12-11 13:29:21 +01:00
|
|
|
data_2 = parse_data(data)
|
|
|
|
solution_part_2 = solve_part_2(data_2)
|
2022-12-11 12:58:35 +01:00
|
|
|
print(f"2. Found {solution_part_2}")
|
|
|
|
if expected_part_2:
|
|
|
|
assert expected_part_2 == solution_part_2
|
|
|
|
|
|
|
|
|
|
|
|
@dataclasses.dataclass
|
|
|
|
class Operation:
|
|
|
|
multiplier: int = 1
|
|
|
|
adder: int = 0
|
|
|
|
square: bool = False
|
|
|
|
|
2022-12-11 13:29:21 +01:00
|
|
|
def compute(self, value: int, worry_decreases: bool, divider: int) -> int:
|
2022-12-11 12:58:35 +01:00
|
|
|
if self.square:
|
|
|
|
value = value * value
|
|
|
|
else:
|
|
|
|
value = value * self.multiplier + self.adder
|
|
|
|
|
2022-12-11 13:29:21 +01:00
|
|
|
if worry_decreases:
|
|
|
|
value = int(value / divider)
|
|
|
|
else:
|
|
|
|
value %= divider
|
|
|
|
return value
|
2022-12-11 12:58:35 +01:00
|
|
|
|
|
|
|
|
|
|
|
@dataclasses.dataclass
|
|
|
|
class Monkey:
|
|
|
|
game: Game
|
|
|
|
items: list[int]
|
|
|
|
divisible_by: int
|
|
|
|
true_monkey: int
|
|
|
|
false_monkey: int
|
|
|
|
operation: Operation
|
|
|
|
inspected_items: int = 0
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_input(cls, text: str, game: Game) -> Monkey:
|
|
|
|
monkey_description = text.split("\n")
|
|
|
|
items = []
|
|
|
|
operation = None
|
|
|
|
divisible_by = 1
|
|
|
|
true_monkey = 0
|
|
|
|
false_monkey = 0
|
|
|
|
for line in monkey_description[1:]:
|
|
|
|
match line.strip().split(": "):
|
|
|
|
case ["Starting items", text_items]:
|
|
|
|
items = [int(item) for item in text_items.split(", ")]
|
|
|
|
case ["Operation", text_operation]:
|
|
|
|
match text_operation.split():
|
|
|
|
case ["new", "=", "old", "*", "old"]:
|
|
|
|
operation = Operation(square=True)
|
|
|
|
case ["new", "=", "old", "*", multiplier]:
|
|
|
|
operation = Operation(multiplier=int(multiplier))
|
|
|
|
case ["new", "=", "old", "+", adder]:
|
|
|
|
operation = Operation(adder=int(adder))
|
|
|
|
case ["Test", text_test]:
|
|
|
|
divisible_by = int(text_test.replace("divisible by", "").strip())
|
|
|
|
case ["If true", text_target]:
|
|
|
|
true_monkey = int(
|
|
|
|
text_target.replace("throw to monkey", "").strip()
|
|
|
|
)
|
|
|
|
case ["If false", text_target]:
|
|
|
|
false_monkey = int(
|
|
|
|
text_target.replace("throw to monkey", "").strip()
|
|
|
|
)
|
|
|
|
monkey = Monkey(game, items, divisible_by, true_monkey, false_monkey, operation)
|
|
|
|
game.monkeys.append(monkey)
|
|
|
|
return monkey
|
|
|
|
|
2022-12-11 13:29:21 +01:00
|
|
|
def play_round(self, worry_decreases: bool) -> None:
|
2022-12-11 12:58:35 +01:00
|
|
|
self.inspected_items += len(self.items)
|
|
|
|
for item in self.items:
|
2022-12-11 13:29:21 +01:00
|
|
|
new_value = self.operation.compute(item, worry_decreases, self.game.divider)
|
2022-12-11 12:58:35 +01:00
|
|
|
if new_value % self.divisible_by == 0:
|
|
|
|
self.throw(self.true_monkey, new_value)
|
|
|
|
else:
|
|
|
|
self.throw(self.false_monkey, new_value)
|
|
|
|
self.items = []
|
|
|
|
|
|
|
|
def throw(self, target: int, item: int) -> None:
|
|
|
|
self.game.monkeys[target].items.append(item)
|
|
|
|
|
|
|
|
def print(self) -> None:
|
|
|
|
print(", ".join(str(item) for item in self.items))
|
|
|
|
|
|
|
|
|
|
|
|
@dataclasses.dataclass
|
|
|
|
class Game:
|
|
|
|
monkeys: list[Monkey] = dataclasses.field(default_factory=list)
|
2022-12-11 13:29:21 +01:00
|
|
|
divider: int = 3
|
2022-12-11 12:58:35 +01:00
|
|
|
|
2022-12-11 13:29:21 +01:00
|
|
|
def play_round(self, worry_decreases: bool) -> None:
|
2022-12-11 12:58:35 +01:00
|
|
|
for monkey in self.monkeys:
|
2022-12-11 13:29:21 +01:00
|
|
|
monkey.play_round(worry_decreases)
|
2022-12-11 12:58:35 +01:00
|
|
|
|
|
|
|
def get_ranked_monkeys(self) -> list[Monkey]:
|
|
|
|
return sorted(
|
|
|
|
self.monkeys, key=lambda monkey: monkey.inspected_items, reverse=True
|
|
|
|
)
|
|
|
|
|
|
|
|
def print_monkeys(self) -> None:
|
|
|
|
for index, monkey in enumerate(self.monkeys):
|
|
|
|
print(f"Monkey {index}", end=": ")
|
|
|
|
monkey.print()
|
|
|
|
|
|
|
|
|
2022-12-11 13:29:21 +01:00
|
|
|
def parse_data(data: list[str]) -> Game:
|
2022-12-11 12:58:35 +01:00
|
|
|
game = Game()
|
|
|
|
for text in data:
|
|
|
|
Monkey.from_input(text, game)
|
|
|
|
return game
|
|
|
|
|
|
|
|
|
2022-12-11 13:29:21 +01:00
|
|
|
def solve_part_1(game: Game) -> int:
|
2022-12-11 12:58:35 +01:00
|
|
|
for _ in range(1, 21):
|
2022-12-11 13:29:21 +01:00
|
|
|
game.play_round(worry_decreases=True)
|
2022-12-11 12:58:35 +01:00
|
|
|
sorted_monkeys = game.get_ranked_monkeys()
|
|
|
|
return sorted_monkeys[0].inspected_items * sorted_monkeys[1].inspected_items
|
|
|
|
|
|
|
|
|
2022-12-11 13:29:21 +01:00
|
|
|
def solve_part_2(game: Game) -> int:
|
|
|
|
game.divider = lcm(*(monkey.divisible_by for monkey in game.monkeys))
|
|
|
|
for _ in range(1, 10_001):
|
|
|
|
game.play_round(worry_decreases=False)
|
|
|
|
sorted_monkeys = game.get_ranked_monkeys()
|
|
|
|
return sorted_monkeys[0].inspected_items * sorted_monkeys[1].inspected_items
|
2022-12-11 12:58:35 +01:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2022-12-11 13:29:21 +01:00
|
|
|
main("inputs/day11-test1", expected_part_1=10605, expected_part_2=2713310158)
|
|
|
|
main("inputs/day11", expected_part_1=95472, expected_part_2=17926061332)
|