Compare commits

...

13 Commits

Author SHA1 Message Date
5ee410d26d Implement day 14 solver 2024-12-13 22:20:06 -08:00
4cbd1c9aee Implement day 13 solver 2024-12-13 17:15:05 -08:00
92b9f5ca01 Implement day 12 solver 2024-12-13 17:15:05 -08:00
978ecb0b5e Implement day 11 solver 2024-12-13 17:15:05 -08:00
b26f516465 Implement day 10 solver 2024-12-13 17:15:05 -08:00
bee7f5e59e Restructure solver modules with classes and unittest suites 2024-12-13 17:15:05 -08:00
7c021ab87f Implement day 9 solver 2024-12-08 23:22:34 -08:00
569714d227 Improve input parsing 2024-12-08 08:26:25 -08:00
b9f544ed1e Implement day 8 solver 2024-12-08 08:20:05 -08:00
ceabdf384d Implement day 7 solver 2024-12-06 23:08:08 -08:00
b6881aeaef Implement day 6 solver 2024-12-05 23:32:28 -08:00
00a3bdd07b Simplify test input 2024-12-04 22:08:54 -08:00
80b85b459e Implement day 5 solver 2024-12-04 22:04:23 -08:00
17 changed files with 1724 additions and 239 deletions

View File

@@ -1,33 +1,48 @@
from collections import Counter
from textwrap import dedent
from typing import override
from unittest import TestCase
test_input_p1 = test_input_p2 = """
3 4
4 3
2 5
1 3
3 9
3 3
""".strip()
test_solution_p1 = 11
test_solution_p2 = 31
from puzzles._solver import Solver
def solve_p1(puzzle_input: str) -> int:
list1, list2 = _parse_lists(puzzle_input)
pairs = zip(sorted(list1), sorted(list2))
distances = (abs(pair[0] - pair[1]) for pair in pairs)
return sum(distances)
class DayOneSolver(Solver):
list1: list[int]
list2: list[int]
def solve_p2(puzzle_input: str) -> int:
list1, list2 = _parse_lists(puzzle_input)
occurrences = Counter(list2)
similarities = (value * occurrences.get(value, 0) for value in list1)
return sum(similarities)
def _parse_lists(puzzle_input: str) -> tuple[list[int], list[int]]:
lines = (line.partition(" ") for line in puzzle_input.split("\n") if line)
@override
def __init__(self, puzzle_input: str):
lines = (line.partition(" ") for line in puzzle_input.strip().split("\n"))
list1, list2 = zip(*((int(line[0]), int(line[2])) for line in lines))
return list1, list2
self.list1 = list(list1)
self.list2 = list(list2)
@override
def solve_p1(self) -> int:
minimum_pairs = zip(sorted(self.list1), sorted(self.list2))
return sum(abs(a - b) for a, b in minimum_pairs)
@override
def solve_p2(self) -> int:
occurrences = Counter(self.list2)
return sum(value * occurrences.get(value, 0) for value in self.list1)
class TestDayOneSolver(TestCase):
def test(self):
solver = DayOneSolver(
dedent(
"""
3 4
4 3
2 5
1 3
3 9
3 3
"""
)
)
self.assertListEqual(solver.list1, [3, 4, 2, 1, 3, 3])
self.assertListEqual(solver.list2, [4, 3, 5, 3, 9, 3])
self.assertEqual(solver.solve_p1(), 11)
self.assertEqual(solver.solve_p2(), 31)

138
puzzles/10.py Normal file
View File

@@ -0,0 +1,138 @@
from dataclasses import dataclass
from textwrap import dedent
from typing import NamedTuple, override
from unittest import TestCase
from puzzles._solver import Solver
class TrailSet(NamedTuple):
trailhead: tuple[int, int]
trailends: set[tuple[int, int]]
count: int
@dataclass
class TopographicalMap:
grid: list[list[str]]
trailheads: set[tuple[int, int]]
@classmethod
def parse(cls, raw_map: str) -> "TopographicalMap":
grid = []
trailheads = set()
for x, row in enumerate(raw_map.strip().splitlines()):
grid_row = []
for y, cell in enumerate(row):
height = int(cell)
grid_row.append(height)
if height == 0:
trailheads.add((x, y))
grid.append(grid_row)
return cls(grid, trailheads)
def trails(self) -> list[TrailSet]:
trails = []
for trailhead in self.trailheads:
trailends: set[tuple, tuple] = set()
count = 0
x, y = trailhead
height = self.grid[x][y]
def follow_trail(x, y, height):
nonlocal count
if height == 9:
trailends.add((x, y))
count += 1
return
if x > 0 and self.grid[x - 1][y] == height + 1:
follow_trail(x - 1, y, height + 1)
if x < len(self.grid) - 1 and self.grid[x + 1][y] == height + 1:
follow_trail(x + 1, y, height + 1)
if y > 0 and self.grid[x][y - 1] == height + 1:
follow_trail(x, y - 1, height + 1)
if y < len(self.grid[0]) - 1 and self.grid[x][y + 1] == height + 1:
follow_trail(x, y + 1, height + 1)
follow_trail(x, y, height)
trails.append(TrailSet(trailhead, trailends, count))
return trails
class DayTenSolver(Solver):
topographical_map: TopographicalMap
@override
def __init__(self, puzzle_input: str):
self.topographical_map = TopographicalMap.parse(puzzle_input)
@override
def solve_p1(self) -> int:
trail_sets = self.topographical_map.trails()
return sum(len(trail_set.trailends) for trail_set in trail_sets)
@override
def solve_p2(self) -> int:
trail_sets = self.topographical_map.trails()
return sum(trail_set.count for trail_set in trail_sets)
class TestDayTenSolver(TestCase):
test_input = dedent(
"""
89010123
78121874
87430965
96549874
45678903
32019012
01329801
10456732
"""
)
def test_parse(self):
solver = DayTenSolver(self.test_input)
self.assertEqual(
solver.topographical_map,
TopographicalMap(
grid=[
[8, 9, 0, 1, 0, 1, 2, 3],
[7, 8, 1, 2, 1, 8, 7, 4],
[8, 7, 4, 3, 0, 9, 6, 5],
[9, 6, 5, 4, 9, 8, 7, 4],
[4, 5, 6, 7, 8, 9, 0, 3],
[3, 2, 0, 1, 9, 0, 1, 2],
[0, 1, 3, 2, 9, 8, 0, 1],
[1, 0, 4, 5, 6, 7, 3, 2],
],
trailheads={
(0, 2),
(0, 4),
(2, 4),
(4, 6),
(5, 2),
(5, 5),
(6, 0),
(6, 6),
(7, 1),
},
),
)
def test_solve_p1(self):
solver = DayTenSolver(self.test_input)
self.assertEqual(solver.solve_p1(), 36)
def test_solve_p2(self):
solver = DayTenSolver(self.test_input)
self.assertEqual(solver.solve_p2(), 81)

