diff --git a/src/holt59/aoc/2015/day1.py b/src/holt59/aoc/2015/day1.py index 8ddf9bc..0c3e64b 100644 --- a/src/holt59/aoc/2015/day1.py +++ b/src/holt59/aoc/2015/day1.py @@ -1,10 +1,12 @@ -import sys +from typing import Any, Iterator -line = sys.stdin.read().strip() - -floor = 0 -floors = [(floor := floor + (1 if c == "(" else -1)) for c in line] +from ..base import BaseSolver -print(f"answer 1 is {floors[-1]}") -print(f"answer 2 is {floors.index(-1)}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + floor = 0 + floors = [(floor := floor + (1 if c == "(" else -1)) for c in input] + + yield floors[-1] + yield floors.index(-1) diff --git a/src/holt59/aoc/2015/day10.py b/src/holt59/aoc/2015/day10.py index 55cdef2..f7569d4 100644 --- a/src/holt59/aoc/2015/day10.py +++ b/src/holt59/aoc/2015/day10.py @@ -1,7 +1,7 @@ import itertools -import sys +from typing import Any, Iterator -line = sys.stdin.read().strip() +from ..base import BaseSolver # see http://www.se16.info/js/lands2.htm for the explanation of 'atoms' (or elements) # @@ -9,7 +9,7 @@ line = sys.stdin.read().strip() # CodeGolf answer https://codegolf.stackexchange.com/a/8479/42148 # fmt: off -atoms = [ +ATOMS: list[tuple[str, tuple[int, ...]]] = [ ("22", (0, )), # 0 ("13112221133211322112211213322112", (71, 90, 0, 19, 2, )), # 1 ("312211322212221121123222112", (1, )), # 2 @@ -105,7 +105,7 @@ atoms = [ ] # fmt: on -starters = [ +STARTERS = [ "1", "11", "21", @@ -122,27 +122,26 @@ def look_and_say_length(s: str, n: int) -> int: if n == 0: return len(s) - if s in starters: + if s in STARTERS: return look_and_say_length( "".join(f"{len(list(g))}{k}" for k, g in itertools.groupby(s)), n - 1 ) - counts = {i: 0 for i in range(len(atoms))} - idx = next(i for i, (a, _) in enumerate(atoms) if s == a) + counts = {i: 0 for i in range(len(ATOMS))} + idx = next(i for i, (a, _) in enumerate(ATOMS) if s == a) counts[idx] = 1 for _ in range(n): - c2 = {i: 0 for i in range(len(atoms))} + c2 = {i: 0 for i in range(len(ATOMS))} for i in counts: - for j in atoms[i][1]: + for j in ATOMS[i][1]: c2[j] += counts[i] counts = c2 - return sum(counts[i] * len(a[0]) for i, a in enumerate(atoms)) + return sum(counts[i] * len(a[0]) for i, a in enumerate(ATOMS)) -answer_1 = look_and_say_length(line, 40) -print(f"answer 1 is {answer_1}") - -answer_2 = look_and_say_length(line, 50) -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any] | None: + yield look_and_say_length(input, 40) + yield look_and_say_length(input, 50) diff --git a/src/holt59/aoc/2015/day11.py b/src/holt59/aoc/2015/day11.py index 7d9ff1f..259ccf9 100644 --- a/src/holt59/aoc/2015/day11.py +++ b/src/holt59/aoc/2015/day11.py @@ -1,5 +1,7 @@ import itertools -import sys +from typing import Any, Iterator + +from ..base import BaseSolver def is_valid(p: str) -> bool: @@ -40,10 +42,8 @@ def find_next_password(p: str) -> str: return p -line = sys.stdin.read().strip() - -answer_1 = find_next_password(line) -print(f"answer 1 is {answer_1}") - -answer_2 = find_next_password(increment(answer_1)) -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + answer_1 = find_next_password(input) + yield answer_1 + yield find_next_password(increment(answer_1)) diff --git a/src/holt59/aoc/2015/day12.py b/src/holt59/aoc/2015/day12.py index bf1667d..9faabb6 100644 --- a/src/holt59/aoc/2015/day12.py +++ b/src/holt59/aoc/2015/day12.py @@ -1,6 +1,7 @@ import json -import sys -from typing import TypeAlias +from typing import Any, Iterator, TypeAlias + +from ..base import BaseSolver JsonObject: TypeAlias = dict[str, "JsonObject"] | list["JsonObject"] | int | str @@ -18,10 +19,9 @@ def json_sum(value: JsonObject, ignore: str | None = None) -> int: return 0 -data: JsonObject = json.load(sys.stdin) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + data: JsonObject = json.loads(input) -answer_1 = json_sum(data) -print(f"answer 1 is {answer_1}") - -answer_2 = json_sum(data, "red") -print(f"answer 2 is {answer_2}") + yield json_sum(data) + yield json_sum(data, "red") diff --git a/src/holt59/aoc/2015/day13.py b/src/holt59/aoc/2015/day13.py index ef0d7a7..3113abd 100644 --- a/src/holt59/aoc/2015/day13.py +++ b/src/holt59/aoc/2015/day13.py @@ -1,10 +1,11 @@ import itertools -import sys from collections import defaultdict -from typing import Literal, cast +from typing import Any, Iterator, Literal, cast import parse # type: ignore +from ..base import BaseSolver + def max_change_in_happiness(happiness: dict[str, dict[str, int]]) -> int: guests = list(happiness) @@ -17,25 +18,23 @@ def max_change_in_happiness(happiness: dict[str, dict[str, int]]) -> int: ) -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -happiness: dict[str, dict[str, int]] = defaultdict(dict) -for line in lines: - u1, gain_or_loose, hap, u2 = cast( - tuple[str, Literal["gain", "lose"], int, str], - parse.parse( # type: ignore - "{} would {} {:d} happiness units by sitting next to {}.", line - ), - ) - happiness[u1][u2] = hap if gain_or_loose == "gain" else -hap + happiness: dict[str, dict[str, int]] = defaultdict(dict) + for line in lines: + u1, gain_or_loose, hap, u2 = cast( + tuple[str, Literal["gain", "lose"], int, str], + parse.parse( # type: ignore + "{} would {} {:d} happiness units by sitting next to {}.", line + ), + ) + happiness[u1][u2] = hap if gain_or_loose == "gain" else -hap + yield max_change_in_happiness(happiness) + for guest in list(happiness): + happiness["me"][guest] = 0 + happiness[guest]["me"] = 0 -answer_1 = max_change_in_happiness(happiness) -print(f"answer 1 is {answer_1}") - -for guest in list(happiness): - happiness["me"][guest] = 0 - happiness[guest]["me"] = 0 - -answer_2 = max_change_in_happiness(happiness) -print(f"answer 2 is {answer_2}") + yield max_change_in_happiness(happiness) diff --git a/src/holt59/aoc/2015/day14.py b/src/holt59/aoc/2015/day14.py index 7f2b1d2..a17e521 100644 --- a/src/holt59/aoc/2015/day14.py +++ b/src/holt59/aoc/2015/day14.py @@ -1,9 +1,10 @@ -import sys from dataclasses import dataclass -from typing import Literal, cast +from typing import Any, Iterator, Literal, cast import parse # type: ignore +from ..base import BaseSolver + @dataclass(frozen=True) class Reindeer: @@ -13,50 +14,50 @@ class Reindeer: rest_time: int -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -reindeers: list[Reindeer] = [] -for line in lines: - reindeer, speed, speed_time, rest_time = cast( - tuple[str, int, int, int], - parse.parse( # type: ignore - "{} can fly {:d} km/s for {:d} seconds, " - "but then must rest for {:d} seconds.", - line, - ), - ) - reindeers.append( - Reindeer(name=reindeer, speed=speed, fly_time=speed_time, rest_time=rest_time) - ) + reindeers: list[Reindeer] = [] + for line in lines: + reindeer, speed, speed_time, rest_time = cast( + tuple[str, int, int, int], + parse.parse( # type: ignore + "{} can fly {:d} km/s for {:d} seconds, " + "but then must rest for {:d} seconds.", + line, + ), + ) + reindeers.append( + Reindeer( + name=reindeer, speed=speed, fly_time=speed_time, rest_time=rest_time + ) + ) -target = 1000 if len(reindeers) <= 2 else 2503 + target = 1000 if len(reindeers) <= 2 else 2503 -states: dict[Reindeer, tuple[Literal["resting", "flying"], int]] = { - reindeer: ("resting", 0) for reindeer in reindeers -} -distances: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers} -points: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers} + states: dict[Reindeer, tuple[Literal["resting", "flying"], int]] = { + reindeer: ("resting", 0) for reindeer in reindeers + } + distances: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers} + points: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers} -for time in range(target): - for reindeer in reindeers: - if states[reindeer][0] == "flying": - distances[reindeer] += reindeer.speed + for time in self.progress.wrap(range(target)): + for reindeer in reindeers: + if states[reindeer][0] == "flying": + distances[reindeer] += reindeer.speed - top_distance = max(distances.values()) - for reindeer in reindeers: - if distances[reindeer] == top_distance: - points[reindeer] += 1 + top_distance = max(distances.values()) + for reindeer in reindeers: + if distances[reindeer] == top_distance: + points[reindeer] += 1 - for reindeer in reindeers: - if states[reindeer][1] == time: - if states[reindeer][0] == "resting": - states[reindeer] = ("flying", time + reindeer.fly_time) - else: - states[reindeer] = ("resting", time + reindeer.rest_time) + for reindeer in reindeers: + if states[reindeer][1] == time: + if states[reindeer][0] == "resting": + states[reindeer] = ("flying", time + reindeer.fly_time) + else: + states[reindeer] = ("resting", time + reindeer.rest_time) - -answer_1 = max(distances.values()) -print(f"answer 1 is {answer_1}") - -answer_2 = max(points.values()) - 1 -print(f"answer 2 is {answer_2}") + yield max(distances.values()) + yield max(points.values()) - 1 diff --git a/src/holt59/aoc/2015/day15.py b/src/holt59/aoc/2015/day15.py index 953dabf..9a894b0 100644 --- a/src/holt59/aoc/2015/day15.py +++ b/src/holt59/aoc/2015/day15.py @@ -1,9 +1,10 @@ import math -import sys -from typing import Sequence, cast +from typing import Any, Iterator, Sequence, cast import parse # type: ignore +from ..base import BaseSolver + def score(ingredients: list[list[int]], teaspoons: Sequence[int]) -> int: return math.prod( @@ -18,40 +19,38 @@ def score(ingredients: list[list[int]], teaspoons: Sequence[int]) -> int: ) -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -ingredients: list[list[int]] = [] -for line in lines: - _, *scores = cast( - tuple[str, int, int, int, int, int], - parse.parse( # type: ignore - "{}: capacity {:d}, durability {:d}, flavor {:d}, " - "texture {:d}, calories {:d}", - line, - ), - ) - ingredients.append(scores) - -total_teaspoons = 100 -calories: list[int] = [] -scores: list[int] = [] - -for a in range(total_teaspoons + 1): - for b in range(total_teaspoons + 1 - a): - for c in range(total_teaspoons + 1 - a - b): - teaspoons = (a, b, c, total_teaspoons - a - b - c) - - scores.append(score(ingredients, teaspoons)) - calories.append( - sum( - ingredient[-1] * teaspoon - for ingredient, teaspoon in zip(ingredients, teaspoons) - ) + ingredients: list[list[int]] = [] + for line in lines: + _, *scores = cast( + tuple[str, int, int, int, int, int], + parse.parse( # type: ignore + "{}: capacity {:d}, durability {:d}, flavor {:d}, " + "texture {:d}, calories {:d}", + line, + ), ) + ingredients.append(scores) + total_teaspoons = 100 + calories: list[int] = [] + scores: list[int] = [] -answer_1 = max(scores) -print(f"answer 1 is {answer_1}") + for a in range(total_teaspoons + 1): + for b in range(total_teaspoons + 1 - a): + for c in range(total_teaspoons + 1 - a - b): + teaspoons = (a, b, c, total_teaspoons - a - b - c) -answer_2 = max(score for score, calory in zip(scores, calories) if calory == 500) -print(f"answer 2 is {answer_2}") + scores.append(score(ingredients, teaspoons)) + calories.append( + sum( + ingredient[-1] * teaspoon + for ingredient, teaspoon in zip(ingredients, teaspoons) + ) + ) + + yield max(scores) + yield max(score for score, calory in zip(scores, calories) if calory == 500) diff --git a/src/holt59/aoc/2015/day16.py b/src/holt59/aoc/2015/day16.py index 8a3b275..e335677 100644 --- a/src/holt59/aoc/2015/day16.py +++ b/src/holt59/aoc/2015/day16.py @@ -1,8 +1,9 @@ import operator as op import re -import sys from collections import defaultdict -from typing import Callable +from typing import Any, Callable, Iterator + +from ..base import BaseSolver MFCSAM: dict[str, int] = { "children": 3, @@ -17,18 +18,10 @@ MFCSAM: dict[str, int] = { "perfumes": 1, } -lines = sys.stdin.readlines() -aunts: list[dict[str, int]] = [ - { - match[1]: int(match[2]) - for match in re.findall(R"((?P[^:, ]+): (?P\d+))", line) - } - for line in lines -] - - -def match(operators: dict[str, Callable[[int, int], bool]]) -> int: +def match( + aunts: list[dict[str, int]], operators: dict[str, Callable[[int, int], bool]] +) -> int: return next( i for i, aunt in enumerate(aunts, start=1) @@ -36,16 +29,29 @@ def match(operators: dict[str, Callable[[int, int], bool]]) -> int: ) -answer_1 = match(defaultdict(lambda: op.eq)) -print(f"answer 1 is {answer_1}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -answer_2 = match( - defaultdict( - lambda: op.eq, - trees=op.gt, - cats=op.gt, - pomeranians=op.lt, - goldfish=op.lt, - ) -) -print(f"answer 2 is {answer_2}") + aunts: list[dict[str, int]] = [ + { + match[1]: int(match[2]) + for match in re.findall( + R"((?P[^:, ]+): (?P\d+))", line + ) + } + for line in lines + ] + + yield match(aunts, defaultdict(lambda: op.eq)) + + yield match( + aunts, + defaultdict( + lambda: op.eq, + trees=op.gt, + cats=op.gt, + pomeranians=op.lt, + goldfish=op.lt, + ), + ) diff --git a/src/holt59/aoc/2015/day17.py b/src/holt59/aoc/2015/day17.py index e4704c3..c954f9b 100644 --- a/src/holt59/aoc/2015/day17.py +++ b/src/holt59/aoc/2015/day17.py @@ -1,5 +1,6 @@ -import sys -from typing import Iterator +from typing import Any, Iterator + +from ..base import BaseSolver def iter_combinations(value: int, containers: list[int]) -> Iterator[tuple[int, ...]]: @@ -16,15 +17,18 @@ def iter_combinations(value: int, containers: list[int]) -> Iterator[tuple[int, yield (containers[i],) + combination -containers = [int(c) for c in sys.stdin.read().split()] -total = 25 if len(containers) <= 5 else 150 +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + containers = [int(c) for c in input.split()] + total = 25 if len(containers) <= 5 else 150 -combinations = [combination for combination in iter_combinations(total, containers)] + combinations = [ + combination for combination in iter_combinations(total, containers) + ] -answer_1 = len(combinations) -print(f"answer 1 is {answer_1}") + yield len(combinations) -min_containers = min(len(combination) for combination in combinations) - -answer_2 = sum(1 for combination in combinations if len(combination) == min_containers) -print(f"answer 2 is {answer_2}") + min_containers = min(len(combination) for combination in combinations) + yield sum( + 1 for combination in combinations if len(combination) == min_containers + ) diff --git a/src/holt59/aoc/2015/day18.py b/src/holt59/aoc/2015/day18.py index b6de077..847b1d7 100644 --- a/src/holt59/aoc/2015/day18.py +++ b/src/holt59/aoc/2015/day18.py @@ -1,66 +1,66 @@ import itertools -import sys +from typing import Any, Iterator import numpy as np from numpy.typing import NDArray -grid0 = np.array([[c == "#" for c in line] for line in sys.stdin.read().splitlines()]) +from ..base import BaseSolver -# add an always off circle around -grid0 = np.concatenate( - [ - np.zeros((grid0.shape[0] + 2, 1), dtype=bool), - np.concatenate( + +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + grid0 = np.array([[c == "#" for c in line] for line in input.splitlines()]) + + # add an always off circle around + grid0 = np.concatenate( [ - np.zeros((1, grid0.shape[1]), dtype=bool), - grid0, - np.zeros((1, grid0.shape[1]), dtype=bool), - ] - ), - np.zeros((grid0.shape[0] + 2, 1), dtype=bool), - ], - axis=1, -) + np.zeros((grid0.shape[0] + 2, 1), dtype=bool), + np.concatenate( + [ + np.zeros((1, grid0.shape[1]), dtype=bool), + grid0, + np.zeros((1, grid0.shape[1]), dtype=bool), + ] + ), + np.zeros((grid0.shape[0] + 2, 1), dtype=bool), + ], + axis=1, + ) -moves = list(itertools.product([-1, 0, 1], repeat=2)) -moves.remove((0, 0)) + moves = list(itertools.product([-1, 0, 1], repeat=2)) + moves.remove((0, 0)) -jjs, iis = np.meshgrid( - np.arange(1, grid0.shape[0] - 1, dtype=int), - np.arange(1, grid0.shape[1] - 1, dtype=int), -) -iis, jjs = iis.flatten(), jjs.flatten() + jjs, iis = np.meshgrid( + np.arange(1, grid0.shape[0] - 1, dtype=int), + np.arange(1, grid0.shape[1] - 1, dtype=int), + ) + iis, jjs = iis.flatten(), jjs.flatten() -ins = iis[:, None] + np.array(moves)[:, 0] -jns = jjs[:, None] + np.array(moves)[:, 1] + ins = iis[:, None] + np.array(moves)[:, 0] + jns = jjs[:, None] + np.array(moves)[:, 1] + def game_of_life(grid: NDArray[np.bool_]) -> NDArray[np.bool_]: + neighbors_on = grid[ins, jns].sum(axis=1) + cells_on = grid[iis, jjs] -def game_of_life(grid: NDArray[np.bool_]) -> NDArray[np.bool_]: - neighbors_on = grid[ins, jns].sum(axis=1) - cells_on = grid[iis, jjs] + grid = np.zeros_like(grid) + grid[iis, jjs] = (neighbors_on == 3) | (cells_on & (neighbors_on == 2)) - grid = np.zeros_like(grid) - grid[iis, jjs] = (neighbors_on == 3) | (cells_on & (neighbors_on == 2)) + return grid - return grid + grid = grid0 + n_steps = 4 if len(grid) < 10 else 100 + for _ in range(n_steps): + grid = game_of_life(grid) + yield grid.sum() -grid = grid0 -n_steps = 4 if len(grid) < 10 else 100 -for _ in range(n_steps): - grid = game_of_life(grid) + n_steps = 5 if len(grid) < 10 else 100 + grid = grid0 + for _ in range(n_steps): + grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True + grid = game_of_life(grid) -answer_1 = grid.sum() -print(f"answer 1 is {answer_1}") + grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True - -n_steps = 5 if len(grid) < 10 else 100 -grid = grid0 -for _ in range(n_steps): - grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True - grid = game_of_life(grid) - -grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True - -answer_2 = sum(cell for line in grid for cell in line) -print(f"answer 2 is {answer_2}") + yield sum(cell for line in grid for cell in line) diff --git a/src/holt59/aoc/2015/day19.py b/src/holt59/aoc/2015/day19.py index db57831..6bcfb41 100644 --- a/src/holt59/aoc/2015/day19.py +++ b/src/holt59/aoc/2015/day19.py @@ -1,56 +1,58 @@ -import sys from collections import defaultdict +from typing import Any, Iterator -replacements_s, molecule = sys.stdin.read().split("\n\n") - -REPLACEMENTS: dict[str, list[str]] = defaultdict(list) -for replacement_s in replacements_s.splitlines(): - p = replacement_s.split(" => ") - REPLACEMENTS[p[0]].append(p[1]) -molecule = molecule.strip() - -generated = [ - molecule[:i] + replacement + molecule[i + len(symbol) :] - for symbol, replacements in REPLACEMENTS.items() - for replacement in replacements - for i in range(len(molecule)) - if molecule[i:].startswith(symbol) -] - -answer_1 = len(set(generated)) -print(f"answer 1 is {answer_1}") - -inversion: dict[str, str] = { - replacement: symbol - for symbol, replacements in REPLACEMENTS.items() - for replacement in replacements -} - -# there is actually only one way to create the molecule, and we can greedily replace -# tokens with their replacements, e.g., if H => OH then we can replace OH by H directly -# without thinking - -count = 0 -while molecule != "e": - i = 0 - m2 = "" - while i < len(molecule): - found = False - for replacement in inversion: - if molecule[i:].startswith(replacement): - m2 += inversion[replacement] - i += len(replacement) - count += 1 - found = True - break - - if not found: - m2 += molecule[i] - i += 1 - - # print(m2) - molecule = m2 +from ..base import BaseSolver -answer_2 = count -print(f"answer 2 is {count}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + replacements_s, molecule = input.split("\n\n") + + REPLACEMENTS: dict[str, list[str]] = defaultdict(list) + for replacement_s in replacements_s.splitlines(): + p = replacement_s.split(" => ") + REPLACEMENTS[p[0]].append(p[1]) + molecule = molecule.strip() + + generated = [ + molecule[:i] + replacement + molecule[i + len(symbol) :] + for symbol, replacements in REPLACEMENTS.items() + for replacement in replacements + for i in range(len(molecule)) + if molecule[i:].startswith(symbol) + ] + + yield len(set(generated)) + + inversion: dict[str, str] = { + replacement: symbol + for symbol, replacements in REPLACEMENTS.items() + for replacement in replacements + } + + # there is actually only one way to create the molecule, and we can greedily replace + # tokens with their replacements, e.g., if H => OH then we can replace OH by H directly + # without thinking + + count = 0 + while molecule != "e": + i = 0 + m2 = "" + while i < len(molecule): + found = False + for replacement in inversion: + if molecule[i:].startswith(replacement): + m2 += inversion[replacement] + i += len(replacement) + count += 1 + found = True + break + + if not found: + m2 += molecule[i] + i += 1 + + # print(m2) + molecule = m2 + + yield count diff --git a/src/holt59/aoc/2015/day2.py b/src/holt59/aoc/2015/day2.py index d2b8ffe..b492305 100644 --- a/src/holt59/aoc/2015/day2.py +++ b/src/holt59/aoc/2015/day2.py @@ -1,20 +1,24 @@ -import sys +from typing import Any, Iterator import numpy as np -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -length, width, height = np.array( - [[int(c) for c in line.split("x")] for line in lines] -).T -lw, wh, hl = (length * width, width * height, height * length) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + length, width, height = np.array( + [[int(c) for c in line.split("x")] for line in input.splitlines()] + ).T -answer_1 = np.sum(2 * (lw + wh + hl) + np.min(np.stack([lw, wh, hl]), axis=0)) -print(f"answer 1 is {answer_1}") + lw, wh, hl = (length * width, width * height, height * length) -answer_2 = np.sum( - length * width * height - + 2 * np.min(np.stack([length + width, length + height, height + width]), axis=0) -) -print(f"answer 2 is {answer_2}") + yield np.sum(2 * (lw + wh + hl) + np.min(np.stack([lw, wh, hl]), axis=0)) + + yield np.sum( + length * width * height + + 2 + * np.min( + np.stack([length + width, length + height, height + width]), axis=0 + ) + ) diff --git a/src/holt59/aoc/2015/day20.py b/src/holt59/aoc/2015/day20.py index 4eb3f71..3e97484 100644 --- a/src/holt59/aoc/2015/day20.py +++ b/src/holt59/aoc/2015/day20.py @@ -1,10 +1,10 @@ import itertools -import sys +from typing import Any, Iterator -target = int(sys.stdin.read()) +from ..base import BaseSolver -def presents(n: int, elf: int, max: int = target) -> int: +def presents(n: int, elf: int, max: int) -> int: count = 0 k = 1 while k * k < n: @@ -21,8 +21,9 @@ def presents(n: int, elf: int, max: int = target) -> int: return count -answer_1 = next(n for n in itertools.count(1) if presents(n, 10) >= target) -print(f"answer 1 is {answer_1}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + target = int(input) -answer_2 = next(n for n in itertools.count(1) if presents(n, 11, 50) >= target) -print(f"answer 2 is {answer_2}") + yield next(n for n in itertools.count(1) if presents(n, 10, target) >= target) + yield next(n for n in itertools.count(1) if presents(n, 11, 50) >= target) diff --git a/src/holt59/aoc/2015/day21.py b/src/holt59/aoc/2015/day21.py index cf71059..39f021b 100644 --- a/src/holt59/aoc/2015/day21.py +++ b/src/holt59/aoc/2015/day21.py @@ -1,7 +1,8 @@ import itertools -import sys from math import ceil -from typing import TypeAlias +from typing import Any, Iterator, TypeAlias + +from ..base import BaseSolver Modifier: TypeAlias = tuple[str, int, int, int] @@ -33,34 +34,31 @@ RINGS: list[Modifier] = [ ] -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -player_hp = 100 + player_hp = 100 -boss_attack = int(lines[1].split(":")[1].strip()) -boss_armor = int(lines[2].split(":")[1].strip()) -boss_hp = int(lines[0].split(":")[1].strip()) + boss_attack = int(lines[1].split(":")[1].strip()) + boss_armor = int(lines[2].split(":")[1].strip()) + boss_hp = int(lines[0].split(":")[1].strip()) + min_cost, max_cost = 1_000_000, 0 + for equipments in itertools.product(WEAPONS, ARMORS, RINGS, RINGS): + if equipments[-1][0] != "" and equipments[-2] == equipments[-1]: + continue -min_cost, max_cost = 1_000_000, 0 -for equipments in itertools.product(WEAPONS, ARMORS, RINGS, RINGS): - if equipments[-1][0] != "" and equipments[-2] == equipments[-1]: - continue + cost, player_attack, player_armor = ( + sum(equipment[1:][k] for equipment in equipments) for k in range(3) + ) - cost, player_attack, player_armor = ( - sum(equipment[1:][k] for equipment in equipments) for k in range(3) - ) + if ceil(boss_hp / max(1, player_attack - boss_armor)) <= ceil( + player_hp / max(1, boss_attack - player_armor) + ): + min_cost = min(cost, min_cost) + else: + max_cost = max(cost, max_cost) - if ceil(boss_hp / max(1, player_attack - boss_armor)) <= ceil( - player_hp / max(1, boss_attack - player_armor) - ): - min_cost = min(cost, min_cost) - else: - max_cost = max(cost, max_cost) - - -answer_1 = min_cost -print(f"answer 1 is {answer_1}") - -answer_2 = max_cost -print(f"answer 2 is {answer_2}") + yield min_cost + yield max_cost diff --git a/src/holt59/aoc/2015/day22.py b/src/holt59/aoc/2015/day22.py index 0c488ec..6d1456f 100644 --- a/src/holt59/aoc/2015/day22.py +++ b/src/holt59/aoc/2015/day22.py @@ -1,8 +1,9 @@ from __future__ import annotations import heapq -import sys -from typing import Literal, TypeAlias, cast +from typing import Any, Iterator, Literal, TypeAlias, cast + +from ..base import BaseSolver PlayerType: TypeAlias = Literal["player", "boss"] SpellType: TypeAlias = Literal["magic missile", "drain", "shield", "poison", "recharge"] @@ -62,17 +63,6 @@ def play( continue visited.add((player, player_hp, player_mana, player_armor, boss_hp, buffs)) - - if hard_mode and player == "player": - player_hp = max(0, player_hp - 1) - - if player_hp == 0: - continue - - if boss_hp == 0: - winning_node = spells - continue - new_buffs: list[tuple[BuffType, int]] = [] for buff, length in buffs: length = length - 1 @@ -88,6 +78,16 @@ def play( if length > 0: new_buffs.append((buff, length)) + if hard_mode and player == "player": + player_hp = player_hp - 1 + + if player_hp <= 0: + continue + + if boss_hp <= 0: + winning_node = spells + continue + buffs = tuple(new_buffs) if player == "boss": @@ -155,23 +155,28 @@ def play( return winning_node -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -player_hp = 50 -player_mana = 500 -player_armor = 0 + player_hp = 50 + player_mana = 500 + player_armor = 0 -boss_hp = int(lines[0].split(":")[1].strip()) -boss_attack = int(lines[1].split(":")[1].strip()) + boss_hp = int(lines[0].split(":")[1].strip()) + boss_attack = int(lines[1].split(":")[1].strip()) -answer_1 = sum( - c - for _, c in play(player_hp, player_mana, player_armor, boss_hp, boss_attack, False) -) -print(f"answer 1 is {answer_1}") + yield sum( + c + for _, c in play( + player_hp, player_mana, player_armor, boss_hp, boss_attack, False + ) + ) -# 1242 (not working) -answer_2 = sum( - c for _, c in play(player_hp, player_mana, player_armor, boss_hp, boss_attack, True) -) -print(f"answer 2 is {answer_2}") + # 1242 (not working) + yield sum( + c + for _, c in play( + player_hp, player_mana, player_armor, boss_hp, boss_attack, True + ) + ) diff --git a/src/holt59/aoc/2015/day3.py b/src/holt59/aoc/2015/day3.py index 0c376c1..73284ab 100644 --- a/src/holt59/aoc/2015/day3.py +++ b/src/holt59/aoc/2015/day3.py @@ -1,7 +1,7 @@ -import sys from collections import defaultdict +from typing import Any, Iterator -line = sys.stdin.read().strip() +from ..base import BaseSolver def process(directions: str) -> dict[tuple[int, int], int]: @@ -27,8 +27,7 @@ def process(directions: str) -> dict[tuple[int, int], int]: return counts -answer_1 = len(process(line)) -print(f"answer 1 is {answer_1}") - -answer_2 = len(process(line[::2]) | process(line[1::2])) -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + yield len(process(input)) + yield len(process(input[::2]) | process(input[1::2])) diff --git a/src/holt59/aoc/2015/day4.py b/src/holt59/aoc/2015/day4.py index 4b8efa9..e23f233 100644 --- a/src/holt59/aoc/2015/day4.py +++ b/src/holt59/aoc/2015/day4.py @@ -1,16 +1,20 @@ import hashlib import itertools -import sys +from typing import Any, Iterator -line = sys.stdin.read().strip() +from ..base import BaseSolver -it = iter(itertools.count(1)) -answer_1 = next( - i for i in it if hashlib.md5(f"{line}{i}".encode()).hexdigest().startswith("00000") -) -print(f"answer 1 is {answer_1}") -answer_2 = next( - i for i in it if hashlib.md5(f"{line}{i}".encode()).hexdigest().startswith("000000") -) -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + it = iter(itertools.count(1)) + yield next( + i + for i in it + if hashlib.md5(f"{input}{i}".encode()).hexdigest().startswith("00000") + ) + yield next( + i + for i in it + if hashlib.md5(f"{input}{i}".encode()).hexdigest().startswith("000000") + ) diff --git a/src/holt59/aoc/2015/day5.py b/src/holt59/aoc/2015/day5.py index 52fe963..7e1509e 100644 --- a/src/holt59/aoc/2015/day5.py +++ b/src/holt59/aoc/2015/day5.py @@ -1,4 +1,6 @@ -import sys +from typing import Any, Iterator + +from ..base import BaseSolver VOWELS = "aeiou" FORBIDDEN = {"ab", "cd", "pq", "xy"} @@ -27,10 +29,8 @@ def is_nice_2(s: str) -> bool: return True -lines = sys.stdin.read().splitlines() - -answer_1 = sum(map(is_nice_1, lines)) -print(f"answer 1 is {answer_1}") - -answer_2 = sum(map(is_nice_2, lines)) -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() + yield sum(map(is_nice_1, lines)) + yield sum(map(is_nice_2, lines)) diff --git a/src/holt59/aoc/2015/day6.py b/src/holt59/aoc/2015/day6.py index 1b5c21a..f46c42d 100644 --- a/src/holt59/aoc/2015/day6.py +++ b/src/holt59/aoc/2015/day6.py @@ -1,33 +1,32 @@ -import sys -from typing import Literal, cast +from typing import Any, Iterator, Literal, cast import numpy as np import parse # type: ignore -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -lights_1 = np.zeros((1000, 1000), dtype=bool) -lights_2 = np.zeros((1000, 1000), dtype=int) -for line in lines: - action, sx, sy, ex, ey = cast( - tuple[Literal["turn on", "turn off", "toggle"], int, int, int, int], - parse.parse("{} {:d},{:d} through {:d},{:d}", line), # type: ignore - ) - ex, ey = ex + 1, ey + 1 - match action: - case "turn on": - lights_1[sx:ex, sy:ey] = True - lights_2[sx:ex, sy:ey] += 1 - case "turn off": - lights_1[sx:ex, sy:ey] = False - lights_2[sx:ex, sy:ey] = np.maximum(lights_2[sx:ex, sy:ey] - 1, 0) - case "toggle": - lights_1[sx:ex, sy:ey] = ~lights_1[sx:ex, sy:ey] - lights_2[sx:ex, sy:ey] += 2 +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lights_1 = np.zeros((1000, 1000), dtype=bool) + lights_2 = np.zeros((1000, 1000), dtype=int) + for line in input.splitlines(): + action, sx, sy, ex, ey = cast( + tuple[Literal["turn on", "turn off", "toggle"], int, int, int, int], + parse.parse("{} {:d},{:d} through {:d},{:d}", line), # type: ignore + ) + ex, ey = ex + 1, ey + 1 -answer_1 = lights_1.sum() -print(f"answer 1 is {answer_1}") + match action: + case "turn on": + lights_1[sx:ex, sy:ey] = True + lights_2[sx:ex, sy:ey] += 1 + case "turn off": + lights_1[sx:ex, sy:ey] = False + lights_2[sx:ex, sy:ey] = np.maximum(lights_2[sx:ex, sy:ey] - 1, 0) + case "toggle": + lights_1[sx:ex, sy:ey] = ~lights_1[sx:ex, sy:ey] + lights_2[sx:ex, sy:ey] += 2 -answer_2 = lights_2.sum() -print(f"answer 2 is {answer_2}") + yield lights_1.sum() + yield lights_2.sum() diff --git a/src/holt59/aoc/2015/day7.py b/src/holt59/aoc/2015/day7.py index a6e3fe5..b45ce6a 100644 --- a/src/holt59/aoc/2015/day7.py +++ b/src/holt59/aoc/2015/day7.py @@ -1,11 +1,7 @@ -import logging import operator -import os -import sys -from typing import Callable +from typing import Any, Callable, Iterator -VERBOSE = os.getenv("AOC_VERBOSE") == "True" -logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING) +from ..base import BaseSolver OPERATORS = { "AND": operator.and_, @@ -36,48 +32,6 @@ def value_of(key: str) -> tuple[str, Callable[[dict[str, int]], int]]: return key, lambda values: values[key] -lines = sys.stdin.read().splitlines() - -signals: Signals = {} -values: dict[str, int] = {"": 0} - -for line in lines: - command, signal = line.split(" -> ") - - if command.startswith("NOT"): - name = command.split(" ")[1] - signals[signal] = ( - (name, ""), - (lambda values, _n=name: values[_n], lambda _v: 0), - lambda a, _b: ~a, - ) - - elif not any(command.find(name) >= 0 for name in OPERATORS): - try: - values[signal] = int(command) - except ValueError: - signals[signal] = ( - (command, ""), - (lambda values, _c=command: values[_c], lambda _v: 0), - lambda a, _b: a, - ) - - else: - op: Callable[[int, int], int] = zero_op - lhs_s, rhs_s = "", "" - - for name in OPERATORS: - if command.find(name) >= 0: - op = OPERATORS[name] - lhs_s, rhs_s = command.split(f" {name} ") - break - - lhs_s, lhs_fn = value_of(lhs_s) - rhs_s, rhs_fn = value_of(rhs_s) - - signals[signal] = ((lhs_s, rhs_s), (lhs_fn, rhs_fn), op) - - def process( signals: Signals, values: dict[str, int], @@ -91,11 +45,52 @@ def process( return values -values_1 = process(signals.copy(), values.copy()) -logging.info("\n" + "\n".join(f"{k}: {values_1[k]}" for k in sorted(values_1))) -answer_1 = values_1["a"] -print(f"answer 1 is {answer_1}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any] | None: + lines = input.splitlines() -values_2 = process(signals.copy(), values | {"b": values_1["a"]}) -answer_2 = values_2["a"] -print(f"answer 2 is {answer_2}") + signals: Signals = {} + values: dict[str, int] = {"": 0} + + for line in lines: + command, signal = line.split(" -> ") + + if command.startswith("NOT"): + name = command.split(" ")[1] + signals[signal] = ( + (name, ""), + (lambda values, _n=name: values[_n], lambda _v: 0), + lambda a, _b: ~a, + ) + + elif not any(command.find(name) >= 0 for name in OPERATORS): + try: + values[signal] = int(command) + except ValueError: + signals[signal] = ( + (command, ""), + (lambda values, _c=command: values[_c], lambda _v: 0), + lambda a, _b: a, + ) + + else: + op: Callable[[int, int], int] = zero_op + lhs_s, rhs_s = "", "" + + for name in OPERATORS: + if command.find(name) >= 0: + op = OPERATORS[name] + lhs_s, rhs_s = command.split(f" {name} ") + break + + lhs_s, lhs_fn = value_of(lhs_s) + rhs_s, rhs_fn = value_of(rhs_s) + + signals[signal] = ((lhs_s, rhs_s), (lhs_fn, rhs_fn), op) + + values_1 = process(signals.copy(), values.copy()) + for k in sorted(values_1): + self.logger.info(f"{k}: {values_1[k]}") + yield values_1["a"] + + yield process(signals.copy(), values | {"b": values_1["a"]})["a"] diff --git a/src/holt59/aoc/2015/day8.py b/src/holt59/aoc/2015/day8.py index be22c22..d6662ad 100644 --- a/src/holt59/aoc/2015/day8.py +++ b/src/holt59/aoc/2015/day8.py @@ -1,35 +1,32 @@ -import logging -import os -import sys +from typing import Any, Iterator -VERBOSE = os.getenv("AOC_VERBOSE") == "True" -logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING) +from ..base import BaseSolver -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -answer_1 = sum( - # left and right quotes (not in memory) - 2 - # each \\ adds one character in the literals (compared to memory) - + line.count(R"\\") - # each \" adds one character in the literals (compared to memory) - + line[1:-1].count(R"\"") - # each \xFF adds 3 characters in the literals (compared to memory), but we must not - # count A\\x (A != \), but we must count A\\\x (A != \) - in practice we should also - # avoid \\\\x, etc., but this does not occur in the examples and the actual input - + 3 * (line.count(R"\x") - line.count(R"\\x") + line.count(R"\\\x")) - for line in lines -) -print(f"answer 1 is {answer_1}") + yield sum( + # left and right quotes (not in memory) + 2 + # each \\ adds one character in the literals (compared to memory) + + line.count(R"\\") + # each \" adds one character in the literals (compared to memory) + + line[1:-1].count(R"\"") + # each \xFF adds 3 characters in the literals (compared to memory), but we must not + # count A\\x (A != \), but we must count A\\\x (A != \) - in practice we should also + # avoid \\\\x, etc., but this does not occur in the examples and the actual input + + 3 * (line.count(R"\x") - line.count(R"\\x") + line.count(R"\\\x")) + for line in lines + ) -answer_2 = sum( - # needs to wrap in quotes (2 characters) - 2 - # needs to escape every \ with an extra \ - + line.count("\\") - # needs to escape every " with an extra \ (including the first and last ones) - + line.count('"') - for line in lines -) -print(f"answer 2 is {answer_2}") + yield sum( + # needs to wrap in quotes (2 characters) + 2 + # needs to escape every \ with an extra \ + + line.count("\\") + # needs to escape every " with an extra \ (including the first and last ones) + + line.count('"') + for line in lines + ) diff --git a/src/holt59/aoc/2015/day9.py b/src/holt59/aoc/2015/day9.py index f6e7176..e055964 100644 --- a/src/holt59/aoc/2015/day9.py +++ b/src/holt59/aoc/2015/day9.py @@ -1,27 +1,28 @@ import itertools -import sys from collections import defaultdict -from typing import cast +from typing import Any, Iterator, cast import parse # type: ignore -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -distances: dict[str, dict[str, int]] = defaultdict(dict) -for line in lines: - origin, destination, length = cast( - tuple[str, str, int], - parse.parse("{} to {} = {:d}", line), # type: ignore - ) - distances[origin][destination] = distances[destination][origin] = length -distance_of_routes = { - route: sum(distances[o][d] for o, d in zip(route[:-1], route[1:])) - for route in map(tuple, itertools.permutations(distances)) -} +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -answer_1 = min(distance_of_routes.values()) -print(f"answer 1 is {answer_1}") + distances: dict[str, dict[str, int]] = defaultdict(dict) + for line in lines: + origin, destination, length = cast( + tuple[str, str, int], + parse.parse("{} to {} = {:d}", line), # type: ignore + ) + distances[origin][destination] = distances[destination][origin] = length -answer_2 = max(distance_of_routes.values()) -print(f"answer 2 is {answer_2}") + distance_of_routes = { + route: sum(distances[o][d] for o, d in zip(route[:-1], route[1:])) + for route in map(tuple, itertools.permutations(distances)) + } + + yield min(distance_of_routes.values()) + yield max(distance_of_routes.values()) diff --git a/src/holt59/aoc/__main__.py b/src/holt59/aoc/__main__.py index c0bad6e..1ff9c44 100644 --- a/src/holt59/aoc/__main__.py +++ b/src/holt59/aoc/__main__.py @@ -178,7 +178,14 @@ def main(): start = datetime.now() last = start - for i_answer, answer in enumerate(solver.solve(data.strip())): + + it = solver.solve(data.strip()) + + if it is None: + solver.logger.error(f"no implementation for {year} day {day}") + exit() + + for i_answer, answer in enumerate(it): current = datetime.now() if api: diff --git a/src/holt59/aoc/base.py b/src/holt59/aoc/base.py index 97cb666..bc644b9 100644 --- a/src/holt59/aoc/base.py +++ b/src/holt59/aoc/base.py @@ -7,10 +7,12 @@ _T = TypeVar("_T") class ProgressHandler(Protocol): @overload - def wrap(self, values: Sequence[_T]) -> Iterator[_T]: ... + def wrap(self, values: Sequence[_T]) -> Iterator[_T]: + ... @overload - def wrap(self, values: Iterable[_T], total: int) -> Iterator[_T]: ... + def wrap(self, values: Iterable[_T], total: int) -> Iterator[_T]: + ... class BaseSolver: @@ -31,4 +33,5 @@ class BaseSolver: self.outputs = outputs @abstractmethod - def solve(self, input: str) -> Iterator[Any]: ... + def solve(self, input: str) -> Iterator[Any] | None: + ...