--- /dev/null
+from dataclasses import dataclass
+from typing import List, Tuple
+from collections import defaultdict
+from copy import copy, deepcopy
+
+
+map_width = 0
+map_height = 0
+
+
+class Position:
+ row: int
+ col: int
+
+ def __init__(self, row: int, col: int):
+ self.row = row
+ self.col = col
+
+ def __repr__(self) -> str:
+ return f"({self.row},{self.col})"
+
+ def __hash__(self):
+ return hash((self.row, self.col))
+
+ def is_valid(self) -> bool:
+ global map_height, map_width
+ return self.row >= 0 and self.row < map_height \
+ and self.col >= 0 and self.col < map_width
+
+ def antinodes_for(self, other: 'Position', part2: bool = False):
+ dr = self.row - other.row
+ dc = self.col - other.col
+ n = 1
+ while True:
+ valid = 0
+ a = Position(self.row + (n * dr), self.col + (n * dc))
+ if a.is_valid():
+ valid += 1
+ yield a
+ b = Position(other.row - (n * dr), other.col - (n * dc))
+ if b.is_valid():
+ valid += 1
+ yield b
+ if not part2:
+ break
+ if valid == 0:
+ break
+ n += 1
+ if part2:
+ yield self
+ yield other
+
+@dataclass
+class Problem:
+ data: List[str]
+ towers: defaultdict
+
+ def run(self, part2: bool = False, debug: bool = False) -> int:
+ newdata = deepcopy(self.data)
+ for tower in self.towers:
+ for ndx, node in enumerate(self.towers[tower]):
+ others = copy(self.towers[tower])
+ del others[ndx]
+ for other in others:
+ for anti in node.antinodes_for(other, part2):
+ newdata[anti.row][anti.col] = '#'
+ if debug:
+ for line in newdata:
+ print("".join(line))
+ res = "".join(["".join(x) for x in newdata])
+ return sum([1 for x in res if x == '#'])
+
+ @staticmethod
+ def from_stream(stream) -> 'Problem':
+ towers = defaultdict(list)
+ data = [list(line.strip()) for line in stream]
+ global map_width, map_height
+ map_height = len(data)
+ map_width = len(data[0])
+ for row, cells in enumerate(data):
+ for col, cell in enumerate(cells):
+ if cell != '.':
+ towers[cell].append(Position(row, col))
+ return Problem(data, towers)
--- /dev/null
+import sys
+import argparse
+from . import Problem
+
+
+def main(args=None):
+ parser = argparse.ArgumentParser(description="AOC 2024 day 8")
+ parser.add_argument('filename', type=str)
+ parser.add_argument('-d', '--debug', action='store_true')
+ parser.add_argument('-2', '--part2', action='store_true')
+ options = parser.parse_args(args)
+
+ with open(options.filename) as f:
+ problem = Problem.from_stream(f)
+ if options.debug:
+ for row in problem.data:
+ print("".join(row))
+ print()
+
+ print(f"result {problem.run(part2=options.part2, debug=options.debug)}")
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))