#! /usr/bin/env python3 from itertools import chain from functools import reduce from typing import List from typing import Optional from typing import Set from typing import Tuple class Tile(object): def __init__(self, tile_num: int, content: List[List[str]]): self.num = tile_num self.content = content # Neighboring tiles self.top: Optional["Tile"] = None self.bottom: Optional["Tile"] = None self.left: Optional["Tile"] = None self.right: Optional["Tile"] = None # All edges for quick match self.edges: Set[str] = set() self.edges.add(self.top_edge) self.edges.add(self.top_edge[::-1]) self.edges.add(self.bottom_edge) self.edges.add(self.bottom_edge[::-1]) self.edges.add(self.left_edge) self.edges.add(self.left_edge[::-1]) self.edges.add(self.right_edge) self.edges.add(self.right_edge[::-1]) @property def left_edge(self) -> str: return "".join( [self.content[y][0] for y in range(0, len(self.content))] ) @property def right_edge(self) -> str: return "".join( [self.content[y][-1] for y in range(0, len(self.content))] ) @property def top_edge(self) -> str: return "".join(self.content[0]) @property def bottom_edge(self) -> str: return "".join(self.content[-1]) def flip_lr(self): for i, row in enumerate(self.content): self.content[i] = row[::-1] def flip_tb(self): self.content = self.content[::-1] def rotate_cw(self): self.content = [ [self.content[j][i] for j in range(len(self.content)-1, -1, -1)] for i in range(len(self.content[0])) ] def rotate_ccw(self): self.content = [ [self.content[j][i] for j in range(len(self.content))] for i in range(len(self.content[0])-1, -1, -1) ] def align_edge(self, edge: str, direction: str): assert edge in self.edges if direction in ("top", "bottom"): while getattr(self, direction+"_edge") != edge: if getattr(self, direction+"_edge")[::-1] == edge: self.flip_lr() return self.rotate_cw() elif direction in ("left", "right"): while getattr(self, direction+"_edge") != edge: if getattr(self, direction+"_edge")[::-1] == edge: self.flip_tb() return self.rotate_cw() else: raise ValueError(f"Unknown direction {direction}") def count_neighbors(self, other_tiles: List["Tile"]) -> int: num_neighbors = 0 for tile in other_tiles: if self.num == tile.num: continue if self.edges & tile.edges: num_neighbors += 1 return num_neighbors def find_bottom( self, other_tiles: List["Tile"], assign: Optional[bool] = True, ) -> Optional["Tile"]: bottom_tile: Optional[Tile] = None for tile in other_tiles: if self.num == tile.num: continue if self.bottom_edge in tile.edges: tile.align_edge(self.bottom_edge, "top") if bottom_tile is None: bottom_tile = tile else: raise ValueError("Multiple match error") if assign: self.bottom = tile tile.top = self return bottom_tile def find_right( self, other_tiles: List["Tile"], assign: Optional[bool] = True, ) -> Optional["Tile"]: right_tile: Optional[Tile] = None for tile in other_tiles: if self.num == tile.num: continue if self.right_edge in tile.edges: tile.align_edge(self.right_edge, "left") if right_tile is None: right_tile = tile else: raise ValueError("Multiple match error") if assign: self.right = tile tile.left = self return right_tile def remove_edges(self): self.content = [ row[1:-1] for row in self.content[1:-1] ] def __add__(self, other) -> "Tile": new_content = [ s + o for s, o in zip(self.content, other.content) ] return Tile(self.num * other.num, new_content) def __truediv__(self, other) -> "Tile": new_content = [ row for row in chain(self.content, other.content) ] return Tile(self.num * other.num, new_content) def __eq__(self, other) -> bool: return self.num == other.num def __hash__(self) -> int: return self.num def __repr__(self) -> str: return f"" def to_str(self) -> str: return "\n".join( "".join(row) for row in self.content ) def total_turbulance(self) -> int: total = 0 for row in self.content: for c in row: if c == "#": total += 1 return total SEAMONSTER = [ " # ", "# ## ## ###", " # # # # # # ", ] IN_THE_SEA: Set[Tuple[int, int]] = set() def search_seamonster( content: List[List[str]], x: int, y: int, ) -> Tuple[bool, int]: found = True turbulance = 0 in_the_sea: Set[Tuple[int, int]] = set() for yi in range(len(SEAMONSTER)): if y+yi >= len(content): return False, 0 row = content[y+yi] for xi in range(len(SEAMONSTER[yi])): p = (x+xi, y+yi) if p in IN_THE_SEA: return False, 0 if x+xi >= len(row): return False, 0 c = row[x+xi] sea = SEAMONSTER[yi][xi] if sea == "#" and c == "#": in_the_sea.add(p) # Not a seamonster match elif sea == "#" and c != "#": return False, 0 elif sea == " " and c == "#": turbulance += 1 elif sea == " " and c == ".": continue else: raise ValueError(f"Uncharted waters: {sea}, {c}") if found: for p in in_the_sea: IN_THE_SEA.add(p) return found, turbulance def read_tiles(filename: str) -> List[Tile]: results: List[Tile] = [] with open(filename) as f: tile_num = -1 content: List[List[str]] = [] for line in f: line = line.strip() if line.startswith("Tile "): tile_num = int(line[5:line.index(":")]) elif line == "": results.append(Tile(tile_num, content)) tile_num = -1 content = [] else: content.append([c for c in line]) results.append(Tile(tile_num, content)) return results def test(): tiles = read_tiles("test-input.txt") corners: List[Tile] = [] for tile in tiles: num_neighbors = tile.count_neighbors(tiles) print(f"Tile: {tile.num}: {num_neighbors}") if num_neighbors == 2: corners.append(tile) product = 1 for t in corners: product *= t.num print("Corner product:", product) def part1(): tiles = read_tiles("input.txt") corners: List[Tile] = [] for tile in tiles: num_neighbors = tile.count_neighbors(tiles) print(f"Tile: {tile.num}: {num_neighbors}") if num_neighbors == 2: corners.append(tile) product = 1 for t in corners: product *= t.num print("Corner product:", product) def part2(): tiles = set(read_tiles("input.txt")) corners: List[Tile] = [] for tile in tiles: num_neighbors = tile.count_neighbors(tiles) print(f"Tile: {tile.num}: {num_neighbors}") if num_neighbors == 2: corners.append(tile) product = 1 for t in corners: product *= t.num top_left: Optional[Tile] = None print("Corner product:", product) for corner in corners: right = corner.find_right(tiles, False) if not right: continue bottom = corner.find_bottom(tiles, False) if not bottom: continue top_left = corner # print(corner.to_str(), corner.right.to_str()) # print(corner.bottom.to_str()) break else: print("no perfect corner") # Init grid g = [[top_left]] print("removing", top_left) tiles.remove(top_left) tile = top_left tile.find_bottom(tiles) # Build rows while tile.bottom: tile = tile.bottom g.append([tile]) print("removing bottom", tile) tiles.remove(tile) tile.find_bottom(tiles) for row in g: tile = row[0] tile.find_right(tiles) while tile.right: print(f"removing right {tile}->{tile.right}") tile = tile.right row.append(tile) tiles.remove(tile) tile.find_right(tiles) v_split = Tile(0, [[" "] for _ in range(len(top_left.content))]) h_split = Tile(0, [[" " for _ in range(len(top_left.content[0]))]]) m = reduce(lambda x, y: x/h_split/y, ( reduce(lambda x, y: x+v_split+y, (tile for tile in row)) for row in g )) m.flip_tb() print(m.to_str()) # Trim all edges for row in g: for t in row: t.remove_edges() m = reduce(lambda x, y: x/y, ( reduce(lambda x, y: x+y, (tile for tile in row)) for row in g )) turbulance = m.total_turbulance() seamonsters = 0 while seamonsters == 0: m.rotate_cw() content = m.content for y in range(len(content)): for x in range(len(content[y])): found, _ = search_seamonster(content, x, y) if found: print("found a seamonster at", x, y) seamonsters += 1 sea_turb = 0 for row in SEAMONSTER: for c in row: if c == "#": sea_turb += 1 turbulance -= sea_turb * seamonsters for p in IN_THE_SEA: m.content[p[1]][p[0]] = "O" print(m.to_str()) print(f"Found {seamonsters} seamonsters with {turbulance} turbulance") if __name__ == "__main__": # part1() part2()