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 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, exception): all_possible_borders = [] for tile_id, tile in tile_dict.items(): if tile_id != exception: all_possible_borders.extend(tile.getPossibleBorderList()) return all_possible_borders def getCornerPieces(tile_dict): corner_pieces = [] for tile_id, tile in tile_dict.items(): impossible_borders = 0 all_other_borders = getAllPossibleBorders(tile_dict, tile_id) for border in tile.getPossibleBorderList(): if border not in all_other_borders: impossible_borders += 1 # we need to find the tiles where 2 borders (and their reverse) don't match any other border if impossible_borders == 4: corner_pieces.append(tile_id) 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)[0] all_pieces.remove(corner_piece) # first - find correct orientation of corner piece possible_borders = getAllPossibleBorders(tile_dict, corner_piece) found = False for _ in range(2): for _ in range(2): for _ in range(4): if tile_dict[corner_piece].getCurrentBorders()[0] not in possible_borders \ and tile_dict[corner_piece].getCurrentBorders()[3] not in possible_borders: found = True break tile_dict[corner_piece].rotateRight() if found: break else: tile_dict[corner_piece].flipHorizontally() if found: break else: tile_dict[corner_piece].flipVertically() # flip diagonally to match with example from aoc webpage tile_dict[corner_piece].rotateRight() tile_dict[corner_piece].flipVertically() 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 oriented = False for _ in range(2): for _ in range(2): for _ in range(4): 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) break tile_dict[possible_tile].rotateRight() if oriented: break else: tile_dict[possible_tile].flipVertically() if oriented: break else: tile_dict[possible_tile].flipHorizontally() # 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