import aoclib import math import re DAY = 20 TEST_SOLUTION_PART1 = 20899048083289 TEST_SOLUTION_PART2 = 273 class Tile: def __init__(self, raw_input): self.image_id = int(raw_input[0][5:-1]) self.image = raw_input[1:] def getPossibleBorderList(self): return [ self.image[0], "".join([x[0] for x in self.image]), "".join([x[-1] for x in self.image]), self.image[-1], "".join(reversed(self.image[0])), "".join(reversed([x[0] for x in self.image])), "".join(reversed([x[-1] for x in self.image])), "".join(reversed(self.image[-1])), ] def getBorderlessImage(self): return ["".join(x for x in y[1:-1]) for y in self.image[1:-1]] def getCurrentBorders(self): # top right bottom left return [ self.image[0], "".join([x[-1] for x in self.image]), self.image[-1], "".join([x[0] for x in self.image]) ] def rotateRight(self): self.image = rotateRight(self.image) def rotateLeft(self): self.image = rotateLeft(self.image) def flipHorizontally(self): self.image = flipHorizontally(self.image) def flipVertically(self): self.image = flipVertically(self.image) def printImage(self): for x in self.image: print(x) def rotateRight(image): return ["".join([x[line_no] for x in reversed(image)]) for line_no in range(len(image))] def rotateLeft(image): return ["".join([x[line_no] for x in image]) for line_no in reversed(range(len(image)))] def flipHorizontally(image): return list(reversed(image)) def flipVertically(image): return ["".join(x for x in reversed(y)) for y in image] def getAllPossibleBorders(tile_dict): all_possible_borders = [] for tile_id, tile in tile_dict.items(): all_possible_borders.extend(tile.getPossibleBorderList()) return all_possible_borders def getCornerPieces(tile_dict, only_get_first_piece=False): corner_pieces = [] all_borders = getAllPossibleBorders(tile_dict) for tile_id, tile in tile_dict.items(): single_borders = 0 for border in tile.getPossibleBorderList(): single_borders += all_borders.count(border) - 1 if single_borders == 4: # every border shows up twice (1x straight, 1x reversed) if only_get_first_piece: return tile_id corner_pieces.append(tile_id) if len(corner_pieces) == 4: break return corner_pieces def part1(test_mode=False): my_input = aoclib.getMultiLineInputAsArray(day=DAY, test=test_mode) tile_dict = {} for tile_input in my_input: tile = Tile(tile_input) tile_dict[tile.image_id] = tile return math.prod(getCornerPieces(tile_dict)) def part2(test_mode=False): my_input = aoclib.getMultiLineInputAsArray(day=DAY, test=test_mode) tile_dict = {} for tile_input in my_input: tile = Tile(tile_input) tile_dict[tile.image_id] = tile borderlen = int(math.sqrt(len(tile_dict))) all_pieces = list(tile_dict.keys()) corner_piece = getCornerPieces(tile_dict, only_get_first_piece=True) all_pieces.remove(corner_piece) # first - find correct orientation of corner piece possible_borders = getAllPossibleBorders(tile_dict) transformations = [ tile_dict[corner_piece].flipHorizontally, tile_dict[corner_piece].flipVertically, tile_dict[corner_piece].flipHorizontally, tile_dict[corner_piece].rotateRight, tile_dict[corner_piece].flipHorizontally, tile_dict[corner_piece].flipVertically, tile_dict[corner_piece].flipHorizontally, ] transformation_counter = 0 found = False while not found: if possible_borders.count(tile_dict[corner_piece].getCurrentBorders()[0]) == 1 \ and possible_borders.count(tile_dict[corner_piece].getCurrentBorders()[3]) == 1: found = True else: transformations[transformation_counter]() transformation_counter += 1 assert found is True full_image = [] for y in range(borderlen): full_image.append([]) for x in range(borderlen): if x == 0 and y == 0: full_image[y].append(corner_piece) continue # find matching piece with correct orientation for the piece to the left (or the top if y > 0 and x == 0) if x == 0: # check against bottom border of upper piece check_border = tile_dict[full_image[y - 1][x]].getCurrentBorders()[2] else: check_border = tile_dict[full_image[y][x - 1]].getCurrentBorders()[1] for possible_tile in all_pieces: if check_border in tile_dict[possible_tile].getPossibleBorderList(): # found my piece ... now orientate it correctly transformations = [ tile_dict[possible_tile].flipHorizontally, tile_dict[possible_tile].flipVertically, tile_dict[possible_tile].flipHorizontally, tile_dict[possible_tile].rotateRight, tile_dict[possible_tile].flipHorizontally, tile_dict[possible_tile].flipVertically, tile_dict[possible_tile].flipHorizontally, ] transformation_counter = 0 oriented = False while not oriented: if (x == 0 and tile_dict[possible_tile].getCurrentBorders()[0] == check_border) \ or (x != 0 and tile_dict[possible_tile].getCurrentBorders()[3] == check_border): oriented = True full_image[y].append(possible_tile) all_pieces.remove(possible_tile) else: transformations[transformation_counter]() transformation_counter += 1 # now that we have the full picture, assemble the borderless full image borderless_image = [] full_hash_count = 0 for y in full_image: for image_line in range(8): borderless_image.append("".join([tile_dict[x].getBorderlessImage()[image_line] for x in y])) full_hash_count += borderless_image[-1].count('#') # and finally scan for our sea monster # seamonster_line_1 = re.compile(r"..................#.") seamonster_line_2 = re.compile(r"#....##....##....###") seamonster_line_3 = re.compile(r".#..#..#..#..#..#...") seamonster_hashcount = 15 # amount of '#' in the sea monster seamonster_count = 0 for _ in range(2): for _ in range(2): for _ in range(4): for x in range(len(borderless_image) - 2): if s_2 := re.split(seamonster_line_2, borderless_image[x+1]): if s_3 := re.split(seamonster_line_3, borderless_image[x+2]): # yes, this is somewhat optimistic, but it results in the correct answer if len(s_2) > 1 and len(s_2) == len(s_3): seamonster_count += len(s_2) - 1 if seamonster_count == 0: borderless_image = rotateRight(borderless_image) else: break if seamonster_count == 0: borderless_image = flipHorizontally(borderless_image) else: break if seamonster_count == 0: borderless_image = flipVertically(borderless_image) else: break return full_hash_count - seamonster_count * seamonster_hashcount