···11+import re, fileinput
22+33+from utils import min_max_xy
44+from utils import firsts, lasts
55+from utils import Point, DIRS
66+77+88+# Read problem input.
99+graph = {}
1010+in_maze = True
1111+start = None
1212+for y, line in enumerate(fileinput.input()):
1313+ if not line.strip():
1414+ in_maze = False
1515+ if in_maze:
1616+ for x, c in enumerate(line.strip("\n")):
1717+ if c != ' ':
1818+ if start is None and c == '.':
1919+ start = Point(x, -y)
2020+ graph[Point(x, -y)] = c
2121+ else:
2222+ instructions = re.findall(r'(\d+|[LR])', line)
2323+2424+2525+def wrapped_links(graph):
2626+ """Compute link dictionary for part 1."""
2727+ links = {}
2828+ min_x, max_x, min_y, max_y = min_max_xy(list(graph.keys()))
2929+3030+ # Construct the left-to-right wrap-around links.
3131+ for y in range(min_y, max_y + 1):
3232+ first = None
3333+ for x in range(min_x, max_x + 1):
3434+ p = Point(x, y)
3535+ if first is None and p in graph:
3636+ first = p
3737+ elif p in graph:
3838+ last = p
3939+4040+ links[last, 1] = (first, 1)
4141+ links[first, 3] = (last, 3)
4242+4343+ # Construct the top-to-bottom wrap-around links.
4444+ for x in range(min_x, max_x + 1):
4545+ first = None
4646+ for y in range(min_y, max_y + 1):
4747+ p = Point(x, y)
4848+ if first is None and p in graph:
4949+ first = p
5050+ elif p in graph:
5151+ last = p
5252+5353+ links[last, 0] = (first, 0)
5454+ links[first, 2] = (last, 2)
5555+5656+ return links
5757+5858+5959+def cube_links():
6060+ """
6161+ This works specifically for my cube, which is shaped as follows:
6262+6363+ 1122
6464+ 1122
6565+ 33
6666+ 33
6767+ 4455
6868+ 4455
6969+ 66
7070+ 66
7171+7272+ """
7373+ links = {}
7474+7575+ # Define 2D arrays representing the 6 different faces of the cube, as numbered
7676+ # above, in the orientation given by the problem input.
7777+ FS = 50
7878+ f1 = [[Point(x, -y) for x in range(FS*1, FS*2)] for y in range(FS*0, FS*1)]
7979+ f2 = [[Point(x, -y) for x in range(FS*2, FS*3)] for y in range(FS*0, FS*1)]
8080+ f3 = [[Point(x, -y) for x in range(FS*1, FS*2)] for y in range(FS*1, FS*2)]
8181+ f4 = [[Point(x, -y) for x in range(FS*0, FS*1)] for y in range(FS*2, FS*3)]
8282+ f5 = [[Point(x, -y) for x in range(FS*1, FS*2)] for y in range(FS*2, FS*3)]
8383+ f6 = [[Point(x, -y) for x in range(FS*0, FS*1)] for y in range(FS*3, FS*4)]
8484+8585+ # "Glue together" the appropriate edges of each of the face pairs that will
8686+ # be touching when the cube is formed. The "facing" direction changes depending
8787+ # on which direction you're coming into the point from.
8888+ for p, np in zip(f1[0], reversed(firsts(f6))):
8989+ links[(p, 0)] = (np, 1)
9090+ links[(np, 3)] = (p, 2)
9191+9292+ for p, np in zip(firsts(f1), reversed(firsts(f4))):
9393+ links[(p, 3)] = (np, 1)
9494+ links[(np, 3)] = (p, 1)
9595+9696+ for p, np in zip(f2[0], f6[-1]):
9797+ links[(p, 0)] = (np, 0)
9898+ links[(np, 2)] = (p, 2)
9999+100100+ for p, np in zip(reversed(lasts(f2)), lasts(f5)):
101101+ links[(p, 1)] = (np, 3)
102102+ links[(np, 1)] = (p, 3)
103103+104104+ for p, np in zip(f2[-1], reversed(lasts(f3))):
105105+ links[(p, 2)] = (np, 3)
106106+ links[(np, 1)] = (p, 0)
107107+108108+ for p, np in zip(firsts(f3), reversed(f4[0])):
109109+ links[(p, 3)] = (np, 2)
110110+ links[(np, 0)] = (p, 1)
111111+112112+ for p, np in zip(f5[-1], reversed(lasts(f6))):
113113+ links[(p, 2)] = (np, 3)
114114+ links[(np, 1)] = (p, 0)
115115+116116+ return links
117117+118118+119119+def simulate(graph, start, instructions, links):
120120+ pos = start
121121+ dir = 1
122122+ for ins in instructions:
123123+ # Turn clockwise or counterclockwise.
124124+ if not ins.isnumeric():
125125+ if ins == 'L':
126126+ dir = (dir - 1) % 4
127127+ else:
128128+ dir = (dir + 1) % 4
129129+ continue
130130+131131+ # Move some number of steps (or stop at a wall).
132132+ for _ in range(int(ins)):
133133+ np = pos + DIRS[dir]
134134+135135+ # Bump into a wall, stop going forward.
136136+ if graph.get(np) == '#':
137137+ break
138138+ elif graph.get(np) == '.':
139139+ pos = np
140140+ continue
141141+ else:
142142+ # We need to wrap around to another spot. The link mapping
143143+ # tells us what the new position is, and what our new facing is.
144144+ np, nf = links[pos, dir]
145145+146146+ if graph.get(np) == '#':
147147+ break
148148+ elif graph.get(np) == '.':
149149+ pos = np
150150+ dir = nf
151151+ continue
152152+153153+ row = -pos.y + 1
154154+ col = pos.x + 1
155155+ facing = (dir - 1) % 4
156156+157157+ return (1000 * row) + (4 * col) + facing
158158+159159+print("Part 1:", simulate(graph, start, instructions, wrapped_links(graph)))
160160+print("Part 2:", simulate(graph, start, instructions, cube_links()))