Advertisement
alexandrajay2002

Advent of Code 2024 day 15 part 1

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