Compare commits

...

10 commits

Author SHA1 Message Date
178d96494a Solve 2023:17 p1-2 "Clumsy Crucible"
Revenge, blast from the past etc!

Had to learn Dijkstra's for this one. Was as stuck
as one can be on AOC. This stopped my progress
2023, and kept me busy at least 20-30h this year
(2024) as well. I never came past part 1, but got
part 2 in minutes when I hit home.

Turns out the initial queue was to blame, after
studying Dijkstras, reading hints on the subreddit
and tutorials in blogs. I was off-by-1 in costs,
since I misplaced a read from the grid.

I also struggled a really, really long time with
a bug where I resetted the steps to aggresively.

What helped me to figure it out was to create
simpler test case grids and step-debug them.

Example 1:

241
321

should give a least heat loss of 6 in pt1.

Example 2:

11199
12199
99199
99131
99111

should give a least heat loss of 9 in pt2.
2025-01-05 00:10:26 +01:00
fb468c2199 Solve 2023:16 "The Floor Will Be Lava" 2025-01-05 00:10:26 +01:00
a90269f7f9 Solve 2023:15 "Lens Library"
WALLOFTEXT for part 2, took me 90 minutes to find
this important text:

> Each step begins with a sequence of letters that
> indicate the label of the lens on which the step
> operates. The result of running the HASH algorithm
> on the label indicates the correct box for that
> step.

It also clarifies how part 2 and part 1 relates.
2025-01-05 00:10:26 +01:00
c832e30dcf Solve 2023:14 "Parabolic Reflector Dish" 2025-01-05 00:10:26 +01:00
3de54ce0e9 Solve 2023:13 "Point of Incidence" 2025-01-05 00:10:26 +01:00
4fa1b1e14c Solve 2023:12 "Hot Springs" 2025-01-05 00:10:26 +01:00
9ca8607f8b Solve 2023:11 "Cosmic Expansion" 2025-01-05 00:10:26 +01:00
a1bf11a5ed Fix flake8 errors for 2023:1-10 2025-01-05 00:10:26 +01:00
b681e5cdb7 Solve 2023:10 "Pipe Maze"
Got completely stuck on part 2. Tried some polygon
area calculations, but none provided the correct
answer, most likely due to the unorthodox polygon
points.

I also tried _shoelace method_ without any luck.
Had not heard about that one earlier, it was a good
learning experience even though I vould not use it
here.

By the subreddit, several people had had luck
using the ray method.

> Part 2 using one of my favorite facts from
> graphics engineering: lets say you have an
> enclosed shape, and you want to color every
> pixel inside of it. How do you know if a given
> pixel is inside the shape or not? Well, it
> turns out: if you shoot a ray in any direction
> from the pixel and it crosses the boundary an
> _odd number_ of times, it's _inside_. if it crosses
> an even number of times, it's outside. Works
> for all enclosed shapes, even self-intersecting
> and non-convex ones.
2025-01-05 00:10:26 +01:00
741f7b89d8 Refactor 2023:05
Increasing speed from 66mins to 4mins. Caching the
location value in the code to keep things at highest
speed.

From the subreddit, the algorithm looks like this.

1. Start att location 0
2. Traverse the whole process backwards, by
   reversing process steps and flipping dest/src
   positions.
3. Output is not a location, instead it's a seed.
4. if seed is in any seed range, use seed to get
   location as in step 1.
5. If not, increase location by 1 and repeat 2-4.
2025-01-05 00:10:26 +01:00
20 changed files with 712 additions and 113 deletions

View file

