aoc-2020/d20/main.py

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()