Skip to content

Commit 62fa42c

Browse files
committed
feat: aoc 2022 day 11 solution + tests
1 parent 8b8bf7b commit 62fa42c

File tree

5 files changed

+343
-0
lines changed

5 files changed

+343
-0
lines changed

_2022/data/day11/puzzle_input.txt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
Monkey 0:
2+
Starting items: 91, 66
3+
Operation: new = old * 13
4+
Test: divisible by 19
5+
If true: throw to monkey 6
6+
If false: throw to monkey 2
7+
8+
Monkey 1:
9+
Starting items: 78, 97, 59
10+
Operation: new = old + 7
11+
Test: divisible by 5
12+
If true: throw to monkey 0
13+
If false: throw to monkey 3
14+
15+
Monkey 2:
16+
Starting items: 57, 59, 97, 84, 72, 83, 56, 76
17+
Operation: new = old + 6
18+
Test: divisible by 11
19+
If true: throw to monkey 5
20+
If false: throw to monkey 7
21+
22+
Monkey 3:
23+
Starting items: 81, 78, 70, 58, 84
24+
Operation: new = old + 5
25+
Test: divisible by 17
26+
If true: throw to monkey 6
27+
If false: throw to monkey 0
28+
29+
Monkey 4:
30+
Starting items: 60
31+
Operation: new = old + 8
32+
Test: divisible by 7
33+
If true: throw to monkey 1
34+
If false: throw to monkey 3
35+
36+
Monkey 5:
37+
Starting items: 57, 69, 63, 75, 62, 77, 72
38+
Operation: new = old * 5
39+
Test: divisible by 13
40+
If true: throw to monkey 7
41+
If false: throw to monkey 4
42+
43+
Monkey 6:
44+
Starting items: 73, 66, 86, 79, 98, 87
45+
Operation: new = old * old
46+
Test: divisible by 3
47+
If true: throw to monkey 5
48+
If false: throw to monkey 2
49+
50+
Monkey 7:
51+
Starting items: 95, 89, 63, 67
52+
Operation: new = old + 2
53+
Test: divisible by 2
54+
If true: throw to monkey 1
55+
If false: throw to monkey 4

