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]
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
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
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
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
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
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
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.
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
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.
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)
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
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
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
def move_forward(self): self._location = aoc.add_coords(self._location, self._direction)
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
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
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
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))