Advertisement
alexandrajay2002

Advent of Code 2024 day 15 part 2

Dec 15th, 2024
58
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 5.05 KB | Source Code | 0 0
  1. from enum import Enum
  2. from argparse import ArgumentParser, FileType
  3.  
  4.  
  5. class Direction(Enum):
  6.     '''A cardinal direction.'''
  7.     UP = (0, -1)
  8.     LEFT = (1, 0)
  9.     DOWN = (0, 1)
  10.     RIGHT = (-1, 0)
  11.  
  12.     @classmethod
  13.     def from_char(cls, char):
  14.         '''Construct a direction from its corresponding character.'''
  15.         if char == '^':
  16.             return cls.UP
  17.         elif char == '>':
  18.             return cls.LEFT
  19.         elif char == 'v':
  20.             return cls.DOWN
  21.         elif char == '<':
  22.             return cls.RIGHT
  23.         else:
  24.             raise ValueError(f'invalid direction: "{char}"')
  25.  
  26.     def move(self, x, y):
  27.         '''Translate the coordinates a distance of 1 in this direction.'''
  28.         dx, dy = self.value
  29.         return x + dx, y + dy
  30.  
  31.  
  32. def parse_room(src):
  33.     '''Parse the room portion of the input into the room dict and the starting
  34.       coordinates of the robot.'''
  35.     lines = src.split('\n')
  36.     robot = None
  37.     room = dict()
  38.  
  39.     for y, line in enumerate(lines):
  40.         x = 0
  41.         for char in line:
  42.             if char == '.':
  43.                 pass
  44.             elif char == '@':
  45.                 robot = (x, y)
  46.             elif char == '#':  # unpushable tiles store None
  47.                 room[(x, y)] = None
  48.                 room[(x + 1, y)] = None
  49.             elif char == 'O':  # pushable tiles store their pair
  50.                 room[(x, y)] = (x + 1, y)
  51.                 room[(x + 1, y)] = (x, y)
  52.             else:
  53.                 raise ValueError(f'invalid room item: "{char}"')
  54.  
  55.             x += 2
  56.  
  57.     assert robot is not None
  58.  
  59.     return robot, room
  60.  
  61.  
  62. def parse(src):
  63.     '''Parse the input into the room dict, the starting coordinates of the
  64.       robot, and an iterable of Directions.'''
  65.     room, moves = src.split('\n\n', 1)
  66.     return (*parse_room(room),
  67.             map(Direction.from_char, moves.replace('\n', '')))
  68.  
  69.  
  70. def print_room(robot, room):
  71.     '''Output a string representation of the room and robot to stdout.'''
  72.     width = max(room.keys(), key=lambda item: item[0])[0] + 1
  73.     height = max(room.keys(), key=lambda item: item[1])[1] + 1
  74.  
  75.     for y in range(height):
  76.         for x in range(width):
  77.             if (x, y) in room:
  78.                 if room[(x, y)] is None:
  79.                     print('#', end='')
  80.                 elif room[(x, y)][0] > x:
  81.                     print('[', end='')
  82.                 else:
  83.                     print(']', end='')
  84.             elif (x, y) == robot:
  85.                 print('@', end='')
  86.             else:
  87.                 print('.', end='')
  88.         print()
  89.     print()
  90.  
  91.  
  92. def wall(x, y, room):
  93.     '''Return true if there is an immovable object at position x, y.'''
  94.     return (x, y) in room and room[(x, y)] is None
  95.  
  96.  
  97. def left_box(x, y, room):
  98.     '''Return true if position x, y contains the left side of a box.'''
  99.     return (x, y) in room and room[(x, y)] is not None and room[(x, y)][0] > x
  100.  
  101.  
  102. def get_boxes(nx, ny, direction, room):
  103.     '''Construct the dict of boxes that would be affected if the box at
  104.       position x, y where pushed. None is returned if a wall is encountered,
  105.       indicating that x, y cannot be pushed.'''
  106.     boxes = dict()
  107.     todo = {(nx, ny), }
  108.     while len(todo) > 0:
  109.         x, y = todo.pop()
  110.  
  111.         if wall(x, y, room):  # there's a wall so the boxes can't be moved
  112.             return None
  113.  
  114.         if (x, y) not in room:
  115.             continue
  116.  
  117.         pair = room[(x, y)]
  118.         boxes[(x, y)] = pair
  119.  
  120.         if pair not in boxes:
  121.             todo.add(pair)
  122.  
  123.         todo.add(direction.move(x, y))
  124.  
  125.     return boxes
  126.  
  127.  
  128. def move(robot, room, direction):
  129.     '''Move the robot in direction, potentially pushing some boxes and updating
  130.       the room.'''
  131.     nx, ny = direction.move(*robot)
  132.  
  133.     if (nx, ny) not in room:  # free to move
  134.         return (nx, ny)
  135.  
  136.     if not room[(nx, ny)]:  # blockage, can't do anything
  137.         return robot
  138.  
  139.     boxes = get_boxes(nx, ny, direction, room)
  140.     if boxes is not None:  # if the next space is free, move them
  141.         robot = direction.move(*robot)
  142.         new_boxes = {direction.move(*box): direction.move(*pair)
  143.                      for box, pair in boxes.items()}
  144.  
  145.         for old_box in boxes:
  146.             del room[old_box]
  147.  
  148.         room |= new_boxes
  149.  
  150.     return robot
  151.  
  152.  
  153. def gps_coords(x, y):
  154.     '''Convert x, y to gps coordinates.'''
  155.     return x + 100 * y
  156.  
  157.  
  158. def main(robot, room, moves, verbose=False):
  159.     '''Perform each movement and return the sum of the gps coordinates of each
  160.       box.'''
  161.     if verbose:
  162.         print_room(robot, room)
  163.  
  164.     for direction in moves:
  165.         robot = move(robot, room, direction)
  166.  
  167.         if verbose:
  168.             print_room(robot, room)
  169.  
  170.     return sum(gps_coords(*xy) for xy in room if left_box(*xy, room))
  171.  
  172.  
  173. arg_parser = ArgumentParser()
  174. arg_parser.add_argument('src', type=FileType('r'))
  175. arg_parser.add_argument('-v', '--verbose', action='store_true')
  176.  
  177. if __name__ == '__main__':
  178.     args = arg_parser.parse_args()
  179.     print(main(*parse(args.src.read()), args.verbose))
  180.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement