Ejemplo n.º 1
0
    def get_map_data(self, coord):
        data = self._map_data.get(coord)

        # Return cached data.
        if data:
            return data

        # Otherwise calculate data to cache.  This will use recursion to
        # calculate most data points.
        if coord == Coord(0,0):
            geologic_index = 0

        elif coord == self._target_coord:
            geologic_index = 0

        elif coord.y_val == 0:
            geologic_index = coord.x_val * 16807

        elif coord.x_val == 0:
            geologic_index = coord.y_val * 48271

        else:
            # Recursively calculate data.
            coord1 = aoc.add_coords(coord, Coord(-1,0))
            coord2 = aoc.add_coords(coord, Coord(0,-1))
            geologic_index = self.get_map_data(coord1).erosion_level * \
                             self.get_map_data(coord2).erosion_level

        erosion_level = (geologic_index + self._depth) % 20183
        erosion_type = self.Type(erosion_level % 3)
        self._map_data[coord] = self.CaveData(erosion_level=erosion_level,
                                              erosion_type=erosion_type)
        return self._map_data[coord]
Ejemplo n.º 2
0
def make_vault_map_four(vault_map):
    for increment in INCREMENT:
        location = aoc.add_coords(vault_map.entrance, increment)
        vault_map.map[location] = '#'

    maps = []
    for increment in aoc.Coord(1, 1), aoc.Coord(-1, 1), aoc.Coord(-1, -1), aoc.Coord(1, -1):
        entrance = aoc.add_coords(vault_map.entrance, increment)
        new_map = vault_map._replace(entrance=entrance)
        maps.append(new_map)

    return maps
Ejemplo n.º 3
0
def simulate(input_list):
    """
    Simulate both part 1 and part 2 at the same time.
    """

    path1, path2 = parse_input(input_list[0], input_list[1])

    # Walk the first wire path and create a map containing the coordinates the first wire touches.
    # We store the minimum length to the coordinate.
    wire_map = {}
    current_coord = Coord(0, 0)
    length = 0

    for segment in path1:
        direction = segment[0]
        distance = int(segment[1:])

        for _ in range(distance):
            length += 1
            current_coord = aoc.add_coords(current_coord,
                                           NEXT_INCREMENT[direction])
            if current_coord not in wire_map:
                wire_map[current_coord] = length

    # Walk the second wire path checking for intersections with the first wire.  If we intersect,
    # calculate both the minimum distance to the port (0,0) and the minimum wire length to the
    # intersection.
    current_coord = Coord(0, 0)
    length = 0

    min_distance_from_port = None
    min_length = None

    for segment in path2:
        direction = segment[0]
        distance = int(segment[1:])

        for _ in range(distance):
            length += 1
            current_coord = aoc.add_coords(current_coord,
                                           NEXT_INCREMENT[direction])
            if current_coord in wire_map:
                distance_from_port = abs(current_coord.x_val) + abs(
                    current_coord.y_val)

                if min_distance_from_port is None or distance_from_port < min_distance_from_port:
                    min_distance_from_port = distance_from_port

                total_length = wire_map[current_coord] + length
                if min_length is None or total_length < min_length:
                    min_length = total_length

    return min_distance_from_port, min_length
Ejemplo n.º 4
0
def calculate_path(scaffolding_map, robot_location):
    path = []

    # Figure out initial turn if needed.  We always turn right.  If we are really tight on program
    # space this can be optimized to do a 'L' instead of 'R', 'R', 'R' when it is closer to turn
    # left.
    robot_direction = scaffolding_map[robot_location]

    while True:
        # Can we move forward?
        movements = MOVEMENT[robot_direction]
        move = movements[0]  # Go forward.
        next_robot_location = aoc.add_coords(robot_location, move.increment)

        if scaffolding_map.get(next_robot_location, '.') == '#':
            # Can move forward.
            break

        # We can't move forward so turn right and try again.
        move = movements[1]
        robot_direction = move.next_direction
        path.append(move.command)

    distance = 0
    moving = True
    while moving:
        moving = False

        movements = MOVEMENT[robot_direction]
        for move in movements:
            next_robot_location = aoc.add_coords(robot_location, move.increment)
            next_robot_direction = move.next_direction

            if scaffolding_map.get(next_robot_location, '.') == '#':
                if move.command is not None:
                    path.append(str(distance))
                    path.append(move.command)
                    # We turned and moved 1 when we start in a new direction.
                    distance = 1
                else:
                    # Moved forward 1 in the existing direction.
                    distance += 1
                robot_location = next_robot_location
                robot_direction = next_robot_direction
                moving = True
                break

        if not moving:
            path.append(str(distance))

    return path
Ejemplo n.º 5
0
    def _count_adjacent(self, level, coord):
        # pylint: disable=too-many-branches

        count = 0
        # Check the four adjacent coordinates
        for increment in (aoc.Coord(1, 0), aoc.Coord(-1, 0), aoc.Coord(0, 1),
                          aoc.Coord(0, -1)):
            if (level, aoc.add_coords(coord, increment)) in self.map_data.map:
                count += 1

        if not self.recursive:
            return count

        # Handle special recursion cases.
        # NB.  This can't use elif because some cases like (0,0) need to use two of the if cases
        # for to count both above/below and left/right squares.
        if coord.x_val == 0:
            coord_next_level = aoc.Coord(1, 2)
            if (level - 1, coord_next_level) in self.map_data.map:
                count += 1

        if coord.x_val == self.LEVEL_SIZE_X - 1:
            coord_next_level = aoc.Coord(3, 2)
            if (level - 1, coord_next_level) in self.map_data.map:
                count += 1

        if coord.y_val == 0:
            coord_next_level = aoc.Coord(2, 1)
            if (level - 1, coord_next_level) in self.map_data.map:
                count += 1

        if coord.y_val == self.LEVEL_SIZE_Y - 1:
            coord_next_level = aoc.Coord(2, 3)
            if (level - 1, coord_next_level) in self.map_data.map:
                count += 1

        if coord == aoc.Coord(2, 1):
            for x_val in range(0, self.LEVEL_SIZE_X):
                if (level + 1, aoc.Coord(x_val, 0)) in self.map_data.map:
                    count += 1

        if coord == aoc.Coord(2, 3):
            for x_val in range(0, self.LEVEL_SIZE_X):
                if (level + 1,
                        aoc.Coord(x_val,
                                  self.LEVEL_SIZE_Y - 1)) in self.map_data.map:
                    count += 1

        if coord == aoc.Coord(1, 2):
            for y_val in range(0, self.LEVEL_SIZE_Y):
                if (level + 1, aoc.Coord(0, y_val)) in self.map_data.map:
                    count += 1

        if coord == aoc.Coord(3, 2):
            for y_val in range(0, self.LEVEL_SIZE_Y):
                if (level + 1, aoc.Coord(self.LEVEL_SIZE_X - 1,
                                         y_val)) in self.map_data.map:
                    count += 1

        return count
Ejemplo n.º 6
0
def solve_bfs(grid, start, end, links):
    frontiers = [start]
    dist = 0
    end_dist = None
    visited = set(frontiers)
    while end_dist is None and frontiers:
        dist += 1
        new_frontiers = []
        for coords in frontiers:
            choices = []
            # Find dots we can walk to.
            for d in ((0, -1), (1, 0), (0, 1), (-1, 0)):
                n_coords = aoc.add_coords(coords, d)
                if read_grid(grid, n_coords) == '.':
                    choices.append(n_coords)
            # Peek through teleporters.
            if coords in links:
                out = links[coords]
                choices.append(out)
            # Remove places we already visited.
            choices = [c for c in choices if c not in visited]
            # Each choice becomes a new frontier.
            for c in choices:
                if c == end:
                    end_dist = dist
                else:
                    new_frontiers.append(c)
                    visited.add(c)
        frontiers = new_frontiers
    if end_dist is None:
        raise Exception('could not find path')
    return end_dist
Ejemplo n.º 7
0
    def find_furthest_room(self):
        # Perform a breadth first search.
        paths = deque([(Coord(0, 0), '')])
        rooms_visited = set(Coord(0, 0))

        num_doors = 0
        num_doors_is_1000_or_more = 0

        while paths:
            current_coord, current_path = paths.popleft()

            for direction, increment in self.direction_mapping.items():
                next_coord = aoc.add_coords(current_coord, increment)
                if next_coord in rooms_visited:
                    continue

                if self._is_door(current_coord, next_coord):
                    next_path = current_path + direction
                    paths.append((next_coord, next_path))
                    num_doors = max(num_doors, len(next_path))

                    # Most of these paths have common beginnings to we only need to re-simulate
                    # if we haven't done next_path already when simulating a previous path.
                    if next_coord not in rooms_visited:
                        rooms_visited.add(next_coord)
                        if num_doors >= 1000:
                            num_doors_is_1000_or_more += 1

        return num_doors, num_doors_is_1000_or_more
Ejemplo n.º 8
0
    def find_shortest_path(self):

        WorkEntry = namedtuple('WorkEntry', ['location', 'steps'])
        work_queue = deque()

        visited = set()
        visited.add(self._entrance)

        work_queue.append(WorkEntry(location=self._entrance, steps=0))

        while work_queue:
            work_entry = work_queue.popleft()
            for increment in self.INCREMENT:
                new_coord = aoc.add_coords(work_entry.location.coord,
                                           increment)
                new_location = work_entry.location._replace(coord=new_coord)

                if new_location in visited:
                    continue

                tile = self._maze.get(new_location.coord, None)

                if new_location == self._exit:
                    work_queue.clear()
                    break

                if tile is None:
                    # This will happen if we try to exit when we are still at the entrance.  Treat
                    # like wall.
                    continue

                if tile == '#':
                    # Wall
                    continue

                if tile == '.':
                    # Move to next square.
                    work_queue.append(
                        WorkEntry(location=new_location,
                                  steps=work_entry[1] + 1))
                    visited.add(new_location)

                else:
                    # Portal
                    if self._maze_type == self.Type.RECURSIVE and \
                            new_location.level == 1 and \
                            self._is_outside_portal(new_location.coord):
                        # In RECURSIVE mode outer portals on level 1 are treated like walls.
                        continue

                    new_location = self._find_portal_exit(tile, new_location)
                    work_queue.append(
                        WorkEntry(location=new_location,
                                  steps=work_entry[1] + 1))
                    visited.add(new_location)

        return work_entry.steps + 1  # +1 for step into the exit square.
Ejemplo n.º 9
0
    def run(self, command):
        self._input_stream.append(command.value)
        self.icc.run(until=self.icc.OUTPUT)
        status = self.StatusCode(self._output_stream[-1])
        if status != self.StatusCode.HIT_WALL:
            self._location = aoc.add_coords(self._location,
                                            self.increment[command])

        return status
Ejemplo n.º 10
0
    def _parse_portals(self, portal_tiles):
        # Figure out portal names, locatons, and pairing.
        while portal_tiles:
            (coord1, tile1) = portal_tiles.popitem()
            for increment in self.INCREMENT:
                coord2 = aoc.add_coords(coord1, increment)

                if coord2 in portal_tiles:
                    tile2 = portal_tiles.pop(coord2)
                    if tile1 < tile2:
                        portal = tile1 + tile2
                    else:
                        portal = tile2 + tile1

                    break

            for coord in coord1, coord2:
                for increment in self.INCREMENT:
                    coord_to_check = aoc.add_coords(coord, increment)
                    tile = self._maze.get(coord_to_check, None)

                    if tile == '.':
                        if portal == "AA":
                            self._entrance = self.Location(
                                level=1, coord=coord_to_check)
                            self._maze[coord_to_check] = '#'
                        elif portal == "ZZ":
                            self._exit = self.Location(level=1,
                                                       coord=coord_to_check)
                            self._maze[coord_to_check] = '#'
                        else:
                            self._maze[coord] = portal
                            self._portals[portal].append(
                                (coord, coord_to_check))

                        break  # Really need to break two levels.
Ejemplo n.º 11
0
    def generate_map(self):
        final_paths = self._find_paths(Coord(0, 0), '')

        paths_simulated = set()

        # Re-walk the final_paths taking detours.
        for path in final_paths:
            new_path = ''
            coord = Coord(0, 0)
            for char in path:
                new_path += char
                coord = aoc.add_coords(coord, self.direction_mapping[char])

                if new_path not in paths_simulated:
                    self._find_paths(coord,
                                     new_path,
                                     max_depth=self._longest_detour,
                                     take_detours=True)
                    paths_simulated.add(new_path)
Ejemplo n.º 12
0
def calculate_calibration_sum(scaffolding_map):
    calibration_sum = 0

    for coord, contents in scaffolding_map.items():
        # Only check scaffolding locations.
        if contents != "#":
            continue

        # An intersection will have scaffolding on all four sides.
        num_connections = 0
        for increment in (aoc.Coord(-1, 0), aoc.Coord(1, 0), aoc.Coord(0, -1), aoc.Coord(0, 1)):
            coord_to_check = aoc.add_coords(coord, increment)
            if scaffolding_map.get(coord_to_check, "X") == "#":
                num_connections += 1

        # Found an intersection.  Add its value to the calibration.
        if num_connections == 4:
            calibration_sum += coord.x_val * coord.y_val

    return calibration_sum
Ejemplo n.º 13
0
def simulate_oxygen_flow(oxygen_system_location, open_locations):
    # Do a breadth first search of all paths from the oxygen system.  Since we are doing a breadth
    # first search, the last object removed from the work queue will be the shortest path to the
    # furthest location.

    work_queue = deque()
    work_queue.append((oxygen_system_location, 0))
    locations_visited = {oxygen_system_location}

    while work_queue:
        location, num_steps = work_queue.popleft()
        for increment in Droid.increment.values():
            new_location = aoc.add_coords(location, increment)
            if new_location not in open_locations:
                # Wall
                continue

            if new_location not in locations_visited:
                work_queue.append((new_location, num_steps + 1))
                locations_visited.add(new_location)

    return num_steps
Ejemplo n.º 14
0
    def _find_paths(self,
                    seed_coord,
                    seed_path,
                    max_depth=-1,
                    take_detours=False):
        # Perform a breadth first search of valid paths to discover the doors.

        PathData = namedtuple('PathData', ['current_coord', 'current_path'])

        paths = deque([PathData(seed_coord, seed_path)])
        final_paths = []

        while paths and max_depth != 0:
            max_depth -= 1

            path_data = paths.popleft()

            path_was_extended = False
            for direction, increment in self.direction_mapping.items():
                new_coord = aoc.add_coords(path_data.current_coord, increment)
                new_path = path_data.current_path + direction

                if take_detours:
                    match = self._map_regex_with_detours.fullmatch(
                        new_path, partial=True)
                else:
                    match = self._map_regex.fullmatch(new_path, partial=True)

                if match:
                    paths.append(PathData(new_coord, new_path))
                    path_was_extended = True
                    self._add_door(path_data.current_coord, new_coord)

            if not path_was_extended:
                final_paths.append(path_data.current_path)

        return final_paths
Ejemplo n.º 15
0
 def move_forward(self):
     self._location = aoc.add_coords(self._location, self._direction)
Ejemplo n.º 16
0
def solve_bfs_part_b(grid, start, end, shrink_links, grow_links):
    frontiers = [(start, 0, 0, [])]
    paused_frontiers = []
    max_size = 0
    end_dist = None
    best_path = None
    visited = {}
    visited[0] = {start: 0}
    while end_dist is None and frontiers:
        #print('%s' % (frontiers,))
        new_frontiers = []
        for coords, size, dist, path in frontiers:
            choices = []
            # Find dots we can walk to.
            for d in ((0, -1), (1, 0), (0, 1), (-1, 0)):
                n_coords = aoc.add_coords(coords, d)
                if read_grid(grid, n_coords) == '.':
                    choices.append((n_coords, size, None))
            # Peek through teleporters.
            if coords in shrink_links:
                t = shrink_links[coords]
                choices.append((t.shrink, size - 1, t))
            if coords in grow_links:
                t = grow_links[coords]
                choices.append((t.grow, size + 1, t))
            # Each choice becomes a new frontier, unless it sucks.
            for coords, size, teleporter in choices:
                new_dist = dist + 1
                # Check this map and every one closer.
                if size not in visited:
                    visited[size] = {}
                vmap = visited[size]
                if coords in vmap and vmap[coords] < new_dist:
                    continue
                vmap[coords] = new_dist

                # New info for this frontier.
                new_path = copy.copy(path)
                if teleporter is None:
                    new_path.append(coords)
                else:
                    new_path.append((teleporter, size))
                if coords == end and size == 0:
                    # Found the exit.
                    end_dist = new_dist
                    best_path = new_path
                else:
                    # Need to keep searching.
                    if size > 0:
                        # Disallow getting larger than the max.
                        continue
                    elif abs(size) <= max_size:
                        f = new_frontiers
                    else:
                        #print('pausing frontier: %s' % (coords,))
                        f = paused_frontiers
                    f.append((coords, size, new_dist, new_path))
        frontiers = new_frontiers
        if not frontiers and end_dist is None:
            max_size += 1
            print('expanding search to %d' % max_size)
            frontiers = paused_frontiers
            paused_frontiers = []
    if end_dist is None:
        raise Exception('could not find path')
    print 'best path: '
    prev_t = Teleporter("AA", None, None)
    skipped = 0
    steps = 0
    for p in best_path:
        if len(p) == 2 and isinstance(p[0], Teleporter):
            t, size = p
            print('walk from %s to %s (%d steps)' %
                  (prev_t.name, t.name, skipped))
            print('Recurse into level %d through %s (1 step)' % (size, t.name))
            steps += skipped + 1
            skipped = 0
            prev_t = t
        else:
            skipped += 1
    steps += skipped
    print('walk from %s to finish (%d steps)' % (prev_t.name, skipped))
    print('total: %d' % steps)
    return end_dist
Ejemplo n.º 17
0
def get_all_keys(vault_maps):
    number_of_robots = len(vault_maps)

    entrance_locations = tuple(vault_maps[i].entrance for i in range(number_of_robots))

    work_queue = deque()
    locations_visited = []
    for robot in range(number_of_robots):
        work_queue.append(
            WorkQueueEntry(
                number_of_steps=0,
                active_robot=robot,
                locations=entrance_locations,
                keys_found=set(),
            )
        )

        locations_visited.append(set())
        locations_visited[robot].add(
            Visited(location=entrance_locations[robot], keys_found=frozenset()))

    while work_queue:
        work_entry = work_queue.popleft()

        # Try to move the current robot each direction.
        for increment in INCREMENT:
            new_location = aoc.add_coords(work_entry.locations[work_entry.active_robot],
                                          increment)
            new_tile = vault_maps[work_entry.active_robot].map[new_location]
            new_keys_found = work_entry.keys_found.copy()
            new_number_of_steps = work_entry.number_of_steps + 1

            new_locations_list = list(work_entry.locations)
            new_locations_list[work_entry.active_robot] = new_location
            new_locations = tuple(new_locations_list)

            if new_tile.islower() and not new_tile in new_keys_found:
                new_keys_found.add(new_tile)

            new_visited = Visited(new_location, frozenset(new_keys_found))

            if new_visited in locations_visited[work_entry.active_robot]:
                continue

            if new_tile == '#':
                # Wall
                continue

            if new_tile.isupper() and new_tile.lower() not in new_keys_found:
                # Locked door
                continue

            locations_visited[work_entry.active_robot].add(new_visited)

            for robot in range(number_of_robots):
                work_queue.append(
                    WorkQueueEntry(
                        number_of_steps=new_number_of_steps,
                        active_robot=robot,
                        locations=new_locations,
                        keys_found=new_keys_found,
                    )
                )

            if len(new_keys_found) == vault_maps[0].number_of_keys:
                # Found all of the keys.
                work_queue.clear()
                break

    return new_number_of_steps
Ejemplo n.º 18
0
    def simulate_rescue(self):
        shortest_rescue_time = 0

        # Store active paths in heap.
        paths_heap = []
        heapq.heappush(paths_heap, (0, Coord(0,0), self.Tools.TORCH))

        # Store shortest time to arrive at this coord with a given tool.  We can
        # have one of two tools deployed in any location.  We keep track of the
        # fastest path to location arriving with each tool.  This effectively
        # makes them appear as two different locations but with the same
        # paths to the next location.
        visited = dict()
        visited[(Coord(0,0),self.Tools.TORCH)] = 0

        while paths_heap:
            (current_time, current_coord, current_tool) = heapq.heappop(paths_heap)
            current_data = self.get_map_data(current_coord)

            if current_coord == self._target_coord:
                arrival_time = current_time

                # Need to swap to torch to see target.
                if current_tool != self.Tools.TORCH:
                    current_time += 7

                if shortest_rescue_time == 0:
                    shortest_rescue_time = current_time
                else:
                    shortest_rescue_time = min(shortest_rescue_time, current_time)

                # We are not guaranteed to be done the first time we get here because
                # if we swapped the torch, there may be other shorter paths that arrive up to
                # 6 seconds later that don't need to swap the torch.
                #
                # We will continue until the arrival time is greater or equal to the fastest
                # rescue time.
                if arrival_time >= shortest_rescue_time:
                    # We are done.  Remove all paths to terminate simulation loop.
                    paths_heap = []

                continue


            for increment in (Coord(0,-1), Coord(-1,0), Coord(1, 0), Coord(0, 1)):
                next_coord = aoc.add_coords(current_coord, increment)
                if next_coord.x_val < 0 or next_coord.y_val < 0:
                    continue

                next_data = self.get_map_data(next_coord)
                next_allowed_tools = self.allowed_tools[next_data.erosion_type]
                if current_tool in next_allowed_tools:
                    # Proceed with current tool.
                    next_tool = current_tool
                    next_time = current_time + 1
                else:
                    # Swap tool and proceed.
                    next_tool = self.other_allowed_tool[(current_data.erosion_type, current_tool)]
                    next_time = current_time + 7 + 1

                visited_time = visited.get((next_coord,next_tool))
                if visited_time is not None and visited_time <= next_time:
                    # This route is now longer then the shortest path we have
                    # found to next_coord.  Abandon it.
                    continue

                visited[next_coord,next_tool] = next_time

                heapq.heappush(paths_heap, (next_time, next_coord, next_tool))

        return shortest_rescue_time
Ejemplo n.º 19
0
 def test_add_coord(self):
     self.assertEqual(aoc.Coord(22, 5),
                      aoc.add_coords(self.coord1, self.coord2))
     self.assertEqual(aoc.Coord(23, 48),
                      aoc.add_coords(self.coord1, self.coord2, self.coord3))