···11+import fileinput
22+import heapq
33+44+from utils import Point, DIRS, N, S, E, W
55+66+77+def minimize_heat_loss(graph, start, goal=None, part_2=False):
88+ def gen_neighbours(node, hist):
99+ for d in DIRS:
1010+ n = node + d
1111+ if n not in graph:
1212+ continue
1313+1414+ if hist:
1515+ # No U-turns allowed.
1616+ if hist[-1] == -d:
1717+ continue
1818+1919+ if part_2:
2020+ # Early ultra crucible turn.
2121+ if len(hist) < 4 and d != hist[-1]:
2222+ continue
2323+2424+ # Early ultra crucible turn.
2525+ if len(set(hist[-4:])) != 1 and d != hist[-1]:
2626+ continue
2727+2828+ # Wobbly ultra crucible.
2929+ if len(hist) == 10 and all(x == d for x in hist):
3030+ continue
3131+ else:
3232+ # Unstable regular crucible.
3333+ if len(hist) >= 3 and all(x == d for x in hist[-3:]):
3434+ continue
3535+3636+ yield n, graph[n], d
3737+3838+ horizon = [(0, start, ())]
3939+ seen = set()
4040+ buf_len = 10 if part_2 else 4
4141+4242+ while horizon:
4343+ depth, curr, hist = heapq.heappop(horizon)
4444+4545+ if (curr, hist) in seen:
4646+ continue
4747+4848+ seen.add((curr, hist))
4949+5050+ # Check if at the bottom-right, and that our last 4 moves
5151+ # were in the same direction if we're solving part 2.
5252+ if curr == goal and (not part_2 or len(set(hist[-4:])) == 1):
5353+ return depth
5454+5555+ for neighbour, weight, nd in gen_neighbours(curr, hist):
5656+ new_cost = weight + depth
5757+ new_hist = hist + (nd,)
5858+ heapq.heappush(horizon, (new_cost, neighbour, new_hist[-buf_len:]))
5959+6060+6161+# Read problem input.
6262+graph = {}
6363+for y, line in enumerate(fileinput.input()):
6464+ for x, c in enumerate(line.strip()):
6565+ graph[Point(x, y)] = int(c)
6666+6767+start = Point(0, 0)
6868+end = Point(x, y)
6969+7070+# Solve problem.
7171+print("Part 1:", minimize_heat_loss(graph, start, end, part_2=False))
7272+print("Part 2:", minimize_heat_loss(graph, start, end, part_2=True))