Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import numpy as np
- import matplotlib.pyplot as plt
- from matplotlib.animation import FuncAnimation
- from copy import deepcopy
- with open('./data/01.txt', 'r') as f:
- data = [line[:-1] for line in f.readlines()]
- board = data[:-2]
- path = data[-2:]
- # convert the ragged lines to a numpy array
- def convert_to_grid(data: list[str]) -> np.ndarray:
- lines = []
- for line in data:
- line = line.replace(' ', '-1,').replace('.', '0,').replace('#', '1,')
- if line.endswith(','):
- line = line[:-1]
- try:
- num_line = [int(i) for i in line.split(',')]
- except ValueError:
- print(line)
- print(line.split(','))
- return line
- lines.append(num_line)
- max_length = max([len(i) for i in lines])
- arrays = []
- for line in lines:
- arr = np.array(line)
- arr = np.pad(
- arr,
- (0, max_length - len(arr)),
- 'constant', constant_values=-1)
- arrays.append(arr)
- return np.array(arrays)
- # ------ PUZZLE 22-01 ------
- # interpret the pathing instructions
- def interpret_path(path: str) -> list:
- # generate a usable pathing list
- new_path = []
- # split the path on all right-turning instructions
- for i, txt in enumerate(path.split('R')):
- try:
- # if the part of the split path is converteable to an integer
- # do that and append to new_path
- txt = int(txt)
- new_path.append(txt)
- # unless we're at the last part of the list, append a
- # turn right instruction to the new path to the replace
- # the ones lost by the splitting on R
- if i != len(path.split('R'))-1:
- # multiplying by i will turn a complex number ccw but due to
- # how the rest is set up, this is the correct multiplyer for
- # turning right
- new_path.append(1j)
- except ValueError:
- # if the part of the split can't be converted to an integer that
- # means we have a turn left indicator somewhere in the split string
- # again split on the turn left indicator and append the integers
- # left to the new path
- for j, txt2 in enumerate(txt.split('L')):
- new_path.append(int(txt2))
- # unless we're at the last part of the L splitting, append a
- # turn left identifier to the new path to make up for the lost
- # ones due to splitting
- if j != len(txt.split('L'))-1:
- # see above for logic of multiplying by -i
- new_path.append(-1j)
- # see above
- if i != len(path.split('R'))-1:
- new_path.append(1j)
- return new_path
- grid = convert_to_grid(board) # convert the board into a numpy array
- # convert the pathing instructions into a list of steps and direction changes
- path = [line for line in path if line != ''][0]
- path = interpret_path(path)
- # generate a path of coordinates from the pathing instructions
- def follow_path(
- grid: np.ndarray,
- path: list,
- start: complex = None,
- vec: complex = None) -> list[list, complex]:
- # unless an explicit starting position is given to the function
- # start at the first walkable part in the top right of the board
- if start is None:
- pos = min(np.where(grid[0, :] == 0)[0]) + 0j
- else:
- pos = start
- # list that holds the coordinates for the full path
- path_list = [pos]
- # facing vector, start with facing to the right unless a facing vector
- # is given to the function
- if vec is None:
- vec = 1.+0.j
- # run through pathing instructions
- for p in path:
- # if the instruction is an integer n, run a loop to walk
- # n steps in the direction of the current facing
- if type(p) == int:
- for _ in range(p):
- # create temporary new position vector t
- t = pos + vec
- try:
- # if the temporary position t is not part of the board
- # modify t to wrap around to the other side
- if grid[int(t.imag), int(t.real)] == -1:
- # wrap around positive x
- if np.sign(vec.real) > 0:
- t = (
- min(np.where(grid[int(pos.imag), :] > -1)[0])
- + pos.imag*1j)
- # wrap around negative x
- elif np.sign(vec.real) < 0:
- t = (
- max(np.where(grid[int(pos.imag), :] > -1)[0])
- + 1j * pos.imag)
- # wrap around positive y
- elif np.sign(vec.imag) > 0:
- t = (
- pos.real
- + 1j * min(
- np.where(grid[:, int(pos.real)] > -1)[0]))
- # wrap around negative y
- else:
- t = (
- pos.real
- + 1j * max(
- np.where(grid[:, int(pos.real)] > -1)[0]))
- # if the temporary position is walkable update position
- # else break the loop and go to the next part of the path
- # instructions
- if grid[int(t.imag), int(t.real)] == 0:
- pos = t
- path_list.append(pos)
- else:
- break
- except IndexError:
- # if the indices run out of range of the grid,
- # apply same logic as above, see above for
- # comments
- if np.sign(vec.real) > 0:
- t = (
- min(np.where(grid[int(pos.imag), :] > -1)[0])
- + pos.imag*1j)
- elif np.sign(vec.real) < 0:
- t = (
- max(np.where(grid[int(pos.imag), :] > -1)[0])
- + 1j * pos.imag)
- elif np.sign(vec.imag) > 0:
- t = (
- pos.real
- + 1j * min(
- np.where(grid[:, int(pos.real)] > -1)[0]))
- else:
- t = (
- pos.real
- + 1j * max(
- np.where(grid[:, int(pos.real)] > -1)[0]))
- if grid[int(t.imag), int(t.real)] == 0:
- pos = t
- path_list.append(pos)
- else:
- break
- # if the instruction is a complex number, apply the rotation to the
- # facing vector
- elif type(p) == complex:
- vec *= p
- return path_list, vec
- # generate the path and the final facing
- full_path, facing = follow_path(grid, path)
- # generate plot of board and path
- fig, ax = plt.subplots(dpi=100, figsize=(9, 12))
- ax.imshow(
- grid,
- cmap=plt.get_cmap('summer_r'))
- ax.scatter(
- [n.real for n in full_path[1:-1]],
- [n.imag for n in full_path[1:-1]],
- label='path',
- edgecolors='none',
- s=7)
- ax.scatter(
- [full_path[0].real],
- [full_path[0].imag],
- label='start',
- edgecolors='none',
- s=12)
- ax.scatter(
- [full_path[-1].real],
- [full_path[-1].imag],
- label='finish',
- c='tab:red',
- edgecolors='none',
- s=15)
- plt.legend()
- fig.tight_layout()
- fig.savefig('./part1.svg')
- plt.show()
- # convert facing to number
- f = 0
- if facing.real < 0:
- f = 2
- elif facing.imag > 0:
- f = 1
- elif facing.image < 0:
- f = 3
- finish = full_path[-1]
- # AoC Indices start with 1, so need to add 1 to each dimension
- print(
- "Answer Puzzle 1:",
- int(4*(finish.real+1) + 1000*(finish.imag+1) + f))
- # ------ PUZZLE 22-02 ------
- # Defines a face with a local coordinate grid upon which the path
- # instructions can be executed until the face is left at some point
- class Face:
- def __init__(self, grid: np.ndarray, top_right: complex = 0+0j) -> None:
- # holds the top right coordinate in the grid coordinate system for
- # later transformation from local grid to global grid
- self.top_right = top_right
- self.grid = grid
- # follow the path instructions from a starting point and a starting
- # direction onward until the path is either finished or the grid
- # of the face is left
- def follow_path(
- self,
- path: list,
- start: complex = None,
- vec: complex = None):
- # unless an explicit starting position is given to the function
- # start at the first walkable part in the top right of the board
- if start is None:
- pos = min(np.where(self.grid[0, :] == 0)[0]) + 0j
- else:
- pos = start
- # list that holds the coordinates for the full path
- path_list = [pos]
- # facing vector, start with facing to the right unless a facing vector
- # is given to the function
- if vec is None:
- vec = 1.+0.j
- # run through pathing instructions
- for instruction_num, p in enumerate(path):
- # if the instruction is an integer n, run a loop to walk
- # n steps in the direction of the current facing
- if type(p) == int:
- for i in range(p):
- # create temporary new position vector t
- t = pos + vec
- if (
- t.imag < self.grid.shape[0]
- and t.imag >= 0
- and t.real < self.grid.shape[0]
- and t.real >= 0):
- # if the temporary position is walkable update position
- # else break the loop and go to the next part of the
- # path
- # instructions
- if self.grid[int(t.imag), int(t.real)] == 0:
- pos = t
- path_list.append(pos)
- else:
- break
- else:
- # if the indices run out of range of the grid,
- # return the rest of the path (adjusted for the
- # number of steps taken in the current action)
- # as well as the new coordinates outside of the
- # grid. These will be used to compute the coordinates
- # on the connected face
- # additionally return the current direction vector
- # and the full list of all steps taken in the
- # coordinates of the unfolded cube grid
- return [
- [path[instruction_num] - (i+1)]
- + path[(instruction_num+1):],
- t,
- vec,
- [p + self.top_right for p in path_list]]
- # if the instruction is a complex number, apply the rotation to the
- # facing vector
- elif type(p) == complex:
- vec *= p
- return [], pos, vec, [p + self.top_right for p in path_list]
- # Defines a cube with 6 Faces
- class Cube:
- def __init__(self, faces: list[Face], positions: list['str']) -> None:
- self.faces = dict(zip(positions, faces))
- self.min = 0
- self.max = 49
- def run_path(
- self,
- path: list) -> list[complex, complex]:
- # start on top Face and run until path is empty
- cur_face_pos = 'top'
- Cur_Face = self.faces[cur_face_pos]
- pos = None
- vec = None
- # is set to True if all of the path instructions are executed
- finished = False
- full_path = [] # collects the path in grid coordinates as we go
- while not finished:
- # run through part of path on the current face, will return here
- # once the path leaves the face
- path, pos, vec, path_list = Cur_Face.follow_path(path, pos, vec)
- full_path.extend(path_list)
- if not path:
- # if the path is emptry stop the loop
- print("finished")
- finished = True
- else:
- # cache values for later checking at the bottom of all the
- # ifs
- cur_face_pos_cache = deepcopy(cur_face_pos)
- pos_cache = deepcopy(pos)
- vec_cache = deepcopy(vec)
- # check from what face the path left in what direction
- # find the correct face to continue on and adjust
- # the position pos and the direction vec accordingly
- if cur_face_pos == 'top':
- # if we leave the top face to the right
- # we end up on the right face with the same
- # y coordinate but at the local x coordinate
- # x = 0 of the face and the same direction
- # vector
- if pos.real > self.max:
- cur_face_pos = 'right'
- pos = 0. + pos.imag * 1j
- vec = vec
- # if we leave the top face to the left
- # we end up on the left face with the inverted
- # y coordinate and at the local x coordinate
- # x = 0 of the face
- # the direction vector points to the right
- elif pos.real < self.min:
- cur_face_pos = 'left'
- pos = 0 + (self.max - pos.imag) * 1j
- vec = 1+0j
- # if we leave the top face to the top
- # we end up on the back face with the old top x
- # coordinate now the new back y coordinate and
- # at the local x coordinate
- # x = 0 of the face
- # the direction vector points to the right
- elif pos.imag < self.max:
- cur_face_pos = 'back'
- pos = 0 + pos.real * 1j
- vec = 1+0j
- # if we leave the top face to the bottom
- # we end up on the front face with the same
- # x coordinate and at the local y coordinate
- # y = 0 of the face
- # the direction vector points stays the same
- elif pos.imag > self.max:
- cur_face_pos = 'front'
- pos = pos.real + 0j
- vec = vec
- # similar logic as above for the other faces
- elif cur_face_pos == 'right':
- if pos.real > self.max:
- cur_face_pos = 'bottom'
- pos = self.max + (self.max - pos.imag) * 1j
- vec = -1+0j
- elif pos.real < self.min:
- cur_face_pos = 'top'
- pos = self.max + pos.imag * 1j
- vec = vec
- elif pos.imag < self.min:
- cur_face_pos = 'back'
- pos = pos.real + self.max * 1j
- vec = 0-1j
- elif pos.imag > self.max:
- cur_face_pos = 'front'
- pos = self.max + pos.real * 1j
- vec = -1+0j
- elif cur_face_pos == 'front':
- if pos.real > self.max:
- cur_face_pos = 'right'
- pos = (pos.imag) + self.max * 1j
- vec = 0-1j
- elif pos.real < self.min:
- cur_face_pos = 'left'
- pos = pos.imag + 0 * 1j
- vec = 0+1j
- elif pos.imag < self.min:
- cur_face_pos = 'top'
- pos = pos.real + self.max * 1j
- vec = vec
- elif pos.imag > self.max:
- cur_face_pos = 'bottom'
- pos = pos.real + 0 * 1j
- vec = vec
- elif cur_face_pos == 'bottom':
- if pos.real > self.max:
- cur_face_pos = 'right'
- pos = self.max + (self.max - pos.imag) * 1j
- vec = -1+0j
- elif pos.real < self.min:
- cur_face_pos = 'left'
- pos = self.max + pos.imag * 1j
- vec = vec
- elif pos.imag < self.min:
- cur_face_pos = 'front'
- pos = pos.real + (self.max) * 1j
- vec = vec
- elif pos.imag > self.max:
- cur_face_pos = 'back'
- pos = self.max + (pos.real) * 1j
- vec = -1+0j
- elif cur_face_pos == 'left':
- if pos.real > self.max:
- cur_face_pos = 'bottom'
- pos = 0 + pos.imag * 1j
- vec = vec
- elif pos.real < self.min:
- cur_face_pos = 'top'
- pos = 0 + (self.max - pos.imag) * 1j
- vec = 1+0j
- elif pos.imag < self.min:
- cur_face_pos = 'front'
- pos = 0 + (pos.real) * 1j
- vec = 1+0j
- elif pos.imag > self.max:
- cur_face_pos = 'back'
- pos = pos.real + 0j
- vec = vec
- elif cur_face_pos == 'back':
- if pos.real > self.max:
- cur_face_pos = 'bottom'
- pos = pos.imag + self.max * 1j
- vec = 0-1j
- elif pos.real < self.min:
- cur_face_pos = 'top'
- pos = pos.imag + 0 * 1j
- vec = 0+1j
- elif pos.imag < self.min:
- cur_face_pos = 'left'
- pos = pos.real + self.max * 1j
- vec = vec
- elif pos.imag > self.max:
- cur_face_pos = 'right'
- pos = pos.real + 0j
- vec = 0+1j
- Cur_Face = self.faces[cur_face_pos]
- # if we would end up on a non-walkable tile on the new face
- # we instead stay on the old face with the old coordinate
- if Cur_Face.grid[int(pos.imag), int(pos.real)] != 0:
- print("Getting Cache")
- cur_face_pos = deepcopy(cur_face_pos_cache)
- pos = deepcopy(pos_cache) - (vec_cache)
- vec = deepcopy(vec_cache)
- Cur_Face = self.faces[cur_face_pos]
- # skip the current action on the path as it's not possible
- # to keep walking in the current direction
- path = path[1:]
- # remove last entry from full path to avoid doubling
- full_path = full_path[:-1]
- return pos + Cur_Face.top_right, vec, full_path
- # Cut Grid into different faces and attach correct names
- # Cube Grid assignment:
- # t: top
- # r: right
- # f: front
- # l: left
- # b: bottom
- # <<: back
- #
- # ## tt rr
- # ## tt rr
- # ## ff ##
- # ## ff ##
- # ll bb ##
- # ll bb ##
- # << ## ##
- # << ## ##
- faces = [
- Face(grid[0:50, 50:100], 50+0j),
- Face(grid[0:50, 100:150], 100+0j),
- Face(grid[50:100, 50:100], 50+50j),
- Face(grid[100:150, 0:50], 0+100j),
- Face(grid[100:150, 50:100], 50+100j),
- Face(grid[150:200, 0:50], 0+150j)]
- face_names = [
- 'top',
- 'right',
- 'front',
- 'left',
- 'bottom',
- 'back'
- ]
- C = Cube(faces, face_names)
- r, facing, pl = C.run_path(path)
- # convert facing to number
- f = 0
- if facing.real < 0:
- f = 2
- elif facing.imag > 0:
- f = 1
- elif facing.imag < 0:
- f = 3
- print(
- "Answer Puzzle 2:",
- int(4*(r.real+1) + 1000*(r.imag+1) + f))
- # generate plot of board and path
- fig, ax = plt.subplots(dpi=100, figsize=(7, 7))
- ax.imshow(
- grid,
- cmap=plt.get_cmap('summer_r'))
- line, = ax.plot(
- [n.real for n in pl],
- [n.imag for n in pl],
- '.',
- label='path',
- markersize=2)
- plt.legend()
- fig.tight_layout()
- fig.savefig('./part2.svg')
- plt.show()
- # generate animation
- fig, ax = plt.subplots(dpi=100, figsize=(7, 7))
- ax.imshow(
- grid,
- cmap=plt.get_cmap('summer_r'))
- line, = ax.plot(
- [n.real for n in pl[:1]],
- [n.imag for n in pl[:1]],
- '.',
- label='path',
- markersize=2)
- plt.legend()
- plt.tight_layout()
- def animate(i):
- if i > 200:
- line.set_data(
- [n.real for n in pl[(i-200):i]],
- [n.imag for n in pl[(i-200):i]])
- else:
- line.set_data(
- [n.real for n in pl[:i]],
- [n.imag for n in pl[:i]])
- return line,
- ani = FuncAnimation(
- fig,
- animate,
- np.arange(1, len(pl)),
- interval=1,
- blit=True)
- plt.show()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement