140 lines
4.6 KiB
Python
140 lines
4.6 KiB
Python
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)
|