import re from tools.aoc import AOCDay from tools.coordinate import Coordinate from tools.grid import Grid, GridTransformation from typing import Any FACING = [ Coordinate(1, 0, 0), Coordinate(0, 1, 0), Coordinate(-1, 0, 0), Coordinate(0, -1, 0) ] def get_password_from_coord(c: Coordinate, face: int) -> int: return (c.y + 1) * 1000 + (c.x + 1) * 4 + face def walk(board: Grid, pos: Coordinate, directions: list) -> (Coordinate, int): face = 0 for direction in directions: steps, turn = direction for _ in range(steps): next_pos = pos + FACING[face] next_face = face if board.get(next_pos) is None or not board.isWithinBoundaries(next_pos): match face: case 0: next_pos = Coordinate(0, next_pos.y, 0) case 1: next_pos = Coordinate(next_pos.x, 0, 0) case 2: next_pos = Coordinate(board.maxX, next_pos.y, 0) case 3: next_pos = Coordinate(next_pos.x, board.maxY, 0) while board.get(next_pos) is None: next_pos += FACING[face] next_val = board.get(next_pos) if not next_val: break else: pos = next_pos face = next_face if turn != 'S': face = (face + (1 if turn == 'R' else -1)) % 4 return pos, face def cube_walk(board: Grid, pos: Coordinate, directions: list) -> (Coordinate, int): face = 0 for direction in directions: steps, turn = direction for _ in range(steps): next_pos = pos + FACING[face] if board.get(next_pos) is False: break elif board.get(next_pos) is None: match face: case 0: # right if board.get(Coordinate(pos.x + 1, pos.y, 1)) is True: board.transform(GridTransformation.COUNTER_ROTATE_Y) next_pos = Coordinate(1, pos.y, 0) else: break case 1: # down if board.get(Coordinate(pos.x, pos.y + 1, 1)) is True: board.transform(GridTransformation.ROTATE_X) next_pos = Coordinate(pos.x, 1, 0) else: break case 2: # left if board.get(Coordinate(0, pos.y, 1)) is True: board.transform(GridTransformation.ROTATE_Y) next_pos = Coordinate(board.maxX - 1, pos.y, 0) else: break case 3: # up if board.get(Coordinate(pos.x, 0, 1)) is True: board.transform(GridTransformation.COUNTER_ROTATE_X) next_pos = Coordinate(pos.x, board.maxY - 1, 0) else: break pos = next_pos if turn != 'S': face = (face + (1 if turn == 'R' else -1)) % 4 return pos, face def fold(board: Grid, start_pos: Coordinate): c_size = 4 if board.maxX < 40 else 50 # account for test case if board.get(start_pos + Coordinate(-1, 0, 0)) is not None: for y in board.rangeY(): for x in range(start_pos.x - 1, board.minX - 1, -1): from_c = Coordinate(x, y, 0) to_c = Coordinate(start_pos.x - 1, y, start_pos.x - x) board.set(to_c, board.get(from_c)) board.set(from_c, None) board.recalcBoundaries() board.transform(GridTransformation.ROTATE_Y) board.shift(shift_z=-board.minZ) fold(board, Coordinate(board.maxX - c_size, 1, 0)) board.transform(GridTransformation.COUNTER_ROTATE_Y) board.shift_zero() start_pos = Coordinate(1, 1, 0) if board.get(start_pos + Coordinate(c_size, 0, 0)) is not None: for y in range(board.maxY + 1): for x in range(start_pos.x + c_size, board.maxX + 1): from_c = Coordinate(x, y, 0) to_c = Coordinate(start_pos.x + c_size, y, x - start_pos.x - c_size + 1) board.set(to_c, board.get(from_c)) board.set(from_c, None) board.recalcBoundaries() board.transform(GridTransformation.COUNTER_ROTATE_Y) board.shift(shift_z=-board.minZ) fold(board, Coordinate(1, 1, 0)) board.transform(GridTransformation.ROTATE_Y) board.shift_zero() if board.get(Coordinate(start_pos.x, 0, 0)) is not None: board.shift(shift_y=1) if list(board.getActiveCells(x=0, z=0)): board.shift(shift_x=1) if board.get(start_pos + Coordinate(0, c_size, 0)) is not None: for y in range(start_pos.y + c_size, board.maxY + 1): for x in board.rangeX(): from_c = Coordinate(x, y, 0) to_c = Coordinate(x, start_pos.y + c_size, y - start_pos.y - c_size + 1) board.set(to_c, board.get(from_c)) board.set(from_c, None) board.recalcBoundaries() board.transform(GridTransformation.ROTATE_X) fold(board, start_pos) board.transform(GridTransformation.COUNTER_ROTATE_X) class Day(AOCDay): inputs = [ [ (6032, "input22_test"), (1428, "input22"), ], [ (5031, "input22_test"), (142380, "input22"), ] ] def get_map_and_directions(self) -> (Grid, list, Coordinate): start_pos = None map_lines, dir_line = self.getMultiLineInputAsArray() board = Grid.from_str("/".join(map_lines), default=None, translate={' ': None, '#': False, '.': True}, mode3d=True) for x in board.rangeX(): if board.get(Coordinate(x, 0, 0)) is not None: start_pos = Coordinate(x, 0, 0) break if dir_line[0][-1] not in ['R', 'L']: dir_line[0] += 'S' directions = [] for d in re.findall(r'\d+[RLS]', dir_line[0]): directions.append((int(d[:-1]), d[-1])) return board, directions, start_pos def part1(self) -> Any: board, directions, start_position = self.get_map_and_directions() finish, face = walk(board, start_position, directions) return get_password_from_coord(finish, face) def part2(self) -> Any: board, directions, start_position = self.get_map_and_directions() board.shift(shift_x=1, shift_y=1) start_position += Coordinate(1, 1, 0) fold(board, start_position) finish, face = cube_walk(board, Coordinate(1, 1, 0), directions) orig_board, _, _ = self.get_map_and_directions() c_size = 4 if orig_board.maxX < 50 else 50 # account for test case for x in range(0, orig_board.maxX + 1, c_size): for y in range(0, orig_board.maxY + 1, c_size): check_pos = finish - Coordinate(1, 1, 0) check_face = face sub_board = orig_board.sub_grid(x, y, x + c_size - 1, y + c_size - 1, 0, 0) if len(sub_board.getActiveCells()) == 0: continue check_board = board.sub_grid(1, 1, c_size, c_size, 0, 0) for _ in range(2): for _ in range(4): check_board.transform(GridTransformation.ROTATE_Z) check_pos = Coordinate(-check_pos.y + sub_board.maxY, check_pos.x, 0) check_face = (check_face + 1) % 4 if sub_board == check_board: check_pos += Coordinate(x, y, 0) return get_password_from_coord(check_pos, check_face) check_board.transform(GridTransformation.FLIP_X) check_pos = Coordinate(abs(check_pos.x - c_size + 1), check_pos.y, 0) check_face = (check_face + 2) % 4 return "" if __name__ == '__main__': day = Day(2022, 22) day.run(verbose=True)