55
puzzles/11.py Normal file
View File

@@ -0,0 +1,55 @@
from functools import cache
from math import log
from typing import override
from unittest import TestCase
from puzzles._solver import Solver
class DayElevenSolver(Solver):
stones: tuple[int, ...]
@override
def __init__(self, puzzle_input: str):
self.stones = tuple(map(int, puzzle_input.strip().split(" ")))
@override
def solve_p1(self) -> int:
return sum(sum_descendants(stone, 25) for stone in self.stones)
@override
def solve_p2(self) -> int:
return sum(sum_descendants(stone, 75) for stone in self.stones)
@cache
def sum_descendants(stone: int, blinks: int) -> int:
if blinks == 0:
return 1
if stone == 0:
return sum_descendants(1, blinks - 1)
digit_len = int(log(stone, 10)) + 1
if digit_len % 2 == 0:
left_digits, right_digits = divmod(stone, 10 ** (digit_len // 2))
return sum_descendants(left_digits, blinks - 1) + sum_descendants(
right_digits, blinks - 1
)
return sum_descendants(stone * 2024, blinks - 1)
class TestDayElevenSolver(TestCase):
def test_sum_descendants(self):
self.assertEqual(sum_descendants(0, 1), 1)
self.assertEqual(sum_descendants(17, 1), 2)
self.assertEqual(sum_descendants(125, 4), 3)
def test_parse(self):
solver = DayElevenSolver("123 4 56\n")
self.assertEqual(solver.stones, (123, 4, 56))
def test_solve_p1(self):
solver = DayElevenSolver("125 17\n")
self.assertEqual(solver.solve_p1(), 55312)

196
puzzles/12.py Normal file
View File

@@ -0,0 +1,196 @@
from dataclasses import dataclass
from functools import cached_property
from itertools import count
from textwrap import dedent
from typing import Iterator, NamedTuple, override
from unittest import TestCase
from more_itertools import first_true, ilen
from multidict import MultiDict
from puzzles._solver import Solver
@dataclass(frozen=True)
class Fence:
inside: tuple[int, int]
outside: tuple[int, int]
def left(self) -> "Fence":
(xi, yi) = self.inside
(xo, yo) = self.outside
dx, dy = (xo - xi, yo - yi)
return Fence((xi + dy, yi + dx), (xo + dy, yo + dx))
def right(self) -> "Fence":
(xi, yi) = self.inside
(xo, yo) = self.outside
dx, dy = (xo - xi, yo - yi)
return Fence((xi - dy, yi - dx), (xo - dy, yo - dx))
@dataclass
class Region:
label: str
plants: set[tuple[int, int]]
@property
def area(self) -> int:
return len(self.plants)
@cached_property
def fences(self) -> set[Fence]:
return {
Fence((x, y), (x + dx, y + dy))
for x, y in self.plants
for dx, dy in ((0, 1), (1, 0), (0, -1), (-1, 0))
if (x + dx, y + dy) not in self.plants
}
@property
def perimeter(self) -> int:
return len(self.fences)
@cached_property
def sides(self) -> set[tuple[int, int]]:
fence_ids: dict[Fence, int] = {}
id_gen = count(0)
for fence in self.fences:
if fence in fence_ids:
continue
id = next(id_gen)
fence_ids[fence] = id
left_fence = fence.left()
while left_fence in self.fences:
fence_ids[left_fence] = id
left_fence = left_fence.left()
right_fence = fence.right()
while right_fence in self.fences:
fence_ids[right_fence] = id
right_fence = right_fence.right()
return len(set(fence_ids.values()))
class DayTwelveSolver(Solver):
regions: MultiDict[Region]
@override
def __init__(self, puzzle_input: str):
self.regions = MultiDict()
for x, row in enumerate(puzzle_input.strip().splitlines()):
for y, plant in enumerate(row):
if plant not in self.regions:
self.regions.add(plant, Region(plant, {(x, y)}))
continue
similar_regions = self.regions.popall(plant)
merge_top_region = first_true(
similar_regions,
None,
pred=lambda region: (x - 1, y) in region.plants,
)
merge_left_region = first_true(
similar_regions,
None,
pred=lambda region: (x, y - 1) in region.plants,
)
if merge_top_region:
similar_regions.remove(merge_top_region)
merge_top_region.plants.add((x, y))
if merge_left_region and merge_left_region is not merge_top_region:
similar_regions.remove(merge_left_region)
merge_top_region.plants.update(merge_left_region.plants)
self.regions.add(plant, merge_top_region)
elif merge_left_region:
similar_regions.remove(merge_left_region)
merge_left_region.plants.add((x, y))
self.regions.add(plant, merge_left_region)
else:
self.regions.add(plant, Region(plant, {(x, y)}))
self.regions.extend(
[(plant, similar_region) for similar_region in similar_regions]
)
@override
def solve_p1(self) -> int:
return sum(region.area * region.perimeter for region in self.regions.values())
@override
def solve_p2(self) -> int:
return sum(region.area * region.sides for region in self.regions.values())
class TestDayTwelveSolver(TestCase):
def test_solve_p1(self):
solver = DayTwelveSolver(
dedent(
"""
AAAA
BBCD
BBCC
EEEC
"""
)
)
self.assertEqual(solver.solve_p1(), 140)
solver = DayTwelveSolver(
dedent(
"""
RRRRIICCFF
RRRRIICCCF
VVRRRCCFFF
VVRCCCJFFF
VVVVCJJCFE
VVIVCCJJEE
VVIIICJJEE
MIIIIIJJEE
MIIISIJEEE
MMMISSJEEE
"""
)
)
self.assertEqual(solver.solve_p1(), 1930)
def test_solve_p2(self):
solver = DayTwelveSolver(
dedent(
"""
AAAA
BBCD
BBCC
EEEC
"""
)
)
for region in solver.regions.values():
print(region.label, region.sides)
self.assertEqual(solver.solve_p2(), 80)
solver = DayTwelveSolver(
dedent(
"""
RRRRIICCFF
RRRRIICCCF
VVRRRCCFFF
VVRCCCJFFF
VVVVCJJCFE
VVIVCCJJEE
VVIIICJJEE
MIIIIIJJEE
MIIISIJEEE
MMMISSJEEE
"""
)
)
self.assertEqual(solver.solve_p2(), 1206)

97
puzzles/13.py Normal file
View File

@@ -0,0 +1,97 @@
import re
from textwrap import dedent
from typing import Iterator, NamedTuple, override
from unittest import TestCase
from puzzles._solver import Solver
class ClawMachine(NamedTuple):
ax: int
ay: int
bx: int
by: int
px: int
py: int
a_spec_pattern = re.compile(r"Button A: X\+(\d+), Y\+(\d+)")
b_spec_pattern = re.compile(r"Button B: X\+(\d+), Y\+(\d+)")
p_spec_pattern = re.compile(r"Prize: X=(\d+), Y=(\d+)")
def solve_machine(m: ClawMachine) -> tuple[float, float]:
c = (m.px * m.by - m.py * m.bx) / (m.ax * m.by - m.ay * m.bx)
d = (m.px - m.ax * c) / m.bx
return c, d
class DayThirteenSolver(Solver):
machines: list[ClawMachine]
@override
def __init__(self, puzzle_input: str):
self.machines = []
for spec in puzzle_input.strip().split("\n\n"):
a_spec, b_spec, p_spec = spec.split("\n")
ax, ay = map(int, a_spec_pattern.match(a_spec).groups())
bx, by = map(int, b_spec_pattern.match(b_spec).groups())
px, py = map(int, p_spec_pattern.match(p_spec).groups())
self.machines.append(ClawMachine(ax, ay, bx, by, px, py))
@override
def solve_p1(self) -> int:
return sum(
int(c * 3 + d)
for c, d in map(solve_machine, self.machines)
if c.is_integer() and d.is_integer()
)
@override
def solve_p2(self) -> int:
corrected_machines = map(
lambda m: ClawMachine(
m.ax,
m.ay,
m.bx,
m.by,
m.px + 10000000000000,
m.py + 10000000000000,
),
self.machines,
)
return sum(
int(c * 3 + d)
for c, d in map(solve_machine, corrected_machines)
if c.is_integer() and d.is_integer()
)
class TestDayThirteenSolver(TestCase):
def test(self):
solver = DayThirteenSolver(
dedent(
"""
Button A: X+94, Y+34
Button B: X+22, Y+67
Prize: X=8400, Y=5400
Button A: X+26, Y+66
Button B: X+67, Y+21
Prize: X=12748, Y=12176
Button A: X+17, Y+86
Button B: X+84, Y+37
Prize: X=7870, Y=6450
Button A: X+69, Y+23
Button B: X+27, Y+71
Prize: X=18641, Y=10279
"""
)
)
self.assertEqual(
solver.machines[0],
ClawMachine(94, 34, 22, 67, 8400, 5400),
)
self.assertEqual(solver.solve_p1(), 480)

133
puzzles/14.py Normal file
View File

@@ -0,0 +1,133 @@
import re
from collections import Counter
from itertools import count
from math import prod
from textwrap import dedent
from typing import NamedTuple, override
from unittest import TestCase
from more_itertools import filter_map, first_true, iterate
from puzzles._solver import Solver
class Robot(NamedTuple):
x: int
y: int
dx: int
dy: int
robot_spec_pattern = re.compile(r"p=(-?\d+),(-?\d+) v=(-?\d+),(-?\d+)")
class DayFourteenSolver(Solver):
robots: list[Robot]
width: int
height: int
@override
def __init__(self, puzzle_input: str, width=101, height=103):
self.robots = [
Robot(*map(int, match.groups()))
for match in robot_spec_pattern.finditer(puzzle_input)
]
self.width = width
self.height = height
@override
def solve_p1(self) -> int:
quadrant_counts = Counter(
filter_map(
lambda r: self.get_quadrant(self.move_robot(r, 100)),
self.robots,
)
)
return prod(quadrant_counts.values())
@override
def solve_p2(self) -> int:
state = iterate(
lambda robots: [self.move_robot(robot, 1) for robot in robots],
self.robots,
)
return first_true(
count(),
pred=lambda i: self.total_overlap(next(state)) == 0,
)
def move_robot(self, robot: Robot, seconds: int) -> Robot:
return Robot(
(robot.x + robot.dx * seconds) % self.width,
(robot.y + robot.dy * seconds) % self.height,
robot.dx,
robot.dy,
)
def get_quadrant(self, robot: Robot) -> int | None:
if robot.x > self.width // 2 and robot.y < self.height // 2:
return 1
if robot.x < self.width // 2 and robot.y < self.height // 2:
return 2
if robot.x < self.width // 2 and robot.y > self.height // 2:
return 3
if robot.x > self.width // 2 and robot.y > self.height // 2:
return 4
return None
def total_overlap(self, robots: list[Robot]) -> int:
locations = Counter((robot.x, robot.y) for robot in robots)
return sum(count for count in locations.values() if count > 1)
class TestDayFourteenSolver(TestCase):
def test(self):
solver = DayFourteenSolver(
dedent(
"""
p=0,4 v=3,-3
p=6,3 v=-1,-3
p=10,3 v=-1,2
p=2,0 v=2,-1
p=0,0 v=1,3
p=3,0 v=-2,-2
p=7,6 v=-1,-3
p=3,0 v=-1,-2
p=9,3 v=2,3
p=7,3 v=-1,2
p=2,4 v=2,-3
p=9,5 v=-3,-3
"""
),
width=11,
height=7,
)
self.assertTupleEqual(
solver.robots[0],
Robot(0, 4, 3, -3),
)
self.assertTupleEqual(
solver.move_robot(solver.robots[0], 1),
Robot(3, 1, 3, -3),
)
self.assertEqual(solver.get_quadrant(Robot(0, 0, 0, 0)), 2)
self.assertEqual(solver.get_quadrant(Robot(5, 0, 0, 0)), None)
self.assertEqual(
solver.total_overlap(
[
Robot(0, 0, 0, 0),
Robot(0, 0, 0, 0),
]
),
2,
)
self.assertEqual(
solver.total_overlap(
[
Robot(0, 0, 0, 0),
Robot(1, 0, 0, 0),
]
),
0,
)
self.assertEqual(solver.solve_p1(), 12)

View File

@@ -1,63 +1,88 @@
from itertools import islice
from typing import Iterator
from dataclasses import dataclass
from functools import cached_property
from itertools import pairwise
from textwrap import dedent
from typing import Iterator, override
from unittest import TestCase
from more_itertools import ilen
test_input_p1 = test_input_p2 = """
7 6 4 2 1
1 2 7 8 9
9 7 6 2 1
1 3 2 4 5
8 6 4 4 1
1 3 6 7 9
""".strip()
test_solution_p1 = 2
test_solution_p2 = 4
from puzzles._solver import Solver
def solve_p1(puzzle_input: str) -> int:
reports = _parse_reports(puzzle_input)
delta_reports = (_deltas(report) for report in reports)
safe_delta_reports = filter(_is_gradual_monotonic, delta_reports)
return ilen(safe_delta_reports)
@dataclass
class Report:
levels: tuple[int, ...]
@cached_property
def deltas(self) -> tuple[int, ...]:
return tuple(b - a for a, b in pairwise(self.levels))
def solve_p2(puzzle_input: str) -> int:
reports = _parse_reports(puzzle_input)
dampened_report_collections = (_dampen_permutations(report) for report in reports)
delta_report_collections = (
(_deltas(report) for report in report_collection)
for report_collection in dampened_report_collections
)
safe_report_collections = filter(
lambda delta_report_collection: any(
_is_gradual_monotonic(delta_report)
for delta_report in delta_report_collection
),
delta_report_collections,
)
return ilen(safe_report_collections)
def _parse_reports(puzzle_input: str) -> Iterator[tuple[int, ...]]:
lines = puzzle_input.splitlines()
return (tuple(int(level) for level in line.split(" ")) for line in lines)
def _deltas(report: tuple[int, ...]) -> tuple[int, ...]:
pairs = zip(report, islice(report, 1, None))
return tuple(b - a for a, b in pairs)
def _is_gradual_monotonic(delta_report: tuple[int]) -> bool:
sign = 1 if delta_report[0] > 0 else -1
def is_gradual_monotonic(self) -> bool:
sign = 1 if self.deltas[0] > 0 else -1
return all(
delta != 0 and delta / abs(delta) == sign and abs(delta) < 4
for delta in delta_report
for delta in self.deltas
)
def permute_dampened_variations(self) -> Iterator["Report"]:
return (
Report(self.levels[:i] + self.levels[i + 1 :])
for i in range(0, len(self.levels) + 1)
)
def _dampen_permutations(report: tuple[int, ...]) -> Iterator[tuple[int, ...]]:
yield report
yield from (report[:i] + report[i + 1 :] for i in range(0, len(report)))
class DayTwoSolver(Solver):
reports: list[Report]
@override
def __init__(self, puzzle_input: str):
self.reports = list(
Report(tuple(int(level) for level in line.split(" ")))
for line in puzzle_input.strip().splitlines()
)
@override
def solve_p1(self) -> int:
safe_reports = filter(Report.is_gradual_monotonic, self.reports)
return ilen(safe_reports)
@override
def solve_p2(self) -> int:
safe_reports = filter(
lambda report: any(
report_variation.is_gradual_monotonic()
for report_variation in report.permute_dampened_variations()
),
self.reports,
)
return ilen(safe_reports)
class TestDayNSolver(TestCase):
def test(self):
solver = DayTwoSolver(
dedent(
"""
7 6 4 2 1
1 2 7 8 9
9 7 6 2 1
1 3 2 4 5
8 6 4 4 1
1 3 6 7 9
"""
)
)
self.assertListEqual(
solver.reports,
[
Report((7, 6, 4, 2, 1)),
Report((1, 2, 7, 8, 9)),
Report((9, 7, 6, 2, 1)),
Report((1, 3, 2, 4, 5)),
Report((8, 6, 4, 4, 1)),
Report((1, 3, 6, 7, 9)),
],
)
self.assertEqual(solver.solve_p1(), 2)
self.assertEqual(solver.solve_p2(), 4)

View File

@@ -1,35 +1,58 @@
import re
from functools import reduce
from typing import Literal, override
from unittest import TestCase
test_input_p1 = "xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))"
test_input_p2 = "xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))"
test_solution_p1 = 161
test_solution_p2 = 48
instruction_pattern = re.compile(r"mul\((\d+),(\d+)\)|(do(?:n't)?)\(\)")
from puzzles._solver import Solver
def solve_p1(puzzle_input: str) -> int:
instructions = (
tuple(map(int, match[:2]))
for match in instruction_pattern.findall(puzzle_input)
if match[0]
class DayThreeSolver(Solver):
memory: tuple[int, int] | Literal["do", "don't"]
@override
def __init__(self, puzzle_input: str):
matches = re.findall(r"mul\((\d+),(\d+)\)|(do(?:n't)?)\(\)", puzzle_input)
self.memory = tuple(
(int(a), int(b)) if a else enable_flag for a, b, enable_flag in matches
)
return sum(a * b for a, b in instructions)
@override
def solve_p1(self) -> int:
total = 0
for match in self.memory:
match match:
case a, b:
total += a * b
return total
@override
def solve_p2(self) -> int:
total = 0
enabled = True
for match in self.memory:
match match:
case "do":
enabled = True
case "don't":
enabled = False
case a, b:
if enabled:
total += a * b
return total
def solve_p2(puzzle_input: str) -> int:
instructions = (
match[2] or tuple(map(int, match[:2]))
for match in instruction_pattern.findall(puzzle_input)
class TestDayThreeSolver(TestCase):
def test_solve_p1(self):
solver = DayThreeSolver(
"xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))\n"
)
return reduce(
lambda a, i: (
(1 if i == "do" else 0, a[1])
if isinstance(i, str)
else (a[0], a[1] + a[0] * i[0] * i[1])
),
instructions,
(1, 0),
)[1]
self.assertEqual(solver.solve_p1(), 161)
def test_solve_p2(self):
solver = DayThreeSolver(
"xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))\n"
)
self.assertEqual(solver.solve_p2(), 48)

View File

@@ -1,84 +1,90 @@
from typing import Iterator
from textwrap import dedent
from typing import Iterator, override
from unittest import TestCase
from more_itertools import sliding_window
from puzzles._solver import Solver
Line = tuple[str, ...]
Grid = tuple[Line, ...]
test_input_p1 = test_input_p2 = """
MMMSXXMASM
MSAMXMSMSA
AMXSXMAAMM
MSAMASMSMX
XMASAMXAMM
XXAMMXXAMA
SMSMSASXSS
SAXAMASAAA
MAMMMXMMMM
MXMXAXMASX
""".strip()
class DayFourSolver(Solver):
grid: Grid
test_solution_p1 = 18
test_solution_p2 = 9
@override
def __init__(self, puzzle_input: str):
self.grid = tuple(tuple(row) for row in puzzle_input.strip().splitlines())
def solve_p1(puzzle_input: str) -> int:
word_grid = _parse_world_grid(puzzle_input)
return sum(_count_xmas(line) for line in _scan_word_grid(word_grid))
def solve_p2(puzzle_input: str) -> int:
word_grid = _parse_world_grid(puzzle_input)
squares = _three_squares(word_grid)
return sum(1 for square in squares if _has_cross_mas(square))
def _parse_world_grid(puzzle_input: str) -> tuple[tuple[str, ...], ...]:
return tuple(tuple(row) for row in puzzle_input.splitlines())
def _scan_word_grid(
char_grid: tuple[tuple[str, ...], ...]
) -> Iterator[tuple[str, ...]]:
yield from (row for row in char_grid)
yield from (tuple(col) for col in zip(*char_grid))
yield from (
_diagonal(char_grid, i)
for i in range(-len(char_grid) + 1, len(char_grid[0]))
)
yield from (
_diagonal(tuple(reversed(char_grid)), i)
for i in range(-len(char_grid) + 1, len(char_grid[0]))
)
def _diagonal(grid: tuple[tuple[str, ...], ...], offset=0) -> tuple[str, ...]:
return tuple(
grid[i][i + offset] for i in range(len(grid)) if 0 <= i + offset < len(grid)
)
def _count_xmas(line: tuple[str, ...]) -> int:
@override
def solve_p1(self) -> int:
targets = (("X", "M", "A", "S"), ("S", "A", "M", "X"))
return sum(
1
for i
in range(len(line) - 3)
if line[i : i + 4] in (("X", "M", "A", "S"), ("S", "A", "M", "X"))
for line in self.scan_lines()
for window in sliding_window(line, 4)
if window in targets
)
ThreeSquare = tuple[
tuple[str, str, str],
tuple[str, str, str],
tuple[str, str, str]
]
@override
def solve_p2(self) -> int:
targets = (("M", "A", "S"), ("S", "A", "M"))
return sum(
1
for square in self.scan_squares(3)
if (square[0][0], square[1][1], square[2][2]) in targets
and (square[0][2], square[1][1], square[2][0]) in targets
)
def _three_squares(word_grid: tuple[tuple[str, ...], ...]) -> Iterator[ThreeSquare]:
def scan_lines(self) -> Iterator[Line]:
yield from self.grid
for col in zip(*self.grid):
yield tuple(col)
for i in range(-len(self.grid) + 1, len(self.grid[0])):
yield self.diagonal(i)
yield self.diagonal(i, inverse=True)
def scan_squares(self, size=3) -> Iterator[Grid]:
yield from (
(word_grid[i][j:j+3], word_grid[i+1][j:j+3], word_grid[i+2][j:j+3])
for i in range(len(word_grid) - 2)
for j in range(len(word_grid[0]) - 2)
(
self.grid[i][j : j + size],
self.grid[i + 1][j : j + size],
self.grid[i + 2][j : j + size],
)
for i in range(len(self.grid) - size + 1)
for j in range(len(self.grid[0]) - size + 1)
)
def _has_cross_mas(square: ThreeSquare):
diag_1 = (square[0][0], square[1][1], square[2][2])
diag_2 = (square[0][2], square[1][1], square[2][0])
return (
(diag_1 == ("M", "A", "S") or diag_1 == ("S", "A", "M"))
and (diag_2 == ("M", "A", "S") or diag_2 == ("S", "A", "M"))
def diagonal(self, offset=0, inverse=False) -> Line:
return tuple(
self.grid[i][len(self.grid[0]) - i - offset - 1 if inverse else i + offset]
for i in range(len(self.grid))
if 0 <= i + offset < len(self.grid)
)
class TestDayFourSolver(TestCase):
def test(self):
solver = DayFourSolver(
dedent(
"""
MMMSXXMASM
MSAMXMSMSA
AMXSXMAAMM
MSAMASMSMX
XMASAMXAMM
XXAMMXXAMA
SMSMSASXSS
SAXAMASAAA
MAMMMXMMMM
MXMXAXMASX
"""
)
)
self.assertEqual(
solver.grid[0],
("M", "M", "M", "S", "X", "X", "M", "A", "S", "M"),
)
self.assertEqual(solver.solve_p1(), 18)
self.assertEqual(solver.solve_p2(), 9)

116
puzzles/5.py Normal file
View File

@@ -0,0 +1,116 @@
from textwrap import dedent
from typing import Iterator, override
from unittest import TestCase
from puzzles._solver import Solver
Update = tuple[int, ...]
class DayFiveSolver(Solver):
ordering_rules: dict[int, set[int]]
updates: list[Update]
@override
def __init__(self, puzzle_input: str):
ordering_rules_input, _, updates_input = puzzle_input.partition("\n\n")
self.ordering_rules = {}
for line in ordering_rules_input.strip().split("\n"):
a, _, b = line.partition("|")
a, b = int(a), int(b)
if b not in self.ordering_rules:
self.ordering_rules[b] = set()
self.ordering_rules[b].add(a)
self.updates = [
tuple(map(int, line.split(",")))
for line in updates_input.strip().split("\n")
]
@override
def solve_p1(self) -> int:
return sum(
update[len(update) // 2]
for update in self.updates
if self.is_correctly_ordered(update)
)
@override
def solve_p2(self) -> int:
return sum(
self.fix_update(update)[len(update) // 2]
for update in self.updates
if not self.is_correctly_ordered(update)
)
def is_correctly_ordered(self, update: Update) -> bool:
contains = set(update)
seen = set()
for i in update:
for j in self.ordering_rules.get(i, ()):
if j in contains and j not in seen:
return False
seen.add(i)
return True
def fix_update(self, update: Update) -> Update:
contains = set(update)
seen = set()
fixed_update = list()
def fix_item(i):
for j in self.ordering_rules.get(i, ()):
if j in contains and j not in seen:
fix_item(j)
fixed_update.append(i)
seen.add(i)
for i in update:
if i not in seen:
fix_item(i)
return tuple(fixed_update)
class TestDayFiveSolver(TestCase):
def test(self):
solver = DayFiveSolver(
dedent(
"""
47|53
97|13
97|61
97|47
75|29
61|13
75|53
29|13
97|29
53|29
61|53
97|53
61|29
47|13
75|47
97|75
47|61
75|61
47|29
75|13
53|13
75,47,61,53,29
97,61,53,29,13
75,29,13
75,97,47,61,53
61,13,29
97,13,75,29,47
"""
)
)
self.assertEqual(solver.solve_p1(), 143)
self.assertEqual(solver.solve_p2(), 123)

160
puzzles/6.py Normal file
View File

@@ -0,0 +1,160 @@
from dataclasses import dataclass
from textwrap import dedent
from typing import Literal, NamedTuple, get_args, override
from unittest import TestCase
from puzzles._solver import Solver
direction_map = {
"^": (-1, 0),
"v": (1, 0),
"<": (0, -1),
">": (0, 1),
}
class GuardPosition(NamedTuple):
x: int
y: int
direction: tuple[int, int]
def move_position(position: GuardPosition) -> GuardPosition:
return GuardPosition(
position.x + position.direction[0],
position.y + position.direction[1],
position.direction,
)
def rotate_position(position: GuardPosition) -> GuardPosition:
return GuardPosition(
position.x,
position.y,
(position.direction[1], -position.direction[0]),
)
class TestGuardPosition(TestCase):
def test(self):
guard = GuardPosition(0, 0, (0, 1))
self.assertEqual(move_position(guard), GuardPosition(0, 1, (0, 1)))
self.assertEqual(rotate_position(guard), GuardPosition(0, 0, (1, 0)))
class TraversalAnalysis(NamedTuple):
visited: set[GuardPosition]
stuck: bool
def unique_locations(traversal_analysis: TraversalAnalysis) -> set[tuple[int, int]]:
return set((p.x, p.y) for p in traversal_analysis.visited)
class TestTraversalAnalysis(TestCase):
def test(self):
analysis = TraversalAnalysis(
{GuardPosition(0, 0, (0, 1)), GuardPosition(2, 5, (1, 0))},
False,
)
self.assertEqual(
unique_locations(analysis),
{(0, 0), (2, 5)},
)
class DaySixSolver(Solver):
puzzle_map: list[list[bool]]
guard_origin: GuardPosition
@override
def __init__(self, puzzle_input: str):
self.puzzle_map = []
guard_origin = None
for x, line in enumerate(puzzle_input.strip().split("\n")):
line_list = list()
for y, cell in enumerate(line):
if cell in direction_map:
guard_origin = GuardPosition(x, y, direction_map[cell])
line_list.append(cell == "#")
self.puzzle_map.append(line_list)
if guard_origin is None:
raise ValueError("Guard not found in map")
self.guard_origin = guard_origin
if not all(len(line) == len(self.puzzle_map[0]) for line in self.puzzle_map):
raise ValueError("Map is not rectangular")
@override
def solve_p1(self) -> int:
traversal_analysis = self.traverse_map()
return len(unique_locations(traversal_analysis))
@override
def solve_p2(self) -> int:
candidate_locations = unique_locations(self.traverse_map())
candidate_locations.discard((self.guard_origin.x, self.guard_origin.y))
verified_locations = set()
for x, y in candidate_locations:
if not self.puzzle_map[x][y]:
self.puzzle_map[x][y] = True
if self.traverse_map().stuck:
verified_locations.add((x, y))
self.puzzle_map[x][y] = False
return len(verified_locations)
def out_of_bounds(self, guard_position: GuardPosition) -> bool:
return (
guard_position.x < 0
or guard_position.x >= len(self.puzzle_map)
or guard_position.y < 0
or guard_position.y >= len(self.puzzle_map[0])
)
def traverse_map(self) -> TraversalAnalysis:
position = self.guard_origin
visited = set({position})
while True:
next_position = move_position(position)
if self.out_of_bounds(next_position):
return TraversalAnalysis(visited, False)
# Obstacle is encountered
if self.puzzle_map[next_position.x][next_position.y]:
next_position = rotate_position(position)
# Guard is stuck
if next_position in visited:
return TraversalAnalysis(visited, True)
position = next_position
visited.add(position)
class TestDaySixSolver(TestCase):
def test(self):
solver = DaySixSolver(
dedent(
"""
....#.....
.........#
..........
..#.......
.......#..
..........
.#..^.....
........#.
#.........
......#...
"""
)
)
self.assertEqual(solver.guard_origin, GuardPosition(6, 4, (-1, 0)))
self.assertEqual(solver.solve_p1(), 41)
self.assertEqual(solver.solve_p2(), 6)

109
puzzles/7.py Normal file
View File

@@ -0,0 +1,109 @@
from functools import partial
from math import log
from textwrap import dedent
from typing import Callable, Iterator, NamedTuple, override
from unittest import TestCase
from puzzles._solver import Solver
class Equation(NamedTuple):
target: int
factors: tuple[int, ...]
Operator = Callable[[int, int], int]
def is_calibrated(operators: list[Operator], equation: Equation) -> bool:
def eval_permutations(factors: tuple[int, ...]) -> Iterator[int]:
assert len(factors) > 0
tail = factors[-1]
if len(factors) == 1:
yield tail
return
for head in eval_permutations(factors[:-1]):
if head > equation.target:
continue
for operator in operators:
yield operator(head, tail)
return any(
result == equation.target for result in eval_permutations(equation.factors)
)
def concat_ints(a: int, b: int) -> int:
return 10 ** int(log(b, 10) + 1) * a + b
class TestEquation(TestCase):
def test_is_calibrated(self):
equation = Equation(190, (10, 19))
self.assertTrue(is_calibrated([int.__add__, int.__mul__], equation))
self.assertFalse(is_calibrated([int.__add__], equation))
def test_concat_ints(self):
self.assertEqual(concat_ints(1, 2), 12)
self.assertEqual(concat_ints(12, 345), 12345)
class DaySevenSolver(Solver):
equations: list[Equation]
@override
def __init__(self, puzzle_input: str):
self.equations = []
for line in puzzle_input.strip().split("\n"):
result_string, _, factors_string = line.partition(": ")
self.equations.append(
Equation(
int(result_string),
tuple(map(int, factors_string.split(" "))),
)
)
@override
def solve_p1(self) -> int:
return sum(
equation.target
for equation in filter(
partial(is_calibrated, [int.__add__, int.__mul__]),
self.equations,
)
)
@override
def solve_p2(self) -> int:
return sum(
equation.target
for equation in filter(
partial(is_calibrated, [int.__add__, int.__mul__, concat_ints]),
self.equations,
)
)
class TestDaySevenSolver(TestCase):
def test(self):
solver = DaySevenSolver(
dedent(
"""
190: 10 19
3267: 81 40 27
83: 17 5
156: 15 6
7290: 6 8 6 15
161011: 16 10 13
192: 17 8 14
21037: 9 7 18 13
292: 11 6 16 20
"""
)
)
self.assertEqual(
solver.equations[0],
Equation(190, (10, 19)),
)
self.assertEqual(solver.solve_p1(), 3749)
self.assertEqual(solver.solve_p2(), 11387)

117
puzzles/8.py Normal file
View File

@@ -0,0 +1,117 @@
from functools import reduce
from itertools import combinations
from textwrap import dedent
from typing import Set, override
from unittest import TestCase
from puzzles._solver import Solver
Point = tuple[int, int]
class DayEightSolver(Solver):
width: int
height: int
antenna_arrays: dict[str, set[Point]]
@override
def __init__(self, puzzle_input: str):
self.antenna_arrays = {}
lines = puzzle_input.strip().split("\n")
self.width = len(lines[0])
self.height = len(lines)
for x, line in enumerate(puzzle_input.strip().split("\n")):
for y, char in enumerate(line):
if char.isalnum():
if char not in self.antenna_arrays:
self.antenna_arrays[char] = set()
self.antenna_arrays[char].add((x, y))
@override
def solve_p1(self) -> int:
return len(
reduce(
Set.union,
(
self.antinodes(a, b, harmonic=False)
for antenna_array in self.antenna_arrays.values()
for a, b in combinations(antenna_array, 2)
),
)
)
@override
def solve_p2(self) -> int:
return len(
reduce(
Set.union,
(
self.antinodes(a, b, harmonic=True)
for antenna_array in self.antenna_arrays.values()
for a, b in combinations(antenna_array, 2)
),
)
)
def antinodes(self, a: Point, b: Point, harmonic=False) -> set[Point]:
points = set()
dx = b[0] - a[0]
dy = b[1] - a[1]
if harmonic:
antinode = a
while self.is_in_grid(antinode):
points.add(antinode)
antinode = (antinode[0] - dx, antinode[1] - dy)
antinode = b
while self.is_in_grid(antinode):
points.add(antinode)
antinode = (antinode[0] + dx, antinode[1] + dy)
else:
antinode = (a[0] - dx, a[1] - dy)
if self.is_in_grid(antinode):
points.add(antinode)
antinode = (b[0] + dx, b[1] + dy)
if self.is_in_grid(antinode):
points.add(antinode)
return points
def is_in_grid(self, point: Point) -> bool:
return 0 <= point[0] < self.height and 0 <= point[1] < self.width
class TestDayEightSolver(TestCase):
def test(self):
solver = DayEightSolver(
dedent(
"""
............
........0...
.....0......
.......0....
....0.......
......A.....
............
............
........A...
.........A..
............
............
"""
)
)
self.assertEqual(solver.width, 12)
self.assertEqual(solver.height, 12)
self.assertSetEqual(
solver.antenna_arrays["0"],
{(1, 8), (2, 5), (3, 7), (4, 4)},
)
self.assertEqual(solver.solve_p1(), 14)
self.assertEqual(solver.solve_p2(), 34)

301
puzzles/9.py Normal file
View File

@@ -0,0 +1,301 @@
import math
from itertools import count, pairwise
from typing import Iterable, NamedTuple, override
from unittest import TestCase
from more_itertools import chunked, ilen
from puzzles._solver import Solver
class LinkedList[T]:
__head: "LinkedListNode[T] | None"
__tail: "LinkedListNode[T] | None"
__slots__ = ("__head", "__tail")
@property
def head(self) -> "LinkedListNode[T] | None":
if not self.__head and self.__tail:
self.__head = self.__tail
while self.__head and self.__head.left:
self.__head = self.__head.left
return self.__head
@property
def tail(self) -> "LinkedListNode[T] | None":
if not self.__tail and self.__head:
self.__tail = self.__head
while self.__tail and self.__tail.right:
self.__tail = self.__tail.right
return self.__tail
def __init__(self, *values: Iterable[T]) -> None:
self.__head = None
self.__tail = None
for value in values:
self.append(value)
def append(self, value: T) -> None:
if tail := self.tail:
self.__tail = LinkedListNode(value, tail, None)
tail.right = self.__tail
else:
self.__tail = LinkedListNode(value, None, None)
def prepend(self, value: T) -> None:
if head := self.head:
self.__head = LinkedListNode(value, None, head)
head.left = self.__head
else:
self.__head = LinkedListNode(value, None, None)
def pop_tail(self) -> T | None:
if (tail := self.tail) is None:
raise IndexError("pop from empty LinkedList")
if tail.left:
tail.left.right = None
self.__tail = tail.left
tail.left = None
return tail.value
def pop_head(self) -> T:
if (head := self.head) is None:
raise IndexError("pop from empty LinkedList")
if head.right:
head.right.left = None
self.__head = head.right
head.right = None
return head.value
def copy(self) -> "LinkedList[T]":
return LinkedList(*self)
def __iter__(self) -> Iterable[T]:
node = self.head
while node is not None:
yield node.value
node = node.right
def __reversed__(self) -> Iterable[T]:
node = self.tail
while node is not None:
yield node.value
node = node.left
def __len__(self) -> int:
return ilen(self)
def __str__(self) -> str:
return f"LinkedList({', '.join(map(str, self))})"
class LinkedListNode[T]:
value: T
left: "LinkedListNode[T] | None"
right: "LinkedListNode[T] | None"
__slots__ = ("value", "left", "right")
def __init__(
self,
value: T,
left: "LinkedListNode[T] | None",
right: "LinkedListNode[T] | None",
) -> None:
self.value = value
self.left = left
self.right = right
def insert_left(self, value: T) -> None:
node = LinkedListNode(value, self.left, self)
if self.left:
self.left.right = node
self.left = node
def insert_right(self, value: T) -> None:
node = LinkedListNode(value, self, self.right)
if self.right:
self.right.left = node
self.right = node
def pop(self) -> T:
if self.left:
self.left.right = self.right
if self.right:
self.right.left = self.left
return self.value
class TestLinkedList(TestCase):
def test(self):
linked_list = LinkedList(1, 2, 3, 4, 5)
self.assertEqual(linked_list.head.value, 1)
self.assertEqual(linked_list.tail.value, 5)
self.assertListEqual(list(linked_list), [1, 2, 3, 4, 5])
self.assertListEqual(list(reversed(linked_list)), [5, 4, 3, 2, 1])
self.assertEqual(len(linked_list), 5)
self.assertEqual(linked_list.pop_tail(), 5)
self.assertEqual(linked_list.tail.value, 4)
self.assertEqual(linked_list.pop_head(), 1)
self.assertEqual(linked_list.head.value, 2)
linked_list.append(6)
self.assertEqual(linked_list.tail.value, 6)
linked_list.prepend(7)
self.assertEqual(linked_list.head.value, 7)
self.assertListEqual(list(linked_list), [7, 2, 3, 4, 6])
middle = linked_list.tail.left.left
self.assertEqual(middle.value, 3)
middle.insert_left(8)
self.assertListEqual(list(linked_list), [7, 2, 8, 3, 4, 6])
middle.insert_right(9)
self.assertListEqual(list(linked_list), [7, 2, 8, 3, 9, 4, 6])
self.assertEqual(middle.pop(), 3)
self.assertListEqual(list(linked_list), [7, 2, 8, 9, 4, 6])
class Block(NamedTuple):
id: int
position: int
size: int
def compact_drive(drive: LinkedList[Block]) -> LinkedList[Block]:
if not len(drive):
return drive
drive = drive.copy()
moving = drive.pop_tail()
cursor = drive.head
assert cursor is not None
while cursor.right is not None:
a = cursor.value
b = cursor.right.value
free_space = b.position - a.position - a.size
if not free_space:
cursor = cursor.right
continue
insertion = Block(
moving.id,
a.position + a.size,
min(free_space, moving.size),
)
cursor.insert_right(insertion)
cursor = cursor.right
if insertion.size < moving.size:
moving = Block(
moving.id,
moving.position,
moving.size - insertion.size,
)
else:
moving = drive.pop_tail()
cursor.insert_right(
Block(
moving.id,
cursor.value.position + cursor.value.size,
moving.size,
)
)
return drive
def compact_drive_without_fragmentation(drive: LinkedList[Block]) -> LinkedList[Block]:
if not len(drive):
return drive
drive = drive.copy()
moving = drive.tail
while moving:
cursor = drive.head
while cursor != moving:
a = cursor.value
b = cursor.right.value
free_space = b.position - a.position - a.size
if free_space >= moving.value.size:
moving.pop()
cursor.insert_right(
Block(
moving.value.id,
a.position + a.size,
moving.value.size,
)
)
break
cursor = cursor.right
moving = moving.left
return drive
def hash_drive(drive: LinkedList[Block]) -> int:
return sum(
block.id * block.size * (block.position * 2 + block.size - 1) // 2
for block in drive
)
def print_drive(drive: LinkedList[Block]) -> None:
snapshot = []
for a, b in pairwise(drive):
snapshot.append(str(a.id) * a.size)
snapshot.append("." * (b.position - a.position - a.size))
snapshot.append(str(drive.tail.value.id) * drive.tail.value.size)
print("".join(snapshot))
class DayNineSolver(Solver):
drive: LinkedList[Block]
@override
def __init__(self, puzzle_input: str):
self.drive = LinkedList()
snapshot = map(int, puzzle_input.strip())
cursor_position = 0
current_id = count()
for pair in chunked(snapshot, 2):
if data_size := pair[0]:
id = next(current_id)
self.drive.append(Block(id, cursor_position, data_size))
cursor_position += data_size
if len(pair) == 2:
cursor_position += pair[1]
@override
def solve_p1(self) -> int:
compacted_drive = compact_drive(self.drive)
return hash_drive(compacted_drive)
@override
def solve_p2(self) -> int:
compacted_drive = compact_drive_without_fragmentation(self.drive)
return hash_drive(compacted_drive)
class TestDayNineSolver(TestCase):
def test(self):
solver = DayNineSolver("2333133121414131402\n")
self.assertEqual(solver.solve_p1(), 1928)
self.assertEqual(solver.solve_p2(), 2858)

12
puzzles/_solver.py Normal file
View File

@@ -0,0 +1,12 @@
from abc import ABC, abstractmethod
class Solver(ABC):
@abstractmethod
def __init__(self, puzzle_input: str): ...
@abstractmethod
def solve_p1(self) -> int: ...
@abstractmethod
def solve_p2(self) -> int: ...

View File

@@ -2,4 +2,4 @@
name = "advent-of-code-2024"
version = "0.0.0"
description = "Advent of Code 2024"
dependencies = ["advent", "more-itertools"]
dependencies = ["advent", "more-itertools", "multidict"]

View File

@@ -1,78 +1,60 @@
import sys
from importlib import import_module
from inspect import getmembers, isclass
from time import time
from unittest import TextTestRunner, defaultTestLoader
from advent.errors import InvalidSessionIDError, PuzzleNotFoundError
from advent.functions import get_puzzle_input
def usage():
print("Usage: python solve.py <day>")
from puzzles._solver import Solver
def main():
if len(sys.argv) != 2:
usage()
sys.exit(1)
raise RuntimeError("Usage: python solve.py <day>")
try:
day = int(sys.argv[1])
except ValueError:
print("Day must be an integer")
usage()
sys.exit(1)
raise RuntimeError("Day must be an integer")
print(f"Loading 2024, day {day} solver...")
try:
print(f"Loading 2024, day {day} solver")
mod = import_module(f"puzzles.{day}")
except ModuleNotFoundError:
print(f"Solver for day {day} not found")
sys.exit(1)
try:
print("Testing part 1 solution...")
start = time()
assert mod.solve_p1(mod.test_input_p1) == mod.test_solution_p1
print(f"Test passed in {time() - start:.3f} seconds")
solvers = getmembers(mod, lambda m: isclass(m) and issubclass(m, Solver))
if len(solvers) == 0:
raise RuntimeError("No solver found")
solver_class: type[Solver] = solvers[0][1]
print("Testing part 2 solution...")
start = time()
assert mod.solve_p2(mod.test_input_p2) == mod.test_solution_p2
print(f"Test passed in {time() - start:.3f} seconds")
test_suite = defaultTestLoader.loadTestsFromModule(mod)
test_runner = TextTestRunner(verbosity=0)
test_result = test_runner.run(test_suite)
if not test_result.wasSuccessful():
raise RuntimeError("Tests failed")
except AssertionError:
print("Test failed")
sys.exit(1)
except Exception as exc:
print("Error:", exc)
sys.exit(1)
try:
print("Fetching puzzle input...")
puzzle_input = get_puzzle_input(2024, int(day))
except (PuzzleNotFoundError, InvalidSessionIDError) as exc:
print(exc)
sys.exit(1)
try:
solver = solver_class(puzzle_input)
print("Solving part 1...")
start = time()
solution_p1 = mod.solve_p1(puzzle_input)
solution_p1 = solver.solve_p1()
print(f"Part 1 solution: {solution_p1} ({time() - start:.3f} seconds)")
print("Solving part 2...")
solution_p2 = mod.solve_p2(puzzle_input)
start = time()
solution_p2 = solver.solve_p2()
print(f"Part 2 solution: {solution_p2} ({time() - start:.3f} seconds)")
except NotImplementedError:
print("Puzzle solver is incomplete. Exiting")
sys.exit(1)
except Exception as exc:
print("Error:", exc)
sys.exit(1)
if __name__ == "__main__":
try:
main()
except Exception as exc:
print(exc)
sys.exit(1)