Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solve 2022 day 11 #29

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
650fc1f
Create solution file for part 1
katzuv Nov 17, 2023
50ebb4b
Create module for `Monkey` class
katzuv Nov 18, 2023
57aa2d2
Create `Monkey` dataclass
katzuv Nov 18, 2023
ad8a578
Add fields to `Monkey` class
katzuv Nov 19, 2023
ec4bac9
Add method to `Monkey` that throws an item to another `Monkey`
katzuv Nov 19, 2023
e941eb8
Add method that empties the items list of a `Monkey`
katzuv Nov 19, 2023
215f964
Create module that contains functions that create a `Monkey` instance
katzuv Nov 18, 2023
94f15e5
Add function that returns the worry level function of a monkey
katzuv Nov 18, 2023
a4a589d
Add a function that creates a `Monkey` object from written attributes
katzuv Nov 19, 2023
b9d73c4
Add a function that creates `Monkey`s from the puzzle input
katzuv Nov 19, 2023
3bb7a51
Create module for the `KeepAway` class
katzuv Nov 19, 2023
634fe7d
Create `KeepAway` class
katzuv Nov 19, 2023
0493361
Add constructor to `KeepAway` class
katzuv Nov 19, 2023
a495ca6
Add relief worry reduction factor class constant
katzuv Nov 19, 2023
f46b438
Add method to `KeepAway` that handles a single item throwing
katzuv Nov 19, 2023
d0f05fc
Add method to `KeepAway` that runs a single monkey turn
katzuv Nov 19, 2023
46cec6e
class game amount of rounds class constant
katzuv Nov 19, 2023
f1da429
Add public method to `KeepAway` that runs a game
katzuv Nov 19, 2023
05e0bc7
Add public method that returns the game's "monkey business" level
katzuv Nov 19, 2023
763056d
Add function that returns the monkey business level when game ends
katzuv Nov 19, 2023
fb6a3cb
Make rounds amount a ctor parameter instead of constant
katzuv Nov 25, 2023
98618d7
Merge branch 'main' into solve/2022-11
katzuv Nov 25, 2023
2e08ec8
Add worry level modifier function to ctor parameters
katzuv Nov 25, 2023
1989f48
Use worry level modifier function in item handling method
katzuv Nov 25, 2023
d373c89
Update part 1 solution to match new `KeepAway` parameters
katzuv Nov 25, 2023
3f44e7c
Create solution file for part 2
katzuv Nov 25, 2023
9577b9e
Create worry level modifier function for part 2 using CRT
katzuv Nov 25, 2023
d2e01f5
Create constant for the amount of rounds in part 2
katzuv Nov 25, 2023
53064df
Add function that returns the monkey business level after 10K rounds
katzuv Nov 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions puzzles/solutions/2022/d11/keep_away.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from typing import Callable, Iterable

from monkey import Monkey


class KeepAway:
"""Class managing the monkeys' Keep Away game."""

def __init__(
self,
monkeys: list[Monkey],
rounds_amount: int,
worry_level_modifier: Callable[[int, Iterable[Monkey]], int],
):
self._monkeys = monkeys
self._rounds_amount = rounds_amount
self._worry_level_modifier = worry_level_modifier

def _handle_item(self, item: int, monkey: Monkey) -> None:
"""
Handle a single item -- calculate its worry level and determine the monkey to which the item will be thrown.
:param item: item to handle
:param monkey: monkey who plays the current turn
"""
worry_level = item
worry_level = monkey.worry_function(worry_level)
worry_level = self._worry_level_modifier(worry_level, self._monkeys)
monkey_number_to_throw_item_to = (
monkey.true_test_result_monkey_number
if worry_level % monkey.test_divisor == 0
else monkey.false_test_result_monkey_number
)
monkey.throw(self._monkeys[monkey_number_to_throw_item_to], worry_level)

def _run_turn(self, monkey: Monkey) -> None:
"""
Run a turn.
:param monkey: monkey who plays the current turn
"""
items = monkey.items[:]
for item in items:
self._handle_item(item, monkey)
monkey.empty_items()

def run(self) -> None:
"""Run the Keep Away game."""
for _ in range(self._rounds_amount):
for monkey in self._monkeys:
self._run_turn(monkey)

def get_monkey_business_level(self) -> int:
"""
:return: monkey business level, which is the product of the inspected items amount of the two most active
monkeys in the game.
"""
two_most_active_monkeys = sorted(
self._monkeys, key=lambda monkey: monkey.inspected_items_amount
)[-2:]
monkey_business = (
two_most_active_monkeys[0].inspected_items_amount
* two_most_active_monkeys[1].inspected_items_amount
)
return monkey_business
33 changes: 33 additions & 0 deletions puzzles/solutions/2022/d11/monkey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import dataclasses
from typing import Callable, Self


@dataclasses.dataclass
class Monkey:
"""Class representing a monkey playing Keep Away."""

items: list[int]
worry_function: Callable[[int], int]
test_divisor: int
true_test_result_monkey_number: int
false_test_result_monkey_number: int
inspected_items_amount: int = 0

