コード例 #1
0
ファイル: a.py プロジェクト: baldrick/aoc
 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
コード例 #2
0
    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))
コード例 #3
0
ファイル: test_aoc.py プロジェクト: zhatt/adventofcode2018
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))
コード例 #4
0
 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
コード例 #5
0
ファイル: test_day10.py プロジェクト: zhatt/adventofcode2019
    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)
コード例 #6
0
ファイル: day18.py プロジェクト: zhatt/adventofcode2019
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
コード例 #7
0
ファイル: day20.py プロジェクト: zhatt/adventofcode2019
    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)
コード例 #8
0
 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
コード例 #9
0
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
コード例 #10
0
ファイル: test_aoc.py プロジェクト: zhatt/adventofcode2019
 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)))
コード例 #11
0
    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)
コード例 #12
0
ファイル: test_day11.py プロジェクト: zhatt/adventofcode2018
    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 )
コード例 #13
0
ファイル: test_aoc.py プロジェクト: zhatt/adventofcode2019
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)))
コード例 #14
0
 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
コード例 #15
0
ファイル: day17.py プロジェクト: zhatt/adventofcode2019
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
コード例 #16
0
ファイル: day17.py プロジェクト: zhatt/adventofcode2019
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
コード例 #17
0
    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
コード例 #18
0
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
コード例 #19
0
    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
コード例 #20
0
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
コード例 #21
0
    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
コード例 #22
0
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
コード例 #23
0
ファイル: a.py プロジェクト: baldrick/aoc
 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))
コード例 #24
0
    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)
コード例 #25
0
ファイル: day18.py プロジェクト: zhatt/adventofcode2019
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)
コード例 #26
0
ファイル: a.py プロジェクト: baldrick/aoc
 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
コード例 #27
0
    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
コード例 #28
0
ファイル: day20.py プロジェクト: zhatt/adventofcode2019
    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
コード例 #29
0
ファイル: day18.py プロジェクト: zhatt/adventofcode2019
        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
コード例 #30
0
ファイル: day20.py プロジェクト: zhatt/adventofcode2019
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.