Restructure solver modules with classes and unittest suites
This commit is contained in:
199
puzzles/8.py
199
puzzles/8.py
@@ -1,110 +1,117 @@
|
||||
from functools import reduce
|
||||
from itertools import combinations
|
||||
from typing import NamedTuple
|
||||
from textwrap import dedent
|
||||
from typing import Set, override
|
||||
from unittest import TestCase
|
||||
|
||||
test_input = """
|
||||
............
|
||||
........0...
|
||||
.....0......
|
||||
.......0....
|
||||
....0.......
|
||||
......A.....
|
||||
............
|
||||
............
|
||||
........A...
|
||||
.........A..
|
||||
............
|
||||
............
|
||||
"""
|
||||
from puzzles._solver import Solver
|
||||
|
||||
test_solution_p1 = 14
|
||||
test_solution_p2 = 34
|
||||
Point = tuple[int, int]
|
||||
|
||||
|
||||
def solve_p1(puzzle_input: str) -> int:
|
||||
grid = _parse_signal_grid(puzzle_input)
|
||||
antinodes = _calculate_antinodes(grid)
|
||||
# _print_grid(grid, antinodes)
|
||||
return len(antinodes)
|
||||
|
||||
|
||||
def solve_p2(puzzle_input: str) -> int:
|
||||
grid = _parse_signal_grid(puzzle_input)
|
||||
antinodes = _calculate_antinodes(grid, harmonic=True)
|
||||
# _print_grid(grid, antinodes)
|
||||
return len(antinodes)
|
||||
|
||||
|
||||
class Point(NamedTuple):
|
||||
x: int
|
||||
y: int
|
||||
|
||||
|
||||
class SignalGrid(NamedTuple):
|
||||
class DayEightSolver(Solver):
|
||||
width: int
|
||||
height: int
|
||||
antenna_arrays: dict[str, set[Point]]
|
||||
|
||||
@override
|
||||
def __init__(self, puzzle_input: str):
|
||||
self.antenna_arrays = {}
|
||||
|
||||
def _parse_signal_grid(puzzle_input: str) -> SignalGrid:
|
||||
lines = puzzle_input.strip().split("\n")
|
||||
antenna_arrays: dict[str, set[Point]] = {}
|
||||
lines = puzzle_input.strip().split("\n")
|
||||
self.width = len(lines[0])
|
||||
self.height = len(lines)
|
||||
|
||||
for x, line in enumerate(lines):
|
||||
for y, char in enumerate(line):
|
||||
if char.isalnum():
|
||||
if char not in antenna_arrays:
|
||||
antenna_arrays[char] = set()
|
||||
antenna_arrays[char].add(Point(x, y))
|
||||
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))
|
||||
|
||||
return SignalGrid(len(lines[0]), len(lines), antenna_arrays)
|
||||
@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
|
||||
|
||||
|
||||
def _calculate_antinodes(grid: SignalGrid, harmonic=False) -> set[Point]:
|
||||
antinodes: set[Point] = set()
|
||||
|
||||
for antenna_array in grid.antenna_arrays.values():
|
||||
for a, b in combinations(antenna_array, 2):
|
||||
dx = b.x - a.x
|
||||
dy = b.y - a.y
|
||||
|
||||
# Left antinodes
|
||||
if harmonic:
|
||||
antinode = a
|
||||
while True:
|
||||
antinodes.add(antinode)
|
||||
antinode = Point(antinode.x - dx, antinode.y - dy)
|
||||
if not _is_in_grid(grid, antinode):
|
||||
break
|
||||
else:
|
||||
antinode = Point(a.x - dx, a.y - dy)
|
||||
if _is_in_grid(grid, antinode):
|
||||
antinodes.add(antinode)
|
||||
|
||||
# Right antinodes
|
||||
if harmonic:
|
||||
antinode = b
|
||||
while True:
|
||||
antinodes.add(antinode)
|
||||
antinode = Point(antinode.x + dx, antinode.y + dy)
|
||||
if not _is_in_grid(grid, antinode):
|
||||
break
|
||||
else:
|
||||
antinode = Point(b.x + dx, b.y + dy)
|
||||
if _is_in_grid(grid, antinode):
|
||||
antinodes.add(antinode)
|
||||
|
||||
return antinodes
|
||||
|
||||
|
||||
def _is_in_grid(grid: SignalGrid, point: Point) -> bool:
|
||||
return 0 <= point.x < grid.height and 0 <= point.y < grid.width
|
||||
|
||||
|
||||
def _print_grid(grid: SignalGrid, antinodes: set[Point]):
|
||||
output = [["."] * grid.width for _ in range(grid.height)]
|
||||
for antinode in antinodes:
|
||||
output[antinode.x][antinode.y] = "#"
|
||||
for frequency, antenna_array in grid.antenna_arrays.items():
|
||||
for antenna in antenna_array:
|
||||
output[antenna.x][antenna.y] = frequency
|
||||
print("\n".join("".join(row) for row in output))
|
||||
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)
|
||||
|
Reference in New Issue
Block a user