Advertisement
Guest User

Untitled

a guest
Dec 22nd, 2022
352
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 21.78 KB | Source Code | 0 0
  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. from matplotlib.animation import FuncAnimation
  4. from copy import deepcopy
  5.  
  6. with open('./data/01.txt', 'r') as f:
  7.     data = [line[:-1] for line in f.readlines()]
  8.  
  9. board = data[:-2]
  10. path = data[-2:]
  11.  
  12.  
  13. # convert the ragged lines to a numpy array
  14. def convert_to_grid(data: list[str]) -> np.ndarray:
  15.     lines = []
  16.     for line in data:
  17.         line = line.replace(' ', '-1,').replace('.', '0,').replace('#', '1,')
  18.         if line.endswith(','):
  19.             line = line[:-1]
  20.         try:
  21.             num_line = [int(i) for i in line.split(',')]
  22.         except ValueError:
  23.             print(line)
  24.             print(line.split(','))
  25.             return line
  26.         lines.append(num_line)
  27.  
  28.     max_length = max([len(i) for i in lines])
  29.  
  30.     arrays = []
  31.     for line in lines:
  32.         arr = np.array(line)
  33.         arr = np.pad(
  34.             arr,
  35.             (0, max_length - len(arr)),
  36.             'constant', constant_values=-1)
  37.         arrays.append(arr)
  38.     return np.array(arrays)
  39.  
  40.  
  41. # ------ PUZZLE 22-01 ------
  42. # interpret the pathing instructions
  43. def interpret_path(path: str) -> list:
  44.     # generate a usable pathing list
  45.     new_path = []
  46.     # split the path on all right-turning instructions
  47.     for i, txt in enumerate(path.split('R')):
  48.         try:
  49.             # if the part of the split path is converteable to an integer
  50.             # do that and append to new_path
  51.             txt = int(txt)
  52.             new_path.append(txt)
  53.             # unless we're at the last part of the list, append a
  54.             # turn right instruction to the new path to the replace
  55.             # the ones lost by the splitting on R
  56.             if i != len(path.split('R'))-1:
  57.                 # multiplying by i will turn a complex number ccw but due to
  58.                 # how the rest is set up, this is the correct multiplyer for
  59.                 # turning right
  60.                 new_path.append(1j)
  61.         except ValueError:
  62.             # if the part of the split can't be converted to an integer that
  63.             # means we have a turn left indicator somewhere in the split string
  64.             # again split on the turn left indicator and append the integers
  65.             # left to the new path
  66.             for j, txt2 in enumerate(txt.split('L')):
  67.                 new_path.append(int(txt2))
  68.                 # unless we're at the last part of the L splitting, append a
  69.                 # turn left identifier to the new path to make up for the lost
  70.                 # ones due to splitting
  71.                 if j != len(txt.split('L'))-1:
  72.                     # see above for logic of multiplying by -i
  73.                     new_path.append(-1j)
  74.             # see above
  75.             if i != len(path.split('R'))-1:
  76.                 new_path.append(1j)
  77.     return new_path
  78.  
  79.  
  80. grid = convert_to_grid(board)  # convert the board into a numpy array
  81.  
  82. # convert the pathing instructions into a list of steps and direction changes
  83. path = [line for line in path if line != ''][0]
  84. path = interpret_path(path)
  85.  
  86.  
  87. # generate a path of coordinates from the pathing instructions
  88. def follow_path(
  89.         grid: np.ndarray,
  90.         path: list,
  91.         start: complex = None,
  92.         vec: complex = None) -> list[list, complex]:
  93.     # unless an explicit starting position is given to the function
  94.     # start at the first walkable part in the top right of the board
  95.     if start is None:
  96.         pos = min(np.where(grid[0, :] == 0)[0]) + 0j
  97.     else:
  98.         pos = start
  99.     # list that holds the coordinates for the full path
  100.     path_list = [pos]
  101.     # facing vector, start with facing to the right unless a facing vector
  102.     # is given to the function
  103.     if vec is None:
  104.         vec = 1.+0.j
  105.     # run through pathing instructions
  106.     for p in path:
  107.         # if the instruction is an integer n, run a loop to walk
  108.         # n steps in the direction of the current facing
  109.         if type(p) == int:
  110.             for _ in range(p):
  111.                 # create temporary new position vector t
  112.                 t = pos + vec
  113.                 try:
  114.                     # if the temporary position t is not part of the board
  115.                     # modify t to wrap around to the other side
  116.                     if grid[int(t.imag), int(t.real)] == -1:
  117.                         # wrap around positive x
  118.                         if np.sign(vec.real) > 0:
  119.                             t = (
  120.                                 min(np.where(grid[int(pos.imag), :] > -1)[0])
  121.                                 + pos.imag*1j)
  122.                         # wrap around negative x
  123.                         elif np.sign(vec.real) < 0:
  124.                             t = (
  125.                                 max(np.where(grid[int(pos.imag), :] > -1)[0])
  126.                                 + 1j * pos.imag)
  127.                         # wrap around positive y
  128.                         elif np.sign(vec.imag) > 0:
  129.                             t = (
  130.                                 pos.real
  131.                                 + 1j * min(
  132.                                     np.where(grid[:, int(pos.real)] > -1)[0]))
  133.                         # wrap around negative y
  134.                         else:
  135.                             t = (
  136.                                 pos.real
  137.                                 + 1j * max(
  138.                                     np.where(grid[:, int(pos.real)] > -1)[0]))
  139.                     # if the temporary position is walkable update position
  140.                     # else break the loop and go to the next part of the path
  141.                     # instructions
  142.                     if grid[int(t.imag), int(t.real)] == 0:
  143.                         pos = t
  144.                         path_list.append(pos)
  145.                     else:
  146.                         break
  147.                 except IndexError:
  148.                     # if the indices run out of range of the grid,
  149.                     # apply same logic as above, see above for
  150.                     # comments
  151.                     if np.sign(vec.real) > 0:
  152.                         t = (
  153.                             min(np.where(grid[int(pos.imag), :] > -1)[0])
  154.                             + pos.imag*1j)
  155.                     elif np.sign(vec.real) < 0:
  156.                         t = (
  157.                             max(np.where(grid[int(pos.imag), :] > -1)[0])
  158.                             + 1j * pos.imag)
  159.                     elif np.sign(vec.imag) > 0:
  160.                         t = (
  161.                             pos.real
  162.                             + 1j * min(
  163.                                 np.where(grid[:, int(pos.real)] > -1)[0]))
  164.                     else:
  165.                         t = (
  166.                             pos.real
  167.                             + 1j * max(
  168.                                 np.where(grid[:, int(pos.real)] > -1)[0]))
  169.                     if grid[int(t.imag), int(t.real)] == 0:
  170.                         pos = t
  171.                         path_list.append(pos)
  172.                     else:
  173.                         break
  174.         # if the instruction is a complex number, apply the rotation to the
  175.         # facing vector
  176.         elif type(p) == complex:
  177.             vec *= p
  178.     return path_list, vec
  179.  
  180.  
  181. # generate the path and the final facing
  182. full_path, facing = follow_path(grid, path)
  183.  
  184. # generate plot of board and path
  185. fig, ax = plt.subplots(dpi=100, figsize=(9, 12))
  186. ax.imshow(
  187.     grid,
  188.     cmap=plt.get_cmap('summer_r'))
  189. ax.scatter(
  190.     [n.real for n in full_path[1:-1]],
  191.     [n.imag for n in full_path[1:-1]],
  192.     label='path',
  193.     edgecolors='none',
  194.     s=7)
  195. ax.scatter(
  196.     [full_path[0].real],
  197.     [full_path[0].imag],
  198.     label='start',
  199.     edgecolors='none',
  200.     s=12)
  201. ax.scatter(
  202.     [full_path[-1].real],
  203.     [full_path[-1].imag],
  204.     label='finish',
  205.     c='tab:red',
  206.     edgecolors='none',
  207.     s=15)
  208. plt.legend()
  209. fig.tight_layout()
  210. fig.savefig('./part1.svg')
  211. plt.show()
  212.  
  213. # convert facing to number
  214. f = 0
  215. if facing.real < 0:
  216.     f = 2
  217. elif facing.imag > 0:
  218.     f = 1
  219. elif facing.image < 0:
  220.     f = 3
  221. finish = full_path[-1]
  222. # AoC Indices start with 1, so need to add 1 to each dimension
  223. print(
  224.     "Answer Puzzle 1:",
  225.     int(4*(finish.real+1) + 1000*(finish.imag+1) + f))
  226.  
  227.  
  228. # ------ PUZZLE 22-02 ------
  229. # Defines a face with a local coordinate grid upon which the path
  230. # instructions can be executed until the face is left at some point
  231. class Face:
  232.     def __init__(self, grid: np.ndarray, top_right: complex = 0+0j) -> None:
  233.         # holds the top right coordinate in the grid coordinate system for
  234.         # later transformation from local grid to global grid
  235.         self.top_right = top_right
  236.         self.grid = grid
  237.  
  238.     # follow the path instructions from a starting point and a starting
  239.     # direction onward until the path is either finished or the grid
  240.     # of the face is left
  241.     def follow_path(
  242.             self,
  243.             path: list,
  244.             start: complex = None,
  245.             vec: complex = None):
  246.         # unless an explicit starting position is given to the function
  247.         # start at the first walkable part in the top right of the board
  248.         if start is None:
  249.             pos = min(np.where(self.grid[0, :] == 0)[0]) + 0j
  250.         else:
  251.             pos = start
  252.         # list that holds the coordinates for the full path
  253.         path_list = [pos]
  254.         # facing vector, start with facing to the right unless a facing vector
  255.         # is given to the function
  256.         if vec is None:
  257.             vec = 1.+0.j
  258.         # run through pathing instructions
  259.         for instruction_num, p in enumerate(path):
  260.             # if the instruction is an integer n, run a loop to walk
  261.             # n steps in the direction of the current facing
  262.             if type(p) == int:
  263.                 for i in range(p):
  264.                     # create temporary new position vector t
  265.                     t = pos + vec
  266.                     if (
  267.                         t.imag < self.grid.shape[0]
  268.                             and t.imag >= 0
  269.                             and t.real < self.grid.shape[0]
  270.                             and t.real >= 0):
  271.                         # if the temporary position is walkable update position
  272.                         # else break the loop and go to the next part of the
  273.                         # path
  274.                         # instructions
  275.                         if self.grid[int(t.imag), int(t.real)] == 0:
  276.                             pos = t
  277.                             path_list.append(pos)
  278.                         else:
  279.                             break
  280.                     else:
  281.                         # if the indices run out of range of the grid,
  282.                         # return the rest of the path (adjusted for the
  283.                         # number of steps taken in the current action)
  284.                         # as well as the new coordinates outside of the
  285.                         # grid. These will be used to compute the coordinates
  286.                         # on the connected face
  287.                         # additionally return the current direction vector
  288.                         # and the full list of all steps taken in the
  289.                         # coordinates of the unfolded cube grid
  290.                         return [
  291.                             [path[instruction_num] - (i+1)]
  292.                             + path[(instruction_num+1):],
  293.                             t,
  294.                             vec,
  295.                             [p + self.top_right for p in path_list]]
  296.             # if the instruction is a complex number, apply the rotation to the
  297.             # facing vector
  298.             elif type(p) == complex:
  299.                 vec *= p
  300.         return [], pos, vec, [p + self.top_right for p in path_list]
  301.  
  302.  
  303. # Defines a cube with 6 Faces
  304. class Cube:
  305.     def __init__(self, faces: list[Face], positions: list['str']) -> None:
  306.         self.faces = dict(zip(positions, faces))
  307.         self.min = 0
  308.         self.max = 49
  309.  
  310.     def run_path(
  311.             self,
  312.             path: list) -> list[complex, complex]:
  313.         # start on top Face and run until path is empty
  314.         cur_face_pos = 'top'
  315.         Cur_Face = self.faces[cur_face_pos]
  316.         pos = None
  317.         vec = None
  318.         # is set to True if all of the path instructions are executed
  319.         finished = False
  320.         full_path = []  # collects the path in grid coordinates as we go
  321.         while not finished:
  322.             # run through part of path on the current face, will return here
  323.             # once the path leaves the face
  324.             path, pos, vec, path_list = Cur_Face.follow_path(path, pos, vec)
  325.             full_path.extend(path_list)
  326.             if not path:
  327.                 # if the path is emptry stop the loop
  328.                 print("finished")
  329.                 finished = True
  330.             else:
  331.                 # cache values for later checking at the bottom of all the
  332.                 # ifs
  333.                 cur_face_pos_cache = deepcopy(cur_face_pos)
  334.                 pos_cache = deepcopy(pos)
  335.                 vec_cache = deepcopy(vec)
  336.                 # check from what face the path left in what direction
  337.                 # find the correct face to continue on and adjust
  338.                 # the position pos and the direction vec accordingly
  339.                 if cur_face_pos == 'top':
  340.                     # if we leave the top face to the right
  341.                     # we end up on the right face with the same
  342.                     # y coordinate but at the local x coordinate
  343.                     # x = 0 of the face and the same direction
  344.                     # vector
  345.                     if pos.real > self.max:
  346.                         cur_face_pos = 'right'
  347.                         pos = 0. + pos.imag * 1j
  348.                         vec = vec
  349.                     # if we leave the top face to the left
  350.                     # we end up on the left face with the inverted
  351.                     # y coordinate and at the local x coordinate
  352.                     # x = 0 of the face
  353.                     # the direction vector points to the right
  354.                     elif pos.real < self.min:
  355.                         cur_face_pos = 'left'
  356.                         pos = 0 + (self.max - pos.imag) * 1j
  357.                         vec = 1+0j
  358.                     # if we leave the top face to the top
  359.                     # we end up on the back face with the old top x
  360.                     # coordinate now the new back y coordinate and
  361.                     # at the local x coordinate
  362.                     # x = 0 of the face
  363.                     # the direction vector points to the right
  364.                     elif pos.imag < self.max:
  365.                         cur_face_pos = 'back'
  366.                         pos = 0 + pos.real * 1j
  367.                         vec = 1+0j
  368.                     # if we leave the top face to the bottom
  369.                     # we end up on the front face with the same
  370.                     # x coordinate and at the local y coordinate
  371.                     # y = 0 of the face
  372.                     # the direction vector points stays the same
  373.                     elif pos.imag > self.max:
  374.                         cur_face_pos = 'front'
  375.                         pos = pos.real + 0j
  376.                         vec = vec
  377.                 # similar logic as above for the other faces
  378.                 elif cur_face_pos == 'right':
  379.                     if pos.real > self.max:
  380.                         cur_face_pos = 'bottom'
  381.                         pos = self.max + (self.max - pos.imag) * 1j
  382.                         vec = -1+0j
  383.                     elif pos.real < self.min:
  384.                         cur_face_pos = 'top'
  385.                         pos = self.max + pos.imag * 1j
  386.                         vec = vec
  387.                     elif pos.imag < self.min:
  388.                         cur_face_pos = 'back'
  389.                         pos = pos.real + self.max * 1j
  390.                         vec = 0-1j
  391.                     elif pos.imag > self.max:
  392.                         cur_face_pos = 'front'
  393.                         pos = self.max + pos.real * 1j
  394.                         vec = -1+0j
  395.                 elif cur_face_pos == 'front':
  396.                     if pos.real > self.max:
  397.                         cur_face_pos = 'right'
  398.                         pos = (pos.imag) + self.max * 1j
  399.                         vec = 0-1j
  400.                     elif pos.real < self.min:
  401.                         cur_face_pos = 'left'
  402.                         pos = pos.imag + 0 * 1j
  403.                         vec = 0+1j
  404.                     elif pos.imag < self.min:
  405.                         cur_face_pos = 'top'
  406.                         pos = pos.real + self.max * 1j
  407.                         vec = vec
  408.                     elif pos.imag > self.max:
  409.                         cur_face_pos = 'bottom'
  410.                         pos = pos.real + 0 * 1j
  411.                         vec = vec
  412.                 elif cur_face_pos == 'bottom':
  413.                     if pos.real > self.max:
  414.                         cur_face_pos = 'right'
  415.                         pos = self.max + (self.max - pos.imag) * 1j
  416.                         vec = -1+0j
  417.                     elif pos.real < self.min:
  418.                         cur_face_pos = 'left'
  419.                         pos = self.max + pos.imag * 1j
  420.                         vec = vec
  421.                     elif pos.imag < self.min:
  422.                         cur_face_pos = 'front'
  423.                         pos = pos.real + (self.max) * 1j
  424.                         vec = vec
  425.                     elif pos.imag > self.max:
  426.                         cur_face_pos = 'back'
  427.                         pos = self.max + (pos.real) * 1j
  428.                         vec = -1+0j
  429.                 elif cur_face_pos == 'left':
  430.                     if pos.real > self.max:
  431.                         cur_face_pos = 'bottom'
  432.                         pos = 0 + pos.imag * 1j
  433.                         vec = vec
  434.                     elif pos.real < self.min:
  435.                         cur_face_pos = 'top'
  436.                         pos = 0 + (self.max - pos.imag) * 1j
  437.                         vec = 1+0j
  438.                     elif pos.imag < self.min:
  439.                         cur_face_pos = 'front'
  440.                         pos = 0 + (pos.real) * 1j
  441.                         vec = 1+0j
  442.                     elif pos.imag > self.max:
  443.                         cur_face_pos = 'back'
  444.                         pos = pos.real + 0j
  445.                         vec = vec
  446.                 elif cur_face_pos == 'back':
  447.                     if pos.real > self.max:
  448.                         cur_face_pos = 'bottom'
  449.                         pos = pos.imag + self.max * 1j
  450.                         vec = 0-1j
  451.                     elif pos.real < self.min:
  452.                         cur_face_pos = 'top'
  453.                         pos = pos.imag + 0 * 1j
  454.                         vec = 0+1j
  455.                     elif pos.imag < self.min:
  456.                         cur_face_pos = 'left'
  457.                         pos = pos.real + self.max * 1j
  458.                         vec = vec
  459.                     elif pos.imag > self.max:
  460.                         cur_face_pos = 'right'
  461.                         pos = pos.real + 0j
  462.                         vec = 0+1j
  463.                 Cur_Face = self.faces[cur_face_pos]
  464.                 # if we would end up on a non-walkable tile on the new face
  465.                 # we instead stay on the old face with the old coordinate
  466.                 if Cur_Face.grid[int(pos.imag), int(pos.real)] != 0:
  467.                     print("Getting Cache")
  468.                     cur_face_pos = deepcopy(cur_face_pos_cache)
  469.                     pos = deepcopy(pos_cache) - (vec_cache)
  470.                     vec = deepcopy(vec_cache)
  471.                     Cur_Face = self.faces[cur_face_pos]
  472.                     # skip the current action on the path as it's not possible
  473.                     # to keep walking in the current direction
  474.                     path = path[1:]
  475.                     # remove last entry from full path to avoid doubling
  476.                     full_path = full_path[:-1]
  477.         return pos + Cur_Face.top_right, vec, full_path
  478.  
  479.  
  480. # Cut Grid into different faces and attach correct names
  481. # Cube Grid assignment:
  482. # t: top
  483. # r: right
  484. # f: front
  485. # l: left
  486. # b: bottom
  487. # <<: back
  488. #
  489. # ## tt rr
  490. # ## tt rr
  491. # ## ff ##
  492. # ## ff ##
  493. # ll bb ##
  494. # ll bb ##
  495. # << ## ##
  496. # << ## ##
  497.  
  498. faces = [
  499.     Face(grid[0:50, 50:100], 50+0j),
  500.     Face(grid[0:50, 100:150], 100+0j),
  501.     Face(grid[50:100, 50:100], 50+50j),
  502.     Face(grid[100:150, 0:50], 0+100j),
  503.     Face(grid[100:150, 50:100], 50+100j),
  504.     Face(grid[150:200, 0:50], 0+150j)]
  505. face_names = [
  506.     'top',
  507.     'right',
  508.     'front',
  509.     'left',
  510.     'bottom',
  511.     'back'
  512. ]
  513.  
  514. C = Cube(faces, face_names)
  515. r, facing, pl = C.run_path(path)
  516.  
  517. # convert facing to number
  518. f = 0
  519. if facing.real < 0:
  520.     f = 2
  521. elif facing.imag > 0:
  522.     f = 1
  523. elif facing.imag < 0:
  524.     f = 3
  525.  
  526. print(
  527.     "Answer Puzzle 2:",
  528.     int(4*(r.real+1) + 1000*(r.imag+1) + f))
  529.  
  530.  
  531. # generate plot of board and path
  532. fig, ax = plt.subplots(dpi=100, figsize=(7, 7))
  533. ax.imshow(
  534.     grid,
  535.     cmap=plt.get_cmap('summer_r'))
  536. line, = ax.plot(
  537.     [n.real for n in pl],
  538.     [n.imag for n in pl],
  539.     '.',
  540.     label='path',
  541.     markersize=2)
  542.  
  543. plt.legend()
  544. fig.tight_layout()
  545.  
  546. fig.savefig('./part2.svg')
  547. plt.show()
  548.  
  549. # generate animation
  550. fig, ax = plt.subplots(dpi=100, figsize=(7, 7))
  551. ax.imshow(
  552.     grid,
  553.     cmap=plt.get_cmap('summer_r'))
  554. line, = ax.plot(
  555.     [n.real for n in pl[:1]],
  556.     [n.imag for n in pl[:1]],
  557.     '.',
  558.     label='path',
  559.     markersize=2)
  560.  
  561. plt.legend()
  562. plt.tight_layout()
  563.  
  564.  
  565. def animate(i):
  566.     if i > 200:
  567.         line.set_data(
  568.             [n.real for n in pl[(i-200):i]],
  569.             [n.imag for n in pl[(i-200):i]])
  570.     else:
  571.         line.set_data(
  572.             [n.real for n in pl[:i]],
  573.             [n.imag for n in pl[:i]])
  574.     return line,
  575.  
  576.  
  577. ani = FuncAnimation(
  578.     fig,
  579.     animate,
  580.     np.arange(1, len(pl)),
  581.     interval=1,
  582.     blit=True)
  583. plt.show()
  584.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement