tiles = {} with open('input.txt') as fh: s = fh.read().rstrip() for block in s.split('\n\n'): label, tile = block.split('\n', 1) i = int(label[5:-1], 10) tiles[i] = tile MONSTER = ( ' # \n' '# ## ## ###\n' ' # # # # # # \n' ) def prod(arr): ret = 1 for x in arr: ret *= x return ret def get_sides(tile): top = tile.split('\n')[0] bottom = tile.split('\n')[-1] left = tile[0::11] right = tile[9::11] return [top, right, bottom, left] def has_side(tile, side): for s in get_sides(tile): if s == side: return True if s == side[::-1]: return True return False def get_edges(tiles): edges = {} for i, tile in tiles.items(): edges[i] = set() for side in get_sides(tile): for j, other in tiles.items(): if i != j and has_side(other, side): edges[i].add(j) assert len(edges[i]) <= 4 return edges def get_corners(edges): return [i for i, e in edges.items() if len(e) == 2] def get_layout(edges, corners): corner = corners[0] neighbors = list(edges[corner]) layout = [ [corner, neighbors[0]], [neighbors[1]], ] size = 12 for k in range(3, 2 * size): for x in range(1, k - 1): y = k - x - 1 if y >= len(layout) or x >= len(layout[0]): continue a = edges[layout[y][x - 1]] b = edges[layout[y - 1][x]] c = a.intersection(b) c.remove(layout[y - 1][x - 1]) if len(c) == 1: i = list(c)[0] layout[y].append(i) if k - 1 == len(layout): b = edges[layout[k - 2][0]] c = b.difference({ layout[k - 3][0], layout[k - 2][1], }) if len(c) == 1: i = list(c)[0] layout.append([i]) if k - 1 == len(layout[0]): a = edges[layout[0][k - 2]] c = a.difference({ layout[0][k - 3], layout[1][k - 2], }) if len(c) == 1: i = list(c)[0] layout[0].append(i) return layout def flip(s): return '\n'.join(reversed(s.split('\n'))) def rotate(tile): lines = tile.split('\n') n = len(lines) ret = [] for y in range(n): ret.append('') for x in range(n): ret[y] += lines[n - 1 - x][y] return '\n'.join(ret) def iter_rotations(tile): for i in range(4): yield tile tile = rotate(tile) tile = flip(tile) for i in range(4): yield tile tile = rotate(tile) def next_coords(x, y): size = 12 if y + 1 == size: if x + 1 == size: raise StopIteration return y, x + 1 elif x == 0: return y + 1, x else: return x - 1, y + 1 def get_image(tiles, layout, x=0, y=0, image=None): if not image: image = [] if len(image) < y + 1: image.append([]) tile = tiles[layout[y][x]] for r in iter_rotations(tile): if x != 0: left = image[y][x - 1] if [l[-1] for l in left.split('\n')] != [l[0] for l in r.split('\n')]: continue if y != 0: top = image[y - 1][x] if top.split('\n')[-1] != r.split('\n')[0]: continue image[y].append(r) try: return get_image(tiles, layout, *next_coords(x, y), image) except StopIteration: return image except ValueError: image[y].pop() raise ValueError def get_composed(image): tile_size = 10 - 2 ret = [] for row in image: for i in range(tile_size): ret.append('') for tile in row: lines = tile.split('\n')[1:-1] for y, line in enumerate(lines): ret[y - tile_size] += line[1:-1] return '\n'.join(ret) def check_monster(lines, x, y): monster = MONSTER.split('\n') for dy in range(len(monster)): for dx in range(len(monster[dy])): try: if monster[dy][dx] == '#' and lines[y + dy][x + dx] != '#': return False except IndexError: return False return True def count_monsters(composed): count = 0 lines = composed.split('\n') for y in range(len(lines)): for x in range(len(lines[y])): if check_monster(lines, x, y): count += 1 return count edges = get_edges(tiles) corners = get_corners(edges) print(prod(corners)) layout = get_layout(edges, corners) image = get_image(tiles, layout) composed = get_composed(image) for r in iter_rotations(composed): count = count_monsters(r) if count: print(r.count('#') - MONSTER.count('#') * count)