_2022/solutions/day11.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""Day 11: Monkey in the Middle
2+
3+
This module provides the solution for Advent of Code 2022 - Day 11.
4+
5+
It simulates monkeys throwing items based on worry levels and divisibility tests,
6+
tracking inspection counts to calculate the level of monkey business.
7+
8+
The module contains a Monkey class for individual monkey behavior and a Solution
9+
class that inherits from SolutionBase for running the simulation.
10+
"""
11+
12+
from collections.abc import Callable
13+
from math import lcm
14+
import re
15+
16+
from aoc.models.base import SolutionBase
17+
18+
19+
class Monkey:
20+
"""Represent a monkey with items, operations, and throwing rules.
21+
22+
Each monkey holds items with worry levels, performs an operation on each
23+
item during inspection, tests divisibility, and throws items to other
24+
monkeys based on the test result.
25+
"""
26+
27+
def __init__(self, data: str) -> None:
28+
"""Parse monkey configuration from input block.
29+
30+
Args:
31+
data: Multi-line string containing monkey ID, starting items,
32+
operation, divisibility test, and throw targets
33+
"""
34+
lines = data.strip().split("\n")
35+
36+
match = re.search(r"Monkey (\d+):", lines[0])
37+
if match is None:
38+
err_msg = "Invalid monkey ID format"
39+
raise ValueError(err_msg)
40+
self._id = int(match.group(1))
41+
42+
match = re.search(r"Starting items: ([\d, ]+)", lines[1])
43+
if match is None:
44+
err_msg = "Invalid starting items format"
45+
raise ValueError(err_msg)
46+
self.items = [int(x) for x in match.group(1).split(", ")]
47+
48+
match = re.search(r"Operation: new = (.*)$", lines[2])
49+
if match is None:
50+
err_msg = "Invalid operation format"
51+
raise ValueError(err_msg)
52+
operation_str = match.group(1)
53+
self.operation = self._parse_operation(operation_str)
54+
55+
match = re.search(r"Test: divisible by (\d+)$", lines[3])
56+
if match is None:
57+
err_msg = "Invalid test format"
58+
raise ValueError(err_msg)
59+
self.divisor = int(match.group(1))
60+
61+
match = re.search(r"If true: throw to monkey (\d+)$", lines[4])
62+
if match is None:
63+
err_msg = "Invalid if_true format"
64+
raise ValueError(err_msg)
65+
self.if_true = int(match.group(1))
66+
67+
match = re.search(r"If false: throw to monkey (\d+)$", lines[5])
68+
if match is None:
69+
err_msg = "Invalid if_false format"
70+
raise ValueError(err_msg)
71+
self.if_false = int(match.group(1))
72+
73+
self.inspection_count = 0
74+
75+
def _parse_operation(self, operation_str: str) -> Callable[[int], int]:
76+
"""Parse operation string into executable function.
77+
78+
Args:
79+
operation_str: Expression like "old * 19" or "old + 6"
80+
81+
Returns
82+
-------
83+
Callable[[int], int]: Function that applies operation to worry level
84+
"""
85+
return lambda old: eval(operation_str, {"old": old}) # noqa: S307
86+
87+
def inspect_items(self, lcm_value: int, *, is_relieved: bool) -> list[tuple[int, int]]:
88+
"""Inspect all held items and determine where to throw them.
89+
90+
For each item: increment inspection count, apply operation, optionally
91+
divide by 3 (Part 1) or modulo by LCM (Part 2), test divisibility,
92+
and determine target monkey.
93+
94+
Args:
95+
is_relieved: If True, divide worry level by 3 after inspection (Part 1)
96+
lcm_value: Least common multiple of all monkey divisors for modulo
97+
operation to keep numbers manageable (Part 2)
98+
99+
Returns
100+
-------
101+
list[tuple[int, int]]: List of (worry_level, target_monkey_id) tuples
102+
for items to be thrown
103+
"""
104+
throws: list[tuple[int, int]] = []
105+
106+
for item in self.items:
107+
self.inspection_count += 1
108+
worry_level = self.operation(item)
109+
110+
if is_relieved:
111+
worry_level //= 3
112+
else:
113+
worry_level %= lcm_value
114+
115+
# Test and determine target
116+
if worry_level % self.divisor == 0:
117+
throws.append((worry_level, self.if_true))
118+
119+
else:
120+
throws.append((worry_level, self.if_false))
121+
122+
self.items = []
123+
return throws
124+
125+
126+
class Solution(SolutionBase):
127+
"""Simulate monkey item-throwing game and calculate monkey business.
128+
129+
This solution simulates monkeys playing keep-away with items. Part 1
130+
runs 20 rounds with worry relief (divide by 3), while Part 2 runs
131+
10,000 rounds without relief, using LCM modulo arithmetic to keep
132+
worry levels manageable.
133+
134+
Monkey business is the product of the two highest inspection counts.
135+
"""
136+
137+
def simulate(self, data: str, rounds: int, *, is_relieved: bool) -> int:
138+
"""Run monkey simulation for specified rounds.
139+
140+
Each round, monkeys take turns inspecting and throwing all their items.
141+
Uses LCM of all divisors to prevent worry levels from growing too large
142+
in Part 2 while preserving divisibility test results.
143+
144+
Args:
145+
data: Raw input containing monkey configurations
146+
rounds: Number of rounds to simulate
147+
is_relieved: If True, divide worry by 3 after each inspection
148+
149+
Returns
150+
-------
151+
int: Monkey business level (product of top 2 inspection counts)
152+
"""
153+
monkeys = [Monkey(block) for block in data.split("\n\n")]
154+
lcm_value = lcm(*[m.divisor for m in monkeys])
155+
156+
for _ in range(rounds):
157+
for monkey in monkeys:
158+
throws = monkey.inspect_items(lcm_value, is_relieved=is_relieved)
159+
for item, target_id in throws:
160+
monkeys[target_id].items.append(item)
161+
162+
counts = sorted([m.inspection_count for m in monkeys], reverse=True)
163+
return counts[0] * counts[1]
164+
165+
def part1(self, data: str) -> int:
166+
"""Calculate monkey business after 20 rounds with worry relief.
167+
168+
After each inspection, worry level is divided by 3 (rounded down)
169+
because you're relieved the item wasn't damaged.
170+
171+
Args:
172+
data: Raw input containing monkey configurations
173+
174+
Returns
175+
-------
176+
int: Product of top 2 inspection counts after 20 rounds
177+
"""
178+
return self.simulate(data, rounds=20, is_relieved=True)
179+
180+
def part2(self, data: str) -> int:
181+
"""Calculate monkey business after 10,000 rounds without relief.
182+
183+
Worry levels are no longer divided by 3, causing them to grow rapidly.
184+
Uses modulo by LCM of all divisors to keep numbers manageable while
185+
preserving divisibility test outcomes.
186+
187+
Args:
188+
data: Raw input containing monkey configurations
189+
190+
Returns
191+
-------
192+
int: Product of top 2 inspection counts after 10,000 rounds
193+
"""
194+
return self.simulate(data, rounds=10_000, is_relieved=False)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Monkey 0:
2+
Starting items: 79, 98
3+
Operation: new = old * 19
4+
Test: divisible by 23
5+
If true: throw to monkey 2
6+
If false: throw to monkey 3
7+
8+
Monkey 1:
9+
Starting items: 54, 65, 75, 74
10+
Operation: new = old + 6
11+
Test: divisible by 19
12+
If true: throw to monkey 2
13+
If false: throw to monkey 0
14+
15+
Monkey 2:
16+
Starting items: 79, 60, 97
17+
Operation: new = old * old
18+
Test: divisible by 13
19+
If true: throw to monkey 1
20+
If false: throw to monkey 3
21+
22+
Monkey 3:
23+
Starting items: 74
24+
Operation: new = old + 3
25+
Test: divisible by 17
26+
If true: throw to monkey 0
27+
If false: throw to monkey 1
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Monkey 0:
2+
Starting items: 79, 98
3+
Operation: new = old * 19
4+
Test: divisible by 23
5+
If true: throw to monkey 2
6+
If false: throw to monkey 3
7+
8+
Monkey 1:
9+
Starting items: 54, 65, 75, 74
10+
Operation: new = old + 6
11+
Test: divisible by 19
12+
If true: throw to monkey 2
13+
If false: throw to monkey 0
14+
15+
Monkey 2:
16+
Starting items: 79, 60, 97
17+
Operation: new = old * old
18+
Test: divisible by 13
19+
If true: throw to monkey 1
20+
If false: throw to monkey 3
21+
22+
Monkey 3:
23+
Starting items: 74
24+
Operation: new = old + 3
25+
Test: divisible by 17
26+
If true: throw to monkey 0
27+
If false: throw to monkey 1

_2022/tests/test_11.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Test suite for Day 11: Monkey in the Middle
2+
3+
This module contains tests for the Day 11 solution, which simulates monkeys
4+
throwing items based on worry levels and divisibility tests. The tests verify:
5+
6+
1. Part 1: Monkey business level after 20 rounds with worry relief
7+
2. Part 2: Monkey business level after 10,000 rounds without worry relief
8+
"""
9+
10+
from aoc.models.tester import TestSolutionUtility
11+
12+
13+
def test_day11_part1() -> None:
14+
"""Test calculating monkey business with 20 rounds and worry relief.
15+
16+
This test runs the solution for Part 1 of the puzzle against the
17+
provided test input and compares the result with the expected output.
18+
"""
19+
TestSolutionUtility.run_test(
20+
year=2022,
21+
day=11,
22+
is_raw=True,
23+
part_num=1,
24+
expected=10605,
25+
)
26+
27+
28+
def test_day11_part2() -> None:
29+
"""Test calculating monkey business with 10,000 rounds without relief.
30+
31+
This test runs the solution for Part 2 of the puzzle against the
32+
provided test input and compares the result with the expected output.
33+
"""
34+
TestSolutionUtility.run_test(
35+
year=2022,
36+
day=11,
37+
is_raw=True,
38+
part_num=2,
39+
expected=2713310158,
40+
)

0 commit comments

Comments
 (0)