advent-of-code/2020/Python/day20.py
2020-12-20 22:06:55 +01:00

246 lines
7.8 KiB
Python

from enum import Enum
from typing import Union, Tuple
class Side(Enum):
UP = 1
DOWN = 2
LEFT = 4
RIGHT = 8
class Tile:
def __init__(self, uid: str, image: list[list[str]]):
self.uid = uid
self.image = image
self.up: Union[Tile, None] = None
self.down: Union[Tile, None] = None
self.left: Union[Tile, None] = None
self.right: Union[Tile, None] = None
def flip_ud(self) -> 'Tile':
cache = self.up
self.up = self.down
self.up = cache
self.image = list(reversed(self.image))
return self
def flip_lr(self) -> 'Tile':
cache = self.left
self.left = self.right
self.right = cache
for i in range(len(self.image)):
self.image[i] = list(reversed(self.image[i]))
return self
def rotate_l(self) -> 'Tile':
new_image = []
for i in range(len(self.image[0])):
new_image.append([])
for line in self.image:
for i in range(len(line)):
new_image[i].append(list(reversed(line))[i])
self.image = new_image
return self
def rotate_r(self) -> 'Tile':
new_image = []
for i in range(len(self.image[0])):
new_image.append([])
for line in reversed(self.image):
for i in range(len(line)):
new_image[i].append(line[i])
self.image = new_image
return self
def align_to(self, edge: str, side: Side):
if edge not in self.get_all_edges():
return
rev = "".join(reversed(edge))
if side == Side.UP:
while "".join(self.image[0]) not in [edge, rev]:
self.rotate_r()
if "".join(self.image[0]) == rev:
self.flip_lr()
elif side == side.DOWN:
while "".join(self.image[len(self.image)-1]) not in [edge, rev]:
self.rotate_r()
if "".join(self.image[len(self.image)-1]) == rev:
self.flip_lr()
elif side == side.LEFT:
while "".join([line[0] for line in self.image]) not in [edge, rev]:
self.rotate_r()
if "".join([line[0] for line in self.image]) == rev:
self.flip_ud()
elif side == side.RIGHT:
while "".join([line[len(self.image[0])-1] for line in self.image]) not in [edge, rev]:
self.rotate_r()
if "".join([line[len(self.image[0])-1] for line in self.image]) == rev:
self.flip_ud()
def get_all_edges(self) -> list[str]:
all_edges = []
up = "".join(self.image[0])
down = "".join(self.image[len(self.image)-1])
left = "".join([line[0] for line in self.image])
right = "".join([line[len(self.image[0])-1] for line in self.image])
all_edges += [up, down, left, right, "".join(reversed(up)), "".join(reversed(down)), "".join(reversed(left)), "".join(reversed(right))]
return all_edges
def count_connections(self) -> int:
return sum([1 if x else 0 for x in [self.up, self.down, self.left, self.right]])
def print(self):
print(f"Tile {self.uid}:")
for line in self.image:
print("".join(line))
def __str__(self):
return f"Tile {self.uid}"
def parse_data() -> list[Tile]:
output = []
current_img = [['.']]
current_title = ""
with open("input.txt") as file:
for line in file:
line = line.rstrip()
if not line:
continue
if line[0] == 'T':
output.append(Tile(current_title, current_img))
current_title = line.split(" ")[1][:-1]
current_img = []
elif line[0] in ['#', '.']:
current_img.append(list(line))
output.append(Tile(current_title, current_img))
return output[1:]
def connect_tiles(data: list[Tile], already_done: set[str], tile: Tile):
if tile.uid in already_done:
return
already_done.add(tile.uid)
up = "".join(tile.image[0])
down = "".join(tile.image[len(tile.image) - 1])
left = "".join([line[0] for line in tile.image])
right = "".join([line[len(tile.image[0]) - 1] for line in tile.image])
for other_tile in data:
if other_tile.uid in already_done or tile.uid == other_tile.uid:
continue
other_edges = other_tile.get_all_edges()
if up in other_edges:
other_tile.align_to(up, Side.DOWN)
tile.up = other_tile
other_tile.down = tile
elif down in other_edges:
other_tile.align_to(down, Side.UP)
tile.down = other_tile
other_tile.up = tile
elif left in other_edges:
other_tile.align_to(left, Side.RIGHT)
tile.left = other_tile
other_tile.right = tile
elif right in other_edges:
other_tile.align_to(right, Side.LEFT)
tile.right = other_tile
other_tile.left = tile
if tile.up:
connect_tiles(data, already_done, tile.up)
if tile.down:
connect_tiles(data, already_done, tile.down)
if tile.left:
connect_tiles(data, already_done, tile.left)
if tile.right:
connect_tiles(data, already_done, tile.right)
def solve_p1(data: list[Tile]) -> int:
result = 1
for tile in data:
if tile.count_connections() == 2:
result *= int(tile.uid)
return result
# SOLVED PART 1
DATA = parse_data()
connect_tiles(DATA, set(), DATA[0].flip_ud())
print(solve_p1(DATA))
def render_full_image(data: list[Tile]) -> Tile:
# Find top-left corner
current_start: Union[None, Tile] = None
for tile in data:
if not tile.left and not tile.up and tile.right and tile.down:
current_start = tile
break
rendered = [[]]
while current_start:
for i in range(1, len(current_start.image[0])-1):
line = []
line += current_start.image[i][1:-1]
current_right = current_start.right
while current_right: # add line from the right side
line += current_right.image[i][1:-1]
current_right = current_right.right
rendered.append(line)
current_start = current_start.down
return Tile("RENDERED", rendered[1:])
def scan_window(window: list[list[str]], start_r: int, start_c: int) -> list[Tuple[int, int]]:
if len(window) != 3 or len(window[0]) != 20:
return []
required = [(0, 18), (1, 0), (1, 5), (1, 6), (1, 11), (1, 12), (1, 17), (1, 18), (1, 19), (2, 1), (2, 4), (2, 7), (2, 10), (2, 13), (2, 16)]
for r, c in required:
if window[r][c] != "#":
return []
return [(r+start_r, c+start_c) for (r, c) in required]
def scan_image(image: list[list[str]]) -> set[Tuple[int, int]]:
found = set()
for i in range(len(image)-2):
for j in range(len(image[0])-19):
window = [image[i][j:j+20], image[i+1][j:j+20], image[i+2][j:j+20]]
for found_r, found_c in scan_window(window, start_r=i, start_c=j):
found.add((found_r, found_c))
return found
def solve_p2(data: list[Tile]) -> int:
rendered = render_full_image(data)
scan_set = set()
for i in range(4):
scan_set = scan_image(rendered.image)
if len(scan_set) > 0:
break
rendered.rotate_r()
if len(scan_set) == 0:
rendered.flip_lr()
for i in range(4):
scan_set = scan_image(rendered.image)
if len(scan_set) > 0:
break
rendered.rotate_r()
# Find not marked characters
counted = 0
for r in range(len(rendered.image)):
for c in range(len(rendered.image[0])):
if rendered.image[r][c] == '#' and (r, c) not in scan_set:
counted += 1
return counted
# SOLVED PART 2
print(solve_p2(DATA))