diff --git a/README.md b/README.md index 7fec37b..d8dc2e0 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ I use python 3.11 without any libraries beyond the standard. | Day | Time | Rank | Score | Time | Rank | Score | | --- | --------- | ----- | ----- | -------- | ----- | ----- | +| 11 | 00:56:44 | 5414 | 0 | 02:42:24 | 7558 | 0 | | 10 | 00:38:16 | 7637 | 0 | 01:16:55 | 7961 | 0 | | 9 | 00:54:18 | 7719 | 0 | 01:07:37 | 4901 | 0 | | 8 | 00:41:51 | 7831 | 0 | 00:59:27 | 6325 | 0 | diff --git a/advent/__main__.py b/advent/__main__.py index 855dac7..3ed1302 100644 --- a/advent/__main__.py +++ b/advent/__main__.py @@ -40,16 +40,20 @@ def run(day: Day, part: int) -> None: def run_from_string(day_str: str) -> None: match day_str.split('/'): case [d]: - day = get_day(int(d)) + day_num = int(d) + day = get_day(day_num) - run(day, 1) - run(day, 2) + if day_num == day.day_num: + run(day, 1) + run(day, 2) case [d, p]: - day = get_day(int(d)) - part = int(p) + day_num = int(d) + day = get_day(day_num) - run(day, part) + if day_num == day.day_num: + part = int(p) + run(day, part) case _: raise Exception(f'{day_str} is not a valid day description') @@ -61,8 +65,9 @@ def main() -> None: try: for day_num in range(1, 25): day = get_day(day_num) - run(day, 1) - run(day, 2) + if day_num == day.day_num: + run(day, 1) + run(day, 2) except ModuleNotFoundError: pass diff --git a/advent/days/day11/__init__.py b/advent/days/day11/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/advent/days/day11/data/input.txt b/advent/days/day11/data/input.txt new file mode 100644 index 0000000..54ae24d --- /dev/null +++ b/advent/days/day11/data/input.txt @@ -0,0 +1,55 @@ +Monkey 0: + Starting items: 97, 81, 57, 57, 91, 61 + Operation: new = old * 7 + Test: divisible by 11 + If true: throw to monkey 5 + If false: throw to monkey 6 + +Monkey 1: + Starting items: 88, 62, 68, 90 + Operation: new = old * 17 + Test: divisible by 19 + If true: throw to monkey 4 + If false: throw to monkey 2 + +Monkey 2: + Starting items: 74, 87 + Operation: new = old + 2 + Test: divisible by 5 + If true: throw to monkey 7 + If false: throw to monkey 4 + +Monkey 3: + Starting items: 53, 81, 60, 87, 90, 99, 75 + Operation: new = old + 1 + Test: divisible by 2 + If true: throw to monkey 2 + If false: throw to monkey 1 + +Monkey 4: + Starting items: 57 + Operation: new = old + 6 + Test: divisible by 13 + If true: throw to monkey 7 + If false: throw to monkey 0 + +Monkey 5: + Starting items: 54, 84, 91, 55, 59, 72, 75, 70 + Operation: new = old * old + Test: divisible by 7 + If true: throw to monkey 6 + If false: throw to monkey 3 + +Monkey 6: + Starting items: 95, 79, 79, 68, 78 + Operation: new = old + 3 + Test: divisible by 3 + If true: throw to monkey 1 + If false: throw to monkey 3 + +Monkey 7: + Starting items: 61, 97, 67 + Operation: new = old + 4 + Test: divisible by 17 + If true: throw to monkey 0 + If false: throw to monkey 5 diff --git a/advent/days/day11/data/test01.txt b/advent/days/day11/data/test01.txt new file mode 100644 index 0000000..c04eddb --- /dev/null +++ b/advent/days/day11/data/test01.txt @@ -0,0 +1,27 @@ +Monkey 0: + Starting items: 79, 98 + Operation: new = old * 19 + Test: divisible by 23 + If true: throw to monkey 2 + If false: throw to monkey 3 + +Monkey 1: + Starting items: 54, 65, 75, 74 + Operation: new = old + 6 + Test: divisible by 19 + If true: throw to monkey 2 + If false: throw to monkey 0 + +Monkey 2: + Starting items: 79, 60, 97 + Operation: new = old * old + Test: divisible by 13 + If true: throw to monkey 1 + If false: throw to monkey 3 + +Monkey 3: + Starting items: 74 + Operation: new = old + 3 + Test: divisible by 17 + If true: throw to monkey 0 + If false: throw to monkey 1 \ No newline at end of file diff --git a/advent/days/day11/solution.py b/advent/days/day11/solution.py new file mode 100644 index 0000000..446faba --- /dev/null +++ b/advent/days/day11/solution.py @@ -0,0 +1,134 @@ +from __future__ import annotations +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from math import prod + +from typing import Callable, Iterator, Self +from advent.parser.parser import P + +day_num = 11 + + +def part1(lines: Iterator[str]) -> int: + horde = Troop_While_Worried.parse(lines) + horde.rounds(20) + return horde.inspected_result() + + +def part2(lines: Iterator[str]) -> int: + horde = Troop_While_Kinda_Relieved.parse(lines) + horde.rounds(10_000) + return horde.inspected_result() + + +def worry_increaser(op: str, value: int | str) -> WorryIncreaser: + match (op, value): + case '*', 'old': return lambda old: old * old + case '*', int(v): return lambda old: old * v + case '+', int(v): return lambda old: old + v + case _: raise Exception(f"Illegal line: {op} {value}") + + +class Parser: + """ All the parsers needed for this solution """ + worry_inc: P[WorryIncreaser] = P.second( + P.tstring("Operation: new = old"), + P.map2(P.one_of('+*'), P.either(P.tstring('old'), P.tsigned()), + worry_increaser)).tline() + monkey_number: P[int] = P.unsigned().between(P.tstring('Monkey'), P.tchar(':')).tline() + items: P[list[int]] = P.second( + P.tstring('Starting items:'), P.unsigned().sep_by(sep=P.tchar(','))).tline() + modulo: P[int] = P.second( + P.tstring("Test: divisible by"), P.unsigned()).tline() + throw_parser: P[int] = P.second( + P.seq( + P.tstring("If"), + P.either(P.tstring("true"), P.tstring("false")), + P.tstring(": throw to monkey")), + P.unsigned()).tline() + test: P[tuple[int, int, int]] = P.seq( + modulo, throw_parser, throw_parser) + monkey: P[Monkey] = P.map4(monkey_number, items, + worry_inc, test, + lambda number, items, worry_inc, test: + Monkey(number, items, worry_inc, *test)) + monkey_list: P[list[Monkey]] = P.first(monkey, P.eol().optional()).many() + + +WorryIncreaser = Callable[[int], int] + + +@dataclass(slots=True) +class Monkey: + number: int + items: list[int] + worry_increaser: WorryIncreaser + modulator: int + target_if_divides: int + catcher_if_not_divides: int + inspected: int = field(default=0, compare=False) + + def inspect_items(self, worry_decrease: int | None) -> Iterator[tuple[int, int]]: + for item in self.items: + self.inspected += 1 + next_level = self.worry_increaser(item) + + if worry_decrease is not None: + next_level //= worry_decrease + + if next_level % self.modulator == 0: + target_monkey = self.target_if_divides + else: + target_monkey = self.catcher_if_not_divides + + yield target_monkey, next_level + self.items.clear() + + def catch_item(self, item: int): + self.items.append(item) + + +@dataclass(slots=True) +class Troop(ABC): + monkeys: list[Monkey] + + @abstractmethod + def single_round(self): + ... + + def rounds(self, count: int): + for _ in range(count): + self.single_round() + + def inspected_result(self): + most = sorted((monkey.inspected for monkey in self.monkeys), reverse=True) + return most[0] * most[1] + + +@dataclass(slots=True) +class Troop_While_Worried(Troop): + """ The """ + @classmethod + def parse(cls, lines: Iterator[str]) -> Self: + monkeys = Parser.monkey_list.parse_iterator(lines).get() + return Troop_While_Worried(monkeys) + + def single_round(self): + for currentMonkey in self.monkeys: + for target_monkey, item in currentMonkey.inspect_items(3): + self.monkeys[target_monkey].catch_item(item) + + +@dataclass(slots=True) +class Troop_While_Kinda_Relieved(Troop): + modulator: int + + @classmethod + def parse(cls, lines: Iterator[str]) -> Self: + monkeys = Parser.monkey_list.parse_iterator(lines).get() + return Troop_While_Kinda_Relieved(monkeys, prod(monkey.modulator for monkey in monkeys)) + + def single_round(self): + for current_monkey in self.monkeys: + for target_monkey, item in current_monkey.inspect_items(None): + self.monkeys[target_monkey].catch_item(item % self.modulator) diff --git a/advent/days/day11/test_solution.py b/advent/days/day11/test_solution.py new file mode 100644 index 0000000..349c609 --- /dev/null +++ b/advent/days/day11/test_solution.py @@ -0,0 +1,56 @@ +from advent.common import input + +from .solution import Troop_While_Kinda_Relieved, Troop_While_Worried, day_num, part1, part2 + + +def test_part1(): + lines = input.read_lines(day_num, 'test01.txt') + expected = 10605 + result = part1(lines) + assert result == expected + + +def test_part2(): + lines = input.read_lines(day_num, 'test01.txt') + expected = 2713310158 + result = part2(lines) + assert result == expected + + +def test_parse_all(): + lines = input.read_lines(day_num, 'test01.txt') + expected = 4 + result = Troop_While_Worried.parse(lines) + assert len(result.monkeys) == expected + + +def test_one_round(): + lines = input.read_lines(day_num, 'test01.txt') + expected = [2080, 25, 167, 207, 401, 1046] + result = Troop_While_Worried.parse(lines) + result.single_round() + assert list(result.monkeys[1].items) == expected + + +def test_rounds(): + lines = input.read_lines(day_num, 'test01.txt') + expected = 101 + result = Troop_While_Worried.parse(lines) + result.rounds(20) + assert result.monkeys[0].inspected == expected + + +def test_inspected(): + lines = input.read_lines(day_num, 'test01.txt') + expected = 10605 + result = Troop_While_Worried.parse(lines) + result.rounds(20) + assert result.inspected_result() == expected + + +def test_inspected2(): + lines = input.read_lines(day_num, 'test01.txt') + expected = 2713310158 + result = Troop_While_Kinda_Relieved.parse(lines) + result.rounds(10_000) + assert result.inspected_result() == expected diff --git a/advent/parser/parser.py b/advent/parser/parser.py index 986f867..79163ec 100644 --- a/advent/parser/parser.py +++ b/advent/parser/parser.py @@ -1,9 +1,9 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from functools import reduce from itertools import chain -from typing import Any, Callable, Generic, Iterator, Self, TypeVar, overload +from typing import Any, Callable, Generic, Iterator, Protocol, Self, TypeVar, overload import unicodedata from .result import Result @@ -17,8 +17,16 @@ T5 = TypeVar('T5') TR = TypeVar('TR') +class ParserInput(Protocol): + def step(self) -> tuple[Self, str]: + ... + + def has_data(self) -> bool: + ... + + @dataclass(slots=True, frozen=True) -class ParserInput: +class SimpleParserInput: input: str start: int @@ -26,7 +34,7 @@ class ParserInput: if self.start >= len(self.input): raise Exception("Already at End of Input") - return ParserInput(self.input, self.start + 1), self.input[self.start] + return SimpleParserInput(self.input, self.start + 1), self.input[self.start] def has_data(self) -> bool: return self.start < len(self.input) @@ -41,6 +49,61 @@ class ParserInput: return f'{self.input[self.start-3:self.start-1]}->[{self.input[self.start:]}]' +@dataclass(slots=True) +class StringDispenser: + lines: Iterator[str] + input: str = field(default="", init=False) + length: int = field(default=0, init=False) + + def read_more(self): + try: + part = next(self.lines) + if self.input: + self.input = f"{self.input}\n{part}" + else: + self.input = part + self.length = len(self.input) + return True + except StopIteration: + return False + + def get_str(self, pos: int) -> str | None: + assert pos >= 0 + if pos < self.length: + return self.input[pos] + elif pos == self.length and pos != 0: + return "\n" + elif self.read_more(): + return self.get_str(pos) + else: + return None + + def has_more(self, pos: int) -> bool: + assert pos >= 0 + if pos <= self.length: + return True + elif self.read_more(): + return self.has_more(pos) + else: + return False + + +@dataclass(slots=True, frozen=True) +class IteratorParserInput: + dispenser: StringDispenser + start: int + + def step(self) -> tuple[Self, str]: + char = self.dispenser.get_str(self.start) + if char is None: + raise Exception("Already at End of Input") + + return IteratorParserInput(self.dispenser, self.start + 1), char + + def has_data(self) -> bool: + return self.dispenser.has_more(self.start) + + ParserResult = Iterator[tuple[ParserInput, T]] ParserFunc = Callable[[ParserInput], ParserResult[T]] @@ -49,8 +112,16 @@ class P(Generic[T]): def __init__(self, func: ParserFunc[T]): self.func = func - def parse(self, s: str, i: int = 0) -> Result[T]: - all_results = self.func(ParserInput(s, i)) + def parse(self, s: str) -> Result[T]: + all_results = self.func(SimpleParserInput(s, 0)) + try: + _, result = next(all_results) + return Result.of(result) + except StopIteration: + return Result.fail("No result") + + def parse_iterator(self, it: Iterator[str]) -> Result[T]: + all_results = self.func(IteratorParserInput(StringDispenser(it), 0)) try: _, result = next(all_results) return Result.of(result) @@ -58,7 +129,7 @@ class P(Generic[T]): return Result.fail("No result") def parse_multi(self, s: str, i: int = 0) -> Iterator[T]: - return (v for _, v in self.func(ParserInput(s, i))) + return (v for _, v in self.func(SimpleParserInput(s, i))) @classmethod def pure(cls, value: T) -> P[T]: @@ -101,7 +172,7 @@ class P(Generic[T]): def replace(self, value: TR) -> P[TR]: return self.fmap(lambda _: value) - def as_unit(self) -> P[tuple[()]]: + def ignore(self) -> P[tuple[()]]: return self.fmap(lambda _: ()) def apply(self, p2: P[Callable[[T], TR]]) -> P[TR]: @@ -363,8 +434,16 @@ class P(Generic[T]): return P.char_func(lambda c: c == cmp) @classmethod - def string(cls, s: str) -> P[str]: - return P.seq(*map(P.char, s)).replace(s) + def string(cls, cmp: str) -> P[str]: + return P.seq(*map(P.char, cmp)).replace(cmp) + + @classmethod + def tchar(cls, cmp: str) -> P[str]: + return P.char(cmp).trim() + + @classmethod + def tstring(cls, cmp: str) -> P[str]: + return P.string(cmp).trim() @classmethod def one_of(cls, s: str) -> P[str]: @@ -390,6 +469,10 @@ class P(Generic[T]): def upper(cls) -> P[str]: return P.char_func(lambda c: c.isupper()) + @classmethod + def eol(cls) -> P[tuple[()]]: + return P.char('\n').ignore() + @classmethod def space(cls) -> P[str]: return P.char_func(lambda c: c.isspace()) @@ -408,6 +491,20 @@ class P(Generic[T]): return P.map2(P.one_of('+-').optional(), P.unsigned(), lambda sign, num: num if sign != '-' else -num) + @classmethod + def tunsigned(cls) -> P[int]: + return P.unsigned().trim() + + @classmethod + def tsigned(cls) -> P[int]: + return P.signed().trim() + + def line(self) -> P[T]: + return P.first(self, P.eol()) + + def tline(self) -> P[T]: + return P.first(self.trim(), P.eol()) + def in_parens(self) -> P[T]: return self.between(P.char('('), P.char(')')) @@ -430,5 +527,5 @@ class P(Generic[T]): return self.between(WHITE_SPACE, WHITE_SPACE) -WHITE_SPACE: P[tuple[()]] = P.space().many().as_unit() -SEP_SPACE: P[tuple[()]] = P.space().some().as_unit() +WHITE_SPACE: P[tuple[()]] = P.space().many().ignore() +SEP_SPACE: P[tuple[()]] = P.space().some().ignore() diff --git a/advent/parser/test_parser.py b/advent/parser/test_parser.py index c8f9817..6c5d9ed 100644 --- a/advent/parser/test_parser.py +++ b/advent/parser/test_parser.py @@ -269,3 +269,19 @@ def test_word2(): expected = [('123', 'a')] result = list(parser.parse_multi(input)) assert result == expected + + +def test_iterator_input(): + input = iter(['1', '2']) + parser = P.unsigned().line().many() + expected = [1, 2] + result = parser.parse_iterator(input).get() + assert result == expected + + +def test_iterator_trim_input(): + input = iter(['1 ', '2 ']) + parser = P.unsigned().trim().line().many() + expected = [1, 2] + result = parser.parse_iterator(input).get() + assert result == expected