def throw(self, other: Self, item_to_throw: int) -> None:
"""
Throw the given item to another monkey.

This method also increments the items inspected amount (every inspected item is eventually thrown).
:param other: other monkey to throw item to
:param item_to_throw: item to throw
"""
other.items.append(item_to_throw)
self.inspected_items_amount += 1

def empty_items(self) -> None:
"""
Empty the items list of the monkey.

Use when the monkey's turn ends (monkeys have no items at the end of the round).
"""
self.items = []
68 changes: 68 additions & 0 deletions puzzles/solutions/2022/d11/monkey_creator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from typing import Sequence, Callable
import re
import operator

from monkey import Monkey


_FIRST_PARAMETER = "first_parameter"
_OPERATOR = "operator"
_SECOND_PARAMETER = "second_parameter"
_OLD = "old"
PLUS_SIGN = "+"


def create_monkey(monkey_attributes: Sequence[str]) -> Monkey:
"""
:param monkey_attributes: list of lines containing the monkey attributes
:return: monkey created from the attributes
"""
starting_items_match = re.findall(r"\b\d+\b", monkey_attributes[0])
starting_items = list(int(item) for item in starting_items_match)

worry_function = _get_worry_function(monkey_attributes[1])

test_divisor_match = re.search(r"\d+", monkey_attributes[2])
test_divisor = int(test_divisor_match.group())

true_test_result_monkey_number_match = re.search(r"\d+", monkey_attributes[3])
true_test_result_monkey_number = int(true_test_result_monkey_number_match.group())

false_test_result_monkey_number_match = re.search(r"\d+", monkey_attributes[4])
false_test_result_monkey_number = int(false_test_result_monkey_number_match.group())

return Monkey(
starting_items,
worry_function,
test_divisor,
true_test_result_monkey_number,
false_test_result_monkey_number,
)


def _get_worry_function(
line: str,
) -> Callable[[int], int]:
"""
:param line: line from the input containing the worry level function
:return: function calculating the new worry level based on the old level
"""
parameters = re.search(
rf"(?P<{_FIRST_PARAMETER}>\d+|{_OLD}) (?P<{_OPERATOR}>[*+]) (?P<{_SECOND_PARAMETER}>\d+|{_OLD})",
line,
).groupdict()

first_parameter = parameters[_FIRST_PARAMETER]
operator_function = (
operator.add if parameters[_OPERATOR] == PLUS_SIGN else operator.mul
)
second_parameter = parameters[_SECOND_PARAMETER]

if first_parameter == _OLD:
if second_parameter == _OLD:
return lambda old: operator_function(old, old)
second_parameter = int(second_parameter)
return lambda old: operator_function(old, second_parameter)
first_parameter = int(first_parameter)
if second_parameter == _OLD:
return lambda old: operator_function(first_parameter, old)
42 changes: 42 additions & 0 deletions puzzles/solutions/2022/d11/p1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import sys
from typing import Iterable

from monkey import Monkey
from monkey_creator import create_monkey
from keep_away import KeepAway


RELIEF_WORRY_REDUCTION_FACTOR = 3
ROUNDS_AMOUNT = 20


def create_monkeys(input_text: str) -> list[Monkey]:
"""
:param input_text: puzzle input
:return: list of monkeys
"""
monkeys = []
for monkey_attributes in input_text.split("\n\n"):
monkey_attributes = tuple(map(str.strip, monkey_attributes.splitlines()))
monkey = create_monkey(monkey_attributes[1:])
monkeys.append(monkey)
return monkeys


def modify_worry_level(worry_level: int, monkeys: Iterable[Monkey]) -> int:
return worry_level // RELIEF_WORRY_REDUCTION_FACTOR


def get_answer(input_text: str):
"""Get the monkey business level at the end of the Keep Away game."""
monkeys = create_monkeys(input_text)
game = KeepAway(monkeys, ROUNDS_AMOUNT, modify_worry_level)
game.run()
return game.get_monkey_business_level()


if __name__ == "__main__":
try:
print(get_answer(sys.argv[1]))
except IndexError:
pass # Don't crash if no input was passed through command line arguments.
38 changes: 38 additions & 0 deletions puzzles/solutions/2022/d11/p2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import sys
from pathlib import Path
from typing import Iterable

import p1
from keep_away import KeepAway
from monkey import Monkey


SOLUTIONS_DIRECTORY = str(Path(__file__).resolve().parents[2])
sys.path.insert(0, SOLUTIONS_DIRECTORY)
from utils import crt


ROUNDS_AMOUNT = 10_000


def modify_worry_level(worry_level: int, monkeys: Iterable[Monkey]) -> int:
moduli_to_remainders = {
current_monkey.test_divisor: worry_level % current_monkey.test_divisor
for current_monkey in monkeys
}
return crt.calculate_chinese_remainder_theorem(moduli_to_remainders)


def get_answer(input_text: str):
"""Get the monkey business level at the end of the Keep Away game."""
monkeys = p1.create_monkeys(input_text)
game = KeepAway(monkeys, ROUNDS_AMOUNT, modify_worry_level)
game.run()
return game.get_monkey_business_level()


if __name__ == "__main__":
try:
print(get_answer(sys.argv[1]))
except IndexError:
pass # Don't crash if no input was passed through command line arguments.