from __future__ import annotations from dataclasses import dataclass import logging from itertools import combinations # from pprint import pformat from typing import NamedTuple class Coordinate(NamedTuple): x: int y: int c: str def is_galaxy(self): return self.c == "#" def distance( self, other: Coordinate, universe: Universe | None = None, factor: int = 1 ) -> int: current = self movements = 0 while current != other: if current.x < other.x: movements += 1 current = Coordinate(current.x + 1, current.y, current.c) elif current.x > other.x: movements += 1 current = Coordinate(current.x - 1, current.y, current.c) if universe is not None: if current.x in universe.empty_cols: movements += -1 + factor if current.y < other.y: movements += 1 current = Coordinate(current.x, current.y + 1, current.c) elif current.y > other.y: movements += 1 current = Coordinate(current.x, current.y - 1, current.c) if universe is not None: if current.y in universe.empty_lines: movements += -1 + factor logging.debug(f"distance between {self} and {other} = {movements}") return movements def pairs_of_stars(universe: Universe) -> list[tuple[Coordinate, Coordinate]]: return list(combinations(universe.stars, 2)) @dataclass(init=False) class Universe: _data: list[list[Coordinate]] stars: list[Coordinate] empty_lines: list[int] | None empty_cols: list[int] | None def __init__(self, data: list[list[Coordinate]], part: int = 1): self._data = data if part == 1: self._expand_universe() self.stars = [] stars = [filter(lambda c: c.is_galaxy(), line) for line in self._data] for line in stars: self.stars.extend(line) if part == 2: self.empty_cols = [] self.empty_lines = [] for x in range(len(self._data[0])): if all( map(lambda c: not c.is_galaxy(), [line[x] for line in self._data]) ): self.empty_cols.append(x) for line in self._data: if all(map(lambda c: not c.is_galaxy(), line)): self.empty_lines.append(line[0].y) def __getitem__(self, key: int) -> list[Coordinate]: return self._data[key] def __len__(self): return len(self._data) def __iter__(self): return iter(self._data) def _expand_universe(self) -> None: # universe expansion on x # first find columns with no galaxy empty_cols: list[int] = [] for x in range(len(self._data[0])): if all(map(lambda c: not c.is_galaxy(), [line[x] for line in self._data])): empty_cols.append(x) for y, line in enumerate(self._data): x_offset = 0 new_line = [] for col in line: if x_offset > 0: new_col = Coordinate(col.x + x_offset, col.y, col.c) new_line.append(new_col) else: new_line.append(col) if col.x in empty_cols: x_offset += 1 new_col = Coordinate(col.x + x_offset, col.y, col.c) new_line.append(new_col) self._data[y] = new_line # universe expansion on y new_universe = [] y_offset = 0 for y, line in enumerate(self._data): if y_offset > 0: new_line = [Coordinate(o.x, o.y + y_offset, o.c) for o in line] new_universe.append(new_line) else: new_universe.append(line) if all(map(lambda c: not c.is_galaxy(), line)): y_offset += 1 new_line = [Coordinate(o.x, o.y + y_offset, o.c) for o in line] new_universe.append(new_line) self._data = new_universe # logging.debug(pformat(self._data)) def get_map(self): return self._data def parse(input_file: str, part: int = 1) -> Universe: data = [] y = 0 with open(input_file, "r", encoding="utf-8") as input_fd: while line := input_fd.readline(): data.append( [Coordinate(x, y, c) for x, c in enumerate(line.rstrip("\n").rstrip())] ) y += 1 # logging.debug(pformat(data)) return Universe(data, part)