question: --- Day 18: RAM Run ---
  
  You and The Historians look a lot more pixelated than you remember. You're inside a computer at the North Pole!
  
  Just as you're about to check out your surroundings, a program runs up to you. "This region of memory isn't safe! The User misunderstood what a pushdown automaton is and their algorithm is pushing whole bytes down on top of us! Run!"
  
  The algorithm is fast - it's going to cause a byte to fall into your memory space once every nanosecond! Fortunately, you're faster, and by quickly scanning the algorithm, you create a list of which bytes will fall (your puzzle input) in the order they'll land in your memory space.
  
  Your memory space is a two-dimensional grid with coordinates that range from 0 to 70 both horizontally and vertically. However, for the sake of example, suppose you're on a smaller grid with coordinates that range from 0 to 6 and the following list of incoming byte positions:
  
  5,4
  4,2
  4,5
  3,0
  2,1
  6,3
  2,4
  1,5
  0,6
  3,3
  2,6
  5,1
  1,2
  5,5
  2,5
  6,5
  1,4
  0,4
  6,4
  1,1
  6,1
  1,0
  0,5
  1,6
  2,0
  Each byte position is given as an X,Y coordinate, where X is the distance from the left edge of your memory space and Y is the distance from the top edge of your memory space.
  
  You and The Historians are currently in the top left corner of the memory space (at 0,0) and need to reach the exit in the bottom right corner (at 70,70 in your memory space, but at 6,6 in this example). You'll need to simulate the falling bytes to plan out where it will be safe to run; for now, simulate just the first few bytes falling into your memory space.
  
  As bytes fall into your memory space, they make that coordinate corrupted. Corrupted memory coordinates cannot be entered by you or The Historians, so you'll need to plan your route carefully. You also cannot leave the boundaries of the memory space; your only hope is to reach the exit.
  
  In the above example, if you were to draw the memory space after the first 12 bytes have fallen (using . for safe and # for corrupted), it would look like this:
  
  ...#...
  ..#..#.
  ....#..
  ...#..#
  ..#..#.
  .#..#..
  #.#....
  You can take steps up, down, left, or right. After just 12 bytes have corrupted locations in your memory space, the shortest path from the top left corner to the exit would take 22 steps. Here (marked with O) is one such path:
  
  OO.#OOO
  .O#OO#O
  .OOO#OO
  ...#OO#
  ..#OO#.
  .#.O#..
  #.#OOOO
  Simulate the first kilobyte (1024 bytes) falling onto your memory space. Afterward, what is the minimum number of steps needed to reach the exit?
solution: from collections import deque
  from typing import List, Tuple
  
  def parse_input(file_path: str) -> List[Tuple[int, int]]:
      """
      Parses the input file and returns a list of tuples containing integer pairs.
  
      Args:
          file_path (str): The path to the input file.
  
      Returns:
          List[Tuple[int, int]]: A list of tuples, where each tuple contains two integers
          parsed from a line in the input file. Each line in the file should contain two
          integers separated by a comma.
      """
      with open(file_path, 'r') as file:
          return [tuple(map(int, line.strip().split(','))) for line in file]
  
  def initialize_grid(size: int) -> List[List[str]]:
      """
      Initializes a square grid of the given size with all cells set to '.'.
  
      Args:
          size (int): The size of the grid (number of rows and columns).
  
      Returns:
          List[List[str]]: A 2D list representing the initialized grid.
      """
      return [['.' for _ in range(size)] for _ in range(size)]
  
  def simulate_falling_bytes(grid: List[List[str]], byte_positions: List[Tuple[int, int]], num_bytes: int):
      """
      Simulates the falling of bytes in a grid.
  
      Args:
          grid (List[List[str]]): A 2D list representing the grid where bytes will fall.
          byte_positions (List[Tuple[int, int]]): A list of tuples representing the (x, y) positions of bytes.
          num_bytes (int): The number of bytes to simulate falling.
  
      Returns:
          None
      """
      for x, y in byte_positions[:num_bytes]:
          grid[y][x] = '#'
  
  def find_shortest_path(grid: List[List[str]]) -> int:
      """
      Finds the shortest path in a grid from the top-left corner to the bottom-right corner.
      The grid is represented as a list of lists of strings, where '.' represents an open cell
      and any other character represents an obstacle.
      Args:
          grid (List[List[str]]): The grid to search, where each element is a string representing a cell.
      Returns:
          int: The number of steps in the shortest path from the top-left to the bottom-right corner.
               Returns -1 if no such path exists.
      """
      size = len(grid)
      directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
      queue = deque([(0, 0, 0)])  # (x, y, steps)
      visited = set([(0, 0)])
      
      while queue:
          x, y, steps = queue.popleft()
          
          if (x, y) == (size - 1, size - 1):
              return steps
          
          for dx, dy in directions:
              nx, ny = x + dx, y + dy
              if 0 <= nx < size and 0 <= ny < size and grid[ny][nx] == '.' and (nx, ny) not in visited:
                  visited.add((nx, ny))
                  queue.append((nx, ny, steps + 1))
      
      return -1  # No path found
  
  if __name__ == "__main__":
      file_path = 'input.txt'
      byte_positions = parse_input(file_path)
      grid_size = 71  # For the actual problem, use 71; for the example, use 7
      grid = initialize_grid(grid_size)
      simulate_falling_bytes(grid, byte_positions, 1024)
      steps = find_shortest_path(grid)
      print(f"Minimum number of steps needed to reach the exit: {steps}")
  