def build_graphs(maze) -> List[Graph]: """ Given the maze as a string, build a graph of the shortest distances between the start and all doors/keys. This function works in three steps: 1. Identify the tunnels, junctions and objects in the maze 2. Do a breadth-first walk through all the tunnels, and build a graph of distances between all junctions and objects (one per starting point; we assume the resulting graphs are distinct). 3. Use that graph to run a second BFS and build a new graph with only the points of interest (start, keys, doors). """ # Boolean map of th tunnels ("not walls") tunnels = maze != "#" # Build a set of all vertices for a first graph: junctions and objects objects_map = np.where(tunnels, maze, ".") != "." nodes = {Vect2D(*p): maze[tuple(p)] for p in np.argwhere(objects_map)} def successors(_pos: Vect2D): for v in UNIT_VECTORS: _new = _pos + v if tunnels[tuple(_new)]: yield _new, 1 def make_graph(origin: Vect2D) -> Graph: return build_graph([origin], nodes.__contains__, nodes.__getitem__, successors,) return [make_graph(Vect2D(*p)) for p in np.argwhere(maze == "@")]
def parse_nodes(df_output): nodes = {} for line in df_output[2:]: x, y, size, used, avail = map(int, RE_NODE.match(line).groups()) pos = Vect2D(x, y) assert size == used + avail nodes[pos] = StorageNode(pos, size, used) return nodes
def get_tiles(code): runner = CodeRunner(code) tiles = {} while True: try: x = next(runner) y = next(runner) tile = TILES[next(runner)] except EndProgram: break tiles[Vect2D(x, y)] = tile return tiles
def locate_square(controller: DroneController, size=100): top_edge = EdgeWalker(controller) low_edge = EdgeWalker(controller, invert=True) square_vect = Vect2D(size - 1, 1 - size) top, low = next(top_edge), next(low_edge) while (ref := top + square_vect) != low: if abs(ref) > abs(low): low = next(low_edge) else: top = next(top_edge)
def process_maze(maze): """ Transform the maze input from its "raw" form to one that's easier to process. To be more specific: - Portal names and positions are extracted, - The outer boundary (with the portal names) is removed - The central hole (also portal names) is filled as a wall - The string input is converted into a boolean map of paths. """ portals = find_portals(maze) void = ~(maze == "#") # Shrink the maze on each side if needed offset = Vect2D(0, 0) if np.all(void[:2, :]): offset += Vect2D(-2, 0) maze = np.delete(maze, slice(None, 2), axis=0) if np.all(void[:, :2]): offset += Vect2D(0, -2) maze = np.delete(maze, slice(None, 2), axis=1) if np.all(void[-2:, :]): maze = np.delete(maze, slice(-2, None), axis=0) if np.all(void[:, -2:]): maze = np.delete(maze, slice(-2, None), axis=1) # Fill in the center and turn the map into booleans donut = (maze == ".") | (maze == "#") maze = np.where(donut, maze, "#") == "." sx, sy = maze.shape new_portals = {} for pos, name in portals: pos = pos + offset outer = pos.x in (0, sx - 1) or pos.y in (0, sy - 1) new_portals[(name, outer)] = pos return maze, new_portals
def find_portals(maze): walls = np.where(maze == ".", " ", maze) != " " pattern = np.array([[-1, 1, -1], [-1, 1, -1], [1, -1, 1]]) portals = [] for i, v in enumerate((RIGHT, UP, LEFT, DOWN)): coords = np.argwhere(convolve_2d(walls, np.rot90(pattern, i)) == 4) for x, y in coords: label = "".join(maze[x - 1:x + 2, y - 1:y + 2].reshape(9)) label = re.sub(r"[\s#.]", "", label) pos = Vect2D(x, y) + v portals.append((pos, label)) return portals
def main(data: str): start = DroidPos(CodeRunner(parse_intcode(data)), Vect2D(0, 0)) frontier = deque((start, )) explored = {start.position} ox_pos = None while frontier: node = frontier.popleft() if node.is_oxygen_system: if ox_pos is not None: raise RuntimeError("Found more than 1 oxygen system") ox_pos = node.position yield len(node.path) for child in node.next_nodes(): if child.position in explored: continue explored.add(child.position) frontier.append(child) if ox_pos is None: raise RuntimeError("Oxygen system not found") has_oxygen = {ox_pos} max_oxygen_time = 0 frontier = deque([(ox_pos, 0)]) while frontier: position, ox_time = frontier.popleft() if ox_time > max_oxygen_time: max_oxygen_time = ox_time for _, _, v in DIRECTIONS: next_pos = position + v if next_pos not in explored or next_pos in has_oxygen: continue has_oxygen.add(next_pos) frontier.append((next_pos, ox_time + 1)) yield max_oxygen_time
def run_robot(code, start_color=0): computer = CodeRunner(code) robot = Walker2D(0, 0, Direction.Up) colors = defaultdict(int) colors[Vect2D(0, 0)] = start_color painted = set() while True: try: computer.send(colors[robot.pos]) new_color = next(computer) direction = next(computer) except EndProgram: break colors[robot.pos] = new_color painted.add(robot.pos) if direction: robot.rot_right() else: robot.rot_left() robot.move() return colors, painted
from functools import partial from hashlib import md5 from typing import List, NamedTuple from libaoc.algo import BFSearch, HighestCostSearch from libaoc.vectors import Vect2D, UP, DOWN, LEFT, RIGHT class MazeState(NamedTuple): position: Vect2D path: str INITIAL = MazeState(Vect2D(0, 3), "") END = Vect2D(3, 0) def open_paths(seed: str, path: str) -> str: head = md5((seed + path).encode()).hexdigest()[:4] return "".join(door for door, c in zip("UDLR", head) if c in "bcdef") DIRS = {"U": UP, "D": DOWN, "L": LEFT, "R": RIGHT} def next_states(seed: str, state: MazeState) -> List[MazeState]: res = [] for open_door in open_paths(seed, state.path): pos = state.position + DIRS[open_door] if not (0 <= pos.x < 4 and 0 <= pos.y < 4): continue
def get_weird_code(lines: List[str]): return get_code(lines, Vect2D(-2, 0), is_weirdpad, conv_weirdpad)
def get_num_code(lines: List[str]): return get_code(lines, Vect2D(0, 0), is_numpad, conv_numpad)
return edge_mat edge_mat[x, :y + 1] = True def detect_ray_surface(controller: DroneController, size=50): top_edge = edge_matrix(controller, size=size).transpose() low_edge = edge_matrix(controller, invert=True, size=size) return np.sum(top_edge & low_edge) def locate_square(controller: DroneController, size=100): top_edge = EdgeWalker(controller) low_edge = EdgeWalker(controller, invert=True) square_vect = Vect2D(size - 1, 1 - size) top, low = next(top_edge), next(low_edge) while (ref := top + square_vect) != low: if abs(ref) > abs(low): low = next(low_edge) else: top = next(top_edge) return top + Vect2D(0, 1 - size) def main(data: str): controller = DroneController(CodeRunner(parse_intcode(data))) yield detect_ray_surface(controller) x, y = locate_square(controller) yield x * 10_000 + y
def find_robot(image): img = np.array([list(line) for line in image]) no_wall = np.where(img == "#", ".", img) x, y = np.argwhere(no_wall != ".")[0] return Vect2D(x, y), READ_DIR[image[x][y]]