@ -36,3 +36,7 @@ Execute separate puzzle on file save (replace `XX` with the puzzle number):
(requires `entr` and `wl-paste`, Mac users can instead use `pbpaste`. If you
prefer X at Linux, use `xclip -selection clipboard -o`).
To lint files:
ls output/*.py | entr -r -c flake8 output --ignore=E741,E501,E203

View file

@ -56,8 +56,8 @@ if __name__ == "__main__":
# import sys; inp = sys.stdin.read().strip()
# uncomment to use AoC provided puzzle input
# with open(f"./input/{padded_no}.txt", "r") as f:
# inp = f.read()
# with open("./input/{padded_no}.txt", "r") as f:
# inp = f.read().strip()
# uncomment to do initial data processing shared by part 1-2
inp = solve(inp)

View file

@ -80,3 +80,59 @@ def vdbg(seen, h, w):
"""Print-debug visited positions of a matrix"""
for r in range(h):
print("".join(["#" if (r, c) in seen else "." for c in range(w)]))
"""
1. Create an array that holds the distance of each vertex from the starting
vertex. Initially, set this distance to infinity for all vertices except
the starting vertex which should be set to 0.
2. Create a priority queue (heap) and insert the starting vertex with its
distance of 0.
3. While there are still vertices left in the priority queue, select the vertex
with the smallest recorded distance from the starting vertex and visit its
neighboring vertices.
4. For each neighboring vertex, check if it is visited already or not. If it
isnt visited yet, calculate its tentative distance by adding its weight
to the smallest distance found so far for its parent/previous node
(starting vertex in case of first-level vertices).
5. If this tentative distance is smaller than previously recorded value
(if any), update it in our distances array.
6. Finally, add this visited vertex with its updated distance to our priority
queue and repeat step-3 until we have reached our destination or exhausted
all nodes.
"""
# https://pieriantraining.com/understanding-dijkstras-algorithm-in-python/
def dijkstra_algorithm(graph, start_node):
def min_distance(distances, visited):
min_val = float("inf")
min_index = -1
for i in range(len(distances)):
if distances[i] < min_val and i not in visited:
min_val = distances[i]
min_index = i
return min_index
num_nodes = len(graph)
distances = [float("inf")] * num_nodes
visited = []
distances[start_node] = 0
for i in range(num_nodes):
current_node = min_distance(distances, visited)
visited.append(current_node)
for j in range(num_nodes):
if graph[current_node][j] != 0:
new_distance = distances[current_node] + graph[current_node][j]
if new_distance < distances[j]:
distances[j] = new_distance
return distances

View file

@ -41,7 +41,7 @@ def part_2(data):
if __name__ == "__main__":
with open(f"./input/01.txt", "r") as f:
with open("./input/01.txt", "r") as f:
inp = f.read().strip()
a = part_1(inp)

View file

@ -39,7 +39,7 @@ def part_2(data):
if __name__ == "__main__":
with open(f"./input/02.txt", "r") as f:
with open("./input/02.txt", "r") as f:
inp = f.read().strip()
a = part_1(inp)

View file

@ -56,7 +56,7 @@ def presolve(data):
if __name__ == "__main__":
with open(f"./input/03.txt", "r") as f:
with open("./input/03.txt", "r") as f:
inp = f.read().strip()
parsed = presolve(inp)

View file

@ -27,7 +27,7 @@ def presolve(data):
b = set(re.findall(r"\d+", b))
ab = len(a & b)
if ab > 0:
scores.append(2**(ab - 1))
scores.append(2 ** (ab - 1))
count[cid] += 1
for i in range(cid + 1, cid + ab + 1):
count[i] += count[cid]
@ -35,7 +35,7 @@ def presolve(data):
if __name__ == "__main__":
with open(f"./input/04.txt", "r") as f:
with open("./input/04.txt", "r") as f:
inp = f.read().strip()
inp = presolve(inp)

View file

@ -1,7 +1,5 @@
import re
from itertools import repeat
from math import inf
from multiprocessing import Pool, freeze_support
from output import answer
n = 5
@ -9,91 +7,67 @@ title = "If You Give A Seed A Fertilizer"
@answer(1, "Nearest location for seed id list is {}")
def part_1(data):
seeds, *process = data.split("\n\n")
seeds = [f"{v} 1" for v in seeds.split()[1:]]
return _bruteforce(seeds, process, 1)
def part_1(presolved):
l, _ = presolved
return l
@answer(2, "Interpreting ranges of seeds, nearest location is {}")
def part_2(data):
def part_2(presolved):
_, l = presolved
return l
def presolve(data):
seeds, *process = data.split("\n\n")
seeds = re.findall(r"\d+ \d+", seeds)
return _bruteforce(seeds, process, 8)
seed_ranges = [[int(x) for x in ar.split()] for ar in re.findall(r"\d+ \d+", seeds)]
seed_values = [int(v) for v in seeds.split()[1:]]
processes = [
[tuple(map(int, line.split())) for line in step.splitlines()[1:]]
for step in process
]
p1 = _process(seed_values, processes)
p2 = 26829000 # takes 5m if starting from 0
while True:
g = _process([p2], processes, reverse=True)
if any(g >= a and g < a + r for a, r in seed_ranges):
break
p2 += 1
return p1, p2
def _bruteforce(seeds, process, p=1):
processes = [[tuple(map(int, line.split())) for line in step.splitlines()[1:]] for step in process]
sm = []
for start_r in seeds:
pool = Pool()
start, r = start_r.split()
d = int(r) // p
parts = [(d * n + int(start), d * n + int(start) + d) for n in range(p)]
sm += pool.starmap(_nearest, zip(parts, repeat(processes)))
return min(sm)
def _process(seeds, processes, reverse=False):
n = inf
for start in seeds:
n = min(n, _nearest(start, processes, reverse=reverse))
return n
def _nearest(start_r, processes):
a, b = start_r
nearest = inf
for i in range(a, b):
v = i
for steps in processes:
nid = -1
for line in steps:
dest, src, r = line
if v >= src and v < src + r:
v = dest + v - src
break
nearest = min(nearest, v)
return nearest
def _nearest(start, processes, reverse=False):
procs = processes if not reverse else processes[::-1]
v = start
for steps in procs:
for line in steps:
dest, src, r = line
if reverse:
dest, src = src, dest
if v >= src and v < src + r:
v = dest + v - src
break
return v
if __name__ == "__main__":
# use dummy data
inp = """
seeds: 79 14 55 13
with open("./input/05.txt", "r") as f:
inp = f.read().strip()
seed-to-soil map:
50 98 2
52 50 48
soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15
fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4
water-to-light map:
88 18 7
18 25 70
light-to-temperature map:
45 77 23
81 45 19
68 64 13
temperature-to-humidity map:
0 69 1
1 0 69
humidity-to-location map:
60 56 37
56 93 4
""".strip()
# inp = parse_input()
inp = presolve(inp)
a = part_1(inp)
b = part_2(inp)
# assert a == 278755257
# assert b == 26829166
assert a == 278755257
assert b == 26829166

View file

@ -35,7 +35,7 @@ def presolve(data):
if __name__ == "__main__":
with open(f"./input/06.txt", "r") as f:
with open("./input/06.txt", "r") as f:
inp = f.read().strip()
inp = presolve(inp)

View file

@ -84,7 +84,7 @@ if __name__ == "__main__":
doctest.testmod()
with open(f"./input/07.txt", "r") as f:
with open("./input/07.txt", "r") as f:
inp = f.read().strip()
a = part_1(inp)

View file

@ -49,7 +49,7 @@ def presolve(data):
if __name__ == "__main__":
with open(f"./input/08.txt", "r") as f:
with open("./input/08.txt", "r") as f:
inp = f.read().strip()
inp = presolve(inp)

View file

@ -30,7 +30,7 @@ def _solve(lines):
if __name__ == "__main__":
with open(f"./input/09.txt", "r") as f:
with open("./input/09.txt", "r") as f:
inp = f.read().strip()
a = part_1(inp)

View file

@ -1,44 +1,94 @@
from collections import deque
from output import answer
n = 10
title = "dddd"
title = "Pipe Maze"
@answer(1, "Answer is {}")
def part_1(data):
return data
D = (-1, 0), (0, 1), (1, 0), (0, -1)
C = {
(-1, 0): ["|", "7", "F"],
(0, 1): ["-", "7", "J"],
(1, 0): ["|", "L", "J"],
(0, -1): ["-", "L", "F"],
}
A = {
"S": [0, 1, 2, 3],
"-": [1, 3],
"|": [0, 2],
"F": [1, 2],
"L": [0, 1],
"7": [2, 3],
"J": [0, 3],
}
@answer(2, "Actually, answer is {}")
def part_2(data):
return data
@answer(1, "Farthest away pipe is at {}")
def part_1(presolved):
return presolved[0]
# uncomment to solve parts in one go
# def presolve(data):
# return data
@answer(2, "{} spots are encapsulated by pipes")
def part_2(presolved):
return presolved[1]
def presolve(data):
matrix = data.split()
w = len(matrix[0])
h = len(matrix)
q = deque()
visited = set()
for r in range(h):
if "S" in matrix[r]:
start = (r, matrix[r].index("S"))
q.append(start)
break
while q:
o = q.popleft()
visited.add(o)
for di in A[matrix[o[0]][o[1]]]:
d = D[di]
r = o[0] + d[0]
c = o[1] + d[1]
if r >= 0 and r < h and c >= 0 and c < w:
t = matrix[r][c]
p = (r, c)
if p not in visited and t != "." and t in C[d]:
q.append(p)
p1 = len(visited) // 2
p2 = 0
for y in range(h):
for x in range(w):
if (y, x) in visited:
continue
crosses = 0
y2, x2 = y, x
while y2 < h and x2 < w:
c2 = matrix[y2][x2]
if (y2, x2) in visited and c2 not in "L7":
crosses += 1
x2 += 1
y2 += 1
if crosses % 2 == 1:
p2 += 1
return p1, p2
if __name__ == "__main__":
# use dummy data
inp = """
replace me
""".strip()
with open("./input/10.txt", "r") as f:
inp = f.read()
# uncomment to instead use stdin
# import sys; inp = sys.stdin.read().strip()
# uncomment to use AoC provided puzzle input
# with open(f"./input/10.txt", "r") as f:
# inp = f.read()
# uncomment to do initial data processing shared by part 1-2
# inp = presolve(inp)
inp = presolve(inp)
a = part_1(inp)
# b = part_2(inp)
b = part_2(inp)
# uncomment and replace 0 with actual output to refactor code
# and ensure nonbreaking changes
# assert a == 0
# assert b == 0
assert a == 6846
assert b == 325

View file

@ -0,0 +1,63 @@
from itertools import combinations
from output import answer
n = 11
title = "Cosmic Expansion"
@answer(1, "Sum of all galaxy shortest distances is {}")
def part_1(data):
return data[0]
@answer(2, "Exapanding by 1M, sum is {}")
def part_2(data):
return data[1]
def presolve(data):
m = data.splitlines()
er = set()
ec = set()
for i, r in enumerate(m):
if "#" not in r:
er.add(i)
for i, c in enumerate(zip(*m)):
if "#" not in c:
ec.add(i)
h = len(m)
w = len(m[0])
g1 = []
g2 = []
e = 1e6
for r in range(h):
for c in range(w):
if m[r][c] == "#":
ro = len(er & set(range(r)))
co = len(ec & set(range(c)))
g1.append((r + ro, c + co))
g2.append((ro * e + r - ro, co * e + c - co))
p1 = sum(
abs(rc1[0] - rc2[0]) + abs(rc1[1] - rc2[1]) for rc1, rc2 in combinations(g1, 2)
)
p2 = int(
sum(
abs(rc1[0] - rc2[0]) + abs(rc1[1] - rc2[1])
for rc1, rc2 in combinations(g2, 2)
)
)
return p1, p2
if __name__ == "__main__":
with open("./input/11.txt", "r") as f:
inp = f.read().strip()
inp = presolve(inp)
a = part_1(inp)
b = part_2(inp)
assert a == 9370588
assert b == 746207878188

View file

@ -0,0 +1,64 @@
from functools import cache
from output import answer
n = 12
title = "Hot Springs"
@answer(1, "sum of all possible combinations is {}")
def part_1(presolved):
return presolved[0]
@answer(2, "sum of all possible combinations is {} when unfolded")
def part_2(presolved):
return presolved[1]
def presolve(data):
lines = [
(a, list(map(int, b.split(","))))
for a, b in (line.split() for line in data.splitlines())
]
p1 = sum(_inspect(a, tuple(b)) for a, b in lines)
p2 = sum(_inspect("?".join([a] * 5), tuple(b * 5)) for a, b in lines)
return p1, p2
@cache
def _inspect(s, cs):
r = len(s)
csl = len(cs)
if r == 0:
return 1 if csl == 0 else 0
o, *f = s
f = "".join(f)
if o == ".":
return _inspect(f, cs)
if o == "?":
return _inspect("." + f, cs) + _inspect("#" + f, cs)
if not csl:
return 0
g = cs[0]
if g > r or "." in s[0:g]:
return 0
elif csl > 1:
if g + 1 > r or s[g] == "#":
return 0
else:
return _inspect(s[g + 1 :], cs[1:])
elif csl == 1:
return _inspect(s[g:], ())
if __name__ == "__main__":
with open("./input/12.txt", "r") as f:
inp = f.read().strip()
inp = presolve(inp)
a = part_1(inp)
b = part_2(inp)
assert a == 7118
assert b == 7030194981795

View file

@ -0,0 +1,56 @@
from output import answer
n = 13
title = "Point of Incidence"
@answer(1, "Summarizing the notes gives {}")
def part_1(presolved):
return presolved[0]
@answer(2, "Summarizing the notes allowing off-by-1 gives {}")
def part_2(presolved):
return presolved[1]
def presolve(data):
g = [l.split() for l in data.split("\n\n")]
p1 = sum(d * n for d, n in _inspect(g))
p2 = sum(d * n for d, n in _inspect(g, 1))
return p1, p2
def _inspect(g, a=0):
af = []
for m in g:
for d, n in [(100, m), (1, tuple(zip(*m)))]:
af.append((d, _compare(n, a)))
return af
def _compare(l, a=0):
for i in range(1, len(l)):
if (
sum(
sum(a != b for a, b in zip(x, y)) for x, y in zip(l[i - 1 :: -1], l[i:])
)
== a
):
return i
return 0
if __name__ == "__main__":
with open("./input/13.txt", "r") as f:
inp = f.read().strip()
inp = presolve(inp)
a = part_1(inp)
b = part_2(inp)
assert a == 29213
assert b == 37453

View file

@ -0,0 +1,83 @@
from output import answer
n = 14
title = "Parabolic Reflector Dish"
@answer(1, "Total initial load on the northern beams: {}")
def part_1(presolved):
return presolved[0]
@answer(2, "After some humble load testing, the northern beam load is {}")
def part_2(presolved):
return presolved[1]
BAEST = 1000_000_000
def presolve(data):
m = [list(l) for l in data.split()]
s = len(m[0])
m1 = _tilt(m)
p1 = sum(sum((s - w) * o.count("O") for o in r) for w, r in enumerate(m1))
def impl(rc):
return "".join(["".join(r) for r in rc])
i = 0
seen = []
while True:
i += 1
for _ in range(4):
m = _tilt(m)
m = _rotate(m)
im = impl(m)
if im in seen:
break
else:
seen.append(im)
m2 = m
c = seen.index(im) + 1
for _ in range((BAEST - i) % (i - c)):
for j in range(4):
m2 = _tilt(m2)
m2 = _rotate(m2)
p2 = sum(sum((s - w) * o.count("O") for o in r) for w, r in enumerate(m2))
return p1, p2
def _rotate(m):
return [list(l) for l in zip(*m[::-1])]
def _tilt(m):
m = [list(l) for l in zip(*m)]
h = len(m[0])
for c in m:
u = True
while u:
u = False
for i in range(h - 1):
j = i + 1
if c[i] == "#" or c[j] == "#":
continue
if c[i] < c[j]:
c[j], c[i] = c[i], c[j]
u = True
return [list(l) for l in zip(*m)]
if __name__ == "__main__":
with open("./input/14.txt", "r") as f:
inp = f.read().strip()
inp = presolve(inp)
a = part_1(inp)
b = part_2(inp)
assert a == 109596
assert b == 96105

View file

@ -0,0 +1,63 @@
from collections import OrderedDict, defaultdict
from output import answer
n = 15
title = "Lens Library"
@answer(1, "Sum of HASH algorithm results: {}")
def part_1(presolved):
return presolved[0]
@answer(2, "Focusing power of the resulting configuration: {}")
def part_2(presolved):
return presolved[1]
def presolve(data):
def h(s):
v = 0
for a in s:
if a == "\n":
continue
v += ord(a)
v *= 17
v = v % 256
return v
p1 = sum(h(c) for c in data.split(","))
b = defaultdict(OrderedDict)
for lr in data.split(","):
if "=" in lr:
l, r = lr.split("=")
if r == "":
continue
k = h(l)
b[k][l] = r
if "-" in lr:
l, _r = lr.split("-")
k = h(l)
if l in b[k]:
del b[k][l]
p2 = 0
for i, c in b.items():
for j, f in enumerate(b[i].values(), 1):
p2 += (i + 1) * j * int(f)
return p1, p2
if __name__ == "__main__":
with open("./input/16.txt", "r") as f:
inp = f.read().strip()
inp = presolve(inp)
a = part_1(inp)
b = part_2(inp)
assert a == 509784
assert b == 230197

View file

@ -0,0 +1,86 @@
from itertools import chain
from output import D, answer, matrix
n = 16
title = "The Floor Will Be Lava"
@answer(1, "Energized tiles count, starting at top-left facing right: {}")
def part_1(presolved):
return presolved[0]
@answer(2, "Max energized tiles count, starting from all edges: {}")
def part_2(presolved):
return presolved[1]
def presolve(data):
m, w, h = matrix(data)
p1 = 0
p2 = 0
for sp in chain(
[(h - 1, n, 0) for n in range(w)],
[(n, 0, 1) for n in range(h)],
[(0, n, 2) for n in range(w)],
[(n, w - 1, 3) for n in range(h)],
):
q = [sp]
seen = set()
while q:
rcd = q.pop(0)
if (rcd) in seen:
continue
r, c, d = rcd
if r < 0 or r >= h or c < 0 or c >= w:
continue
seen.add((r, c, d))
match m[r][c]:
case ".":
o1, o2 = D[d]
q.append((o1 + r, o2 + c, d))
case "|":
if d in [0, 2]:
o1, o2 = D[d]
q.append((o1 + r, o2 + c, d))
else:
for d in [(d - 1) % 4, (d + 1) % 4]:
o1, o2 = D[d]
q.append((o1 + r, o2 + c, d))
case "-":
if d in [1, 3]:
o1, o2 = D[d]
q.append((o1 + r, o2 + c, d))
else:
for d in [(d - 1) % 4, (d + 1) % 4]:
o1, o2 = D[d]
q.append((o1 + r, o2 + c, d))
case "\\":
d += 1 if d in [1, 3] else -1
d = d % 4
o1, o2 = D[d]
q.append((o1 + r, o2 + c, d))
case "/":
d += 1 if d in [0, 2] else -1
d = d % 4
o1, o2 = D[d % 4]
q.append((o1 + r, o2 + c, d))
b = len(set([(r, c) for r, c, d in seen]))
if sp == (0, 0, 1):
p1 = b
p2 = max(p2, b)
return p1, p2
if __name__ == "__main__":
with open("./input/16.txt", "r") as f:
inp = f.read().strip()
inp = presolve(inp)
a = part_1(inp)
b = part_2(inp)
assert a == 7884
assert b == 8185

View file

@ -0,0 +1,100 @@
from heapq import heappop, heappush
from output import answer # D, DD, ADJ, ints, mhd, mdbg, vdbg
n = 17
title = "Clumsy Crucible"
@answer(1, "Using std crucible, least heat loss incured: {}")
def part_1(presolved):
return presolved[0]
@answer(2, "Using ultra crucible, least heat loss incured: {}")
def part_2(presolved):
return presolved[1]
def solve(data):
grid = {
(r, c): int(col)
for r, row in enumerate(data.split())
for c, col in enumerate(row)
}
p1 = least_heat_loss(grid, 1, 3)
p2 = least_heat_loss(grid, 4, 10)
return p1, p2
def least_heat_loss(grid, minsteps, maxsteps):
target = max(grid)
seen = set()
queue = [(0, (0, 0), (0, 1), 0)]
while queue:
cost, pos, direction, steps = heappop(queue)
y, x = pos
dy, dx = direction
if pos == target:
return cost
if ((pos, direction, steps)) in seen:
continue
seen.add((pos, direction, steps))
if steps >= minsteps:
cwdy, cwdx = clockwise(*direction)
if (cw := (y + cwdy, x + cwdx)) in grid:
cwy, cwx = cw
heappush(queue, (cost + grid[cw], (cwy, cwx), (cwdy, cwdx), 1))
ccwdy, ccwdx = counterclockwise(*direction)
if (ccw := (y + ccwdy, x + ccwdx)) in grid:
ccwy, ccwx = ccw
heappush(queue, (cost + grid[ccw], (ccwy, ccwx), (ccwdy, ccwdx), 1))
if steps < maxsteps and (fwd := (y + dy, x + dx)) in grid:
fwdy, fwdx = fwd
heappush(queue, (cost + grid[fwd], (fwdy, fwdx), direction, steps + 1))
return -1
def clockwise(y, x):
"""
>>> clockwise(-1, 0)
(0, 1)
>>> clockwise(0, 1)
(1, 0)
>>> clockwise(1, 0)
(0, -1)
>>> clockwise(0, -1)
(-1, 0)
"""
return (x, y) if y == 0 else (x, -y)
def counterclockwise(y, x):
"""
>>> counterclockwise(-1, 0)
(0, -1)
>>> counterclockwise(0, -1)
(1, 0)
>>> counterclockwise(1, 0)
(0, 1)
>>> counterclockwise(0, 1)
(-1, 0)
"""
return (x, y) if x == 0 else (-x, y)
if __name__ == "__main__":
with open("./input/17.txt", "r") as f:
inp = f.read().strip()
inp = solve(inp)
a = part_1(inp)
b = part_2(inp)