Advertisement
illuminati229

AoC 2023 Day 16

Dec 17th, 2023
1,128
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 7.05 KB | None | 0 0
  1. from time import time
  2.  
  3.  
  4. def timer_func(func):
  5.     # This function shows the execution time of
  6.     # the function object passed
  7.     def wrap_func(*args, **kwargs):
  8.         t1 = time()
  9.         result = func(*args, **kwargs)
  10.         t2 = time()
  11.         print(f'Function {func.__name__!r} executed in {(t2 - t1):.4f}s')
  12.         return result
  13.  
  14.     return wrap_func
  15.  
  16.  
  17. class Grid2d:
  18.  
  19.     def __init__(self, grid):
  20.         self.grid = grid
  21.         self.height = len(grid)
  22.         self.width = len(grid[0])
  23.  
  24.     def __getitem__(self, item):
  25.         # returns None if outside the grid
  26.         if len(item) > 2:
  27.             raise ValueError('Grid2D expected a list like of length 2. Length of accessor longer than expected')
  28.         x, y = item
  29.         if 0 <= x < self.height:
  30.             if 0 <= y < self.width:
  31.                 return self.grid[x][y]
  32.         return None
  33.  
  34.  
  35. class LaserBeam:
  36.  
  37.     def __init__(self, pos, heading):
  38.         self.pos = pos
  39.         self.heading = heading
  40.  
  41.     def next_loc(self):
  42.         x, y = self.pos
  43.         h = self.heading
  44.         if h == 'u':
  45.             return x - 1, y
  46.         elif h == 'd':
  47.             return x + 1, y
  48.         elif h == 'l':
  49.             return x, y - 1
  50.         elif h == 'r':
  51.             return x, y + 1
  52.  
  53.  
  54. class BeamMap:
  55.  
  56.     def __init__(self, grid, start=((0, -1), 'r')):
  57.         self.grid = Grid2d(grid)
  58.         self.beams = [LaserBeam(*start)]
  59.         self.energized = set()  # set of coordinates (tuple) that are energized (x, y)
  60.         self.beam_tracking = set()  # set of coordinates and heading of locations beams have gone through ((x, y), d)
  61.         self.BOUNCE_DICT = {'l': {'/': ('d', None),
  62.                                   '\\': ('u', None),
  63.                                   '-': ('l', None),
  64.                                   '|': ('u', 'd'),
  65.                                   '.': ('l', None)},
  66.                             'r': {'/': ('u', None),
  67.                                   '\\': ('d', None),
  68.                                   '-': ('r', None),
  69.                                   '|': ('u', 'd'),
  70.                                   '.': ('r', None)},
  71.                             'u': {'/': ('r', None),
  72.                                   '\\': ('l', None),
  73.                                   '-': ('l', 'r'),
  74.                                   '|': ('u', None),
  75.                                   '.': ('u', None)},
  76.                             'd': {'/': ('l', None),
  77.                                   '\\': ('r', None),
  78.                                   '-': ('l', 'r'),
  79.                                   '|': ('d', None),
  80.                                   '.': ('d', None)}}
  81.  
  82.     def project_beams(self):
  83.         # list for new beams that form off splitters
  84.         new_beams = []
  85.         # list for beams the leave the grid or go into a loop
  86.         beams_left_grid = []
  87.         while self.beams:
  88.             # clear the new beams and deleted beams list
  89.             new_beams.clear()
  90.             beams_left_grid.clear()
  91.             # loop over the current beams
  92.             for i, beam in enumerate(self.beams):
  93.                 # get the next location for the beam
  94.                 nl = beam.next_loc()
  95.                 # get the value of the grid
  96.                 ng = self.grid[nl]
  97.                 # if the next point is in the grid
  98.                 if ng:
  99.                     # if the next point is a mirror or splitter, not empty space
  100.                     if ng != '.':
  101.                         # update the beam position
  102.                         beam.pos = nl
  103.                         # add to the energized set
  104.                         self.energized.add(nl)
  105.                         # find the new heading(s)
  106.                         beam.heading, split_b = self.BOUNCE_DICT[beam.heading][ng]
  107.                         # check to see if the current beam heading has been seen before
  108.                         if self.beam_tracking.intersection([(nl, beam.heading)]):
  109.                             # beam has been seen before and will start looping, add the delete list
  110.                             beams_left_grid.append(i)
  111.                         else:
  112.                             # beam hasn't been seen before, add the record to the tracking set
  113.                             self.beam_tracking.add((nl, beam.heading))
  114.                         # if a split exists, and it isn't in the beam tracking set
  115.                         if split_b and not self.beam_tracking.intersection([(nl, split_b)]):
  116.                             # add the new beam to the new beam list
  117.                             new_beams.append(LaserBeam(nl, split_b))
  118.                             # add the new beam to the beam tracking set
  119.                             self.beam_tracking.add((nl, split_b))
  120.                     # beam position is empty space
  121.                     else:
  122.                         # update beam position
  123.                         beam.pos = nl
  124.                         # add to energized list
  125.                         self.energized.add(nl)
  126.                         # add to beam tracking list
  127.                         self.beam_tracking.add((nl, beam.heading))
  128.                 # point is outside the grid
  129.                 else:
  130.                     # add to delete list
  131.                     beams_left_grid.append(i)
  132.             # deleting beams that moved outside the grid or got onto a loop
  133.             if beams_left_grid:
  134.                 for i in beams_left_grid[::-1]:  # starting at the end to not mess up indexing
  135.                     self.beams.pop(i)
  136.             # adding the new beams to the list
  137.             self.beams += new_beams
  138.  
  139.     def count_energized(self):
  140.         return len(self.energized)
  141.  
  142.     def new_start(self, start):
  143.         self.energized.clear()
  144.         self.beam_tracking.clear()
  145.         self.beams.clear()
  146.         self.beams.append(LaserBeam(*start))
  147.  
  148.  
  149. @timer_func
  150. def day16(filepath, part2=False):
  151.     with open(filepath) as fin:
  152.         lines = [line.strip() for line in fin.readlines()]
  153.  
  154.     beam_map = BeamMap(lines)
  155.     beam_map.project_beams()
  156.     if not part2:
  157.         return beam_map.count_energized()
  158.     else:
  159.         max_e = beam_map.count_energized()
  160.         for d, y in [['r', -1], ['l', len(lines[0])]]:
  161.             for x in range(len(lines)):
  162.                 if x == 0 and d == 'r':
  163.                     continue
  164.                 beam_map.new_start(((x, y), d))
  165.                 beam_map.project_beams()
  166.                 e_c = beam_map.count_energized()
  167.                 if e_c > max_e:
  168.                     max_e = e_c
  169.         for d, x in [['u', len(lines)], ['d', -1]]:
  170.             for y in range(len(lines[0])):
  171.                 beam_map.new_start(((x, y), d))
  172.                 beam_map.project_beams()
  173.                 e_c = beam_map.count_energized()
  174.                 if e_c > max_e:
  175.                     max_e = e_c
  176.         return max_e
  177.  
  178.  
  179. def main():
  180.     assert day16('test16') == 46
  181.     print(f"Part 1: {day16('input16')}")
  182.  
  183.     assert day16('test16', True) == 51
  184.     print(f"Part 2: {day16('input16', True)}")
  185.  
  186.  
  187. if __name__ == '__main__':
  188.     main()
  189.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement