Restructure solver modules with classes and unittest suites

This commit is contained in:
2024-12-09 20:47:50 -08:00
parent 7c021ab87f
commit d020cc8015
8 changed files with 551 additions and 455 deletions

View File

@@ -1,33 +1,48 @@
from collections import Counter from collections import Counter
from textwrap import dedent
from typing import override
from unittest import TestCase
test_input = """ from puzzles._solver import Solver
3 4
4 3
2 5
1 3
3 9
3 3
"""
test_solution_p1 = 11
test_solution_p2 = 31
def solve_p1(puzzle_input: str) -> int: class DayOneSolver(Solver):
list1, list2 = _parse_lists(puzzle_input) list1: list[int]
pairs = zip(sorted(list1), sorted(list2)) list2: list[int]
distances = (abs(pair[0] - pair[1]) for pair in pairs)
return sum(distances)
@override
def solve_p2(puzzle_input: str) -> int: def __init__(self, puzzle_input: str):
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.strip().split("\n")) lines = (line.partition(" ") for line in puzzle_input.strip().split("\n"))
list1, list2 = zip(*((int(line[0]), int(line[2])) for line in lines)) 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)

View File

@@ -1,63 +1,88 @@
from itertools import islice from dataclasses import dataclass
from typing import Iterator 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 from more_itertools import ilen
test_input = """ from puzzles._solver import Solver
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
"""
test_solution_p1 = 2
test_solution_p2 = 4
def solve_p1(puzzle_input: str) -> int: @dataclass
reports = _parse_reports(puzzle_input) class Report:
delta_reports = (_deltas(report) for report in reports) levels: tuple[int, ...]
safe_delta_reports = filter(_is_gradual_monotonic, delta_reports)
return ilen(safe_delta_reports)
@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: def is_gradual_monotonic(self) -> bool:
reports = _parse_reports(puzzle_input) sign = 1 if self.deltas[0] > 0 else -1
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.strip().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
return all( return all(
delta != 0 and delta / abs(delta) == sign and abs(delta) < 4 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, ...]]: class DayTwoSolver(Solver):
yield report reports: list[Report]
yield from (report[:i] + report[i + 1 :] for i in range(0, len(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,40 +1,58 @@
import re import re
from functools import reduce from typing import Literal, override
from unittest import TestCase
test_input_p1 = """ from puzzles._solver import Solver
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)?)\(\)")
def solve_p1(puzzle_input: str) -> int: class DayThreeSolver(Solver):
instructions = ( memory: tuple[int, int] | Literal["do", "don't"]
tuple(map(int, match[:2]))
for match in instruction_pattern.findall(puzzle_input) @override
if match[0] 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: class TestDayThreeSolver(TestCase):
instructions = ( def test_solve_p1(self):
match[2] or tuple(map(int, match[:2])) solver = DayThreeSolver(
for match in instruction_pattern.findall(puzzle_input) "xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))\n"
) )
return reduce( self.assertEqual(solver.solve_p1(), 161)
lambda a, i: (
(1 if i == "do" else 0, a[1]) def test_solve_p2(self):
if isinstance(i, str) solver = DayThreeSolver(
else (a[0], a[1] + a[0] * i[0] * i[1]) "xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))\n"
), )
instructions, self.assertEqual(solver.solve_p2(), 48)
(1, 0),
)[1]

View File

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

View File

@@ -1,110 +1,114 @@
from typing import Iterator from textwrap import dedent
from typing import Iterator, override
from unittest import TestCase
test_input = """ from puzzles._solver import Solver
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
"""
test_solution_p1 = 143
test_solution_p2 = 123
def solve_p1(puzzle_input: str) -> int: class DayFiveSolver(Solver):
rules, updates = _parse_rules_and_updates(puzzle_input) ordering_rules: dict[int, set[int]]
return sum( updates: list[tuple[int, ...]]
update[len(update) // 2]
for update in updates
if _is_correctly_ordered(rules, update)
)
@override
def __init__(self, puzzle_input: str):
ordering_rules_input, _, updates_input = puzzle_input.partition("\n\n")
self.ordering_rules = {}
def solve_p2(puzzle_input: str) -> int: for line in ordering_rules_input.strip().split("\n"):
rules, updates = _parse_rules_and_updates(puzzle_input)
corrected_updates = (
_fix_update(rules, update)
for update in updates
if not _is_correctly_ordered(rules, update)
)
return sum(update[len(update) // 2] for update in corrected_updates)
OrderingRules = dict[int, set[int]]
Update = tuple[int, ...]
def _parse_rules_and_updates(
puzzle_input: str,
) -> tuple[OrderingRules, Iterator[Update]]:
ordering_rules, _, updates = puzzle_input.partition("\n\n")
rules: OrderingRules = {}
for line in ordering_rules.strip().split("\n"):
a, _, b = line.partition("|") a, _, b = line.partition("|")
a, b = int(a), int(b) a, b = int(a), int(b)
if b not in rules: if b not in self.ordering_rules:
rules[b] = set() self.ordering_rules[b] = set()
rules[b].add(a) self.ordering_rules[b].add(a)
return ( self.updates = [
rules, tuple(map(int, line.split(",")))
(tuple(map(int, line.split(","))) for line in updates.strip().split("\n")), 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(rules: OrderingRules, update: Update) -> bool: def is_correctly_ordered(self, update: tuple[int, ...]) -> bool:
contains = set(update) contains = set(update)
seen = set() seen = set()
for i in update: for i in update:
for j in rules.get(i, ()): for j in self.ordering_rules.get(i, ()):
if j in contains and j not in seen: if j in contains and j not in seen:
return False return False
seen.add(i) seen.add(i)
return True return True
def fix_update(self, update: tuple[int, ...]) -> tuple[int, ...]:
def _fix_update(rules: OrderingRules, update: Update) -> Update:
contains = set(update) contains = set(update)
seen = set() seen = set()
fixed_update = list() fixed_update = list()
def _fix_item(i): def fix_item(i):
for j in rules.get(i, ()): for j in self.ordering_rules.get(i, ()):
if j in contains and j not in seen: if j in contains and j not in seen:
_fix_item(j) fix_item(j)
fixed_update.append(i) fixed_update.append(i)
seen.add(i) seen.add(i)
for i in update: for i in update:
if i not in seen: if i not in seen:
_fix_item(i) fix_item(i)
return tuple(fixed_update) 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)

View File

@@ -1,122 +1,160 @@
from typing import NamedTuple from dataclasses import dataclass
from textwrap import dedent
from typing import Literal, NamedTuple, get_args, override
from unittest import TestCase
test_input = """ from puzzles._solver import Solver
....#.....
.........#
..........
..#.......
.......#..
..........
.#..^.....
........#.
#.........
......#...
"""
test_solution_p1 = 41 direction_map = {
test_solution_p2 = 6 "^": (-1, 0),
"v": (1, 0),
"<": (0, -1),
def solve_p1(puzzle_input: str) -> int: ">": (0, 1),
map_info = _parse_map(puzzle_input)
traversal_analysis = _traverse_map(map_info)
unique_locations = set((a, b) for a, b, _ in traversal_analysis.visited)
return len(unique_locations)
def solve_p2(puzzle_input: str) -> int:
map_info = _parse_map(puzzle_input)
added_obstacles_candidates = set()
for i, line in enumerate(map_info.map):
for j, cell in enumerate(line):
if not cell:
map_info.map[i][j] = True
if _traverse_map(map_info).stuck:
added_obstacles_candidates.add((i, j))
map_info.map[i][j] = False
# Remove guard location from candidates
added_obstacles_candidates.discard((map_info.guard[:2]))
return len(added_obstacles_candidates)
class MapInfo(NamedTuple):
map: list[list[bool]]
guard: tuple[int, int, str]
def _parse_map(puzzle_input: str) -> MapInfo:
puzzle_map = []
guard = None
for x, line in enumerate(puzzle_input.strip().split("\n")):
if not line:
continue
line_list = list()
for y, cell in enumerate(line):
if cell in "^v<>":
guard = (x, y, cell)
line_list.append(cell == "#")
puzzle_map.append(line_list)
assert guard is not None, "Guard not found in map"
assert all(
len(line) == len(puzzle_map[0]) for line in puzzle_map
), "Map is not rectangular"
return MapInfo(puzzle_map, guard)
class TraversalAnalysis(NamedTuple):
visited: set[tuple[int, int]]
stuck: bool
_rotation_map = {
"^": ">",
"v": "<",
"<": "^",
">": "v",
} }
def _traverse_map(map_info: MapInfo) -> TraversalAnalysis: class GuardPosition(NamedTuple):
x, y, direction = map_info.guard x: int
visited = set() y: int
visited.add((x, y, direction)) 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: while True:
# Determine next cell next_position = move_position(position)
match direction: if self.out_of_bounds(next_position):
case "^":
next_x, next_y = x - 1, y
case "v":
next_x, next_y = x + 1, y
case "<":
next_x, next_y = x, y - 1
case ">":
next_x, next_y = x, y + 1
# Guard patrols out of bounds
if (
next_x < 0
or next_x >= len(map_info.map)
or next_y < 0
or next_y >= len(map_info.map[0])
):
break
# Turn right if an obstacle is encountered
if map_info.map[next_x][next_y]:
direction = _rotation_map[direction]
# Otherwise, move to next cell
else:
x, y = next_x, next_y
if (x, y, direction) in visited:
return TraversalAnalysis(visited, True)
visited.add((x, y, direction))
return TraversalAnalysis(visited, False) 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)

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

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