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)
Example #5
0
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
Example #6
0
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
Example #8
0
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
Example #9
0
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
Example #10
0
def get_weird_code(lines: List[str]):
    return get_code(lines, Vect2D(-2, 0), is_weirdpad, conv_weirdpad)
Example #11
0
def get_num_code(lines: List[str]):
    return get_code(lines, Vect2D(0, 0), is_numpad, conv_numpad)
Example #12
0
            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
Example #13
0
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]]