def fold(self, index): f = self.folds[index].split() c = f[2].split('=') axis = c[0] xy = int(c[1]) foldedCoords = aoc.SetOfCoords( f"After fold {index} - {self.folds[index]}") if axis == 'x': #print(f"fold on x at {xy}") for coord in self.coords.coords: if coord.col == xy: print(f"Error, folding along a line with a #: x={xy}") return if coord.col < xy: foldedCoords.add(coord) else: #print(f"folding {coord.xy()} at x{xy} to {xy - (coord.col - xy)}, {coord.row}") foldedCoords.add( aoc.Coord(coord.row, xy - (coord.col - xy))) else: #print(f"fold on y at {xy}") for coord in self.coords.coords: if coord.row == xy: print(f"Error, folding along a line with a #: y={xy}") return if coord.row < xy: foldedCoords.add(coord) else: #print(f"folding {coord.xy()} at y{xy} to {coord.col}, {xy - (coord.row - xy)}") foldedCoords.add( aoc.Coord(xy - (coord.row - xy), coord.col)) self.coords = foldedCoords
def simulate(self): new_map = set() if self.recursive: # In recursive mode we gain two levels each minute min_level = self.map_data.min_level - 1 max_level = self.map_data.max_level + 1 else: min_level = self.map_data.min_level max_level = self.map_data.max_level for level in range(min_level, max_level + 1): for y_index in range(self.LEVEL_SIZE_Y): for x_index in range(self.LEVEL_SIZE_X): coord = aoc.Coord(x_index, y_index) # Skip the center square in recursive mode. if self.recursive and coord == aoc.Coord(2, 2): continue adjacent_count = self._count_adjacent(level, coord) if (level, coord) in self.map_data.map: # Bug with exactly one bug adjacent gets to live. if adjacent_count == 1: new_map.add((level, coord)) else: # Empty square with 1 or 2 bugs adjacent gets infested. if adjacent_count in (1, 2): new_map.add((level, coord)) self.map_data = self.map_data._replace(min_level=min_level, max_level=max_level, map=frozenset(new_map))
class TestAoc(unittest.TestCase): coord1 = aoc.Coord(1, 3) coord2 = aoc.Coord(21, 2) coord3 = aoc.Coord(1, 43) 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)) def test_min_bound_coord(self): self.assertEqual(aoc.Coord(1, 2), aoc.min_bound_coord(self.coord1, self.coord2)) self.assertEqual( aoc.Coord(1, 2), aoc.min_bound_coord(self.coord1, self.coord2, self.coord3)) def test_min_bound_coord(self): self.assertEqual(aoc.Coord(1, 43), aoc.max_bound_coord(self.coord1, self.coord3)) self.assertEqual( aoc.Coord(21, 43), aoc.max_bound_coord(self.coord1, self.coord2, self.coord3))
def __init__(self, int_code, ship, start_color=0): self._input_stream = [start_color] self._output_stream = [] self._computer = int_code_computer.IntCodeComputer(int_code, self._input_stream, self._output_stream) self._direction = aoc.Coord(0, 1) self._location = aoc.Coord(0, 0) self._ship = ship self._paint_used = 0
def test_parse_input(self): input_list = [ ".#.", "...", "# #", ] expected_map = {aoc.Coord(1, 0), aoc.Coord(0, 2), aoc.Coord(2, 2)} asteroid_map = day10.parse_input(input_list) self.assertEqual(expected_map, asteroid_map)
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 __init__(self, input_list, maze_type): self._maze_type = maze_type self._entrance = None self._exit = None self._upper_left_coord = aoc.Coord(2, 2) self._lower_right_coord = aoc.Coord( len(input_list[0]) - 3, len(input_list) - 3) self._maze = {} self._portals = defaultdict(list) self._parse_input(input_list)
def flashIfReady(self, flashes): row = 0 while row < len(self.grid): col = 0 while col < self.grid[row].len(): if flashes.contains(aoc.Coord(row, col)): #print(f"{Coord(row,col)} already flashed") col += 1 continue if self.grid[row].row[col] > 9: #print(f"Flashing {Coord(row,col)}") self.flash(row, col) flashes.add(aoc.Coord(row, col)) col += 1 row += 1
def part1(input_list): int_code = int_code_computer.IntCodeComputer.parse_input(input_list[0]) computer = Computer(int_code) area = 0 # Walk the area. This could be optimized like part 2 but run time is okay and this makes # is easier to print the beam shape. for y_val in range(50): for x_val in range(50): in_beam = computer.check_point(aoc.Coord(x_val, y_val)) if VERBOSE: if in_beam: print("#", end='') else: print(".", end='') if in_beam: area += 1 if VERBOSE: print() return area
def test_coord_relation(self): self.assertEqual( 0, aoc.coord_relation_negy(aoc.Coord(1, 2), aoc.Coord(1, 2))) self.assertEqual( 1, aoc.coord_relation_negy(aoc.Coord(1, 2), aoc.Coord(2, 2))) self.assertEqual( -1, aoc.coord_relation_negy(aoc.Coord(10, 2), aoc.Coord(2, 2))) self.assertEqual( 1, aoc.coord_relation_negy(aoc.Coord(-10, -10), aoc.Coord(-2, -2)))
def __init__(self, int_code): self._location = aoc.Coord(0, 0) self._input_stream = [] self._output_stream = [] self.icc = int_code_computer.IntCodeComputer(int_code, self._input_stream, self._output_stream)
def test_calc_power_delta_y( self ): power1 = day11.calc_power_box( self.grid, aoc.Coord( 0, 0 ), 2 ) expect = day11.calc_power_box( self.grid, aoc.Coord( 0, 1 ), 2 ) power = day11.calc_power_delta_y( power1, self.grid, aoc.Coord( 0, 1 ), 2 ) self.assertEqual( power, expect ) power1 = day11.calc_power_box( self.grid, aoc.Coord( 0, 1 ), 2 ) expect = day11.calc_power_box( self.grid, aoc.Coord( 0, 2 ), 2 ) power = day11.calc_power_delta_y( power1, self.grid, aoc.Coord( 0, 2 ), 2 ) self.assertEqual( power, expect ) power1 = day11.calc_power_box( self.grid, aoc.Coord( 1, 0 ), 3 ) expect = day11.calc_power_box( self.grid, aoc.Coord( 1, 1 ), 3 ) power = day11.calc_power_delta_y( power1, self.grid, aoc.Coord( 1, 1 ), 3 ) self.assertEqual( power, expect )
class TestAoc(unittest.TestCase): coord1 = aoc.Coord(1, 3) coord2 = aoc.Coord(21, 2) coord3 = aoc.Coord(1, 43) 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)) def test_distance_manhattan(self): self.assertEqual( 20 + 1, aoc.distance_manhattan_coords(self.coord1, self.coord2)) def test_min_bound_coord(self): self.assertEqual(aoc.Coord(1, 2), aoc.min_bound_coord(self.coord1, self.coord2)) self.assertEqual(aoc.Coord(1, 2), aoc.min_bound_coord(self.coord1, self.coord2, \ self.coord3)) def test_max_bound_coord(self): self.assertEqual(aoc.Coord(1, 43), aoc.max_bound_coord(self.coord1, self.coord3)) self.assertEqual(aoc.Coord(21, 43), aoc.max_bound_coord(self.coord1, self.coord2, \ self.coord3)) def test_slope(self): self.assertEqual(Fraction(2, -1), aoc.slope_negy(aoc.Coord(0, 0), aoc.Coord(4, 8))) self.assertEqual(Fraction(8, 3), aoc.slope_negy(aoc.Coord(0, 0), aoc.Coord(3, -8))) def test_coord_relation(self): self.assertEqual( 0, aoc.coord_relation_negy(aoc.Coord(1, 2), aoc.Coord(1, 2))) self.assertEqual( 1, aoc.coord_relation_negy(aoc.Coord(1, 2), aoc.Coord(2, 2))) self.assertEqual( -1, aoc.coord_relation_negy(aoc.Coord(10, 2), aoc.Coord(2, 2))) self.assertEqual( 1, aoc.coord_relation_negy(aoc.Coord(-10, -10), aoc.Coord(-2, -2)))
def flash(self, row, col): # Increment energy of surrounding octopuses dr = -1 while dr <= 1: dc = -1 while dc <= 1: if self.inGrid(aoc.Coord(row, col), dc, dr): #print(f"Inc energy at {Coord(row,col)}") self.grid[row+dr].row[col+dc] += 1 dc += 1 dr += 1
def make_scaffolding_map(camera_output): # Returns a map of tile contents indexed by Coord and the robot location. x_val = 0 y_val = 0 scaffolding_map = {} robot_location = None for map_tile in camera_output: if map_tile in "><^v": robot_location = aoc.Coord(x_val, y_val) if map_tile == "\n": y_val += 1 x_val = 0 else: scaffolding_map[aoc.Coord(x_val, y_val)] = map_tile x_val += 1 return scaffolding_map, robot_location
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 calculate_biodiversity(self, level): rating = 0 points = 1 for y_index in range(self.LEVEL_SIZE_Y): for x_index in range(self.LEVEL_SIZE_X): coord = aoc.Coord(x_index, y_index) if (level, coord) in self.map_data.map: rating += points points *= 2 return rating
class Droid: class InputCommand(Enum): NORTH = 1 SOUTH = 2 WEST = 3 EAST = 4 increment = { InputCommand.NORTH: aoc.Coord(0, 1), InputCommand.SOUTH: aoc.Coord(0, -1), InputCommand.WEST: aoc.Coord(-1, 0), InputCommand.EAST: aoc.Coord(1, 0), } class StatusCode(Enum): HIT_WALL = 0 MOVE_COMPLETED = 1 OXYGEN_LOCATION = 2 def __init__(self, int_code): self._location = aoc.Coord(0, 0) self._input_stream = [] self._output_stream = [] self.icc = int_code_computer.IntCodeComputer(int_code, self._input_stream, self._output_stream) 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 get_location(self): return self._location
def get_map_string(self, level): string = "" for y_index in range(self.LEVEL_SIZE_Y): for x_index in range(self.LEVEL_SIZE_X): coord = aoc.Coord(x_index, y_index) if (level, coord) in self.map_data.map: string += "#" else: string += "." string += "\n" return string
def find_oxygen_system_and_draw_map(input_list): int_code = int_code_computer.IntCodeComputer.parse_input(input_list[0]) # Do a breadth first search of all paths to the oxygen system. The work queue will # contain Droid objects that each with a current path it has walked. This probably is more # space overhead than just keeping track of where it is but that way we don't need to resimulate # the full path each time we take a previous path out of the queue. # # locations_visited is used to keep track of previously visited coordinates. Since we are # doing a breadth first search a previous visit to the coordinate means the previous path is # shorter (or equal) in length than the current path so we don't need to continue simulating # the current path. # # Prime the work queue with the starting position which is arbitrarily located at the origin. # The actual location doesn't really matter since all moves are relative. # # We also need to generate a map so instead of ending the search when we find the oxygen # system, we continue until all paths are taken. The locations_visited is actually also # a map as it contains all of the open spaces. Any coordinate not in the map is a wall. work_queue = deque() work_queue.append((Droid(int_code.copy()), 0)) locations_visited = {aoc.Coord(0, 0)} oxygen_system_location = None number_of_steps_to_oxygen_system = None while work_queue: droid, num_steps = work_queue.popleft() for command in Droid.InputCommand: new_droid = copy.deepcopy(droid) status = new_droid.run(command) if status == Droid.StatusCode.OXYGEN_LOCATION and number_of_steps_to_oxygen_system is \ None: oxygen_system_location = new_droid.get_location() number_of_steps_to_oxygen_system = num_steps + 1 locations_visited.add(new_droid.get_location()) elif status == Droid.StatusCode.MOVE_COMPLETED: if new_droid.get_location() not in locations_visited: work_queue.append((new_droid, num_steps + 1)) locations_visited.add(new_droid.get_location()) else: # Else we got here via a shorter path so we can prune this path. pass else: # We hit a wall so we can prune this path. pass return number_of_steps_to_oxygen_system, oxygen_system_location, locations_visited
def get_hull_pattern(self): min_coord = aoc.min_bound_coord(*list(self._hull.keys())) max_coord = aoc.max_bound_coord(*list(self._hull.keys())) output_string = "" for y_index in range(max_coord.y_val, min_coord.y_val - 1, -1): for x_index in range(min_coord.x_val, max_coord.x_val + 1): if self.get_color(aoc.Coord(x_index, y_index)) == 1: output_string += "*" else: output_string += ' ' output_string += "\n" return output_string
def find_tip_of_tractor_beam(int_code): """ :param int_code: :return: aoc.Coord coordinate of tip of tractor region. """ computer = Computer(int_code) # We probably shouldn't see this case. My input doesn't hit this and I expect there is always # a gap between the origin and the tip but just in case. if computer.check_point(aoc.Coord(1, 1)) or \ computer.check_point(aoc.Coord(0, 1)) or \ computer.check_point(aoc.Coord(1, 0)): return aoc.Coord(0, 0) for y_val in range(1, 11): for x_val in range(1, 11): coord = aoc.Coord(x_val, y_val) if computer.check_point(coord): return coord # If we get here, we to rework the algorithm because the tip is not for 10 by 10 region. assert False return None
def __init__(self, lines): self.folds = [] self.coords = aoc.SetOfCoords("start") folds = False for line in lines: if folds: self.folds.append(line) else: if line == "": folds = True continue xy = line.split(',') x = int(xy[0]) y = int(xy[1]) self.coords.add(aoc.Coord(y, x))
def test_turning(self): # pylint: disable=protected-access bot = day11.PaintingRobot([], None) self.assertEqual(aoc.Coord(0, 1), bot._direction) bot.turn_left_and_move() self.assertEqual(aoc.Coord(-1, 0), bot._direction) bot.turn_left_and_move() self.assertEqual(aoc.Coord(0, -1), bot._direction) bot.turn_left_and_move() self.assertEqual(aoc.Coord(1, 0), bot._direction) bot.turn_left_and_move() self.assertEqual(aoc.Coord(0, 1), bot._direction) bot.turn_right_and_move() self.assertEqual(aoc.Coord(1, 0), bot._direction) bot.turn_right_and_move() self.assertEqual(aoc.Coord(0, -1), bot._direction) bot.turn_right_and_move() self.assertEqual(aoc.Coord(-1, 0), bot._direction) bot.turn_right_and_move() self.assertEqual(aoc.Coord(0, 1), bot._direction)
def parse_input(input_list): entrance = None number_of_keys = 0 vault_map = {} for y_loc, line in enumerate(input_list): for x_loc, tile in enumerate(line): coord = aoc.Coord(x_loc, y_loc) vault_map[coord] = tile if tile == '@': entrance = coord if tile.islower(): number_of_keys += 1 return VaultMap(vault_map, entrance, number_of_keys)
def __repr__(self): xmax = 0 ymax = 0 for c in self.coords.coords: if c.row > ymax: ymax = c.row if c.col > xmax: xmax = c.col s = f"{self.coords.name}\n" for y in range(0, ymax + 1): s += f"{y}: " for x in range(0, xmax + 1): if self.coords.contains(aoc.Coord(y, x)): s += '#' else: s += ' ' s += '\n' s += f"Num dots: {self.coords.size()}\n" return s
def __init__(self, input_list, recursive=False): MapData = namedtuple('MapData', ['min_level', 'max_level', 'map']) """ Parse input to a set of coordinate locations. Only locations with a bugs will be put in the map. """ data = set() x_loc = 0 # for pylint y_loc = 0 for y_loc, row in enumerate(input_list): for x_loc, value in enumerate(row): if value == '#': data.add((0, aoc.Coord(x_loc, y_loc))) assert x_loc == self.LEVEL_SIZE_X - 1 assert y_loc == self.LEVEL_SIZE_Y - 1 self.map_data = MapData(min_level=0, max_level=0, map=frozenset(data)) self.recursive = recursive
def _parse_maze(self, input_list): # Parse the input to the maze. Save the location and names of portals for pairing. portal_tiles = {} for y_val, line in enumerate(input_list): for x_val, tile in enumerate(line): coord = aoc.Coord(x_val, y_val) if tile in "#.": self._maze[coord] = tile elif tile == " ": # Don't store. pass elif tile.isupper(): portal_tiles[coord] = tile else: assert False return portal_tiles
for x_loc, tile in enumerate(line): coord = aoc.Coord(x_loc, y_loc) vault_map[coord] = tile if tile == '@': entrance = coord if tile.islower(): number_of_keys += 1 return VaultMap(vault_map, entrance, number_of_keys) Visited = namedtuple("Visited", ["location", "keys_found"]) INCREMENT = {aoc.Coord(0, 1), aoc.Coord(0, -1), aoc.Coord(-1, 0), aoc.Coord(1, 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
class Maze: # pylint: disable=too-few-public-methods INCREMENT = { aoc.Coord(0, 1), aoc.Coord(0, -1), aoc.Coord(-1, 0), aoc.Coord(1, 0) } Location = namedtuple('Location', ['level', 'coord']) class Type(Enum): PLAIN = auto() RECURSIVE = auto() def __init__(self, input_list, maze_type): self._maze_type = maze_type self._entrance = None self._exit = None self._upper_left_coord = aoc.Coord(2, 2) self._lower_right_coord = aoc.Coord( len(input_list[0]) - 3, len(input_list) - 3) self._maze = {} self._portals = defaultdict(list) self._parse_input(input_list) def _is_inside_portal(self, coord): return self._upper_left_coord.x_val < coord.x_val < self._lower_right_coord.x_val and \ self._upper_left_coord.y_val < coord.y_val < self._lower_right_coord.y_val def _is_outside_portal(self, coord): return not self._is_inside_portal(coord) def _find_portal_exit(self, portal, entrance_location): exit_location = None portal_info_list = self._portals[portal] for portal_info in portal_info_list: if portal_info[0] != entrance_location.coord: exit_location = self.Location(level=entrance_location.level, coord=portal_info[1]) break if self._maze_type == self.Type.RECURSIVE: if self._is_inside_portal(entrance_location.coord): exit_location = exit_location._replace( level=exit_location.level + 1) else: exit_location = exit_location._replace( level=exit_location.level - 1) return exit_location def _parse_input(self, input_list): portal_tiles = self._parse_maze(input_list) self._parse_portals(portal_tiles) def _parse_maze(self, input_list): # Parse the input to the maze. Save the location and names of portals for pairing. portal_tiles = {} for y_val, line in enumerate(input_list): for x_val, tile in enumerate(line): coord = aoc.Coord(x_val, y_val) if tile in "#.": self._maze[coord] = tile elif tile == " ": # Don't store. pass elif tile.isupper(): portal_tiles[coord] = tile else: assert False return portal_tiles 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 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.