401 lines
10 KiB
Python
Executable File
401 lines
10 KiB
Python
Executable File
#! /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"<Tile no={self.num}>"
|
|
|
|
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()
|