Time for a really tricky graph puzzle. Today’s puzzle involves maneuvering a crucible full of lava through a map. Each tile has a one-digit number indicating how much heat will be lost on entering that block. More interestingly, the crucible can only turn left or right or go straight, and it must turn after going three tiles in the same direction.
This amounts to a familiar challenge: graph pathfinding, while keeping track of some kind of state in addition to node position. After some tricky implementation, this part proved straightforward.
Part 2 introduces “ultra crucibles.” These devices must go straight at least four and at most ten tiles before turning. This constraint makes the graph bigger and the implementation more complicated, but it isn’t too challenging.
```{python}
import heapq
from collections import defaultdict
from functools import cache
from math import inf
from operator import attrgetter
from utils.utils import split_lines
class State:
def __init__(self, coord, direction, remaining, traversed):
self.coord = coord
self.direction = direction
self.remaining = remaining
self.traversed = traversed
def __lt__(self, other):
return self.traversed < other.traversed
def __hash__(self):
return hash((self.coord, self.direction, self.remaining))
def __repr__(self) -> str:
return (self.coord, self.direction, self.remaining, self.traversed).__repr__()
def parse(lines):
return {
complex(i, j): int(char)
for j, line in enumerate(lines)
for i, char in enumerate(line)
}
@cache
def manhattan(x, y):
return abs(x.real - y.real) + abs(x.imag - y.imag)
def A_star(graph, start, max_straight, min_turn_time):
= ymin = 0
xmin = max_straight - min_turn_time
move_threshold = int(max(graph.keys(), key=attrgetter("real")).real)
xmax = int(max(graph.keys(), key=attrgetter("imag")).imag)
ymax = complex(xmax, ymax)
goal
= defaultdict(lambda: inf)
dist = [
queue 1, max_straight, 0),
State(start,
State(
start,1j,
max_straight,0,
),
]
heapq.heapify(queue)# Tip from Discord
= max(graph.values()) * (xmax + ymax)
answer
def verify_coord(coord):
return xmin <= coord.real <= xmax and ymin <= coord.imag <= ymax
def add_neighbor(new_coord, direction, this_traversed, new_remaining):
heapq.heappush(
queue,
State(
new_coord,
direction,
new_remaining,
this_traversed ,
),
)
= ((-1, 1), (-1j, 1j))
directions while True:
try:
= heapq.heappop(queue)
current except IndexError:
break
= current.coord + current.direction
new_coord if not verify_coord(new_coord):
continue
= current.traversed + graph[new_coord]
this_traversed = current.remaining - 1
new_remaining = (new_coord, current.direction, new_remaining)
key
if not (
> -1
new_remaining and this_traversed < dist[key]
and this_traversed < answer
):continue
= this_traversed
dist[key]
if new_coord == goal and new_remaining <= move_threshold:
= this_traversed
answer continue
if new_remaining <= move_threshold:
for dir in directions[bool(current.direction.real)]:
dir, this_traversed, max_straight)
add_neighbor(new_coord,
add_neighbor(new_coord, current.direction, this_traversed, new_remaining)
return answer
raw_input = split_lines("inputs/day17.txt")
= parse(raw_input)
graph = A_star(graph, 0, 3, 0)
part1 print(part1)
= A_star(parse(raw_input), 0, 10, 4)
part2 print(part2)
```