commit 97a4330bc0000e92b3a751f967652ee065d52c6c
Author: Geoffrey “Frogeye” Preud'homme <geoffrey@frogeye.fr>
Date:   Wed Dec 25 12:58:02 2024 +0100

    Initial commit

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3923250
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+# Don't include prompts and inputs as they're not free to copy
+**/input*
+**/prompt
+# Demos are also part of the prompt...
+**/demo*
+# ... except when I wrote those myself
+!**/demog*
diff --git a/2023/1/obfuscated.py b/2023/1/obfuscated.py
new file mode 100644
index 0000000..b7ea355
--- /dev/null
+++ b/2023/1/obfuscated.py
@@ -0,0 +1,5 @@
+import re
+d="zero|one|two|three|four|five|six|seven|eight|nine|\\d)"
+def f(l,p):
+ m=re.search(p,l)[1];return str(d.split("|").index(m)) if m in d else m
+print(sum(map(lambda l:int(f(l,f"({d}.*")+f(l,".*("+d)),open(0).readlines())))
diff --git a/2023/1/script.py b/2023/1/script.py
new file mode 100644
index 0000000..6514aa3
--- /dev/null
+++ b/2023/1/script.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python3
+
+import re
+
+digits = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]
+group = "|".join(digits + ["[0-9]"])
+
+tot = 0
+with open("lines.txt") as lines:
+    for line in lines.readlines():
+        print()
+        line = line.rstrip()
+        print(line)
+        last = re.search(rf"^.*({group})", line)
+        first = re.search(rf"({group}).*$", line)
+        print(first, last)
+        f = first[1]
+        l = last[1]
+        print(f, l)
+        if f in digits:
+            f = str(digits.index(f))
+        if l in digits:
+            l = str(digits.index(l))
+        print(f, l)
+        numb = int(f + l)
+        tot += numb
+        print(numb)
+print()
+
+print(tot)
diff --git a/2023/2/one.py b/2023/2/one.py
new file mode 100644
index 0000000..cdf394c
--- /dev/null
+++ b/2023/2/one.py
@@ -0,0 +1,28 @@
+maxs = {"red": 12, "green": 13, "blue": 14}
+
+gid = 0
+possible_gid_sum = 0
+
+with open("input") as lines:
+    for line in lines.readlines():
+        gid += 1
+        line = line.rstrip()
+        game_full, sets = line.split(":")
+        game, gid_str = game_full.split(" ")
+        assert int(gid_str) == gid
+        possible = True
+        for seet in sets.split(";"):
+            gcs = {"red": 0, "green": 0, "blue": 0}
+            for color in seet.split(","):
+                amount_str, color = color.strip().split(" ")
+                amount = int(amount_str)
+                gcs[color] += amount
+            for color, amount in gcs.items():
+                max = maxs[color]
+                if amount > max:
+                    possible = False
+        if possible:
+            possible_gid_sum += gid
+        print(gid, possible)
+
+print(possible_gid_sum)
diff --git a/2023/2/two.py b/2023/2/two.py
new file mode 100644
index 0000000..16443bb
--- /dev/null
+++ b/2023/2/two.py
@@ -0,0 +1,24 @@
+maxs = {"red": 12, "green": 13, "blue": 14}
+
+gid = 0
+power_sum = 0
+
+with open("input") as lines:
+    for line in lines.readlines():
+        gid += 1
+        line = line.rstrip()
+        game_full, sets = line.split(":")
+        game, gid_str = game_full.split(" ")
+        assert int(gid_str) == gid
+        possible = True
+        gcs = {"red": 0, "green": 0, "blue": 0}
+        for seet in sets.split(";"):
+            for color in seet.split(","):
+                amount_str, color = color.strip().split(" ")
+                amount = int(amount_str)
+                gcs[color] = max(amount, gcs[color])
+        power = gcs["red"] * gcs["green"] * gcs["blue"]
+        print(gid, power)
+        power_sum += power
+
+print(power_sum)
diff --git a/2023/24/one.py b/2023/24/one.py
new file mode 100644
index 0000000..5deaaf0
--- /dev/null
+++ b/2023/24/one.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+hailstones = []
+
+with open(input_file) as fd:
+    for line in fd.readlines():
+        line = line.rstrip()
+        line.replace("@", ",")
+        hailstone = [int(h) for h in line.split(",")]
+        hailstones.append(hailstone)
+
+
diff --git a/2023/3/one.py b/2023/3/one.py
new file mode 100644
index 0000000..e9d18ab
--- /dev/null
+++ b/2023/3/one.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+sum = 0
+for i in range(height):
+    line = lines[i]
+    pn_str = ""
+    for j in range(width):
+        c = line[j]
+        # print(19, c)
+        if c.isnumeric():
+            if not pn_str:
+                left = j
+            pn_str += c
+            # print(20, c, pn_str)
+        if pn_str and (j == width - 1 or not line[j + 1].isnumeric()):
+            print(25, pn_str)
+            adj = False
+            for ii in range(max(i - 1, 0), min(i + 1, height - 1) + 1):
+                for jj in range(max(left - 1, 0), min(j + 1, width - 1) + 1):
+                    cc = lines[ii][jj]
+                    print(ii, jj, cc)
+                    if not cc.isnumeric() and cc != ".":
+                        print("!")
+                        adj = True
+            # print(pn_str, adj)
+            if adj:
+                pn = int(pn_str)
+                sum += pn
+            pn_str = ""
+
+print(sum)
diff --git a/2023/3/two.py b/2023/3/two.py
new file mode 100644
index 0000000..69193b1
--- /dev/null
+++ b/2023/3/two.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+gears = dict()
+
+sum = 0
+for i in range(height):
+    line = lines[i]
+    pn_str = ""
+    for j in range(width):
+        c = line[j]
+        # print(19, c)
+        if c.isnumeric():
+            if not pn_str:
+                left = j
+            pn_str += c
+            # print(20, c, pn_str)
+        if pn_str and (j == width - 1 or not line[j + 1].isnumeric()):
+            for ii in range(max(i - 1, 0), min(i + 1, height - 1) + 1):
+                for jj in range(max(left - 1, 0), min(j + 1, width - 1) + 1):
+                    cc = lines[ii][jj]
+                    # print(ii, jj, cc)
+                    if cc == "*":
+                        gears.setdefault((ii, jj), list())
+                        gears[(ii, jj)].append(int(pn_str))
+            pn_str = ""
+for gear_numbers in gears.values():
+    if len(gear_numbers) != 2:
+        continue
+    gear_ratio = gear_numbers[0] * gear_numbers[1]
+    sum += gear_ratio
+
+print(sum)
diff --git a/2024/1/one.py b/2024/1/one.py
new file mode 100644
index 0000000..f8f782a
--- /dev/null
+++ b/2024/1/one.py
@@ -0,0 +1,26 @@
+
+listl = []
+listr = []
+with open("input") as lines:
+    for line in lines.readlines():
+        line = line.rstrip()
+        print(line)
+        spli = line.split(" ")
+        listl.append(int(spli[0]))
+        listr.append(int(spli[-1]))
+
+assert len(listl) == len(listr)
+
+listl.sort()
+listr.sort()
+
+dtot = 0
+
+for i in range(len(listl)):
+    l = listl[i]
+    r = listr[i]
+    d = abs(l-r)
+    dtot += d
+    print(l, r, d)
+
+print(dtot)
diff --git a/2024/1/two.py b/2024/1/two.py
new file mode 100644
index 0000000..5f3e8c3
--- /dev/null
+++ b/2024/1/two.py
@@ -0,0 +1,25 @@
+
+listl = []
+listr = []
+with open("input") as lines:
+    for line in lines.readlines():
+        line = line.rstrip()
+        print(line)
+        spli = line.split(" ")
+        listl.append(int(spli[0]))
+        listr.append(int(spli[-1]))
+
+assert len(listl) == len(listr)
+
+listl.sort()
+listr.sort()
+
+dtot = 0
+
+for i in range(len(listl)):
+    l = listl[i]
+    d = listr.count(l) * l
+    dtot += d
+    print(l, d)
+
+print(dtot)
diff --git a/2024/10/one.py b/2024/10/one.py
new file mode 100644
index 0000000..3fb0210
--- /dev/null
+++ b/2024/10/one.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+tmap: list[list[int]] = [[int(a) for a in line] for line in lines]
+
+directions = [
+    (0, 1),
+    (0, -1),
+    (1, 0),
+    (-1, 0),
+]
+
+
+def print_path(path: list[tuple[int, int]]) -> None:
+    viz = [["."] * width for _ in range(height)]
+    for c, pos in enumerate(path):
+        i, j = pos
+        viz[i][j] = str(c)
+    for line in viz:
+        print("".join(line))
+    print()
+
+
+def score(pos: tuple[int, int], path: list[tuple[int, int]]) -> set[tuple[int, int]]:
+    path = path + [pos]
+    i, j = pos
+    c = tmap[i][j]
+    if c == 9:
+        return {pos}
+    reachable = set()
+    for direction in directions:
+        ii, jj = i + direction[0], j + direction[1]
+        if ii not in range(height) or jj not in range(width):
+            continue
+        cc = tmap[ii][jj]
+        if cc != c + 1:
+            continue
+        reachable |= score((ii, jj), path)
+    return reachable
+
+
+tscore = 0
+for i in range(height):
+    for j in range(width):
+        c = tmap[i][j]
+        if c != 0:
+            continue
+        cscore = len(score((i, j), []))
+        # print(i, j, cscore)
+        tscore += cscore
+
+print(tscore)
diff --git a/2024/10/two.py b/2024/10/two.py
new file mode 100644
index 0000000..b95cbe1
--- /dev/null
+++ b/2024/10/two.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+tmap: list[list[int]] = [[int(a) for a in line] for line in lines]
+
+directions = [
+    (0, 1),
+    (0, -1),
+    (1, 0),
+    (-1, 0),
+]
+
+
+def print_path(path: list[tuple[int, int]]) -> None:
+    viz = [["."] * width for _ in range(height)]
+    for c, pos in enumerate(path):
+        i, j = pos
+        viz[i][j] = str(c)
+    for line in viz:
+        print("".join(line))
+    print()
+
+
+def score(pos: tuple[int, int], path: list[tuple[int, int]]) -> int:
+    path = path + [pos]
+    i, j = pos
+    c = tmap[i][j]
+    if c == 9:
+        # print_path(path)
+        return 1
+    cscore = 0
+    for direction in directions:
+        ii, jj = i + direction[0], j + direction[1]
+        if ii not in range(height) or jj not in range(width):
+            continue
+        cc = tmap[ii][jj]
+        if cc != c + 1:
+            continue
+        cscore += score((ii, jj), path)
+    return cscore
+
+
+tscore = 0
+for i in range(height):
+    for j in range(width):
+        c = tmap[i][j]
+        if c != 0:
+            continue
+        cscore = score((i, j), [])
+        print(i, j, cscore)
+        tscore += cscore
+    #     break
+    # break
+
+print(tscore)
diff --git a/2024/11/one.py b/2024/11/one.py
new file mode 100644
index 0000000..bbbfd0e
--- /dev/null
+++ b/2024/11/one.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+stones = [int(stone) for stone in lines[0].split()]
+
+for _ in range(25):
+    new_stones = []
+    for stone in stones:
+        stone_str = str(stone)
+        if stone == 0:
+            new_stones.append(1)
+        elif len(stone_str) % 2 == 0:
+            mid = int(len(stone_str) / 2)
+            new_stones.append(int(stone_str[:mid]))
+            new_stones.append(int(stone_str[mid:]))
+        else:
+            new_stones.append(stone * 2024)
+    stones = new_stones
+    # print(" ".join(str(stone) for stone in stones))
+
+print(len(stones))
diff --git a/2024/11/two.py b/2024/11/two.py
new file mode 100644
index 0000000..09ef476
--- /dev/null
+++ b/2024/11/two.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+
+import bisect
+import sys
+import time
+import functools
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+start = time.time()
+
+stones = [int(stone) for stone in lines[0].split()]
+
+# for i in range(75):
+#     print(i, time.time() - start, len(stones))
+#     # for s, stone in enumerate(stones):
+#     #     if stone == 0:
+#     #         stones[s] = 1
+#     #         continue
+#     #     stone_str = str(stone)
+#     #     if len(stone_str) % 2 == 0:
+#     #         mid = int(len(stone_str) / 2)
+#     #         stones[s] = int(stone_str[:mid])
+#     #         stones.insert(s, int(stone_str[mid:]))
+#     #     else:
+#     #         stones[s] *= 2024
+#     new_stones = []
+#     for stone in stones:
+#         stone_str = str(stone)
+#         if stone == 0:
+#             new_stones.append(1)
+#         elif len(stone_str) % 2 == 0:
+#             mid = int(len(stone_str) / 2)
+#             new_stones.append(int(stone_str[:mid]))
+#             new_stones.append(int(stone_str[mid:]))
+#         else:
+#             new_stones.append(stone * 2024)
+#     stones = new_stones
+
+target = int(sys.argv[2])
+
+its = [0] * len(stones)
+
+start_stones = stones.copy()
+
+
+pow10 = list()
+for i in range(25):
+    pow10.append(10**i)
+
+
+def num_digits(a: int) -> int:
+    # for i, p in enumerate(pow10):
+    #     if a < p:
+    #         # assert len(str(a)) == i
+    #         return i
+    # raise NotImplementedError
+    # return bisect.bisect(pow10, a)
+    # # nb = bisect.bisect(pow10, a)
+    num = 0
+    while a > 0:
+        num += 1
+        a //= 10
+    # assert nb == num
+    return num
+
+
+# lstones = 0
+# for e, sstone in enumerate(start_stones):
+#     print(f"47 {e}/{len(start_stones)} {time.time() - start}")
+#     stones = [sstone]
+#     while stones:
+#         stone = stones.pop(0)
+#         it = its.pop(0)
+#         lstones += 1
+#         if stone == 0:
+#             stone = 1
+#             it += 1
+#         nd = num_digits(stone)
+#         for i in range(it, target):
+#             print(stone)
+#             if nd % 2 == 0:
+#                 mid = nd // 2
+#                 left, right = divmod(stone, pow10[mid])
+#                 # left, right = divmod(stone, 10**mid)
+#                 stone = left
+#                 stones.insert(0, right)
+#                 its.insert(0, i + 1)
+#                 nd = mid
+#             else:
+#                 stone *= 2024
+#                 nd = num_digits(stone)
+#         # print(f"64 {stone}")
+
+# @functools.lru_cache(maxsize=1024)
+@functools.cache
+def proc(stone: int, target: int) -> int:
+    if target == 0:
+        return 1
+    target -= 1
+    if stone == 0:
+        return proc(1, target)
+    nd = num_digits(stone)
+    if nd % 2 == 0:
+        mid = nd // 2
+        left, right = divmod(stone, pow10[mid])
+        return proc(left, target) + proc(right, target)
+    else:
+        return proc(stone * 2024, target)
+
+
+lstones = 0
+for e, stone in enumerate(stones):
+    print(f"47 {e}/{len(stones)} {time.time() - start}")
+    lstones += proc(stone, target)
+
+print(f"{target=}")
+print(lstones)
diff --git a/2024/12/one.py b/2024/12/one.py
new file mode 100644
index 0000000..506ec1d
--- /dev/null
+++ b/2024/12/one.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+visited: set[tuple[int, int]] = set()
+
+directions = [
+    (0, 1),
+    (0, -1),
+    (1, 0),
+    (-1, 0),
+]
+
+
+def get_region(
+    pos: tuple[int, int], region: set[tuple[int, int]]
+) -> set[tuple[int, int]]:
+    region.add(pos)
+    i, j = pos
+    ochar = lines[i][j]
+    for direction in directions:
+        i, j = pos[0] + direction[0], pos[1] + direction[1]
+        if i not in range(height) or j not in range(width):
+            continue
+        if (i, j) in region:
+            continue
+        char = lines[i][j]
+        if char != ochar:
+            continue
+        region |= get_region((i, j), region)
+    return region
+
+
+def get_perimeter(region: set[tuple[int, int]]) -> int:
+    peri = 0
+    for axis in (0, 1):
+        oaxis = 0 if axis else 1
+        iss = set([pos[axis] for pos in region])
+        print(47, iss, peri)
+        for i in iss:
+            line = [pos[oaxis] for pos in region if pos[axis] == i]
+            line.sort()
+            last_j = None
+            for j in line:
+                if last_j is None:
+                    peri += 1
+                elif last_j == j - 1:
+                    pass
+                else:
+                    peri += 2
+                last_j = j
+            if last_j is not None:
+                peri += 1
+            print(62, i, peri, line)
+    return peri
+
+
+tprice = 0
+for i in range(height):
+    for j in range(width):
+        pos = i, j
+        if pos in visited:
+            continue
+        region = get_region(pos, set())
+        visited |= region
+
+        area = len(region)
+        peri = get_perimeter(region)
+        price = area * peri
+        tprice += price
+
+        char = lines[i][j]
+        print(f"{char}: {area} × {peri} = {price}$")
+
+print(tprice)
diff --git a/2024/12/reddit_test b/2024/12/reddit_test
new file mode 100644
index 0000000..eeb664d
--- /dev/null
+++ b/2024/12/reddit_test
@@ -0,0 +1,5 @@
+AAXXX
+AAXAX
+AAAAX
+AAXAX
+AAXXX
diff --git a/2024/12/two.py b/2024/12/two.py
new file mode 100644
index 0000000..ac1b48f
--- /dev/null
+++ b/2024/12/two.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+visited: set[tuple[int, int]] = set()
+
+directions = [
+    (0, 1),
+    (0, -1),
+    (1, 0),
+    (-1, 0),
+]
+
+
+def get_region(
+    pos: tuple[int, int], region: set[tuple[int, int]]
+) -> set[tuple[int, int]]:
+    region.add(pos)
+    i, j = pos
+    ochar = lines[i][j]
+    for direction in directions:
+        i, j = pos[0] + direction[0], pos[1] + direction[1]
+        if i not in range(height) or j not in range(width):
+            continue
+        if (i, j) in region:
+            continue
+        char = lines[i][j]
+        if char != ochar:
+            continue
+        region |= get_region((i, j), region)
+    return region
+
+
+# def get_sides(region: set[tuple[int, int]]) -> int:
+#     peri = 0
+#     for axis in (0, 1):
+#         oaxis = 0 if axis else 1
+#         iss = set([pos[axis] for pos in region])
+#         sta: set[int] = set()
+#         sto: set[int] = set()
+#         for i in iss:
+#             line = [pos[oaxis] for pos in region if pos[axis] == i]
+#             line.sort()
+#             last_j = None
+#             for j in line:
+#                 if last_j is None:
+#                     sta.add(j)
+#                 elif last_j == j - 1:
+#                     pass
+#                 else:
+#                     sta.add(j)
+#                     sto.add(last_j)
+#                 last_j = j
+#             if last_j is not None:
+#                 sto.add(last_j)
+#         peri += len(sta) + len(sto)
+#     return peri
+
+def get_perimeter(region: set[tuple[int, int]]) -> int:
+    peri = 0
+    for axis in (0, 1):
+        oaxis = 0 if axis else 1
+        iss = set([pos[axis] for pos in region])
+        for dire in (-1, 1):
+            print(47, axis, dire, iss, peri)
+            for i in iss:
+                oi = i + dire
+                line = [pos[oaxis] for pos in region if pos[axis] == i]
+                line.sort()
+                # last_j = None
+                for j in line:
+                    if not axis:
+                        opos = oi, j
+                    else:
+                        opos = j, oi
+                    if opos in region:
+                        continue
+                    peri += 1
+    return peri
+
+
+def get_sides(region: set[tuple[int, int]]) -> int:
+    peri = 0
+    for axis in (0, 1):
+        oaxis = 0 if axis else 1
+        iss = set([pos[axis] for pos in region])
+        for dire in (-1, 1):
+            print(47, axis, dire, iss, peri)
+            for i in iss:
+                oi = i + dire
+                line = [pos[oaxis] for pos in region if pos[axis] == i]
+                line.sort()
+                last_j = None
+                for j in line:
+                    if not axis:
+                        opos = oi, j
+                    else:
+                        opos = j, oi
+                    if opos in region:
+                        last_j = None
+                        continue
+                    if last_j == j - 1:
+                        pass
+                    else:
+                        peri += 1
+
+                    last_j = j
+    return peri
+
+
+tprice = 0
+for i in range(height):
+    for j in range(width):
+        pos = i, j
+        if pos in visited:
+            continue
+        region = get_region(pos, set())
+        visited |= region
+
+        area = len(region)
+        sides = get_sides(region)
+        price = area * sides
+        tprice += price
+
+        char = lines[i][j]
+        print(f"{char}: {area} × {sides} = {price}$")
+
+print(tprice)
diff --git a/2024/13/Notes.xopp b/2024/13/Notes.xopp
new file mode 100644
index 0000000..9f67c23
Binary files /dev/null and b/2024/13/Notes.xopp differ
diff --git a/2024/13/demog b/2024/13/demog
new file mode 100644
index 0000000..008e6f3
--- /dev/null
+++ b/2024/13/demog
@@ -0,0 +1,12 @@
+Button A: X+3, Y+1
+Button B: X+4, Y+2
+Prize: X=17, Y=7
+
+Button A: X+1, Y+1
+Button B: X+3, Y+3
+Prize: X=7, Y=7
+
+Button A: X+3, Y+3
+Button B: X+1, Y+1
+Prize: X=7, Y=7
+
diff --git a/2024/13/one.py b/2024/13/one.py
new file mode 100644
index 0000000..c13b92a
--- /dev/null
+++ b/2024/13/one.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+
+import functools
+import re
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+coords = tuple[int, int]
+prizes: list[coords] = list()
+buttons: list[tuple[coords, coords]] = list()
+
+for li, line in enumerate(lines):
+    machine = li // 4
+    offset = li % 4
+    if offset == 0:
+        match = re.match(r"^Button A: X\+([0-9]+), Y\+([0-9]+)$", line)
+        assert match
+        button_a = int(match[1]), int(match[2])
+    elif offset == 1:
+        match = re.match(r"^Button B: X\+([0-9]+), Y\+([0-9]+)$", line)
+        assert match
+        button_b = int(match[1]), int(match[2])
+        buttons.append((button_a, button_b))
+    elif offset == 2:
+        match = re.match("^Prize: X=([0-9]+), Y=([0-9]+)$", line)
+        assert match
+        prize = int(match[1]), int(match[2])
+        prizes.append(prize)
+
+assert len(prizes) == len(buttons)
+
+ttoks = 0
+for arcade, prize in enumerate(prizes):
+    butts = buttons[arcade]
+    button_a, button_b = butts
+
+    @functools.lru_cache(4096)
+    def fun(x: int, y: int, rem_a: int, rem_b: int) -> int | None:
+        if (x, y) == prize:
+            return 0
+        if x > prize[0] or y > prize[1]:
+            return None
+        ba = (
+            fun(x + button_a[0], y + button_a[1], rem_a - 1, rem_b)
+            if rem_a > 0
+            else None
+        )
+        bb = (
+            fun(x + button_b[0], y + button_b[1], rem_a, rem_b - 1)
+            if rem_b > 0
+            else None
+        )
+        if ba is not None:
+            ba += 3
+        if bb is not None:
+            bb += 1
+        if ba is None:
+            if bb is None:
+                return None
+            else:
+                return bb
+        else:
+            if bb is None or ba < bb:
+                return ba
+            else:
+                return bb
+
+    toks = fun(0, 0, 100, 100)
+    print(43, arcade, toks)
+    if toks is not None:
+        ttoks += toks
+    # break
+
+print(ttoks)
diff --git a/2024/13/two.py b/2024/13/two.py
new file mode 100644
index 0000000..d8f655e
--- /dev/null
+++ b/2024/13/two.py
@@ -0,0 +1,223 @@
+#!/usr/bin/env python3
+
+import math
+import re
+import sys
+
+import rich.progress
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+coords = tuple[int, int]
+prizes: list[coords] = list()
+buttons: list[tuple[coords, coords]] = list()
+
+for li, line in enumerate(lines):
+    machine = li // 4
+    offset = li % 4
+    if offset == 0:
+        match = re.match(r"^Button A: X\+([0-9]+), Y\+([0-9]+)$", line)
+        assert match
+        button_a = int(match[1]), int(match[2])
+    elif offset == 1:
+        match = re.match(r"^Button B: X\+([0-9]+), Y\+([0-9]+)$", line)
+        assert match
+        button_b = int(match[1]), int(match[2])
+        buttons.append((button_a, button_b))
+    elif offset == 2:
+        match = re.match("^Prize: X=([0-9]+), Y=([0-9]+)$", line)
+        assert match
+        prize = int(match[1]), int(match[2])
+        prize = prize[0] + 10000000000000, prize[1] + 10000000000000
+        prizes.append(prize)
+
+assert len(prizes) == len(buttons)
+
+
+def slope(point: coords) -> float:
+    return point[1] / point[0]
+
+
+def norm(point: coords) -> float:
+    return math.sqrt(math.pow(point[1], 2) + math.pow(point[0], 2))
+
+
+#
+# def in_range(p: coords, a: coords, b: coords) -> bool:
+#     slope_a = slope(button_a)
+#     slope_b = slope(button_b)
+#     slope_p = slope(p)
+#     slope_but_min = min(slope_a, slope_b)
+#     slope_but_max = max(slope_a, slope_b)
+#     return not (slope_p < slope_but_min or slope_p > slope_but_max)
+
+
+ttoks = 0
+token_a, token_b = 3, 1
+for arcade, prize in enumerate(prizes):
+    butts = buttons[arcade]
+    button_a, button_b = butts
+
+    print(43, prize, button_a, button_b)
+    toks = None
+
+    max_a_x = int(math.ceil(prize[0] / button_a[0]))
+    max_a_y = int(math.ceil(prize[1] / button_a[1]))
+    max_a = min(max_a_x, max_a_y)
+    max_b_x = int(math.ceil(prize[0] / button_b[0]))
+    max_b_y = int(math.ceil(prize[1] / button_b[1]))
+    max_b = min(max_b_x, max_b_y)
+
+    slope_a = slope(button_a)
+    slope_b = slope(button_b)
+    slope_prize = slope(prize)
+    slope_but_min = min(slope_a, slope_b)
+    slope_but_max = max(slope_a, slope_b)
+    print(57, slope_but_min, slope_prize, slope_but_max)
+    if slope_prize < slope_but_min or slope_prize > slope_but_max:
+        print("Not in range")
+        continue
+
+    norm_a = norm(button_a)
+    norm_b = norm(button_b)
+    speed_a = norm_a / 3
+    speed_b = norm_b / 1
+
+    if speed_a > speed_b:
+        button_fastest, button_slowest = button_a, button_b
+        token_fastest, token_slowest = token_a, token_b
+        max_fastest, max_slowest = max_a, max_b
+        # slope_fastest, slope_slowes = slope_a, slope_b
+        # norm_fastest, norm_slowest = norm_a, norm_b
+    else:
+        button_fastest, button_slowest = button_b, button_a
+        token_fastest, token_slowest = token_b, token_a
+        max_fastest, max_slowest = max_b, max_a
+        # slope_fastest, slope_slowes = slope_b, slope_a
+        # norm_fastest, norm_slowest = norm_b, norm_a
+    toks = 0
+
+    # pri_x, pri_y = prize
+    # slope_pri = slope((pri_x, pri_y))
+    # while slope_pri >= slope_but_min and slope_pri <= slope_but_max:
+    #     toks += token_fastest
+    #     pri_x -= button_fastest[0]
+    #     pri_y -= button_fastest[1]
+    #     slope_pri = slope((pri_x, pri_y))
+    #     # print(98, pri_x, pri_y, slope_pri, toks)
+    # pri_x += button_fastest[0]
+    # pri_y += button_fastest[1]
+    # toks -= token_fastest
+    # print(100, token_fastest, toks / token_fastest, toks)
+
+    min_presses_fastest = 0
+    max_presses_fastest = max_fastest
+    while min_presses_fastest + 1 < max_presses_fastest:
+        presses_fastest = int(
+            math.floor((min_presses_fastest + max_presses_fastest) / 2)
+        )
+        print(120, min_presses_fastest, max_presses_fastest, presses_fastest)
+        pri_x, pri_y = (
+            prize[0] - button_fastest[0] * presses_fastest,
+            prize[1] - button_fastest[1] * presses_fastest,
+        )
+        slope_pri = slope((pri_x, pri_y))
+        if slope_pri >= slope_but_min and slope_pri <= slope_but_max:
+            min_presses_fastest = presses_fastest
+        else:
+            max_presses_fastest = presses_fastest
+
+    presses_fastest = max_presses_fastest
+    pri_x, pri_y = (
+        prize[0] - button_fastest[0] * presses_fastest,
+        prize[1] - button_fastest[1] * presses_fastest,
+    )
+    pri_x += button_fastest[0]
+    pri_y += button_fastest[1]
+    toks = presses_fastest * token_fastest
+    toks -= token_fastest
+
+    print(101, token_fastest, toks / token_fastest, toks)
+
+    # while pri_x > 0 and pri_y > 0:
+    #     toks += token_slowest
+    #     pri_x -= button_slowest[0]
+    #     pri_y -= button_slowest[1]
+    # print(103, token_slowest, toks)
+    # if (pri_x, pri_y) != (0, 0):
+    #     toks = None
+
+    presses_slowest, remainder = divmod(pri_x, button_slowest[0])
+    if remainder == 0 and (pri_y == presses_slowest * button_slowest[1]):
+        toks += presses_slowest * token_slowest
+    else:
+        toks = None
+    # dist = norm((pri_x, pri_y))
+    # rem_presses, remainder = divmod(dist, norm_slowest)
+    # presses_slowest = dist / norm_slowest
+    # if remainder == 0:
+    #     toks += rem_presses * token_slowest
+    # else:
+    #     toks = None
+
+    #
+    # with rich.progress.Progress() as progress:
+    #     nb_a = max_a
+    #     nb_b = 0
+    #     task_a = progress.add_task("Button A", total=max_a)
+    #     task_b = progress.add_task("Button B", total=max_b)
+    #     x = nb_a * button_a[0] + nb_b * button_b[0]
+    #     while nb_a > 0 or x < prize[0]:
+    #         # print(54, nb_a, nb_b)
+    #         if x == prize[0]:
+    #             y = nb_a * button_a[1] + nb_b * button_b[1]
+    #             if y == prize[1]:
+    #                 tok = 3 * nb_a + 1 * nb_b
+    #                 if toks is None or tok < toks:
+    #                     toks = tok
+    #         if x >= prize[0]:
+    #             # print(67)
+    #             nb_a -= 1
+    #             # progress.update(task_a, advance=1)
+    #         elif x < prize[0]:
+    #             nb_b += 1
+    #             # print(71)
+    #             # progress.update(task_b, advance=1)
+    #             if nb_b > max_b:
+    #                 break
+    #         x = nb_a * button_a[0] + nb_b * button_b[0]
+
+    # @functools.lru_cache(4096)
+    # def fun(x: int, y: int) -> int | None:
+    #     if (x, y) == prize:
+    #         return 0
+    #     if x > prize[0] or y > prize[1]:
+    #         return None
+    #     ba = fun(x + button_a[0], y + button_a[1])
+    #     bb = fun(x + button_b[0], y + button_b[1])
+    #     if ba is not None:
+    #         ba += 3
+    #     if bb is not None:
+    #         bb += 1
+    #     if ba is None:
+    #         if bb is None:
+    #             return None
+    #         else:
+    #             return bb
+    #     else:
+    #         if bb is None or ba < bb:
+    #             return ba
+    #         else:
+    #             return bb
+    #
+    # toks = fun(0, 0)
+
+    print(43, arcade, toks)
+    if toks is not None:
+        ttoks += toks
+    # break
+
+print(ttoks)
diff --git a/2024/13/two_clean.py b/2024/13/two_clean.py
new file mode 100644
index 0000000..8a719c3
--- /dev/null
+++ b/2024/13/two_clean.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+
+import math
+import re
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+coords = tuple[int, int]
+prizes: list[coords] = list()
+buttons: list[tuple[coords, coords]] = list()
+
+for li, line in enumerate(lines):
+    machine = li // 4
+    offset = li % 4
+    if offset == 0:
+        match = re.match(r"^Button A: X\+([0-9]+), Y\+([0-9]+)$", line)
+        assert match
+        button_a = int(match[1]), int(match[2])
+    elif offset == 1:
+        match = re.match(r"^Button B: X\+([0-9]+), Y\+([0-9]+)$", line)
+        assert match
+        button_b = int(match[1]), int(match[2])
+        buttons.append((button_a, button_b))
+    elif offset == 2:
+        match = re.match("^Prize: X=([0-9]+), Y=([0-9]+)$", line)
+        assert match
+        prize = int(match[1]), int(match[2])
+        # prize = prize[0] + 10000000000000, prize[1] + 10000000000000
+        prizes.append(prize)
+
+assert len(prizes) == len(buttons)
+
+
+def slope(point: coords) -> float:
+    return point[1] / point[0]
+
+
+def norm(point: coords) -> float:
+    return math.sqrt(math.pow(point[1], 2) + math.pow(point[0], 2))
+
+
+ttoks = 0
+token_a, token_b = 3, 1
+for arcade, prize in enumerate(prizes):
+    butts = buttons[arcade]
+    button_a, button_b = butts
+
+    print(43, prize, button_a, button_b)
+    toks = None
+
+    max_a_x = int(math.ceil(prize[0] / button_a[0]))
+    max_a_y = int(math.ceil(prize[1] / button_a[1]))
+    max_a = min(max_a_x, max_a_y)
+    max_b_x = int(math.ceil(prize[0] / button_b[0]))
+    max_b_y = int(math.ceil(prize[1] / button_b[1]))
+    max_b = min(max_b_x, max_b_y)
+
+    slope_a = slope(button_a)
+    slope_b = slope(button_b)
+    slope_prize = slope(prize)
+    slope_but_min = min(slope_a, slope_b)
+    slope_but_max = max(slope_a, slope_b)
+    if slope_prize < slope_but_min or slope_prize > slope_but_max:
+        print("Not in range")
+        continue
+
+    norm_a = norm(button_a)
+    norm_b = norm(button_b)
+    speed_a = norm_a / 3
+    speed_b = norm_b / 1
+
+    if speed_a > speed_b:
+        button_fastest, button_slowest = button_a, button_b
+        token_fastest, token_slowest = token_a, token_b
+        max_fastest = max_a
+    else:
+        button_fastest, button_slowest = button_b, button_a
+        token_fastest, token_slowest = token_b, token_a
+        max_fastest = max_b
+    toks = 0
+
+    min_presses_fastest = 0
+    max_presses_fastest = max_fastest
+    while min_presses_fastest + 1 < max_presses_fastest:
+        presses_fastest = int(
+            math.floor((min_presses_fastest + max_presses_fastest) / 2)
+        )
+        pri_x, pri_y = (
+            prize[0] - button_fastest[0] * presses_fastest,
+            prize[1] - button_fastest[1] * presses_fastest,
+        )
+        slope_pri = slope((pri_x, pri_y))
+        if slope_pri >= slope_but_min and slope_pri <= slope_but_max:
+            min_presses_fastest = presses_fastest
+        else:
+            max_presses_fastest = presses_fastest
+
+    presses_fastest = max_presses_fastest
+    pri_x, pri_y = (
+        prize[0] - button_fastest[0] * presses_fastest,
+        prize[1] - button_fastest[1] * presses_fastest,
+    )
+    pri_x += button_fastest[0]
+    pri_y += button_fastest[1]
+    toks = presses_fastest * token_fastest
+    toks -= token_fastest
+
+
+    presses_slowest, remainder = divmod(pri_x, button_slowest[0])
+    if remainder == 0 and (pri_y == presses_slowest * button_slowest[1]):
+        toks += presses_slowest * token_slowest
+    else:
+        toks = None
+
+    print(76, toks)
+    if toks is not None:
+        ttoks += toks
+
+print(ttoks)
diff --git a/2024/13/two_reddit.py b/2024/13/two_reddit.py
new file mode 100644
index 0000000..b44f0e6
--- /dev/null
+++ b/2024/13/two_reddit.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+
+"""
+Implementing:
+https://www.reddit.com/r/adventofcode/comments/1hd7irq/2024_day_13_an_explanation_of_the_mathematics/
+"""
+
+import re
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+coords = tuple[int, int]
+prizes: list[coords] = list()
+buttons: list[tuple[coords, coords]] = list()
+
+for li, line in enumerate(lines):
+    machine = li // 4
+    offset = li % 4
+    if offset == 0:
+        match = re.match(r"^Button A: X\+([0-9]+), Y\+([0-9]+)$", line)
+        assert match
+        button_a = int(match[1]), int(match[2])
+    elif offset == 1:
+        match = re.match(r"^Button B: X\+([0-9]+), Y\+([0-9]+)$", line)
+        assert match
+        button_b = int(match[1]), int(match[2])
+        buttons.append((button_a, button_b))
+    elif offset == 2:
+        match = re.match("^Prize: X=([0-9]+), Y=([0-9]+)$", line)
+        assert match
+        prize = int(match[1]), int(match[2])
+        # prize = prize[0] + 10000000000000, prize[1] + 10000000000000
+        prizes.append(prize)
+
+assert len(prizes) == len(buttons)
+
+ttoks = 0
+token_a, token_b = 3, 1
+for arcade, prize in enumerate(prizes):
+    butts = buttons[arcade]
+    button_a, button_b = butts
+
+    print(43, prize, button_a, button_b)
+    p_x, p_y = prize
+    a_x, a_y = button_a
+    b_x, b_y = button_b
+
+    denom = a_x * b_y - a_y * b_x
+    a = (p_x * b_y - p_y * b_x) / denom
+    b = (a_x * p_y - a_y * p_x) / denom
+
+    if not a.is_integer() or not b.is_integer():
+        print(76, None)
+        continue
+
+    toks = int(a) * token_a + int(b) * token_b
+    print(76, toks)
+    ttoks += toks
+
+print(ttoks)
diff --git a/2024/13/two_simpy.py b/2024/13/two_simpy.py
new file mode 100644
index 0000000..cbce3d6
--- /dev/null
+++ b/2024/13/two_simpy.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python3
+
+"""
+Someone mentionned sympy on reddit, wanted to see what I could do with it.
+"""
+
+import re
+import sys
+
+import sympy
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+coords = tuple[int, int]
+prizes: list[coords] = list()
+buttons: list[tuple[coords, coords]] = list()
+
+for li, line in enumerate(lines):
+    machine = li // 4
+    offset = li % 4
+    if offset == 0:
+        match = re.match(r"^Button A: X\+([0-9]+), Y\+([0-9]+)$", line)
+        assert match
+        button_a = int(match[1]), int(match[2])
+    elif offset == 1:
+        match = re.match(r"^Button B: X\+([0-9]+), Y\+([0-9]+)$", line)
+        assert match
+        button_b = int(match[1]), int(match[2])
+        buttons.append((button_a, button_b))
+    elif offset == 2:
+        match = re.match("^Prize: X=([0-9]+), Y=([0-9]+)$", line)
+        assert match
+        prize = int(match[1]), int(match[2])
+        # prize = prize[0] + 10000000000000, prize[1] + 10000000000000
+        prizes.append(prize)
+
+assert len(prizes) == len(buttons)
+
+sympy.init_printing()
+
+a, b, Ax, Ay, Bx, By, Px, Py = sympy.symbols(
+    "a b Ax Ay Bx By Px Py", positive=True, integer=True
+)
+x_eq = sympy.Eq(a * Ax + b * Bx, Px)
+y_eq = sympy.Eq(a * Ay + b * By, Py)
+tokens = 3 * a + 1 * b
+sols = sympy.solve([x_eq, y_eq], a, b, dict=True)
+# In that case, should use linsolve directly (solve ain't great)
+# Would allow to .subs the whole solution set at once.
+
+ttoks = sympy.Integer(0)
+for arcade, prize in enumerate(prizes):
+    button_a, button_b = buttons[arcade]
+
+    print(43, prize, button_a, button_b)
+
+    vars = {
+        Ax: button_a[0],
+        Ay: button_a[1],
+        Bx: button_b[0],
+        By: button_b[1],
+        Px: prize[0],
+        Py: prize[1],
+    }
+    toks = None
+    for sol in sols:
+        a_presses, b_presses = sol[a].subs(vars), sol[b].subs(vars)
+        if not a_presses.is_integer or not b_presses.is_integer:
+            continue
+        ntoks = tokens.subs({a: a_presses, b: b_presses})
+        if toks is None or ntoks < toks:
+            toks = ntoks
+
+    print(76, toks)
+    if toks is not None:
+        ttoks += toks
+
+assert ttoks.is_integer
+print(int(ttoks.evalf()))
diff --git a/2024/14/one.py b/2024/14/one.py
new file mode 100644
index 0000000..fc1c0f9
--- /dev/null
+++ b/2024/14/one.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+
+
+import functools
+import re
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+vec = tuple[int, int]
+poss: list[vec] = list()
+vits: list[vec] = list()
+
+for line in lines:
+    match = re.findall(r"(-?\d+)", line)
+    assert match
+    pos = int(match[0]), int(match[1])
+    vit = int(match[2]), int(match[3])
+    poss.append(pos)
+    vits.append(vit)
+
+print(poss, vits)
+
+
+def print_poss(poss: list[vec]) -> None:
+    viz = [[0] * width for _ in range(height)]
+    for pos in poss:
+        px, py = pos
+        viz[py][px] += 1
+    for line in viz:
+        print("".join([(str(c) if c > 0 else ".") for c in line]))
+    print()
+
+
+# x→ y↓
+if input_file == "input":
+    width = 101
+    height = 103
+else:
+    width = 11
+    height = 7
+if input_file == "demo1":
+    secs = 5
+else:
+    secs = 100
+
+print_poss(poss)
+
+for s in range(secs):
+    for r, vit in enumerate(vits):
+        px, py = poss[r]
+        px += vit[0]
+        py += vit[1]
+        while px >= width:
+            px -= width
+        while py >= height:
+            py -= height
+        while px < 0:
+            px += width
+        while py < 0:
+            py += height
+        poss[r] = px, py
+    print(s)
+    print_poss(poss)
+
+
+half_width = width // 2
+half_height = height // 2
+# <<<<<|>>>>>
+# <= first quadrant
+
+quadrants = [0, 0, 0, 0]
+for pos in poss:
+    px, py = pos
+    q = 0
+    if px == half_width or py == half_height:
+        continue
+    if px > half_width:
+        q += 1
+    if py > half_height:
+        q += 2
+    quadrants[q] += 1
+
+print(quadrants)
+print(functools.reduce(int.__mul__, quadrants))
diff --git a/2024/14/two.py b/2024/14/two.py
new file mode 100644
index 0000000..360ded0
--- /dev/null
+++ b/2024/14/two.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+
+
+import functools
+import re
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+vec = tuple[int, int]
+poss: list[vec] = list()
+vits: list[vec] = list()
+
+for line in lines:
+    match = re.findall(r"(-?\d+)", line)
+    assert match
+    pos = int(match[0]), int(match[1])
+    vit = int(match[2]), int(match[3])
+    poss.append(pos)
+    vits.append(vit)
+
+print(poss, vits)
+
+
+def print_poss(poss: list[vec]) -> None:
+    stop = True
+    viz = [[0] * width for _ in range(height)]
+    for pos in poss:
+        px, py = pos
+        if viz[py][px] > 0:
+            stop = False
+        viz[py][px] += 1
+    if stop:
+        for line in viz:
+            print("".join([(str(c) if c > 0 else ".") for c in line]))
+        input()
+
+
+# x→ y↓
+if input_file == "input":
+    width = 101
+    height = 103
+else:
+    width = 11
+    height = 7
+if input_file == "demo1":
+    secs = 5
+else:
+    secs = 100
+
+print_poss(poss)
+
+# for s in range(secs):
+s = 0
+while True:
+    s += 1
+    for r, vit in enumerate(vits):
+        px, py = poss[r]
+        px += vit[0]
+        py += vit[1]
+        while px >= width:
+            px -= width
+        while py >= height:
+            py -= height
+        while px < 0:
+            px += width
+        while py < 0:
+            py += height
+        poss[r] = px, py
+    print(s)
+    print_poss(poss)
+#
+#
+# half_width = width // 2
+# half_height = height // 2
+# # <<<<<|>>>>>
+# # <= first quadrant
+#
+# quadrants = [0, 0, 0, 0]
+# for pos in poss:
+#     px, py = pos
+#     q = 0
+#     if px == half_width or py == half_height:
+#         continue
+#     if px > half_width:
+#         q += 1
+#     if py > half_height:
+#         q += 2
+#     quadrants[q] += 1
+#
+# print(quadrants)
+# print(functools.reduce(int.__mul__, quadrants))
diff --git a/2024/14/two_extra.py b/2024/14/two_extra.py
new file mode 100644
index 0000000..75f7e7f
--- /dev/null
+++ b/2024/14/two_extra.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+
+
+import functools
+import re
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+vec = tuple[int, int]
+poss: list[vec] = list()
+vits: list[vec] = list()
+
+for line in lines:
+    match = re.findall(r"(-?\d+)", line)
+    assert match
+    pos = int(match[0]), int(match[1])
+    vit = int(match[2]), int(match[3])
+    poss.append(pos)
+    vits.append(vit)
+
+print(poss, vits)
+
+
+def print_poss(poss: list[vec]) -> None:
+    stop = False
+    viz = [[0] * width for _ in range(height)]
+    for pos in poss:
+        px, py = pos
+        viz[py][px] += 1
+    for line in viz:
+        lin = "".join([(str(c) if c > 0 else ".") for c in line])
+        print(lin)
+        if "111111111" in lin:
+            stop = True
+    if stop:
+        input()
+
+
+# x→ y↓
+if input_file.startswith("input"):
+    width = 101
+    height = 103
+else:
+    width = 11
+    height = 7
+if input_file == "demo1":
+    secs = 5
+else:
+    secs = 100
+
+print_poss(poss)
+
+# for s in range(secs):
+s = 0
+while True:
+    s += 1
+    for r, vit in enumerate(vits):
+        px, py = poss[r]
+        px += vit[0]
+        py += vit[1]
+        while px >= width:
+            px -= width
+        while py >= height:
+            py -= height
+        while px < 0:
+            px += width
+        while py < 0:
+            py += height
+        poss[r] = px, py
+    print(s)
+    print_poss(poss)
+#
+#
+# half_width = width // 2
+# half_height = height // 2
+# # <<<<<|>>>>>
+# # <= first quadrant
+#
+# quadrants = [0, 0, 0, 0]
+# for pos in poss:
+#     px, py = pos
+#     q = 0
+#     if px == half_width or py == half_height:
+#         continue
+#     if px > half_width:
+#         q += 1
+#     if py > half_height:
+#         q += 2
+#     quadrants[q] += 1
+#
+# print(quadrants)
+# print(functools.reduce(int.__mul__, quadrants))
diff --git a/2024/15/demog b/2024/15/demog
new file mode 100644
index 0000000..6493017
--- /dev/null
+++ b/2024/15/demog
@@ -0,0 +1,6 @@
+#######
+#...O..
+#......
+#...@..
+
+>
diff --git a/2024/15/one.py b/2024/15/one.py
new file mode 100644
index 0000000..334e3ed
--- /dev/null
+++ b/2024/15/one.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+coords = tuple[int, int]
+
+pos: coords
+box = list()
+moves: list[coords] = list()
+
+directions = {
+    "^": (-1, 0),  # ^
+    ">": (0, 1),  # >
+    "v": (1, 0),  # v
+    "<": (0, -1),  # <
+}
+
+boxing = True
+for i, line in enumerate(lines):
+    if not line:
+        boxing = False
+    elif boxing:
+        bline = list(line)
+        if "@" in bline:
+            j = bline.index("@")
+            pos = i, j
+            # bline[j] = "."
+        box.append(bline)
+    else:
+        for c in line:
+            direction = directions[c]
+            moves.append(direction)
+
+
+def print_box() -> None:
+    for bline in box:
+        print("".join(bline))
+    print()
+
+
+print_box()
+for move in moves:
+    first = pos[0] + move[0], pos[1] + move[1]
+    last = first
+    possible = True
+    while True:
+        c = box[last[0]][last[1]]
+        # print(c)
+        if c == ".":
+            break
+        elif c == "#":
+            possible = False
+            break
+        last = last[0] + move[0], last[1] + move[1]
+    if possible:
+        if first != last:
+            box[last[0]][last[1]] = "O"
+        box[pos[0]][pos[1]] = "."
+        box[first[0]][first[1]] = "@"
+        pos = first
+    print(move, possible, pos, first, last)
+    print_box()
+
+
+score = 0
+for i, bline in enumerate(box):
+    for j, char in enumerate(bline):
+        if char == "O":
+            score += 100 * i + j
+print(score)
diff --git a/2024/15/two.py b/2024/15/two.py
new file mode 100644
index 0000000..4c8c001
--- /dev/null
+++ b/2024/15/two.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+vec = tuple[int, int]
+
+pos: vec
+box = list()
+moves: list[vec] = list()
+
+directions = {
+    "^": (-1, 0),  # ^
+    ">": (0, 1),  # >
+    "v": (1, 0),  # v
+    "<": (0, -1),  # <
+}
+
+sub = {
+    "#": "##",
+    "O": "[]",
+    ".": "..",
+    "@": "@.",
+}
+
+boxing = True
+for i, line in enumerate(lines):
+    if not line:
+        boxing = False
+    elif boxing:
+        bline = list()
+        for char in line:
+            bline += list(sub[char])
+        if "@" in bline:
+            j = bline.index("@")
+            pos = i, j
+            # bline[j] = "."
+        box.append(bline)
+    else:
+        for c in line:
+            direction = directions[c]
+            moves.append(direction)
+
+
+def print_box() -> None:
+    for bline in box:
+        print("".join(bline))
+    print()
+
+
+print_box()
+for move in moves:
+
+    def do(i: int, j: int) -> None | set[vec]:
+        ii, jj = i + move[0], j + move[1]
+        char = box[ii][jj]
+        moving = {(i, j)}
+        if char == "#":
+            return None
+        elif char == ".":
+            return moving
+        else:
+            r = do(ii, jj)
+            if r is None:
+                return None
+            moving |= r
+            if char == "[":
+                npos = (ii, jj + 1)
+                if npos not in moving:
+                    r = do(npos[0], npos[1])
+                    if r is None:
+                        return None
+                    moving |= r
+            elif char == "]":
+                npos = (ii, jj - 1)
+                if npos not in moving:
+                    r = do(npos[0], npos[1])
+                    if r is None:
+                        return None
+                    moving |= r
+            return moving
+
+    moving = do(pos[0], pos[1])
+    if moving is None:
+        continue
+    olc: list[str] = list()
+    for i, j in moving:
+        olc.append(box[i][j])
+        box[i][j] = "."
+    print(82, moving)
+    print(83, olc)
+    for i, j in moving:
+        ii, jj = i + move[0], j + move[1]
+        box[ii][jj] = olc.pop(0)
+    # box[pos[0]][pos[1]] = "."
+    pos = pos[0] + move[0], pos[1] + move[1]
+    print(move, pos)
+    # print_box()
+
+
+score = 0
+for i, bline in enumerate(box):
+    for j, char in enumerate(bline):
+        if char == "[":
+            score += 100 * i + j
+print(score)
diff --git a/2024/16/:w b/2024/16/:w
new file mode 100644
index 0000000..5494d1e
--- /dev/null
+++ b/2024/16/:w
@@ -0,0 +1,4 @@
+####
+#.E#
+#S##
+####
diff --git a/2024/16/README.md b/2024/16/README.md
new file mode 100644
index 0000000..ba6d8f6
--- /dev/null
+++ b/2024/16/README.md
@@ -0,0 +1,23 @@
+Oh boy. My first time in a while doing something with mazes.
+I decided to go with a filling thing.
+I realized I need to put the direction as a dimension in the table of visited scores.
+So basically it would go up to the very last tile, and then be like "op, what if I went back to the start?", although I did not know that at the time.
+I did exhaust Python call stack, so I increased it, again and again.
+But I thought that surely, even with this dumb of an algorithm it should be able to do, so I decided to look for a bug.
+I used the depth variable (initally only used to indent print statements) as my own call stack limit that would still print the maze.
+I realized that even at 1000 depth, the maze was already filled, which puzzled me.
+I submitted the answer... and it was correct x)
+
+For part 2 I first implemented something that would print only one best path.
+Then I realized my mistake, and then did something that would work if not for the fact that I optimized out
+2 paths arriving with the same score. A <= changed to < later and it was fixed.
+The optimisation didn't cost much, the "allocating a list every recursion", very much so.
+
+Since it was taking a long time to compute I realized maybe I could do something clever with only considering crossways,
+since there's quite a lot of corridors.
+But I decided to instead firsrt code two2.py, which would add the position to the set of best places only when the recursion hit the best score,
+which was hardcoded from part 1 (it took 48s to run alone, sooo).
+I tried it on demo, got None as answer to part 1 and 0 as answer to part 2, completly overlooked that, and let it cook on the real input.
+In the end, the first iteration of two.py ended up being cooked first, 11 minutes after, with the right answer.
+
+...a win is a win I guess
diff --git a/2024/16/demog b/2024/16/demog
new file mode 100644
index 0000000..7aed542
--- /dev/null
+++ b/2024/16/demog
@@ -0,0 +1,15 @@
+################
+############..E#
+###########..#.#
+##########..##.#
+#########..###.#
+########..##...#
+#######..###.###
+######..####...#
+#####..#######.#
+####..##.......#
+###..###.#######
+##..####.......#
+#..###########.#
+#S.............#
+################
diff --git a/2024/16/demog0 b/2024/16/demog0
new file mode 100644
index 0000000..2f01db5
--- /dev/null
+++ b/2024/16/demog0
@@ -0,0 +1,4 @@
+####
+#.E#
+#S.#
+####
diff --git a/2024/16/networkx_test.py b/2024/16/networkx_test.py
new file mode 100644
index 0000000..6e8151e
--- /dev/null
+++ b/2024/16/networkx_test.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+
+import sys
+import typing
+
+import matplotlib.pyplot as plt
+import networkx as nx
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+directions = [
+    (-1, 0),  # ^ North
+    (0, 1),  # > East
+    (1, 0),  # v South
+    (0, -1),  # < West
+]
+
+# Parse input
+g = nx.DiGraph()
+for i in range(height):
+    for j in range(width):
+        char = lines[i][j]
+        if char == "#":
+            continue
+        for d, direction in enumerate(directions):
+            cur = (i, j, d)
+            # Start/end
+            if char == "E":
+                g.add_edge(cur, "end", weight=0)
+            elif char == "S" and d == 1:
+                g.add_edge("start", cur, weight=0)
+            # Rotate
+            g.add_edge(cur, (i, j, (d + 1) % len(directions)), weight=1000)
+            g.add_edge(cur, (i, j, (d - 1) % len(directions)), weight=1000)
+            # Advance
+            ii, jj = i + direction[0], j + direction[1]
+            if lines[ii][jj] == "#":
+                continue
+            g.add_edge(cur, (ii, jj, d), weight=1)
+
+# Part 1
+score = nx.shortest_path_length(g, "start", "end", weight="weight")
+print(f"{score=}")
+
+# Part 2
+paths = nx.all_shortest_paths(g, "start", "end", weight="weight")
+best_orientations = set()
+for path in paths:
+    best_orientations |= set(path)
+path_edges = list(zip(path, path[1:]))  # Will be one random best path
+best_places = set(bo[:2] for bo in best_orientations - {"start", "end"})
+print(f"{len(best_places)=}")
+
+# Draw graph
+if len(g.nodes) > 1000:
+    sys.exit(0)
+
+node_colors = ["blue" if node in best_orientations else "cyan" for node in g.nodes()]
+edge_colors = ["red" if edge in path_edges else "black" for edge in g.edges()]
+node_pos: dict[typing.Any, tuple[float, float]] = dict()
+for node in g.nodes():
+    pos: tuple[float, float]
+    if node == "start":
+        pos = height - 1, 0
+    elif node == "end":
+        pos = 0, width - 1
+    else:
+        i, j, d = node
+        direction = directions[d]
+        pos = i + direction[0] / 3, j + direction[1] / 3
+    node_pos[node] = pos[1], pos[0] * -1
+
+nx.draw_networkx_nodes(g, node_pos, node_color=node_colors)
+nx.draw_networkx_edges(g, node_pos, edge_color=edge_colors)
+# nx.draw_networkx_labels(g, node_pos)
+# nx.draw_networkx_edge_labels(
+#     g, node_pos, edge_labels={(u, v): d["weight"] for u, v, d in g.edges(data=True)}
+# )
+
+plt.show()
diff --git a/2024/16/numpy_test.py b/2024/16/numpy_test.py
new file mode 100644
index 0000000..0b723fd
--- /dev/null
+++ b/2024/16/numpy_test.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python3
+
+import sys
+
+import numpy as np
+import scipy as sp
+
+dtype = np.int32
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+start = height - 2, 1
+stop = 1, width - 2
+
+directions = [
+    (-1, 0),  # ^ North
+    (0, 1),  # > East
+    (1, 0),  # v South
+    (0, -1),  # < West
+]
+directions_keys = ("^", ">", "v", "<")
+
+walls = np.zeros((height, width), dtype)
+for i in range(height):
+    for j in range(width):
+        if lines[i][j] == "#":
+            walls[i, j] = 1
+invwalls = 1 - walls
+
+# print("Walls")
+# print(walls)
+
+# TEST 4
+
+scores = np.zeros((height, width, len(directions)), dtype)
+scores[start[0], start[1], 1] = 1
+
+for i in range(1000):
+    print("Step", i)
+    oldscores = scores.copy()
+    for od, odir in enumerate(directions):
+        for nd, ndir in enumerate(directions):
+            score = scores[:, :, nd]
+            moved = sp.ndimage.shift(oldscores[:, :, od], ndir)
+            increment = 1 if nd == od else 1001
+            moved = (moved + increment) * (moved > 0)
+            moved = moved * invwalls
+            mask = (moved > 0) & ((score == 0) | (score > moved))
+            scores[:, :, nd] = (score * ~mask) + (moved * mask)
+
+    # for d, dir in enumerate(directions):
+    #     print("Direction", directions_keys[d])
+    #     print(scores[:, :, d])
+
+    if (scores == oldscores).all():
+        break
+else:
+    print("Limit!")
+
+end_score = min(filter(lambda d: d > 0, scores[stop])) - 1
+print(f"{end_score=}")
+
+# TEST 3
+
+# scores = [np.zeros((height, width), dtype) for _ in directions]
+# scores[1][start] = 1
+#
+# for i in range(100):
+#     print("Step", i)
+#     for od, odir in enumerate(directions):
+#         oldscore = scores[od].copy()
+#         for nd, ndir in enumerate(directions):
+#             score = scores[nd]
+#             moved = sp.ndimage.shift(oldscore, ndir)
+#             increment = 1 if nd == od else 1001
+#             moved = (moved + increment) * (moved > 0)
+#             moved = moved * invwalls
+#             mask = (moved > 0) & ((score == 0) | (score > moved))
+#             scores[nd] = (score * ~mask) + (moved * mask)
+#
+#     final_score = None
+#     for d, dir in enumerate(directions):
+#         print("Direction", directions_keys[d])
+#         print(scores[d])
+#         end_score = scores[d][stop]
+#         if end_score > 0:
+#             if final_score is None or end_score < final_score:
+#                 final_score = end_score
+#
+#     if final_score:
+#         final_score -= 1
+#         print(f"{final_score=}")
+#         break
+# else:
+#     print("Limit!")
+
+
+# TEST 2
+
+# score = np.zeros((height, width), dtype)
+# score[start] = 1
+#
+# for i in range(10):
+#     print("Step", i)
+#     oldscore = score.copy()
+#     for nd, ndir in enumerate(directions):
+#         moved = sp.ndimage.shift(oldscore, ndir)
+#         moved = (moved + 1) * (moved > 0)
+#         moved = moved * invwalls
+#         mask = (moved > 0) & ((score == 0) | (score > moved))
+#         score = (score * ~mask) + (moved * mask)
+#     print(score)
+
+# TEST 1
+
+# directions = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype)
+#
+# visited = np.zeros((height, width), dtype)
+# visited[start] = 1
+#
+# for i in range(1000):
+#     print("Step", i)
+#     new = sp.signal.convolve2d(visited, directions, mode="same")
+#     visited = (((new > 0) - walls) > 0)
+#     print(visited * 1)
+#     if visited[stop]:
+#         break
+# else:
+#     print("Limit!")
+# print(i)
diff --git a/2024/16/one.py b/2024/16/one.py
new file mode 100644
index 0000000..de41b0b
--- /dev/null
+++ b/2024/16/one.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+
+import pprint
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+vec = tuple[int, int]
+
+directions = [
+    (-1, 0),  # ^ North
+    (0, 1),  # > East
+    (1, 0),  # v South
+    (0, -1),  # < West
+]
+
+directions_keys = ("^", ">", "v", "<")
+
+min_scores: list[list[list[None | int]]] = list()
+for _ in range(height):
+    line: list[list[None | int]] = list()
+    for _ in range(width):
+        cell: list[None | int] = [None] * len(directions)
+        line.append(cell)
+    min_scores.append(line)
+
+start = height - 2, 1
+
+
+def walk(pos: vec, dir: int, score: int, depth: int = 0) -> int | None:
+    i, j = pos
+    char = lines[i][j]
+
+    if depth > 1000:
+        return None
+
+    # print("-" * depth, 28, pos, char, directions_keys[dir], score)
+    if char == "E":
+        return score
+    elif char == "#":
+        return None
+
+    min_score = min_scores[i][j][dir]
+    if min_score is not None and score >= min_score:
+        # print("-" * depth, f" 32 already taken {score} >= {min_score}")
+        return None
+    min_scores[i][j][dir] = score
+
+    mscore = None
+    for ndir, direction in enumerate(directions):
+        ii, jj = i + direction[0], j + direction[1]
+        price = 1 if dir == ndir else 1001
+        nscore = walk((ii, jj), ndir, score + price, depth+1)
+        if nscore is None:
+            continue
+        if mscore is None or nscore < mscore:
+            mscore = nscore
+    return mscore
+
+sys.setrecursionlimit(9000)
+tot_score = walk(start, 1, 0)
+
+# Viz
+for i in range(height):
+    cline = ""
+    for j in range(width):
+        char = lines[i][j]
+        min_score = None
+        for d in range(len(directions)):
+            score = min_scores[i][j][d]
+            if score is None:
+                continue
+            if min_score is None or score < min_score:
+                char = directions_keys[d]
+                min_score = score
+        cline += char
+    print(cline)
+print()
+
+print(tot_score)
diff --git a/2024/16/one_opti.py b/2024/16/one_opti.py
new file mode 100644
index 0000000..23e70ed
--- /dev/null
+++ b/2024/16/one_opti.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+
+import pprint
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+vec = tuple[int, int]
+
+directions = [
+    (-1, 0),  # ^ North
+    (0, 1),  # > East
+    (1, 0),  # v South
+    (0, -1),  # < West
+]
+
+directions_keys = ("^", ">", "v", "<")
+
+min_scores: list[list[list[None | int]]] = list()
+for _ in range(height):
+    line: list[list[None | int]] = list()
+    for _ in range(width):
+        cell: list[None | int] = [None] * len(directions)
+        line.append(cell)
+    min_scores.append(line)
+
+start = height - 2, 1
+
+
+def walk(pos: vec, dir: int, score: int, depth: int = 0) -> int | None:
+    i, j = pos
+    char = lines[i][j]
+
+    if depth > 1000:
+        return None
+
+    # print("-" * depth, 28, pos, char, directions_keys[dir], score)
+    if char == "E":
+        return score
+    elif char == "#":
+        return None
+
+    min_score = min_scores[i][j][dir]
+    for d, min_score in enumerate(min_scores[i][j]):
+        if min_score is None:
+            continue
+        if d == dir:
+            if score >= min_score:
+                return None
+        else:
+            if score >= min_score + 1000:
+                return None
+    min_scores[i][j][dir] = score
+
+    mscore = None
+    for ndir, direction in enumerate(directions):
+        ii, jj = i + direction[0], j + direction[1]
+        price = 1 if dir == ndir else 1001
+        nscore = walk((ii, jj), ndir, score + price, depth + 1)
+        if nscore is None:
+            continue
+        if mscore is None or nscore < mscore:
+            mscore = nscore
+    return mscore
+
+
+sys.setrecursionlimit(3000)
+tot_score = walk(start, 1, 0)
+
+# Viz
+for i in range(height):
+    cline = ""
+    for j in range(width):
+        char = lines[i][j]
+        min_score = None
+        for d in range(len(directions)):
+            score = min_scores[i][j][d]
+            if score is None:
+                continue
+            if min_score is None or score < min_score:
+                char = directions_keys[d]
+                min_score = score
+        cline += char
+    print(cline)
+print()
+
+print(tot_score)
diff --git a/2024/16/reddit_edge_case b/2024/16/reddit_edge_case
new file mode 100644
index 0000000..00cc512
--- /dev/null
+++ b/2024/16/reddit_edge_case
@@ -0,0 +1,27 @@
+###########################
+#######################..E#
+######################..#.#
+#####################..##.#
+####################..###.#
+###################..##...#
+##################..###.###
+#################..####...#
+################..#######.#
+###############..##.......#
+##############..###.#######
+#############..####.......#
+############..###########.#
+###########..##...........#
+##########..###.###########
+#########..####...........#
+########..###############.#
+#######..##...............#
+######..###.###############
+#####..####...............#
+####..###################.#
+###..##...................#
+##..###.###################
+#..####...................#
+#.#######################.#
+#S........................#
+###########################
diff --git a/2024/16/reddit_open_maze b/2024/16/reddit_open_maze
new file mode 100644
index 0000000..74fc386
--- /dev/null
+++ b/2024/16/reddit_open_maze
@@ -0,0 +1,14 @@
+####################################################
+#......................................#..........E#
+#......................................#...........#
+#....................#.................#...........#
+#....................#.................#...........#
+#....................#.................#...........#
+#....................#.................#...........#
+#....................#.................#...........#
+#....................#.................#...........#
+#....................#.................#...........#
+#....................#.................#...........#
+#....................#.............................#
+#S...................#.............................#
+####################################################
diff --git a/2024/16/two.py b/2024/16/two.py
new file mode 100644
index 0000000..cbf5b91
--- /dev/null
+++ b/2024/16/two.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+
+import pprint
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+vec = tuple[int, int]
+
+directions = [
+    (-1, 0),  # ^ North
+    (0, 1),  # > East
+    (1, 0),  # v South
+    (0, -1),  # < West
+]
+
+directions_keys = ("^", ">", "v", "<")
+
+min_scores: list[list[list[None | int]]] = list()
+for _ in range(height):
+    line: list[list[None | int]] = list()
+    for _ in range(width):
+        cell: list[None | int] = [None] * len(directions)
+        line.append(cell)
+    min_scores.append(line)
+
+start = height - 2, 1
+
+valid_paths: list[tuple[int, set[vec]]] = list()
+
+
+def walk(pos: vec, dir: int, score: int, path: list[vec]) -> int | None:
+    i, j = pos
+    char = lines[i][j]
+
+    depth = len(path)
+    path = path + [pos]
+
+    if depth > 1000:
+        return None
+
+    # print("-" * depth, 28, pos, char, directions_keys[dir], score)
+    if char == "E":
+        valid_paths.append((score, set(path)))
+        return score
+    elif char == "#":
+        return None
+
+    min_score = min_scores[i][j][dir]
+    if min_score is not None and score > min_score:
+        # print("-" * depth, f" 32 already taken {score} >= {min_score}")
+        return None
+    min_scores[i][j][dir] = score
+
+    mscore = None
+    for ndir, direction in enumerate(directions):
+        ii, jj = i + direction[0], j + direction[1]
+        price = 1 if dir == ndir else 1001
+        nscore = walk((ii, jj), ndir, score + price, path)
+        if nscore is None:
+            continue
+        if mscore is None or nscore < mscore:
+            mscore = nscore
+    return mscore
+
+
+sys.setrecursionlimit(9000)
+tot_score = walk(start, 1, 0, [])
+
+print(76, len(valid_paths))
+all_best: set[vec] = set()
+for s, path in valid_paths:
+    if s != tot_score:
+        continue
+    print(81, "BEST")
+    all_best |= path
+
+# Viz
+for i in range(height):
+    cline = ""
+    for j in range(width):
+        char = lines[i][j]
+        pos = i, j
+        if pos in all_best:
+            min_score = None
+            for d in range(len(directions)):
+                score = min_scores[i][j][d]
+                if score is None:
+                    continue
+                if min_score is None or score < min_score:
+                    char = directions_keys[d]
+                    min_score = score
+        cline += char
+    print(cline)
+print()
+
+
+print(tot_score)
+print(len(all_best))
diff --git a/2024/16/two2.py b/2024/16/two2.py
new file mode 100644
index 0000000..117693f
--- /dev/null
+++ b/2024/16/two2.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+
+import pprint
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+vec = tuple[int, int]
+
+directions = [
+    (-1, 0),  # ^ North
+    (0, 1),  # > East
+    (1, 0),  # v South
+    (0, -1),  # < West
+]
+
+directions_keys = ("^", ">", "v", "<")
+
+min_scores: list[list[list[None | int]]] = list()
+for _ in range(height):
+    line: list[list[None | int]] = list()
+    for _ in range(width):
+        cell: list[None | int] = [None] * len(directions)
+        line.append(cell)
+    min_scores.append(line)
+
+start = height - 2, 1
+
+best: set[vec] = set()
+
+def walk(pos: vec, dir: int, score: int, depth: int) -> int | None:
+    i, j = pos
+    char = lines[i][j]
+
+    if depth > 1000:
+        return None
+
+    # print("-" * depth, 28, pos, char, directions_keys[dir], score)
+    if char == "E":
+        if score == 90440:
+            return score
+        else:
+            return None
+    elif char == "#":
+        return None
+
+    min_score = min_scores[i][j][dir]
+    if min_score is not None and score > min_score:
+        # print("-" * depth, f" 32 already taken {score} >= {min_score}")
+        return None
+    min_scores[i][j][dir] = score
+
+    mscore = None
+    for ndir, direction in enumerate(directions):
+        ii, jj = i + direction[0], j + direction[1]
+        price = 1 if dir == ndir else 1001
+        nscore = walk((ii, jj), ndir, score + price, depth + 1)
+        if nscore is None:
+            continue
+        best.add((ii, jj))
+        # if mscore is None or nscore < mscore:
+        #     mscore = nscore
+    return mscore
+
+
+sys.setrecursionlimit(9000)
+tot_score = walk(start, 1, 0, 0)
+
+# Viz
+for i in range(height):
+    cline = ""
+    for j in range(width):
+        char = lines[i][j]
+        pos = i, j
+        min_score = None
+        for d in range(len(directions)):
+            score = min_scores[i][j][d]
+            if score is None:
+                continue
+            if min_score is None or score < min_score:
+                char = directions_keys[d]
+                min_score = score
+        cline += char
+    print(cline)
+print()
+
+
+print(tot_score)
+print(len(best))
diff --git a/2024/16/two_opti.py b/2024/16/two_opti.py
new file mode 100644
index 0000000..904f525
--- /dev/null
+++ b/2024/16/two_opti.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+
+import pprint
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+vec = tuple[int, int]
+
+directions = [
+    (-1, 0),  # ^ North
+    (0, 1),  # > East
+    (1, 0),  # v South
+    (0, -1),  # < West
+]
+
+directions_keys = ("^", ">", "v", "<")
+
+min_scores: list[list[list[None | int]]] = list()
+for _ in range(height):
+    line: list[list[None | int]] = list()
+    for _ in range(width):
+        cell: list[None | int] = [None] * len(directions)
+        line.append(cell)
+    min_scores.append(line)
+
+start = height - 2, 1
+
+valid_paths: list[tuple[int, set[vec]]] = list()
+
+
+def walk(pos: vec, dir: int, score: int, path: list[vec]) -> int | None:
+    i, j = pos
+    char = lines[i][j]
+
+    depth = len(path)
+    path = path + [pos]
+
+    if depth > 1000:
+        return None
+
+    # print("-" * depth, 28, pos, char, directions_keys[dir], score)
+    if char == "E":
+        valid_paths.append((score, set(path)))
+        return score
+    elif char == "#":
+        return None
+
+    min_score = min_scores[i][j][dir]
+    for d, min_score in enumerate(min_scores[i][j]):
+        if min_score is None:
+            continue
+        if d == dir:
+            if score > min_score:
+                return None
+        else:
+            if score > min_score + 1000:
+                return None
+    min_scores[i][j][dir] = score
+
+    mscore = None
+    for ndir, direction in enumerate(directions):
+        ii, jj = i + direction[0], j + direction[1]
+        price = 1 if dir == ndir else 1001
+        nscore = walk((ii, jj), ndir, score + price, path)
+        if nscore is None:
+            continue
+        if mscore is None or nscore < mscore:
+            mscore = nscore
+    return mscore
+
+
+sys.setrecursionlimit(9000)
+tot_score = walk(start, 1, 0, [])
+
+print(76, len(valid_paths))
+all_best: set[vec] = set()
+for s, path in valid_paths:
+    if s != tot_score:
+        continue
+    print(81, "BEST")
+    all_best |= path
+
+# Viz
+for i in range(height):
+    cline = ""
+    for j in range(width):
+        char = lines[i][j]
+        pos = i, j
+        if pos in all_best:
+            min_score = None
+            for d in range(len(directions)):
+                score = min_scores[i][j][d]
+                if score is None:
+                    continue
+                if min_score is None or score < min_score:
+                    char = directions_keys[d]
+                    min_score = score
+        cline += char
+    print(cline)
+print()
+
+
+print(tot_score)
+print(len(all_best))
diff --git a/2024/17/Notes.xopp b/2024/17/Notes.xopp
new file mode 100644
index 0000000..b5931ef
Binary files /dev/null and b/2024/17/Notes.xopp differ
diff --git a/2024/17/README.md b/2024/17/README.md
new file mode 100644
index 0000000..7da08d3
--- /dev/null
+++ b/2024/17/README.md
@@ -0,0 +1,4 @@
+two.py and two_bf.py were written before I got the 2 stars.
+
+First fails on demo2, seconds takes 23 years (literally).
+
diff --git a/2024/17/demog b/2024/17/demog
new file mode 100644
index 0000000..556168c
--- /dev/null
+++ b/2024/17/demog
@@ -0,0 +1,5 @@
+Register A: 41644071
+Register B: 0
+Register C: 0
+
+Program: 2,4,1,2,0,3,5,5,3,0
diff --git a/2024/17/one.py b/2024/17/one.py
new file mode 100644
index 0000000..7529642
--- /dev/null
+++ b/2024/17/one.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+# program 3 bits numbers
+# registers A, B, C: any int
+# instruction 3 bits, operand 3 bits
+# literal operand, combo operand: 0-3 literal, 4-6: register A,B,C
+# instruction pointer starts at 0
+
+# 0 adv: truncated division, numerator = A, denominator 2**combo operand -> A
+# 1 bxl: bitwise XOR, B ^ literal operand -> B
+# 2 bst: combo operand % 8 -> B
+# 3 jnz: { A = 0: nothing; A != 0: literal operand -> IP } # no increase there!
+# 4 bxc: bitwise XOR, B ^ C -> B # operand ignored
+# 5 out: combo operand % 8 -> output
+# 6 bdv: truncated division, numerator = A, denominator 2**combo operand -> B
+# 7 cdv: truncated division, numerator = A, denominator 2**combo operand -> C
+
+A = int(lines[0].split(":")[1])
+B = int(lines[1].split(":")[1])
+C = int(lines[2].split(":")[1])
+
+program = [int(p) for p in lines[4].split(":")[1].split(",")]
+
+output: list[int] = list()
+
+ip = 0
+while ip in range(len(program)):
+    inst = program[ip]
+    liop = program[ip + 1]
+    coop = liop
+    if liop == 4:
+        coop = A
+    elif liop == 5:
+        coop = B
+    elif liop == 6:
+        coop = C
+    trunc_div = A // 2**coop
+
+    if inst == 0:
+        A = trunc_div
+    elif inst == 1:
+        B = B ^ liop
+    elif inst == 2:
+        B = coop % 8
+    elif inst == 3:
+        if A != 0:
+            ip = liop
+            continue
+    elif inst == 4:
+        B = B ^ C
+    elif inst == 5:
+        output.append(coop % 8)
+    elif inst == 6:
+        B = trunc_div
+    elif inst == 7:
+        C = trunc_div
+    else:
+        raise NotImplementedError()
+
+    ip += 2
+
+print(",".join([str(o) for o in output]))
diff --git a/2024/17/two.py b/2024/17/two.py
new file mode 100644
index 0000000..57ba257
--- /dev/null
+++ b/2024/17/two.py
@@ -0,0 +1,202 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+# program 3 bits numbers
+# registers A, B, C: any int
+# instruction 3 bits, operand 3 bits
+# literal operand, combo operand: 0-3 literal, 4-6: register A,B,C
+# instruction pointer starts at 0
+
+# 0 adv: truncated division, numerator = A, denominator 2**combo operand -> A
+# 1 bxl: bitwise XOR, B ^ literal operand -> B
+# 2 bst: combo operand % 8 -> B
+# 3 jnz: { A = 0: nothing; A != 0: literal operand -> IP } # no increase there!
+# 4 bxc: bitwise XOR, B ^ C -> B # operand ignored
+# 5 out: combo operand % 8 -> output
+# 6 bdv: truncated division, numerator = A, denominator 2**combo operand -> B
+# 7 cdv: truncated division, numerator = A, denominator 2**combo operand -> C
+
+instnames = ["adv", "bxl", "bst", "jnz", "bxc", "out", "bdv", "cdv"]
+
+oA = int(lines[0].split(":")[1])
+oB = int(lines[1].split(":")[1])
+oC = int(lines[2].split(":")[1])
+
+program = [int(p) for p in lines[4].split(":")[1].split(",")]
+
+
+def test(oA: int) -> list[int]:
+    # print(32, "Testing", A)
+
+    ip = 0
+    A = oA
+    B = oB
+    C = oC
+
+    output: list[int] = list()
+    oi = 0
+
+    # jumped: set[tuple[int, int, int, int]] = set()
+
+    while ip in range(len(program)):
+        inst = program[ip]
+        liop = program[ip + 1]
+        # print(50, ip, instnames[inst], liop)
+
+        if inst == 1:
+            B = B ^ liop
+        elif inst == 3:
+            if A != 0:
+                ip = liop
+
+                # Infinite loop prevention
+                # state = (ip, A, B, C)
+                # # print(66, state, jumped)
+                # if state in jumped:
+                #     print("Infinite loop!")
+                #     return False
+                # jumped.add(state)
+
+                continue
+        elif inst == 4:
+            B = B ^ C
+        else:
+            coop = liop
+            if liop == 4:
+                coop = A
+            elif liop == 5:
+                coop = B
+            elif liop == 6:
+                coop = C
+
+            if inst == 2:
+                B = coop % 8
+            elif inst == 5:
+                ou = coop % 8
+                output.append(ou)
+                # if oi >= len(program) or program[oi] != ou:
+                #     # if len(output) >= 6:
+                #     #     print(84, oA, output)
+                #     return False
+                oi += 1
+            else:
+                trunc_div = A // 2**coop
+                if inst == 0:
+                    A = trunc_div
+                elif inst == 6:
+                    B = trunc_div
+                elif inst == 7:
+                    C = trunc_div
+                else:
+                    raise NotImplementedError()
+
+        ip += 2
+
+    print(102, oA, output, len(output))
+    # return oi == len(program)
+    return output
+
+
+print(program, len(program))
+for i in range(0, len(program), 2):
+    inst = program[i]
+    liop = program[i + 1]
+    print(106, i, instnames[inst], liop)
+print()
+
+# Bruteforce
+indexes = list(range(len(program)))
+
+
+def to_number(inps: list[int]) -> int:
+    A = 0
+    for i in indexes[::-1]:
+        A <<= 3
+        A += inps[i]
+    return A
+
+
+inps = program.copy()
+res = test(to_number(inps))
+while res != program:
+    for i in indexes[::-1]:
+        j = 0
+        revi = - len(program) + i
+        while res[revi] != program[i]:
+            inps[i] = j
+            A = to_number(inps)
+            res = test(A)
+            j += 1
+    print("Hi")
+
+# i = 3
+# for j in range(8):
+#     inps = program.copy()
+#     inps[i] = j
+#     A = to_number(inps)
+#     test(A)
+
+print()
+
+# indexes = list(range(len(inps)))
+# for i in indexes[::-1]:
+#     An = A & 0b111
+#     A <<= 3
+#     inp = program[i]
+#     A += inp ^ 2
+# A <<= 3
+# A += 1
+
+# # Smartforce
+# A = 0
+# indexes = list(range(len(program)))
+# for i in indexes[::-1]:
+#     An = A & 0b111
+#     A <<= 3
+#     inp = program[i]
+#     A += inp ^ 2
+
+
+# # I thought it would be easy
+# assoc: dict[tuple[int, int], int] = dict()
+# for i in range(8):
+#     for j in range(8):
+#         out = test(i)[0]
+#         assoc[out] = i, j
+#
+# A = 0
+# for p in program:
+#     A = A << 3
+#     A += assoc[p]
+#
+# A = assoc[7] << 6
+
+res = test(A)
+print("Ref", program)
+print("Res", res)
+print("Cor", res == program)
+print(A)
+
+
+# print(test(100000000))
+#
+# Amin = 2**(3*(len(program)-1))
+# Amax = 2**(3*len(program))
+# Amin = 0
+# Amax = 7
+# for A in range(Amin, Amax + 1):
+#     # if A % 65536 == 0:
+#     #     print(91, A)
+#     if test(A):
+#         print(A)
+#         break
+#     A += 1
+
+
+# print(",".join([str(o) for o in output]))
diff --git a/2024/17/two_bf.py b/2024/17/two_bf.py
new file mode 100644
index 0000000..8a6a0ff
--- /dev/null
+++ b/2024/17/two_bf.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+
+import sys
+import rich.progress
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+# program 3 bits numbers
+# registers A, B, C: any int
+# instruction 3 bits, operand 3 bits
+# literal operand, combo operand: 0-3 literal, 4-6: register A,B,C
+# instruction pointer starts at 0
+
+# 0 adv: truncated division, numerator = A, denominator 2**combo operand -> A
+# 1 bxl: bitwise XOR, B ^ literal operand -> B
+# 2 bst: combo operand % 8 -> B
+# 3 jnz: { A = 0: nothing; A != 0: literal operand -> IP } # no increase there!
+# 4 bxc: bitwise XOR, B ^ C -> B # operand ignored
+# 5 out: combo operand % 8 -> output
+# 6 bdv: truncated division, numerator = A, denominator 2**combo operand -> B
+# 7 cdv: truncated division, numerator = A, denominator 2**combo operand -> C
+
+instnames = ["adv", "bxl", "bst", "jnz", "bxc", "out", "bdv", "cdv"]
+
+oA = int(lines[0].split(":")[1])
+oB = int(lines[1].split(":")[1])
+oC = int(lines[2].split(":")[1])
+
+program = [int(p) for p in lines[4].split(":")[1].split(",")]
+
+
+def test(oA: int) -> bool:
+    # print(32, "Testing", A)
+
+    ip = 0
+    A = oA
+    B = oB
+    C = oC
+
+    # output: list[int] = list()
+    oi = 0
+
+    # jumped: set[tuple[int, int, int, int]] = set()
+
+    while ip in range(len(program)):
+        inst = program[ip]
+        liop = program[ip + 1]
+        # print(50, ip, instnames[inst], liop)
+
+        if inst == 1:
+            B = B ^ liop
+        elif inst == 3:
+            if A != 0:
+                ip = liop
+
+                # Infinite loop prevention
+                # state = (ip, A, B, C)
+                # # print(66, state, jumped)
+                # if state in jumped:
+                #     print("Infinite loop!")
+                #     return False
+                # jumped.add(state)
+
+                continue
+        elif inst == 4:
+            B = B ^ C
+        else:
+            coop = liop
+            if liop == 4:
+                coop = A
+            elif liop == 5:
+                coop = B
+            elif liop == 6:
+                coop = C
+
+            if inst == 2:
+                B = coop % 8
+            elif inst == 5:
+                ou = coop % 8
+                # output.append(ou)
+                if oi >= len(program) or program[oi] != ou:
+                    # if len(output) >= 6:
+                    #     print(84, oA, output)
+                    return False
+                oi += 1
+            else:
+                trunc_div = A // 2**coop
+                if inst == 0:
+                    A = trunc_div
+                elif inst == 6:
+                    B = trunc_div
+                elif inst == 7:
+                    C = trunc_div
+                else:
+                    raise NotImplementedError()
+
+        ip += 2
+
+
+    return oi == len(program)
+    # return output == program
+
+# print(program)
+# for i in range(0, len(program), 2):
+#     inst = program[i]
+#     liop = program[i + 1]
+#     print(106, i, instnames[inst], liop)
+# print()
+#
+# print(test(100000000))
+
+Amin = 2**(3*(len(program)-1))
+Amax = 2**(3*len(program))
+
+for A in rich.progress.track(range(Amin, Amax+1)):
+    if test(A):
+        print(A)
+        break
+    A += 1
+
+
+# print(",".join([str(o) for o in output]))
diff --git a/2024/17/two_dfs.py b/2024/17/two_dfs.py
new file mode 100644
index 0000000..3f34fe9
--- /dev/null
+++ b/2024/17/two_dfs.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+original_B = int(lines[1].split(":")[1])
+original_C = int(lines[2].split(":")[1])
+program = [int(p) for p in lines[4].split(":")[1].split(",")]
+
+
+def output_given_A(A: int) -> list[int]:
+    instruction_pointer = 0
+    B = original_B
+    C = original_C
+
+    output: list[int] = list()
+
+    while instruction_pointer in range(len(program)):
+        instruction = program[instruction_pointer]
+        literal_operator = program[instruction_pointer + 1]
+
+        if instruction == 1:
+            B = B ^ literal_operator
+        elif instruction == 3:
+            if A != 0:
+                instruction_pointer = literal_operator
+                continue
+        elif instruction == 4:
+            B = B ^ C
+        else:
+            combined_operator = literal_operator
+            if literal_operator == 4:
+                combined_operator = A
+            elif literal_operator == 5:
+                combined_operator = B
+            elif literal_operator == 6:
+                combined_operator = C
+
+            if instruction == 2:
+                B = combined_operator % 8
+            elif instruction == 5:
+                output.append(combined_operator % 8)
+            else:
+                trunc_div = A >> combined_operator
+                if instruction == 0:
+                    A = trunc_div
+                elif instruction == 6:
+                    B = trunc_div
+                else:
+                    C = trunc_div
+        instruction_pointer += 2
+
+    return output
+
+
+def input_to_a(input_numbers: list[int]) -> int:
+    A = 0
+    for number in reversed(input_numbers):
+        A = (A << 3) + number
+    return A
+
+
+def dig(depth: int, input_numbers: list[int]) -> int | None:
+    input_numbers = input_numbers.copy()
+    depth += 1
+    for i in range(8):
+        input_numbers[-depth] = i
+        A = input_to_a(input_numbers)
+        output_numbers = output_given_A(A)
+        if output_numbers[-depth:] == program[-depth:]:
+            if depth == len(program):
+                return A
+            res = dig(depth, input_numbers)
+            if res is not None:
+                return res
+    return None
+
+
+print(dig(0, [0] * len(program)))
diff --git a/2024/17/two_fixed.py b/2024/17/two_fixed.py
new file mode 100644
index 0000000..0f387bf
--- /dev/null
+++ b/2024/17/two_fixed.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+original_B = int(lines[1].split(":")[1])
+original_C = int(lines[2].split(":")[1])
+program = [int(p) for p in lines[4].split(":")[1].split(",")]
+
+
+def output_given_A(A: int) -> list[int]:
+    instruction_pointer = 0
+    B = original_B
+    C = original_C
+
+    output: list[int] = list()
+
+    while instruction_pointer in range(len(program)):
+        instruction = program[instruction_pointer]
+        literal_operator = program[instruction_pointer + 1]
+
+        if instruction == 1:
+            B = B ^ literal_operator
+        elif instruction == 3:
+            if A != 0:
+                instruction_pointer = literal_operator
+                continue
+        elif instruction == 4:
+            B = B ^ C
+        else:
+            combined_operator = literal_operator
+            if literal_operator == 4:
+                combined_operator = A
+            elif literal_operator == 5:
+                combined_operator = B
+            elif literal_operator == 6:
+                combined_operator = C
+
+            if instruction == 2:
+                B = combined_operator % 8
+            elif instruction == 5:
+                output.append(combined_operator % 8)
+            else:
+                trunc_div = A >> combined_operator
+                if instruction == 0:
+                    A = trunc_div
+                elif instruction == 6:
+                    B = trunc_div
+                else:
+                    C = trunc_div
+        instruction_pointer += 2
+
+    return output
+
+
+def input_to_a(input_numbers: list[int]) -> int:
+    A = 0
+    for number in reversed(input_numbers):
+        A = (A << 3) + number
+    return A
+
+
+input_numbers: list[int] = program.copy()
+output_numbers: list[int] = output_given_A(input_to_a(input_numbers))
+while output_numbers != program:
+    for i in reversed(range(len(input_numbers))):
+        attempted_number = 0
+        while len(output_numbers) != len(program) or output_numbers[i] != program[i]:
+            input_numbers[i] = attempted_number
+            A = input_to_a(input_numbers)
+            output_numbers = output_given_A(A)
+            attempted_number += 1
+
+print(A)
diff --git a/2024/18/one.py b/2024/18/one.py
new file mode 100644
index 0000000..871e393
--- /dev/null
+++ b/2024/18/one.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+falls = list()
+for line in lines:
+    xs, ys = line.split(",")
+    falls.append((int(xs), int(ys)))
+
+if input_file.startswith("input"):
+    cote = 71
+    simu = 1024
+else:
+    cote = 7
+    simu = 12
+
+vec = tuple[int, int]
+directions = [
+    # COMMENT NOT CORRECT BUT WHO CARES
+    (-1, 0),  # ^ North
+    (0, 1),  # > East
+    (1, 0),  # v South
+    (0, -1),  # < West
+]
+
+exit = cote - 1, cote - 1
+fallen = set(falls[:simu])
+
+visited: set[vec] = set()
+
+def print_grid() -> None:
+    for y in range(cote):
+        line = ""
+        for x in range(cote):
+            char = "."
+            pos = x, y
+            if pos in fallen:
+                char = "#"
+            elif pos in visited:
+                char = "O"
+            line += char
+        print(line)
+    print()
+
+
+print_grid()
+
+curs: set[vec] = {(0, 0)}
+steps = 0
+
+
+while exit not in curs:
+    print("Step", steps)
+    visited |= curs
+    ncurs: set[vec] = set()
+    for x, y in curs:
+        for direction in directions:
+            xx, yy = x + direction[0], y + direction[1]
+            npos = xx, yy
+            if npos in visited or npos in fallen:
+                continue
+            if x not in range(cote) or y not in range(cote):
+                continue
+            ncurs.add(npos)
+    curs = ncurs
+    steps += 1
+
+print_grid()
+print(steps)
+
+# visited: dict[vec, int] = dict()
+#
+# def dig(pos: vec, steps: int) -> int | None:
+#     if steps > 300:
+#         return None
+#     # print(" " * steps, 55, pos)
+#     if pos == exit:
+#         return steps
+#     if pos in fallen:
+#         return None
+#     x, y = pos
+#     if x not in range(cote) or y not in range(cote):
+#         return None
+#     if pos in visited and visited[pos] < steps:
+#         return None
+#     visited[pos] = steps
+#
+#     mini = None
+#     steps += 1
+#     for direction in directions:
+#         xx, yy = x + direction[0], y + direction[1]
+#         res = dig((xx, yy), steps)
+#         if res is None:
+#             continue
+#         if mini is None or res < mini:
+#             mini = res
+#     return mini
+
+# sys.setrecursionlimit(9000)
+# res = dig((0, 0), 0)
+#
+# print_grid()
+#
+# print(res)
diff --git a/2024/18/two.py b/2024/18/two.py
new file mode 100644
index 0000000..6070c35
--- /dev/null
+++ b/2024/18/two.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+falls = list()
+for line in lines:
+    xs, ys = line.split(",")
+    falls.append((int(xs), int(ys)))
+
+if input_file.startswith("input"):
+    cote = 71
+    simu = 1024
+else:
+    cote = 7
+    simu = 12
+
+vec = tuple[int, int]
+directions = [
+    # COMMENT NOT CORRECT BUT WHO CARES
+    (-1, 0),  # ^ North
+    (0, 1),  # > East
+    (1, 0),  # v South
+    (0, -1),  # < West
+]
+
+exit = cote - 1, cote - 1
+
+visited: set[vec] = set()
+fallen: set[vec] = set()
+
+
+def print_grid() -> None:
+    for y in range(cote):
+        line = ""
+        for x in range(cote):
+            char = "."
+            pos = x, y
+            if pos in fallen:
+                char = "#"
+            elif pos in visited:
+                char = "O"
+            line += char
+        print(line)
+    print()
+
+
+for simu in range(len(falls)):
+    print("Simu", simu)
+
+    fallen = set(falls[:simu])
+    visited = set()
+    curs: set[vec] = {(0, 0)}
+    steps = 0
+
+    found = False
+    while exit not in curs:
+        # print("Step", steps)
+        if not curs:
+            break
+        visited |= curs
+        ncurs: set[vec] = set()
+        for x, y in curs:
+            for direction in directions:
+                xx, yy = x + direction[0], y + direction[1]
+                npos = xx, yy
+                if npos in visited or npos in fallen:
+                    continue
+                if x not in range(cote) or y not in range(cote):
+                    continue
+                ncurs.add(npos)
+        curs = ncurs
+        steps += 1
+    else:
+        found = True
+
+    if not found:
+        break
+
+print_grid()
+print(simu)
+print(falls[simu-1])
+
+# visited: dict[vec, int] = dict()
+#
+# def dig(pos: vec, steps: int) -> int | None:
+#     if steps > 300:
+#         return None
+#     # print(" " * steps, 55, pos)
+#     if pos == exit:
+#         return steps
+#     if pos in fallen:
+#         return None
+#     x, y = pos
+#     if x not in range(cote) or y not in range(cote):
+#         return None
+#     if pos in visited and visited[pos] < steps:
+#         return None
+#     visited[pos] = steps
+#
+#     mini = None
+#     steps += 1
+#     for direction in directions:
+#         xx, yy = x + direction[0], y + direction[1]
+#         res = dig((xx, yy), steps)
+#         if res is None:
+#             continue
+#         if mini is None or res < mini:
+#             mini = res
+#     return mini
+
+# sys.setrecursionlimit(9000)
+# res = dig((0, 0), 0)
+#
+# print_grid()
+#
+# print(res)
diff --git a/2024/19/.gitignore b/2024/19/.gitignore
new file mode 100644
index 0000000..496c1f7
--- /dev/null
+++ b/2024/19/.gitignore
@@ -0,0 +1 @@
+colors
diff --git a/2024/19/colors.py b/2024/19/colors.py
new file mode 100644
index 0000000..86a2f27
--- /dev/null
+++ b/2024/19/colors.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    print(fd.read().replace("w", "⬜").replace("u", "🟦").replace("b", "⬛").replace("r", "🟥").replace("g", "🟩"))
diff --git a/2024/19/one.py b/2024/19/one.py
new file mode 100644
index 0000000..d65f7fc
--- /dev/null
+++ b/2024/19/one.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+towels = set(map(str.strip, lines[0].split(",")))
+patterns = lines[2:]
+
+def possible(pattern: str) -> bool:
+    if not pattern:
+        return True
+    for towel in towels:
+        if pattern.startswith(towel):
+            if possible(pattern[len(towel):]):
+                return True
+    return False
+
+possible_count = 0
+for pattern in patterns:
+    if possible(pattern):
+        possible_count += 1
+
+print(possible_count)
+
diff --git a/2024/19/two.py b/2024/19/two.py
new file mode 100644
index 0000000..ea3a9e9
--- /dev/null
+++ b/2024/19/two.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+
+import sys
+import functools
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+towels = set(map(str.strip, lines[0].split(",")))
+patterns = lines[2:]
+
+
+@functools.cache
+def possible(pattern: str) -> int:
+    if not pattern:
+        return 1
+    possible_count = 0
+    for towel in towels:
+        if pattern.startswith(towel):
+            possible_count += possible(pattern[len(towel) :])
+    return possible_count
+
+
+possible_count = 0
+for pattern in patterns:
+    res = possible(pattern)
+    print(27, pattern, res)
+    possible_count += res
+
+print(possible_count)
diff --git a/2024/2/one.py b/2024/2/one.py
new file mode 100644
index 0000000..b015955
--- /dev/null
+++ b/2024/2/one.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+
+safe_num = 0
+for line in lines:
+    report = [int(level) for level in line.split(" ")]
+    acc = sorted(report)
+    dec = acc[::-1]
+    if report != acc and report != dec:
+        continue
+    for i in range(len(report)-1):
+        diff = abs(report[i] - report[i+1])
+        if diff < 1 or diff > 3:
+            break
+    else:
+        safe_num += 1
+print(safe_num)
+
+
diff --git a/2024/2/two.py b/2024/2/two.py
new file mode 100644
index 0000000..7ffe836
--- /dev/null
+++ b/2024/2/two.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+
+def is_safe(report) -> bool:
+    acc = sorted(report)
+    dec = acc[::-1]
+    if report != acc and report != dec:
+        return False
+    for i in range(len(report) - 1):
+        diff = abs(report[i] - report[i + 1])
+        if diff < 1 or diff > 3:
+            return False
+    else:
+        return True
+
+
+safe_num = 0
+for line in lines:
+    report = [int(level) for level in line.split(" ")]
+    possible_reports = [report]
+    for i in range(len(report)):
+        rep = report.copy()
+        rep.pop(i)
+        possible_reports.append(rep)
+    for rep in possible_reports:
+        if is_safe(rep):
+            safe_num += 1
+            break
+
+print(safe_num)
diff --git a/2024/20/README.md b/2024/20/README.md
new file mode 100644
index 0000000..fea7afd
--- /dev/null
+++ b/2024/20/README.md
@@ -0,0 +1,2 @@
+Reading comprehension got me on the second part,
+a one byte change helped 🙃
diff --git a/2024/20/demog b/2024/20/demog
new file mode 100644
index 0000000..9eccf17
--- /dev/null
+++ b/2024/20/demog
@@ -0,0 +1,4 @@
+#####
+#S#E#
+#...#
+#####
diff --git a/2024/20/demog2 b/2024/20/demog2
new file mode 100644
index 0000000..9fb6018
--- /dev/null
+++ b/2024/20/demog2
@@ -0,0 +1,32 @@
+#############################################################################################
+#S#........................................................................................E#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#.#########################################################################################.#
+#...........................................................................................#
+#############################################################################################
diff --git a/2024/20/demog3 b/2024/20/demog3
new file mode 100644
index 0000000..cb01c5e
--- /dev/null
+++ b/2024/20/demog3
@@ -0,0 +1,5 @@
+######
+#...E#
+#S####
+#....#
+######
diff --git a/2024/20/one.py b/2024/20/one.py
new file mode 100644
index 0000000..1909ce9
--- /dev/null
+++ b/2024/20/one.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python3
+
+import collections
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+vec = tuple[int, int]
+directions = [
+    (-1, 0),  # ^ North
+    (0, 1),  # > East
+    (1, 0),  # v South
+    (0, -1),  # < West
+]
+
+Cheat = tuple[vec, int]
+
+cheats: set[Cheat] = set()
+for i, line in enumerate(lines):
+    if "S" in line:
+        j = line.index("S")
+        start = i, j
+    if i in range(1, height - 1):
+        for j in range(1, width - 1):
+            char = lines[i][j]
+            if char != "#":
+                continue
+            for d, direction in enumerate(directions):
+                ii, jj = i + direction[0], j + direction[1]
+                if ii not in range(1, height - 1) or jj not in range(1, width - 1):
+                    continue
+                cchar = lines[ii][jj]
+                if cchar == "#":
+                    continue
+                cheats.add(((i, j), d))
+
+
+def print_grid(visited: list[list[int | None]], cheat: Cheat) -> None:
+    for i in range(height):
+        line = ""
+        for j in range(width):
+            char = lines[i][j]
+            if visited[i][j] is not None:
+                char = "O"
+            if cheat[0] == (i, j):
+                char = "X"
+            line += char
+        print(line)
+    print()
+
+
+def time(cheat: Cheat) -> int | None:
+    visited: list[list[int | None]] = list()
+    for _ in range(height):
+        visited.append([None] * width)
+    stack: set[vec] = {start}
+    for s in range(1, 10000):
+        nstack: set[vec] = set()
+        for pos in stack:
+            i, j = pos
+            for d, direction in enumerate(directions):
+                if (i, j) == cheat[0]:
+                    if d != cheat[1]:
+                        continue
+                ii, jj = i + direction[0], j + direction[1]
+                cchar = lines[ii][jj]
+                if cchar == "#" and cheat != ((ii, jj), d):
+                    continue
+                elif cchar == "E":
+                    # if s == 84 - 8:
+                    #     print_grid(visited, cheat)
+                    return s
+                previs = visited[ii][jj]
+                if previs is not None and previs < s:
+                    continue
+                visited[ii][jj] = s
+                nstack.add((ii, jj))
+        stack = nstack
+
+        # print("Second", s)
+        # print_grid(visited)
+    else:
+        return None
+
+
+canon_saves: collections.Counter[int] = collections.Counter()
+for k, v in (
+    {
+        2: 14,
+        4: 14,
+        6: 2,
+        8: 4,
+        10: 2,
+        12: 3,
+        20: 1,
+        36: 1,
+        38: 1,
+        40: 1,
+        64: 1,
+    }
+).items():
+    canon_saves[k] = v
+
+normal = time(((0, 0), 0))
+assert normal
+saves: collections.Counter[int] = collections.Counter()
+saves_mo100 = 0
+print(f"{normal=}")
+print(f"{len(cheats)=}")
+for c, cheat in enumerate(cheats):
+    print("Cheat", c, "/", len(cheats))
+    ntime = time(cheat)
+    assert ntime
+    diff = normal - ntime
+    saves[diff] += 1
+    if diff >= 100:
+        saves_mo100 += 1
+del saves[0]
+print(f"{saves=}")
+print(f"{canon_saves=}")
+print(f"{(saves == canon_saves)=}")
+print(saves_mo100)
+# 1282: too low
diff --git a/2024/20/reddit_part3 b/2024/20/reddit_part3
new file mode 100644
index 0000000..20dff19
--- /dev/null
+++ b/2024/20/reddit_part3
@@ -0,0 +1,41 @@
+#########################################
+#...#.............#.....#.....#.....#...#
+###.#.###.#########.###.###.#####.###.#.#
+#...#...#.#.#.....#...#...#.#.........#.#
+#..##.###.#.#####.#####.#.#.#.#####.#.#.#
+#.......#.....#.#.....#.#...#...#...#.#.#
+#.###########.#.#.####.####.#.###########
+#.#.#...#...#.....#.................#...#
+#.#.#.#.#.#.###.#.#.###.#########.#####.#
+#.....#...#.....#...#.........#...#.#.#.#
+#####.#####.#####.#.#.#.#.#######.#.#.#.#
+#.....#.........#.#.#...#...#...#.#...#.#
+#.#########.#######.#####.#.##..###.###.#
+#...#.......#.....#.#...#.#...#.....#...#
+#.###.###########.#.###.#.#.###.#######.#
+#.#.#.............#.....#.#...#...#.....#
+###.#.#####.#####.#.###.#.#####.#####.###
+#...#.#.........#.#...#...#...#.#.....#.#
+###.###.#.#########.#####.###.#.#.#.#.#.#
+#S#.#...#.#.....#.....#.........#.#.#..E#
+#.#.#.#########.#.#########.#.###.#####.#
+#.....#.........#...#.#...#.#.....#...#.#
+###.#####..##.#.#####.#.###.#####.###.###
+#.#.#...#.#.#.#.#...#...#...#.........#.#
+#.#.###.###.#.#.#.#####.####.##.#.#####.#
+#.#.#.#.#.#...#.........#.#...#.#.#...#.#
+#.#.#.#.#.#####.###.#.#.#.###.#.###.###.#
+#...#.......#...#...#.#.#.........#.#...#
+#######.#####.#####.###.#.#.#####.#.###.#
+#.............#.....#.#.#.#.....#.......#
+###############.#####.#.#########.#.#.###
+#.....#...#.#.........#.#...#...#.#.#.#.#
+#.#.#.#.#.#.###.#########.###.###.#####.#
+#.#.#.#.#...........#.#.............#...#
+###.#.#.###.#######.#.#.#.###.###.#.#.###
+#...#...#...#.#...#.#...#...#.#.#.#.#...#
+###.#.#######.#.#.#.###.#####.#..##.#.###
+#.#.#...#.....#.#.#.......#.#.#...#.....#
+#.#.#####.###.#.#.#.#.#####.#####.###.#.#
+#.....#.....#.......#.............#...#.#
+#########################################
diff --git a/2024/20/reddit_part3g b/2024/20/reddit_part3g
new file mode 100644
index 0000000..c9c3729
--- /dev/null
+++ b/2024/20/reddit_part3g
@@ -0,0 +1,41 @@
+#########################################
+#...#.............#.....#.....#.....#...#
+###.#.###.#########.###.###.#####.###.#.#
+#...#...#.#.#.....#...#...#.#.........#.#
+#..##.###.#.#####.#####.#.#.#.#####.#.#.#
+#.......#.....#.#.....#.#...#...#...#.#.#
+#.###########.#.#.####.####.#.###########
+#.#.#...#...#.....#.................#...#
+#.#.#.#.#.#.###.#.#.###.#########.#####.#
+#.....#...#.....#...#.........#...#.#.#.#
+#####.#####.#####.#.#.#.#.#######.#.#.#.#
+#.....#.........#.#.#...#...#...#.#...#.#
+#.#########.#######.#####.#.##..###.###.#
+#...#.......#.....#.#...#.#...#.....#...#
+#.###.###########.#.###.#.#.###.#######.#
+#.#.#.............#.....#.#...#...#.....#
+###.#.#####.#####.#.###.#.#####.#####.###
+#...#.#.........#.#...#...#...#.#.....#.#
+###.###.#.#########.#####.###.#.#.#.#.#.#
+#S#.#...#.#.....#.....#.........#.#.#..E#
+#.#.#.#########.#.#########.#.###.#####.#
+#.....#.........#...#.#...#.#.....#...#.#
+###.#####..##.#.#####.#.###.#####.###.###
+#.#.#...#.#.#.#.#...#...#...#.........#.#
+#.#.###.###.#.#.#.#####.####.##.#.#####.#
+#.#.#.#.#.#...#.........#.#...#.#.#...#.#
+#.#.#.#.#.#####.###.#.#.#.###.#.###.###.#
+#...#.......#...#...#.#.#.........#.#...#
+#######.#####.#####.###.#.#.#####.#.###.#
+#.............#.....#.#.#.#.....#.......#
+#####################.#.#########.#.#.###
+#.....#...#.#.........#.#...#...#.#.#.#.#
+#.#.#.#.#.#.###.#########.###.###.#####.#
+#.#.#.#.#...........#.#.............#...#
+###.#.#.###.#######.#.#.#.###.###.#.#.###
+#...#...#...#.#...#.#...#...#.#.#.#.#...#
+###.#.#######.#.#.#.###.#####.#..##.#.###
+#.#.#...#.....#.#.#.......#.#.#...#.....#
+#.#.#####.###.#.#.#.#.#####.#####.###.#.#
+#.....#.....#.......#.............#...#.#
+#########################################
diff --git a/2024/20/two.py b/2024/20/two.py
new file mode 100644
index 0000000..a12dd27
--- /dev/null
+++ b/2024/20/two.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python3
+
+import collections
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+# Adjust parameters for part/file
+part = int(sys.argv[2])
+assert part in (1, 2)
+
+if input_file.startswith("input"):
+    minic = 100
+else:
+    if part == 1:
+        minic = 1
+    else:
+        minic = 50
+skips = 1 if part == 1 else 20
+canon: collections.Counter[int] = collections.Counter()
+demo = {}
+if input_file == "demo":
+    if skips == 1:
+        demo = {2: 14, 4: 14, 6: 2, 8: 4, 10: 2} | (
+            {12: 3, 20: 1, 36: 1, 38: 1, 40: 1, 64: 1}
+        )
+    elif skips == 20:
+        demo = {50: 32, 52: 31, 54: 29, 56: 39, 58: 25, 60: 23} | (
+            {62: 20, 64: 19, 66: 12, 68: 14, 70: 12, 72: 22, 74: 4, 76: 3}
+        )
+    for k, v in demo.items():
+        canon[k] = v
+
+vec = tuple[int, int]
+directions = [
+    (-1, 0),  # ^ North
+    (0, 1),  # > East
+    (1, 0),  # v South
+    (0, -1),  # < West
+]
+
+# Find start position
+for i, line in enumerate(lines):
+    if "S" in line:
+        j = line.index("S")
+        start = i, j
+
+
+# Visit normally
+normal = None
+visited: list[list[int | None]] = list()
+for _ in range(height):
+    visited.append([None] * width)
+visited[start[0]][start[1]] = 0
+stack: set[vec] = {start}
+s = 0
+while stack:
+    s += 1
+    nstack: set[vec] = set()
+    for pos in stack:
+        i, j = pos
+        for d, direction in enumerate(directions):
+            ii, jj = i + direction[0], j + direction[1]
+
+            previs = visited[ii][jj]
+            if previs is not None and previs < s:
+                continue
+            visited[ii][jj] = s
+
+            cchar = lines[ii][jj]
+            if cchar == "#":
+                continue
+            elif cchar == "E":
+                if normal is None:
+                    normal = s
+            nstack.add((ii, jj))
+    stack = nstack
+assert normal
+
+# Print
+for i in range(height):
+    line = ""
+    for j in range(width):
+        char = lines[i][j]
+        if visited[i][j] is not None:
+            if char == "#":
+                char = "@"
+            else:
+                char = "O"
+        line += char
+    print(line)
+print()
+
+
+# Find cheats
+saves: collections.Counter[int] = collections.Counter()
+for i in range(1, height - 1):
+    if height > 100:
+        print(103, i, "/", height)
+    for j in range(1, width - 1):
+        char = lines[i][j]
+        if char == "#":
+            continue
+        ovis = visited[i][j]
+        if ovis is None:
+            continue
+        if ovis >= normal:
+            continue
+        # for di in range(-skips, skips):
+        #     ii = i + di
+        #     G
+        for ii in range(1, height - 1):
+            for jj in range(1, width - 1):
+                manh = abs(i - ii) + abs(j - jj)
+                if manh > skips:
+                    continue
+                cchar = lines[ii][jj]
+                if cchar == "#":
+                    continue
+                nvis = visited[ii][jj]
+                if nvis is None:
+                    continue
+                orem = normal - ovis
+                nrem = abs(normal - nvis) + manh
+                save = orem - nrem
+                if save < minic:
+                    continue
+                saves[save] += 1
+
+
+print(f"{normal=}")
+print(f"{dict(sorted(saves.items()))=}")
+if demo:
+    print(f"{dict(sorted(canon.items()))=}")
+    diff = canon.copy()
+    diff.subtract(saves)
+    print(f"{dict(sorted(diff.items()))=}")
+    print(f"{(saves == canon)=}")
+    print(f"{saves.total()=}")
+    print(f"{canon.total()=}")
+    difft = 0
+    for v in diff.values():
+        difft += abs(v)
+    print(f"{difft=}")
+print(saves.total())
+# 1119834 too high
+# 982425 correct!
diff --git a/2024/20/two_correct.py b/2024/20/two_correct.py
new file mode 100644
index 0000000..81b428d
--- /dev/null
+++ b/2024/20/two_correct.py
@@ -0,0 +1,212 @@
+#!/usr/bin/env python3
+
+import collections
+import colorsys
+import sys
+
+import rich.console
+
+import rich.text
+import rich.progress
+
+console = rich.console.Console()
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+# Adjust parameters for part/file
+part = int(sys.argv[2])
+assert part in (1, 2)
+
+if input_file.startswith("input"):
+    minic = 100
+elif input_file.startswith("reddit_part3"):
+    minic = 30
+else:
+    if part == 1:
+        minic = 1
+    else:
+        minic = 50
+skips = 2 if part == 1 else 20
+canon: collections.Counter[int] = collections.Counter()
+demo = {}
+if input_file == "demo":
+    if part == 1:
+        demo = {2: 14, 4: 14, 6: 2, 8: 4, 10: 2} | (
+            {12: 3, 20: 1, 36: 1, 38: 1, 40: 1, 64: 1}
+        )
+    elif part == 2:
+        demo = {50: 32, 52: 31, 54: 29, 56: 39, 58: 25, 60: 23} | (
+            {62: 20, 64: 19, 66: 12, 68: 14, 70: 12, 72: 22, 74: 4, 76: 3}
+        )
+    for k, v in demo.items():
+        canon[k] = v
+
+vec = tuple[int, int]
+directions = [
+    (-1, 0),  # ^ North
+    (0, 1),  # > East
+    (1, 0),  # v South
+    (0, -1),  # < West
+]
+
+# Find start position
+for i, line in enumerate(lines):
+    if "S" in line:
+        j = line.index("S")
+        start = i, j
+    if "E" in line:
+        j = line.index("E")
+        stop = i, j
+
+
+# Visit forward
+normal = None
+forward: list[list[int | None]] = list()
+for _ in range(height):
+    forward.append([None] * width)
+forward[start[0]][start[1]] = 0
+stack: set[vec] = {start}
+s = 0
+while stack:
+    s += 1
+    nstack: set[vec] = set()
+    for pos in stack:
+        i, j = pos
+        for d, direction in enumerate(directions):
+            ii, jj = i + direction[0], j + direction[1]
+
+            cchar = lines[ii][jj]
+            if cchar == "#":
+                continue
+
+            previs = forward[ii][jj]
+            if previs is not None and previs < s:
+                continue
+            forward[ii][jj] = s
+
+            if cchar == "E":
+                if normal is None:
+                    normal = s
+            nstack.add((ii, jj))
+    stack = nstack
+assert normal
+
+# Visit backwards
+backward: list[list[int | None]] = list()
+for _ in range(height):
+    backward.append([None] * width)
+backward[stop[0]][stop[1]] = 0
+stack = {stop}
+s = 0
+while stack:
+    s += 1
+    nstack = set()
+    for pos in stack:
+        i, j = pos
+        for d, direction in enumerate(directions):
+            ii, jj = i + direction[0], j + direction[1]
+
+            cchar = lines[ii][jj]
+            if cchar == "#":
+                continue
+
+            previs = backward[ii][jj]
+            if previs is not None and previs < s:
+                continue
+            backward[ii][jj] = s
+
+            if cchar == "E":
+                assert s == normal
+            nstack.add((ii, jj))
+    stack = nstack
+
+# Print
+
+
+def perc2color(perc: float) -> str:
+    rgb = colorsys.hsv_to_rgb(perc, 1.0, 1.0)
+    r, g, b = tuple(round(c * 255) for c in rgb)
+    return f"rgb({r},{g},{b})"
+
+
+text = rich.text.Text()
+for i in range(height):
+    for j in range(width):
+        fg = "white"
+        bg = "black"
+        char = lines[i][j]
+        forw = forward[i][j]
+        if char == ".":
+            if forw is not None:
+                fg = perc2color(forw / normal)
+                char = str(forw % 10)
+            bckw = backward[i][j]
+            if bckw is not None:
+                bg = perc2color(bckw / normal)
+        if char == "#":
+            char = "█"
+        text.append(char, style=f"{fg} on {bg}")
+    text.append("\n")
+console.print(text)
+
+
+# Find cheats
+saves: collections.Counter[int] = collections.Counter()
+for i in rich.progress.track(range(1, height - 1), description="Finding cheats"):
+    for j in range(1, width - 1):
+        char = lines[i][j]
+        if char == "#":
+            continue
+        ovis = forward[i][j]
+        if ovis is None:
+            continue
+        if ovis >= normal:
+            continue
+        min_i = max(1, i - skips)
+        max_i = min(height - 1, i + skips)
+        for ii in range(min_i, max_i + 1):
+            rem = skips - abs(ii - i)
+            min_j = max(1, j - rem)
+            max_j = min(width - 1, j + rem)
+            for jj in range(min_j, max_j + 1):
+                manh = abs(i - ii) + abs(j - jj)
+                if manh > skips:
+                    continue
+                cchar = lines[ii][jj]
+                if cchar == "#":
+                    continue
+                nvis = backward[ii][jj]
+                if nvis is None:
+                    continue
+                orem = normal - ovis
+                nrem = nvis + manh
+                save = orem - nrem
+                if save < minic:
+                    continue
+                saves[save] += 1
+
+log = console.log
+
+log(f"{normal=}")
+log(f"{dict(sorted(saves.items()))=}")
+if demo:
+    log(f"{dict(sorted(canon.items()))=}")
+    diff = canon.copy()
+    diff.subtract(saves)
+    log(f"{dict(sorted(diff.items()))=}")
+    log(f"{(saves == canon)=}")
+    log(f"{saves.total()=}")
+    log(f"{canon.total()=}")
+    difft = 0
+    for v in diff.values():
+        difft += abs(v)
+    log(f"{difft=}")
+print(saves.total())
+# 1119834 too high
+# 982425 correct!
diff --git a/2024/20/two_fast.py b/2024/20/two_fast.py
new file mode 100644
index 0000000..dd6f11d
--- /dev/null
+++ b/2024/20/two_fast.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python3
+
+import collections
+import colorsys
+import sys
+import rich.console
+
+console = rich.console.Console()
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+# Adjust parameters for part/file
+part = int(sys.argv[2])
+assert part in (1, 2)
+
+if input_file.startswith("input"):
+    minic = 100
+elif input_file.startswith("reddit_part3"):
+    minic = 30
+else:
+    if part == 1:
+        minic = 1
+    else:
+        minic = 50
+skips = 2 if part == 1 else 20
+canon: collections.Counter[int] = collections.Counter()
+demo = {}
+if input_file == "demo":
+    if part == 1:
+        demo = {2: 14, 4: 14, 6: 2, 8: 4, 10: 2} | (
+            {12: 3, 20: 1, 36: 1, 38: 1, 40: 1, 64: 1}
+        )
+    elif part == 2:
+        demo = {50: 32, 52: 31, 54: 29, 56: 39, 58: 25, 60: 23} | (
+            {62: 20, 64: 19, 66: 12, 68: 14, 70: 12, 72: 22, 74: 4, 76: 3}
+        )
+    for k, v in demo.items():
+        canon[k] = v
+
+vec = tuple[int, int]
+directions = [
+    (-1, 0),  # ^ North
+    (0, 1),  # > East
+    (1, 0),  # v South
+    (0, -1),  # < West
+]
+
+# Find start position
+for i, line in enumerate(lines):
+    if "S" in line:
+        j = line.index("S")
+        start = i, j
+
+
+# Visit normally
+normal = None
+visited: list[list[int | None]] = list()
+for _ in range(height):
+    visited.append([None] * width)
+visited[start[0]][start[1]] = 0
+stack: set[vec] = {start}
+s = 0
+while stack:
+    s += 1
+    nstack: set[vec] = set()
+    for pos in stack:
+        i, j = pos
+        for d, direction in enumerate(directions):
+            ii, jj = i + direction[0], j + direction[1]
+
+            cchar = lines[ii][jj]
+            if cchar == "#":
+                continue
+
+            previs = visited[ii][jj]
+            if previs is not None and previs < s:
+                continue
+            visited[ii][jj] = s
+
+            if cchar == "E":
+                if normal is None:
+                    normal = s
+            nstack.add((ii, jj))
+    stack = nstack
+assert normal
+
+# Print
+for i in range(height):
+    line = ""
+    for j in range(width):
+        char = lines[i][j]
+        vis = visited[i][j]
+        if (i, j) == (19, 1):
+            char = "[bold red on black]@"
+        elif (i, j) == (15, 1):
+            char = "[bold red on black]a"
+        elif vis is not None and char == ".":
+            hue = vis / normal
+            rgb = colorsys.hsv_to_rgb(hue, 1.0, 1.0)
+            r, g, b = tuple(round(c * 255) for c in rgb)
+            char = f"[on rgb({r},{g},{b})]{vis % 10}"
+        elif char == "#":
+            char = "[white]█"
+        else:
+            char = f"[bold green on black]{char}"
+        line += char
+    console.print(line)
+print()
+
+
+# Find cheats
+saves: collections.Counter[int] = collections.Counter()
+for i in range(1, height - 1):
+    if height > 100:
+        print(103, i, "/", height-2)
+    for j in range(1, width - 1):
+        char = lines[i][j]
+        if char == "#":
+            continue
+        ovis = visited[i][j]
+        if ovis is None:
+            continue
+        if ovis >= normal:
+            continue
+        min_i = max(1, i-skips)
+        max_i = min(height-1, i+skips)
+        for ii in range(min_i, max_i+1):
+            rem = skips - abs(ii - i)
+            min_j = max(1, j-rem)
+            max_j = min(width-1, j+rem)
+            for jj in range(min_j, max_j+1):
+                manh = abs(i - ii) + abs(j - jj)
+                if manh > skips:
+                    continue
+                cchar = lines[ii][jj]
+                if cchar == "#":
+                    continue
+                nvis = visited[ii][jj]
+                if nvis is None:
+                    continue
+                orem = normal - ovis
+                # Works if there's space after the E, but catches unrelated paths
+                nrem = abs(normal - nvis) + manh
+                save = orem - nrem
+                if save < minic:
+                    continue
+                saves[save] += 1
+
+
+print(f"{normal=}")
+print(f"{dict(sorted(saves.items()))=}")
+if demo:
+    print(f"{dict(sorted(canon.items()))=}")
+    diff = canon.copy()
+    diff.subtract(saves)
+    print(f"{dict(sorted(diff.items()))=}")
+    print(f"{(saves == canon)=}")
+    print(f"{saves.total()=}")
+    print(f"{canon.total()=}")
+    difft = 0
+    for v in diff.values():
+        difft += abs(v)
+    print(f"{difft=}")
+print(saves.total())
+# 1119834 too high
+# 982425 correct!
diff --git a/2024/21/Notes.xopp b/2024/21/Notes.xopp
new file mode 100644
index 0000000..7fc861c
Binary files /dev/null and b/2024/21/Notes.xopp differ
diff --git a/2024/21/Spreadsheet.ods b/2024/21/Spreadsheet.ods
new file mode 100644
index 0000000..f37f05a
Binary files /dev/null and b/2024/21/Spreadsheet.ods differ
diff --git a/2024/21/demog b/2024/21/demog
new file mode 100644
index 0000000..44442d2
--- /dev/null
+++ b/2024/21/demog
@@ -0,0 +1 @@
+3A
diff --git a/2024/21/one.py b/2024/21/one.py
new file mode 100644
index 0000000..74e16cd
--- /dev/null
+++ b/2024/21/one.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+numeric_keypad = ["789", "456", "123", " 0A"]
+directional_keypad = [" ^A", "<v>"]
+
+vec = tuple[int, int]
+directions = {
+    "^": (-1, 0),  # ^ North
+    ">": (0, 1),  # > East
+    "v": (1, 0),  # v South
+    "<": (0, -1),  # < West
+}
+directional_keypad_buttons = tuple(directions.keys()) + ("A",)
+
+complexity = int(sys.argv[2])
+keypads = [directional_keypad] * complexity + [numeric_keypad]
+
+
+def but_pos(but: str, keypad: list[str]) -> vec:
+    for i, line in enumerate(keypad):
+        if but in line:
+            return i, line.index(but)
+    raise IndexError("No such button")
+
+
+def in_bounds(i: int, j: int, keypad: list[str]) -> bool:
+    if j not in range(3) or i not in range(len(keypad)):
+        return False
+    return keypad[i][j] != " "
+
+last_but = "A"
+all_a = [but_pos("A", keypad) for keypad in keypads]
+
+score = 0
+for code in lines:
+    print("Code", code)
+    topresses = 0
+    for desir_but in code:
+        print("Button", desir_but)
+        all_a[-1] = but_pos(last_but, keypads[-1])
+        start_poss = tuple(all_a)
+        all_a[-1] = but_pos(desir_but, keypads[-1])
+        desir_poss = tuple(all_a)
+
+        stack = {start_poss}
+        seen = set()
+        presses = 0
+        while desir_poss not in stack:
+            # print("Press", presses, stack)
+            presses += 1
+            nstack = set()
+            for poss in stack:
+                for but in directional_keypad_buttons:
+                    # Find which keypad this will move
+                    k = 0
+                    while but == "A" and k < len(keypads) - 1:
+                        i, j = poss[k]
+                        but = keypads[k][i][j]
+                        k += 1
+
+                    # Do not press the final keypad
+                    if k == len(keypads) - 1 and but == "A":
+                        continue
+
+                    # Move
+                    direction = directions[but]
+                    i, j = poss[k]
+                    ii, jj = i + direction[0], j + direction[1]
+                    if not in_bounds(ii, jj, keypads[k]):
+                        continue
+
+                    # Ensure we haven't been in this state before
+                    state = poss[:k] + ((ii, jj),) + poss[k + 1 :]
+                    if state in seen:
+                        continue
+                    seen.add(state)
+
+                    # print(" Kept", state)
+                    nstack.add(state)
+
+            stack = nstack
+        topresses += presses + 1
+        last_but = desir_but
+
+    numpart = int("0" + code.replace("A", ""))
+    print(f"{topresses=} * {numpart=}")
+    score += topresses * numpart
+
+
+
+print(score)
diff --git a/2024/21/onet.py b/2024/21/onet.py
new file mode 100644
index 0000000..19ec3ff
--- /dev/null
+++ b/2024/21/onet.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+numeric_keypad = ["789", "456", "123", " 0A"]
+directional_keypad = [" ^A", "<v>"]
+
+vec = tuple[int, int]
+directions = {
+    "^": (-1, 0),  # ^ North
+    ">": (0, 1),  # > East
+    "v": (1, 0),  # v South
+    "<": (0, -1),  # < West
+}
+directional_keypad_buttons = tuple(directions.keys()) + ("A",)
+
+complexity = int(sys.argv[2])
+keypads = [directional_keypad] * complexity + [numeric_keypad]
+
+
+def but_pos(but: str, keypad: list[str]) -> vec:
+    for i, line in enumerate(keypad):
+        if but in line:
+            return i, line.index(but)
+    raise IndexError("No such button")
+
+
+def in_bounds(i: int, j: int, keypad: list[str]) -> bool:
+    if j not in range(3) or i not in range(len(keypad)):
+        return False
+    return keypad[i][j] != " "
+
+last_but = "A"
+all_a = [but_pos("A", keypad) for keypad in keypads]
+
+score = 0
+for code in lines:
+    # print("Code", code)
+    topresses = 0
+    for desir_but in code:
+        # print("Button", desir_but)
+        all_a[-1] = but_pos(last_but, keypads[-1])
+        start_poss = tuple(all_a)
+        all_a[-1] = but_pos(desir_but, keypads[-1])
+        if len(keypads) > 1:
+            all_a[-2] = but_pos("^", keypads[-2])
+        desir_poss = tuple(all_a)
+
+        stack = {start_poss}
+        seen = set()
+        presses = 0
+        while desir_poss not in stack:
+            # print("Press", presses, stack)
+            presses += 1
+            nstack = set()
+            for poss in stack:
+                for but in directional_keypad_buttons:
+                    # Find which keypad this will move
+                    k = 0
+                    while but == "A" and k < len(keypads) - 1:
+                        i, j = poss[k]
+                        but = keypads[k][i][j]
+                        k += 1
+
+                    # Do not press the final keypad
+                    if k == len(keypads) - 1 and but == "A":
+                        continue
+
+                    # Move
+                    direction = directions[but]
+                    i, j = poss[k]
+                    ii, jj = i + direction[0], j + direction[1]
+                    if not in_bounds(ii, jj, keypads[k]):
+                        continue
+
+                    # Ensure we haven't been in this state before
+                    state = poss[:k] + ((ii, jj),) + poss[k + 1 :]
+                    if state in seen:
+                        continue
+                    seen.add(state)
+
+                    # print(" Kept", state)
+                    nstack.add(state)
+
+            stack = nstack
+        topresses += presses + 0
+        last_but = desir_but
+
+    score += topresses
+
+print(score)
+
diff --git a/2024/21/two.py b/2024/21/two.py
new file mode 100644
index 0000000..1ebbf45
--- /dev/null
+++ b/2024/21/two.py
@@ -0,0 +1,281 @@
+#!/usr/bin/env python3
+
+import sys
+import typing
+import functools
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+numeric_keypad = ["789", "456", "123", " 0A"]
+directional_keypad = [" ^A", "<v>"]
+
+vec = tuple[int, int]
+directions = {
+    "^": (-1, 0),  # ^ North
+    ">": (0, 1),  # > East
+    "v": (1, 0),  # v South
+    "<": (0, -1),  # < West
+}
+directional_keypad_buttons = tuple(directions.keys()) + ("A",)
+
+complexity = int(sys.argv[2])
+keypads = [numeric_keypad] + ([directional_keypad] * complexity)
+
+
+def but_pos(but: str, keypad: list[str]) -> vec:
+    for i, line in enumerate(keypad):
+        if but in line:
+            return i, line.index(but)
+    raise IndexError(f"No such button: {but} in {keypad}")
+
+
+def in_bounds(i: int, j: int, keypad: list[str]) -> bool:
+    if j not in range(3) or i not in range(len(keypad)):
+        return False
+    return keypad[i][j] != " "
+
+
+# FIFTH TRY
+# Using 2 as a base
+
+
+@functools.cache
+def press(buts: str, depth: int) -> int:
+    if depth == len(keypads):
+        return len(buts)
+    keypad = keypads[depth]
+    i, j = but_pos("A", keypad)
+    nums = 0
+    for but in buts:
+        nnbuts = ""
+        ni, nj = but_pos(but, keypad)
+        bounded = True
+        while nj < j:
+            nnbuts += "<"
+            j -= 1
+            bounded &= in_bounds(i, j, keypad)
+        while ni > i:
+            nnbuts += "v"
+            i += 1
+            bounded &= in_bounds(i, j, keypad)
+        while ni < i:
+            nnbuts += "^"
+            i -= 1
+            bounded &= in_bounds(i, j, keypad)
+        while nj > j:
+            nnbuts += ">"
+            j += 1
+            bounded &= in_bounds(i, j, keypad)
+        if not bounded:
+            nnbuts = nnbuts[::-1]
+        nnbuts += "A"
+        nums += press(nnbuts, depth + 1)
+    return nums
+
+
+score = 0
+for code in lines:
+    print("Code", code)
+    topresses = press(code, 0)
+
+    numpart = int("0" + code.replace("A", ""))
+    print(f"{topresses=} * {numpart=}")
+    score += topresses * numpart
+
+print(score)
+
+
+# FOURTH TRY
+
+
+# def press4(buts: str) -> int:
+#     poss = [list(but_pos("A")) for keypad in keypads]
+#     indexes = [0 for _ in keypads]
+#     combis = ["" for _ in keypads]
+#     combis[0] = buts
+#     while indexes[0] != len(buts):
+#         for k in len(keypads):
+#         pass
+#     return 0
+#
+#
+# score = 0
+# for code in lines:
+#     print("Code", code)
+#
+#     topresses = press4(code)
+#
+#     numpart = int("0" + code.replace("A", ""))
+#     print(f"{topresses=} * {numpart=}")
+#     score += topresses * numpart
+#
+# print(score)
+
+sys.exit(0)
+
+# THIRD TRY
+
+
+def press3(buts: str, depth: int) -> typing.Generator[str, None, None]:
+    if depth >= len(keypads):
+        yield from buts
+        return
+
+    keypad = keypads[::-1][depth]
+    but_poss: dict[str, vec] = dict()
+    for i, line in enumerate(keypad):
+        for j, but in enumerate(line):
+            but_poss[but] = i, j
+    i, j = but_poss["A"]
+    ai, ij = but_poss[" "]
+
+    for but in press3(buts, depth + 1):
+        nnbuts = ""
+        ni, nj = but_poss[but]
+        bounded = True
+        if nj < j:
+            nnbuts += "<" * (j - nj)
+            j = nj
+            bounded &= (ai, ij) != (i, j)
+        if ni > i:
+            nnbuts += "v" * (ni - i)
+            i = ni
+            bounded &= (ai, ij) != (i, j)
+        if ni < i:
+            nnbuts += "^" * (i - ni)
+            i = ni
+            bounded &= (ai, ij) != (i, j)
+        if nj > j:
+            nnbuts += ">" * (nj - j)
+            j = nj
+        if not bounded:
+            nnbuts = nnbuts[::-1]
+        nnbuts += "A"
+        yield from nnbuts
+
+
+score = 0
+for code in lines:
+    print("Code", code)
+
+    topresses = 0
+    for _ in press3(code, 0):
+        topresses += 1
+
+    numpart = int("0" + code.replace("A", ""))
+    print(f"{topresses=} * {numpart=}")
+    score += topresses * numpart
+
+print(score)
+
+# SECOND TRY
+
+# # Shouldn't move over the bounds, repeat movements when possible, also use movements
+# # that start further to A then closer to A, because we're going to press A after anyways
+# moves = {
+#     "AA": "",
+#     "A^": "<",
+#     "A>": "v",
+#     "Av": "<v",  # sort
+#     "A<": "v<<",  # hole avoid
+#     "^A": ">",
+#     "^^": "",
+#     "^>": "v>",  # sort
+#     "^v": "v",
+#     "^<": "v<",  # hole avoid
+#     ">A": "^",
+#     ">^": "<^",  # sort
+#     ">>": "",
+#     ">v": "<",
+#     "><": "<<",
+#     "vA": "^>",  # symetric. but lower layer sort!
+#     "v^": "^",
+#     "v>": ">",
+#     "vv": "",
+#     "v<": "<",
+#     "<A": ">>^",  # hole avoid
+#     "<^": ">^",  # hole avoid
+#     "<>": ">>",
+#     "<v": ">",
+#     "<<": "",
+# }
+#
+#
+# def press(buts: str, depth: int) -> str:
+#     if depth == len(keypads):
+#         return buts
+#     keypad = keypads[depth]
+#     numerical = keypad == numeric_keypad
+#     prev_but = "A"
+#     i, j = but_pos(prev_but, keypad)
+#     nbuts = ""
+#     for but in buts:
+#         if numerical:
+#             nnbuts = ""
+#             ni, nj = but_pos(but, keypad)
+#             # right before down
+#             # up before left
+#             bounded = True
+#             while nj < j:
+#                 nnbuts += "<"
+#                 j -= 1
+#                 bounded &= in_bounds(i, j, keypad)
+#             while ni > i:
+#                 nnbuts += "v"
+#                 i += 1
+#                 bounded &= in_bounds(i, j, keypad)
+#             while ni < i:
+#                 nnbuts += "^"
+#                 i -= 1
+#                 bounded &= in_bounds(i, j, keypad)
+#             while nj > j:
+#                 nnbuts += ">"
+#                 j += 1
+#                 bounded &= in_bounds(i, j, keypad)
+#             if not bounded:
+#                 nnbuts = nnbuts[::-1]
+#             nbuts += nnbuts
+#         else:
+#             move = prev_but + but
+#             nbuts += moves[move]
+#         nbuts += "A"
+#         prev_but = but
+#     return press(nbuts, depth + 1)
+#
+#
+# score = 0
+# for code in lines:
+#     print("Code", code)
+#     presses = press(code, 0)
+#     print(f"{presses=}")
+#     topresses = len(presses)
+#
+#     numpart = int("0" + code.replace("A", ""))
+#     print(f"{topresses=} * {numpart=}")
+#     score += topresses * numpart
+#
+# print(score)
+
+# FIRST TRY
+
+# keys = {
+#     "A": "A",
+#     "^": "<A" + ">",
+#     ">": "VA" + "^",
+#     "V": "V<A" + ">^",
+#     "<": "V<<A" + ">>^",
+# }
+#
+# def press(but: str, depth: int) -> str:
+#     if depth <= 0:
+#         return but
+#     depth -= 1
+#     return "".join(press(b, depth) for b in keys[but])
+#
+#
+# res = press("^", complexity)
+# # res += press("A", complexity)
+# print(len(res), res)
diff --git a/2024/21/two_fast.py b/2024/21/two_fast.py
new file mode 100644
index 0000000..f59791c
--- /dev/null
+++ b/2024/21/two_fast.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+
+import functools
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+numeric_keypad = ["789", "456", "123", " 0A"]
+directional_keypad = [" ^A", "<v>"]
+
+vec = tuple[int, int]
+
+complexity = int(sys.argv[2])
+keypads = [numeric_keypad] + ([directional_keypad] * complexity)
+
+
+@functools.cache
+def press(buts: str, depth: int) -> int:
+    if depth == len(keypads):
+        return len(buts)
+
+    keypad = keypads[depth]
+    but_poss: dict[str, vec] = dict()
+    for i, line in enumerate(keypad):
+        for j, but in enumerate(line):
+            but_poss[but] = i, j
+    i, j = but_poss["A"]
+    ai, aj = but_poss[" "]
+
+    depth += 1
+    nums = 0
+    for but in buts:
+        nnbuts = ""
+        ni, nj = but_poss[but]
+        crossing_gap = ((i == ai) and (nj == aj)) or ((ni == ai) and (j == aj))
+        if nj < j:
+            nnbuts += "<" * (j - nj)
+        if ni < i:
+            nnbuts += "^" * (i - ni)
+        elif ni > i:
+            nnbuts += "v" * (ni - i)
+        if nj > j:
+            nnbuts += ">" * (nj - j)
+        i, j = ni, nj
+        if crossing_gap:
+            nnbuts = nnbuts[::-1]
+        nnbuts += "A"
+        nums += press(nnbuts, depth)
+    return nums
+
+
+score = 0
+for code in lines:
+    print("Code", code)
+    topresses = press(code, 0)
+
+    numpart = int("0" + code.replace("A", ""))
+    print(f"{topresses=} * {numpart=}")
+    score += topresses * numpart
+
+print(score)
diff --git a/2024/22/README.md b/2024/22/README.md
new file mode 100644
index 0000000..36fa2aa
--- /dev/null
+++ b/2024/22/README.md
@@ -0,0 +1,20 @@
+Struggled on the fact that "rounding *down* to the **nearest** integer" doesn't mean `round()` it means `floor()` for part1.
+Got the idea in the back of my mind this is the thing that could be heavily paralellized.
+
+For part2 I found a solution that would take 12h in brute force.
+Removing a list creation I got it down to 3 hours (`two_bf.py`). I let this run in the background.
+Using numpy shenanigans I got it down to 20 minutes.
+It worked before I could do further optimisation.
+
+I have no fucking idea what could be the optimisations here,
+and not super interested in figuring out. I'll spoil myself.
+
+---
+
+OH
+
+🤚: buyer × buyer's sequences
+👉: all possible sequences × buyers
+
+Yeah alright
+
diff --git a/2024/22/one.py b/2024/22/one.py
new file mode 100644
index 0000000..df18a72
--- /dev/null
+++ b/2024/22/one.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+
+def prng(secwet: int) -> int:
+    secwet ^= secwet * 64
+    secwet %= 16777216
+    secwet ^= secwet // 32
+    secwet %= 16777216
+    secwet ^= secwet * 2048
+    secwet %= 16777216
+    return secwet
+
+
+tot = 0
+for line in lines:
+    secwet = int(line)
+    print(f"Init {secwet=}")
+    for i in range(2000):
+        secwet = prng(secwet)
+    print(f" -> {secwet=}")
+    tot += secwet
+print(tot)
diff --git a/2024/22/two.py b/2024/22/two.py
new file mode 100644
index 0000000..b55dc4c
--- /dev/null
+++ b/2024/22/two.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+
+import sys
+
+import numpy as np
+import rich.progress
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+buyers = len(lines)
+
+
+def prng(secwet: int) -> int:
+    secwet ^= secwet * 64
+    secwet %= 16777216
+    secwet ^= secwet // 32
+    secwet %= 16777216
+    secwet ^= secwet * 2048
+    secwet %= 16777216
+    return secwet
+
+
+buyers_ban: list[list[int]] = []
+buyers_dif: list[list[int]] = []
+
+for line in rich.progress.track(lines, description="Calculating future banana prices"):
+    secwet = int(line)
+    buyer_ban: list[int] = []
+    buyer_dif: list[int] = []
+    last_ban = secwet % 10
+    for i in range(2000):
+        secwet = prng(secwet)
+        ban = secwet % 10
+        buyer_ban.append(ban)
+        dif = ban - last_ban
+        buyer_dif.append(dif)
+        last_ban = ban
+    #     print(f"{secwet=} {ban=} {dif=}")
+    # print(f"{buyer_ban=}")
+    # print(f"{buyer_dif=}")
+    buyers_ban.append(buyer_ban)
+    buyers_dif.append(buyer_dif)
+
+buyers_dif_np = np.array(buyers_dif)
+buyers_ban_np = np.array(buyers_ban)
+
+sequence = tuple[int, int, int, int]
+
+
+def totbans(seq: sequence) -> int:
+    match = None
+    for i, num in enumerate(seq):
+        nmatch = buyers_dif_np == num
+        if match is not None:
+            # nmatch = sp.ndimage.shift(nmatch, (0, -i))
+            nmatch = np.roll(nmatch, -i, axis=1)
+            nmatch &= match
+        match = nmatch
+
+    # bans = buyers_ban_np * match
+    # found = match.max(axis=1)
+    # indexes = np.argmax(match > 0, axis=1)
+
+    tot = 0
+    assert match is not None
+    for b, buyer_match in enumerate(match):
+        if not buyer_match.max():
+            continue
+        arg: int = np.argmax(buyer_match > 0)
+        try:
+            ban = buyers_ban_np[b, arg+3]
+            tot += ban
+        except IndexError:
+            pass # shrug
+    return tot
+
+    for buyer in range(buyers):
+        buyer_dif = buyers_dif[buyer]
+        for i in range(2000 - 4):
+            if (
+                buyer_dif[i] == seq[0]
+                and buyer_dif[i + 1] == seq[1]
+                and buyer_dif[i + 2] == seq[2]
+                and buyer_dif[i + 3] == seq[3]
+            ):
+                # if tuple(buyer_dif[i : i + 4]) == seq:
+                tot += buyers_ban[buyer][i + 3]
+                break
+    return tot
+
+
+# print(f"{totbans((6, -1, -1, 0))=}") # demo0
+# print(f"{totbans((-2, 1, -1, 3))=}") # demo2 aoc
+# print(f"{totbans((6, -4, 4, -9))=}") # demo2 first
+
+all_seqs: set[sequence] = set()
+
+for buyer in rich.progress.track(
+    range(buyers), description="Generating possible sequences"
+):
+    buyer_dif = buyers_dif[buyer]
+    for i in range(2000 - 4):
+        seq: sequence = tuple(buyer_dif[i : i + 4])
+        all_seqs.add(seq)
+print(f"{len(all_seqs)=}")
+
+maxi = 0
+max_seq = None
+for seq in rich.progress.track(all_seqs, description="Finding score for sequences"):
+    tb = totbans(seq)
+    if tb > maxi:
+        maxi = tb
+        max_seq = seq
+
+
+print(f"{max_seq=}")
+print(maxi)
+# 1909 too low
diff --git a/2024/22/two_bf.py b/2024/22/two_bf.py
new file mode 100644
index 0000000..96bbe2a
--- /dev/null
+++ b/2024/22/two_bf.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+
+import sys
+
+import rich.progress
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+buyers = len(lines)
+
+
+def prng(secwet: int) -> int:
+    secwet ^= secwet * 64
+    secwet %= 16777216
+    secwet ^= secwet // 32
+    secwet %= 16777216
+    secwet ^= secwet * 2048
+    secwet %= 16777216
+    return secwet
+
+
+buyers_ban: list[list[int]] = []
+buyers_dif: list[list[int]] = []
+
+for line in rich.progress.track(lines, description="Calculating future banana prices"):
+    secwet = int(line)
+    buyer_ban: list[int] = []
+    buyer_dif: list[int] = []
+    last_ban = secwet % 10
+    for i in range(2000):
+        secwet = prng(secwet)
+        ban = secwet % 10
+        buyer_ban.append(ban)
+        dif = ban - last_ban
+        buyer_dif.append(dif)
+        last_ban = ban
+    #     print(f"{secwet=} {ban=} {dif=}")
+    # print(f"{buyer_ban=}")
+    # print(f"{buyer_dif=}")
+    buyers_ban.append(buyer_ban)
+    buyers_dif.append(buyer_dif)
+
+sequence = tuple[int, int, int, int]
+
+
+def totbans(seq: sequence) -> int:
+    tot = 0
+    for buyer in range(buyers):
+        buyer_dif = buyers_dif[buyer]
+        for i in range(2000 - 4):
+            if (
+                buyer_dif[i] == seq[0]
+                and buyer_dif[i + 1] == seq[1]
+                and buyer_dif[i + 2] == seq[2]
+                and buyer_dif[i + 3] == seq[3]
+            ):
+                # if tuple(buyer_dif[i : i + 4]) == seq:
+                tot += buyers_ban[buyer][i + 3]
+                break
+    return tot
+
+
+all_seqs: set[sequence] = set()
+
+for buyer in rich.progress.track(
+    range(buyers), description="Generating possible sequences"
+):
+    buyer_dif = buyers_dif[buyer]
+    for i in range(2000 - 4):
+        seq: sequence = tuple(buyer_dif[i : i + 4])
+        all_seqs.add(seq)
+print(f"{len(all_seqs)=}")
+
+maxi = 0
+max_seq = None
+for seq in rich.progress.track(all_seqs, description="Finding score for sequences"):
+    tb = totbans(seq)
+    if tb > maxi:
+        maxi = tb
+        max_seq = seq
+
+
+print(f"{max_seq=}")
+print(maxi)
diff --git a/2024/22/two_fast.py b/2024/22/two_fast.py
new file mode 100644
index 0000000..3789c14
--- /dev/null
+++ b/2024/22/two_fast.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+
+import collections
+import math
+import sys
+
+import numpy as np
+import rich.progress
+
+dtype = np.int32
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    buyers_init_secret = np.array([int(line) for line in fd.readlines()], dtype=dtype)
+
+buyers = len(buyers_init_secret)
+
+ITERATIONS = 2000
+SEQUENCE_LENGTH = 4
+
+
+buyers_prices = np.ndarray((buyers, ITERATIONS), dtype=dtype)
+buyers_diffs = np.ndarray((buyers, ITERATIONS), dtype=dtype)
+
+buyers_secret = buyers_init_secret.copy()
+last_buyers_price = buyers_init_secret % 10
+for i in rich.progress.track(range(ITERATIONS), description="Simulating stock market"):
+    buyers_secret ^= buyers_secret * 64
+    buyers_secret %= 16777216
+    buyers_secret ^= buyers_secret // 32
+    buyers_secret %= 16777216
+    buyers_secret ^= buyers_secret * 2048
+    buyers_secret %= 16777216
+    buyers_price = buyers_secret % 10
+    buyers_diff = buyers_price - last_buyers_price
+    buyers_prices[:, i] = buyers_price
+    buyers_diffs[:, i] = buyers_diff
+    last_buyers_price = buyers_price
+
+# Compress sequence tuples into a single integer
+
+# Move to positive numbers
+trans = -buyers_diffs.min()
+buyers_diffs_translated = buyers_diffs + trans
+# Decide on a value to shift
+shift = math.ceil(math.log2(buyers_diffs_translated.max()))
+
+buyers_sequences = buyers_diffs_translated.copy()
+for i in range(1, SEQUENCE_LENGTH):
+    buyers_sequences += np.roll(buyers_diffs_translated << (shift * i), i, axis=1)
+# Make first few sequences invalid
+buyers_sequences[:, :SEQUENCE_LENGTH] = -1
+
+# NEW (knowing it's best to iterate per buyer than per sequence) ~ 5 seconds
+# Inspired by the following, which is even faster, probably because data locality
+# achieves more than SIMD-fying?
+# https://github.com/mkern75/AdventOfCodePython/blob/23b6becdc873c6b865e783122a7dbce0b5f40f60/year2024/Day22.py
+
+max_sequence = 1 << shift * SEQUENCE_LENGTH
+sequences_total = np.zeros(max_sequence, dtype=dtype)
+seen = np.zeros((buyers, max_sequence), dtype=bool)
+
+for b in rich.progress.track(range(buyers), description="Evaluating each buyer"):
+    for i in range(SEQUENCE_LENGTH, ITERATIONS):
+        seq = buyers_sequences[b, i]
+
+        if seen[b, seq]:
+            continue
+        seen[b, seq] = True
+
+        sequences_total[seq] += buyers_prices[b, i]
+
+print(f"{sequences_total.argmax()=}")
+print(sequences_total.max())
+
+sys.exit(0)
+
+# OLD (knowing you can compresses sequences only) ~ 1.5 minute
+
+
+def totbans(seq: int) -> int:
+    match = buyers_sequences == seq
+    found = match.max(axis=1)
+    indexes = np.argmax(match, axis=1)
+    bans = buyers_prices[range(buyers), indexes]
+    bans *= found
+    return bans.sum()
+
+
+def seq_to_int(seq: tuple[int, ...]) -> int:
+    tot = 0
+    for s, num in enumerate(seq):
+        tot += (num + trans) << (SEQUENCE_LENGTH - s - 1) * shift
+    return tot
+
+
+print(f"{totbans(seq_to_int((-2, 1, -1, 3)))=}")
+
+all_seqs: set[int] = set(buyers_sequences.flat) - {-1}
+
+maxi = 0
+max_seq = None
+for seq in rich.progress.track(all_seqs, description="Finding score for sequences"):
+    tb = totbans(seq)
+    if tb > maxi:
+        maxi = tb
+        max_seq = seq
+
+
+print(f"{max_seq=}")
+print(maxi)
diff --git a/2024/23/both_networkx.py b/2024/23/both_networkx.py
new file mode 100644
index 0000000..01157c9
--- /dev/null
+++ b/2024/23/both_networkx.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+
+import sys
+
+import matplotlib.pyplot as plt
+import networkx
+import networkx as nx
+
+input_file = sys.argv[1]
+
+G = nx.Graph()
+with open(input_file) as fd:
+    for line in fd.readlines():
+        a, b = line.rstrip().split("-")
+        G.add_edge(a, b)
+
+trio_cliques: list[list[str]] = list()
+lan = None
+for clique in nx.enumerate_all_cliques(G):
+    if lan is None or len(clique) > len(lan):
+        lan = clique
+    if len(clique) != 3:
+        continue
+    if not any(c.startswith("t") for c in clique):
+        continue
+    trio_cliques.append(clique)
+
+
+part1_ans = len(trio_cliques)
+assert lan is not None
+part2_ans = ",".join(sorted(lan))
+
+print(f"{part1_ans=}")
+print(f"{part2_ans=}")
+
+
+trio_nodes = set(node for trio_clique in trio_cliques for node in trio_clique)
+trio_edges = set(
+    edge
+    for clique in trio_cliques
+    for edge in list(nx.edge_boundary(G, clique, clique))
+)
+lan_edges = set(nx.edge_boundary(G, lan, lan))
+
+for node in trio_nodes:
+    G.nodes[node]["color"] = "green"
+for edge in trio_edges:
+    G.edges[edge]["color"] = "green"
+    G.edges[edge]["weight"] = 2
+
+for node in lan:
+    G.nodes[node]["color"] = "red"
+for edge in lan_edges:
+    # G.edges[edge]["color"] = "red"
+    G.edges[edge]["weight"] = 5
+
+node_colors = [G.nodes[node].get("color", "blue") for node in G.nodes()]
+edge_colors = [G.edges[edge].get("color", "blue") for edge in G.edges()]
+node_pos = nx.layout.spring_layout(G)
+
+
+nx.draw(
+    G, node_color=node_colors, edge_color=edge_colors, pos=node_pos, with_labels=True
+)
+plt.show()
diff --git a/2024/23/one.py b/2024/23/one.py
new file mode 100644
index 0000000..f5803bd
--- /dev/null
+++ b/2024/23/one.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+
+import collections
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+connections = [tuple(line.split("-")) for line in lines]
+
+codi: collections.defaultdict[str, set[str]] = collections.defaultdict(set)
+
+for connection in connections:
+    a, b = connection
+    codi[a].add(b)
+    codi[b].add(a)
+
+threes: set[tuple[str, ...]] = set()
+for connection in connections:
+    a, b = connection
+    ac, bc = codi[a], codi[b]
+    iis = ac.intersection(bc)
+    for i in iis:
+        threel = [a, b, i]
+        if not (a.startswith("t") or b.startswith("t") or i.startswith("t")):
+            continue
+        threel.sort()
+        threes.add(tuple(threel))
+
+for three in threes:
+    print(three)
+
+print(len(threes))
+# 11011 too high (forgot starts with t)
diff --git a/2024/23/two.py b/2024/23/two.py
new file mode 100644
index 0000000..eef61f6
--- /dev/null
+++ b/2024/23/two.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+
+import collections
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+connections = [set(line.split("-")) for line in lines]
+
+codi: collections.defaultdict[str, set[str]] = collections.defaultdict(set)
+
+for connection in connections:
+    a, b = connection
+    codi[a].add(b)
+    codi[b].add(a)
+
+threes: set[tuple[str, ...]] = set()
+for connection in connections:
+    a, b = connection
+    ac, bc = codi[a], codi[b]
+    iis = ac.intersection(bc)
+    for i in iis:
+        threel = [a, b, i]
+        threel.sort()
+        threes.add(tuple(threel))
+
+j = 0
+while len(threes) > 1:
+
+    inthrees: set[str] = set()
+    for three in threes:
+        inthrees.update(set(three))
+
+    print(j, len(threes), len(inthrees))
+
+    fours: set[tuple[str, ...]] = set()
+    for three in threes:
+        threeset = set(three)
+        for comp in inthrees - threeset:
+            compc = codi[comp]
+            if threeset.issubset(compc):
+                fourl = list(threeset) + [comp]
+                fourl.sort()
+                fours.add(tuple(fourl))
+
+    threes = fours
+
+threesl = list(threes)
+if len(threesl) == 1:
+    three = threesl[0]
+    print(",".join(three))
+print(None)
diff --git a/2024/24/one.py b/2024/24/one.py
new file mode 100644
index 0000000..5a7c026
--- /dev/null
+++ b/2024/24/one.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+
+import functools
+import sys
+import typing
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+gates: dict[str, tuple[str, typing.Callable, str]] = dict()
+varis: dict[str, int] = dict()
+funs = {
+    "AND": int.__and__,
+    "OR": int.__or__,
+    "XOR": int.__xor__,
+}
+
+step = False
+for line in lines:
+    if not line:
+        step = True
+    elif step:
+        a, op, b, _, dest = line.split()
+        fun = funs[op]
+        gates[dest] = (a, fun, b)
+    else:
+        dest, val = line.split(":")
+        varis[dest] = int(val)
+
+
+@functools.cache
+def get_var(var: str) -> int:
+    if var in varis:
+        return varis[var]
+    a, fun, b = gates[var]
+    avar = get_var(a)
+    bvar = get_var(b)
+    return fun(avar, bvar)
+
+
+zees = sorted([key for key in gates.keys() if key.startswith("z")])
+bits = reversed([str(get_var(key)) for key in zees])
+res = int("".join(bits), base=2)
+
+print(res)
diff --git a/2024/24/two.py b/2024/24/two.py
new file mode 100644
index 0000000..f7456d5
--- /dev/null
+++ b/2024/24/two.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+
+import functools
+import sys
+import typing
+
+import matplotlib.pyplot as plt
+import networkx as nx
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+gates: dict[str, tuple[str, typing.Callable, str]] = dict()
+varis: dict[str, int] = dict()
+funs = {
+    "AND": int.__and__,  # orange
+    "OR": int.__or__,  # green
+    "XOR": int.__xor__,  # purple
+}
+G = nx.DiGraph()
+
+swaps = [
+    ("ncd", "nfj"),
+    ("z37", "vkg"),
+    ("z20", "cqr"),
+    ("z15", "qnw"),
+]
+swapdict: dict[str, str] = dict()
+for a, b in swaps:
+    swapdict[a] = b
+    swapdict[b] = a
+
+step = False
+for line in lines:
+    if not line:
+        step = True
+    elif step:
+        a, op, b, _, dest = line.split()
+        dest = swapdict.get(dest, dest)
+        fun = funs[op]
+        gates[dest] = (a, fun, b)
+        G.add_node(dest, op=op)
+        G.add_edge(a, dest)
+        G.add_edge(b, dest)
+    else:
+        dest, val = line.split(":")
+        varis[dest] = int(val)
+
+
+def swap(a: str, b: str) -> None:
+    temp = gates[a]
+    gates[a] = gates[b]
+    gates[b] = temp
+
+
+@functools.cache
+def get_var(var: str) -> int:
+    if var in varis:
+        return varis[var]
+    a, fun, b = gates[var]
+    avar = get_var(a)
+    bvar = get_var(b)
+    return fun(avar, bvar)
+
+
+all_keys = list(gates.keys()) + list(varis.keys())
+all_keys.sort(reverse=True)
+
+
+def get_number(prefix: str) -> int:
+    tot = 0
+    keys = [key for key in all_keys if key.startswith(prefix)]
+    for key in keys:
+        tot <<= 1
+        tot |= get_var(key)
+    return tot
+
+
+X = get_number("x")
+Y = get_number("y")
+Z = get_number("z")
+print(f"{X+Y=} = {X=} + {Y=}")
+print(f"  {Z=} {Z == X + Y=}")
+
+print(",".join(sorted(swapdict.keys())))
+
+# Viz
+
+
+@functools.cache
+def get_node_pos(node: str) -> tuple[float, float]:
+    x: float
+    y: float
+    if node.startswith("x"):
+        x = -int(node[1:]) * 2
+        y = 0
+    elif node.startswith("y"):
+        x = -int(node[1:]) * 2 - 1
+        y = 0
+    elif node.startswith("z"):
+        x = -int(node[1:]) * 2
+        y = 100
+    else:
+        a, _, b = gates[node]
+        ax, ay = get_node_pos(a)
+        bx, by = get_node_pos(b)
+        x = (ax + bx) / 2
+        y = max(ay, by) + 1
+    return x, y
+
+
+colors = {
+    "AND": "orange",
+    "OR": "green",
+    "XOR": "purple",
+}
+node_colors = []
+node_pos: dict[str, tuple[float, float]] = dict()
+node_fixed: set[str] = set()
+for node in G.nodes():
+    op = G.nodes[node].get("op")
+    node_colors.append(colors.get(op, "cyan" if node.startswith("x") else "blue"))
+
+    x: float
+    y: float
+    fixed = True
+    if node.startswith("x"):
+        x = -int(node[1:]) * 2
+        y = 0
+    elif node.startswith("y"):
+        x = -int(node[1:]) * 2 - 1
+        y = 0
+    elif node.startswith("z"):
+        x = -int(node[1:]) * 2
+        y = 50
+    else:
+        fixed = False
+        x = -23
+        y = 25
+
+    node_pos[node] = x, y
+    if fixed:
+        node_fixed.add(node)
+
+# My own layout
+for i in range(50):
+    for node in G.nodes():
+        if node in node_fixed:
+            continue
+        neighs = list(G.succ[node]) + list(G.pred[node])
+        x = sum(node_pos[neigh][0] for neigh in neighs) / len(neighs)
+        y = sum(node_pos[neigh][1] for neigh in neighs) / len(neighs)
+        node_pos[node] = x, y
+node_fixed = set(G.nodes())
+
+node_layout = nx.layout.spring_layout(G, pos=node_pos, fixed=node_fixed)
+
+nx.draw(G, pos=node_layout, node_color=node_colors, with_labels=True)
+plt.show()
diff --git a/2024/24/two_test.py b/2024/24/two_test.py
new file mode 100644
index 0000000..d76b70c
--- /dev/null
+++ b/2024/24/two_test.py
@@ -0,0 +1,163 @@
+#!/usr/bin/env python3
+
+import functools
+import sys
+import typing
+
+import matplotlib.pyplot as plt
+import networkx as nx
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+gates: dict[str, tuple[str, typing.Callable, str]] = dict()
+varis: dict[str, int] = dict()
+funs = {
+    "AND": int.__and__,  # orange
+    "OR": int.__or__,  # green
+    "XOR": int.__xor__,  # purple
+}
+G = nx.DiGraph()
+
+swaps = [
+    # ("ncd", "nfj"),
+    # ("z37", "vkg"),
+    # ("z20", "cqr"),
+    # ("z15", "qnw"),
+]
+swapdict: dict[str, str] = dict()
+for a, b in swaps:
+    swapdict[a] = b
+    swapdict[b] = a
+
+step = False
+for line in lines:
+    if not line:
+        step = True
+    elif step:
+        a, op, b, _, dest = line.split()
+        dest = swapdict.get(dest, dest)
+        fun = funs[op]
+        gates[dest] = (a, fun, b)
+        G.add_node(dest, op=op)
+        G.add_edge(a, dest)
+        G.add_edge(b, dest)
+    else:
+        dest, val = line.split(":")
+        varis[dest] = int(val)
+
+
+def swap(a: str, b: str) -> None:
+    temp = gates[a]
+    gates[a] = gates[b]
+    gates[b] = temp
+
+
+@functools.cache
+def get_var(var: str) -> int:
+    if var in varis:
+        return varis[var]
+    a, fun, b = gates[var]
+    avar = get_var(a)
+    bvar = get_var(b)
+    return fun(avar, bvar)
+
+
+all_keys = list(gates.keys()) + list(varis.keys())
+all_keys.sort(reverse=True)
+
+
+def get_number(prefix: str) -> int:
+    tot = 0
+    keys = [key for key in all_keys if key.startswith(prefix)]
+    for key in keys:
+        tot <<= 1
+        tot |= get_var(key)
+    return tot
+
+
+X = get_number("x")
+Y = get_number("y")
+Z = get_number("z")
+print(f"{X+Y=} = {X=} + {Y=}")
+print(f"  {Z=} {Z == X + Y=}")
+
+print(",".join(sorted(swapdict.keys())))
+
+# Viz
+
+
+@functools.cache
+def get_node_pos(node: str) -> tuple[float, float]:
+    x: float
+    y: float
+    if node.startswith("x"):
+        x = -int(node[1:]) * 2
+        y = 0
+    elif node.startswith("y"):
+        x = -int(node[1:]) * 2 - 1
+        y = 0
+    elif node.startswith("z"):
+        x = -int(node[1:]) * 2
+        y = 100
+    else:
+        a, _, b = gates[node]
+        ax, ay = get_node_pos(a)
+        bx, by = get_node_pos(b)
+        x = (ax + bx) / 2
+        y = max(ay, by) + 1
+    return x, y
+
+
+colors = {
+    "AND": "orange",
+    "OR": "green",
+    "XOR": "purple",
+}
+node_colors = []
+node_pos: dict[str, tuple[float, float]] = dict()
+node_fixed: set[str] = set()
+for node in G.nodes():
+    op = G.nodes[node].get("op")
+    node_colors.append(colors.get(op, "cyan" if node.startswith("x") else "blue"))
+
+    x: float
+    y: float
+    fixed = True
+    if node.startswith("x"):
+        x = -int(node[1:]) * 2
+        y = 0
+    elif node.startswith("y"):
+        x = -int(node[1:]) * 2 - 1
+        y = 0
+    elif node.startswith("z"):
+        x = -int(node[1:]) * 2
+        y = 50
+    else:
+        fixed = False
+        x = -23
+        y = 25
+
+    node_pos[node] = x, y
+    if fixed:
+        node_fixed.add(node)
+
+# # My own layout
+# for i in range(50):
+#     for node in G.nodes():
+#         if node in node_fixed:
+#             continue
+#         neighs = list(G.succ[node]) + list(G.pred[node])
+#         x = sum(node_pos[neigh][0] for neigh in neighs) / len(neighs)
+#         y = sum(node_pos[neigh][1] for neigh in neighs) / len(neighs)
+#         node_pos[node] = x, y
+# node_fixed = set(G.nodes())
+
+node_layout = nx.layout.spring_layout(
+    G.to_undirected(), k=1, iterations=1000, pos=node_pos, fixed=node_fixed
+)
+
+nx.draw(G, pos=node_layout, node_color=node_colors, with_labels=True)
+plt.show()
diff --git a/2024/25/one.py b/2024/25/one.py
new file mode 100644
index 0000000..d8b1804
--- /dev/null
+++ b/2024/25/one.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+lockey = tuple[int, int, int, int, int]
+locks: list[lockey] = list()
+keys: list[lockey] = list()
+
+i = 0
+for line in lines:
+    if not line:
+        i = 0
+    elif i == 0:
+        is_lock = line == "#####"
+        i = 1
+        sharps = [0, 0, 0, 0, 0]
+    elif i == 6:
+        if is_lock:
+            locks.append(tuple(sharps))
+        else:
+            keys.append(tuple(sharps))
+    else:
+        for j in range(5):
+            if line[j] == "#":
+                sharps[j] += 1
+        i += 1
+
+print(locks)
+print(keys)
+
+fit = 0
+for lock in locks:
+    for key in keys:
+        print(39, lock, key)
+        for i in range(5):
+            if lock[i] + key[i] > 5:
+                break
+        else:
+            fit += 1
+print(fit)
diff --git a/2024/3/one.py b/2024/3/one.py
new file mode 100644
index 0000000..4460641
--- /dev/null
+++ b/2024/3/one.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+
+import sys
+import re
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+t = 0
+reg = r"mul\(([0-9]+),([0-9]+)\)"
+for line in lines:
+    for match in re.findall(reg, line):
+        ast, bst = match
+        a, b = int(ast), int(bst)
+        m = a * b
+        t += m
+print(t)
diff --git a/2024/3/two.py b/2024/3/two.py
new file mode 100644
index 0000000..bfc2c33
--- /dev/null
+++ b/2024/3/two.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+
+import sys
+import re
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+t = 0
+en = True
+reg = r"mul\(([0-9]+),([0-9]+)\)|do\(\)|don't\(\)"
+for line in lines:
+    for match in re.finditer(reg, line):
+        all = match.group(0)
+        if all == "do()":
+            en = True
+        elif all == "don't()":
+            en = False
+        elif en:
+            ast, bst = match.group(1), match.group(2)
+            a, b = int(ast), int(bst)
+            m = a * b
+            t += m
+print(t)
diff --git a/2024/4/one.py b/2024/4/one.py
new file mode 100644
index 0000000..c545e2f
--- /dev/null
+++ b/2024/4/one.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+word = "XMAS"
+
+directions = [
+    (0, 1),
+    (1, 1),
+    (1, 0),
+    (1, -1),
+    (0, -1),
+    (-1, -1),
+    (-1, 0),
+    (-1, 1),
+]
+
+arrows = ["➡️", "↘️", "⬇️", "↙️", "⬅️", "↖️", "⬆️", "↗️"]
+
+assert len(directions) == len(set(directions))
+
+
+viz = [["."] * width for i in range(height)]
+
+count = 0
+for i in range(height):
+    for j in range(width):
+        for direction in directions:
+            ii = i
+            jj = j
+            for letter in word:
+                if (
+                    ii not in range(height)
+                    or jj not in range(width)
+                    or lines[ii][jj] != letter
+                ):
+                    break
+                ii += direction[0]
+                jj += direction[1]
+            else:
+                count += 1
+                # d = directions.index(direction)
+                # viz[i][j] = arrows[d]
+                ii = i
+                jj = j
+                for letter in word:
+                    viz[ii][jj] = letter
+                    ii += direction[0]
+                    jj += direction[1]
+for line in viz:
+    print("".join(line))
+print(count)
diff --git a/2024/4/two.py b/2024/4/two.py
new file mode 100644
index 0000000..41ada97
--- /dev/null
+++ b/2024/4/two.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+count = 0
+for i in range(1, height - 1):
+    for j in range(1, width - 1):
+        if lines[i][j] != "A":
+            continue
+        tl = lines[i - 1][j - 1]
+        br = lines[i + 1][j + 1]
+        tr = lines[i - 1][j + 1]
+        bl = lines[i + 1][j - 1]
+        if not ((tl, br) == ("M", "S") or (tl, br) == ("S", "M")) or not (
+            (tr, bl) == ("M", "S") or (tr, bl) == ("S", "M")
+        ):
+            continue
+        count += 1
+print(count)
diff --git a/2024/5/one.py b/2024/5/one.py
new file mode 100644
index 0000000..8b4fcf9
--- /dev/null
+++ b/2024/5/one.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+page_mode = False
+orders: list[tuple[int, int]] = list()
+updates: list[list[int]] = list()
+
+for line in lines:
+    if not page_mode:
+        if line == "":
+            page_mode = True
+        else:
+            order = tuple(int(a) for a in line.split("|"))
+            assert len(order) == 2
+            orders.append(order)
+    else:
+        update = list(int(a) for a in line.split(","))
+        updates.append(update)
+
+total = 0
+for update in updates:
+    for fi, se in orders:
+        try:
+            ifi = update.index(fi)
+            ise = update.index(se)
+        except ValueError:
+            continue
+        if ifi > ise:
+            break
+    else:
+        imid = int(len(update)/2)
+        mid = update[imid]
+        total += mid
+print(total)
+
diff --git a/2024/5/two.py b/2024/5/two.py
new file mode 100644
index 0000000..b20b0fe
--- /dev/null
+++ b/2024/5/two.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+page_mode = False
+orders: list[tuple[int, int]] = list()
+
+class Page:
+    def __init__(self, pn: int):
+        self.pn = pn
+
+    def __lt__(self, other: "Page") -> bool:
+        a = self.pn
+        b = other.pn
+        for fi, se in orders:
+            if a == fi and b == se:
+                return True
+            elif a == se and b == fi:
+                return False
+        raise RuntimeError
+
+
+updates: list[list[Page]] = list()
+
+for line in lines:
+    if not page_mode:
+        if line == "":
+            page_mode = True
+        else:
+            order = tuple(int(a) for a in line.split("|"))
+            assert len(order) == 2
+            orders.append(order)
+    else:
+        update = list(Page(int(a)) for a in line.split(","))
+        updates.append(update)
+
+total = 0
+for update in updates:
+    update_sorted = sorted(update)
+
+    if update == update_sorted:
+        continue
+
+    update = update_sorted
+
+    # Add
+    imid = int(len(update)/2)
+    mid = update[imid]
+    total += mid.pn
+
+print(total)
+
diff --git a/2024/6/one.py b/2024/6/one.py
new file mode 100644
index 0000000..3fb7e49
--- /dev/null
+++ b/2024/6/one.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+for i in range(height):
+    if "^" in lines[i]:
+        j = lines[i].index("^")
+        break
+
+d = 0
+directions = [
+    (-1, 0),  # ^
+    (0, 1),  # >
+    (1, 0),  # v
+    (0, -1),  # <
+]
+
+vis = [[False] * width for h in range(height)]
+
+while True:
+    print(i, j)
+    vis[i][j] = True
+    ii, jj = i + directions[d][0], j + directions[d][1]
+    if ii not in range(height) or jj not in range(width):
+        break
+    if lines[ii][jj] == "#":
+        d += 1
+        d %= len(directions)
+        continue
+    i, j = ii, jj
+
+count = 0
+for i in range(height):
+    for j in range(width):
+        if vis[i][j]:
+            count += 1
+print(count)
+
+
diff --git a/2024/6/two.py b/2024/6/two.py
new file mode 100644
index 0000000..53060f2
--- /dev/null
+++ b/2024/6/two.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+for i_start in range(height):
+    if "^" in lines[i_start]:
+        j_start = lines[i_start].index("^")
+        break
+
+directions = [
+    (-1, 0),  # ^
+    (0, 1),  # >
+    (1, 0),  # v
+    (0, -1),  # <
+]
+
+positions: set[tuple[int, int]] = set()
+
+i, j = i_start, j_start
+d = 0
+
+while True:
+    positions.add((i, j))
+    ii, jj = i + directions[d][0], j + directions[d][1]
+    if ii not in range(height) or jj not in range(width):
+        break
+    if lines[ii][jj] == "#":
+        d += 1
+        d %= len(directions)
+        continue
+    i, j = ii, jj
+
+print(len(positions))
+positions.remove((i_start, j_start))
+
+tot = 0
+for obstacle in positions:
+    i, j = i_start, j_start
+    d = 0
+    path: set[tuple[int, int, int]] = set()
+
+    while True:
+        state = (i, j, d)
+        if state in path:
+            loop = True
+            tot += 1
+            break
+        path.add(state)
+        ii, jj = i + directions[d][0], j + directions[d][1]
+        if ii not in range(height) or jj not in range(width):
+            loop = False
+            break
+        if lines[ii][jj] == "#" or (ii, jj) == obstacle:
+            d += 1
+            d %= len(directions)
+            continue
+        i, j = ii, jj
+
+print(tot)
diff --git a/2024/7/one.py b/2024/7/one.py
new file mode 100644
index 0000000..a975fca
--- /dev/null
+++ b/2024/7/one.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+final = 0
+for line in lines:
+    spli = line.split()
+    res = int(spli[0][:-1])
+    nums = [int(num) for num in spli[1:]]
+
+    def check(tot: int, nums: list[int]) -> bool:
+        for op in (int.__add__, int.__mul__):
+            ntot = op(tot, nums[0])
+            if ntot > res:
+                continue
+            if len(nums) == 1:
+                if ntot == res:
+                    return True
+            else:
+                if check(ntot, nums[1:]):
+                    return True
+        return False
+
+    if check(nums[0], nums[1:]):
+        final += res
+
+print(final)
+
+# 2664444091381: too low
diff --git a/2024/7/two.py b/2024/7/two.py
new file mode 100644
index 0000000..8c90d5d
--- /dev/null
+++ b/2024/7/two.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+
+def concat(a: int, b: int) -> int:
+    return int(str(a) + str(b))
+
+
+final = 0
+for line in lines:
+    spli = line.split()
+    res = int(spli[0][:-1])
+    nums = [int(num) for num in spli[1:]]
+
+    def check(tot: int, nums: list[int]) -> bool:
+        for op in (int.__add__, int.__mul__, concat):
+            ntot = op(tot, nums[0])
+            if ntot > res:
+                continue
+            if len(nums) == 1:
+                if ntot == res:
+                    return True
+            else:
+                if check(ntot, nums[1:]):
+                    return True
+        return False
+
+    if check(nums[0], nums[1:]):
+        final += res
+
+print(final)
diff --git a/2024/8/one.py b/2024/8/one.py
new file mode 100644
index 0000000..7f66364
--- /dev/null
+++ b/2024/8/one.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+antennas: dict[str, list[tuple[int, int]]] = dict()
+
+for i in range(height):
+    for j in range(width):
+        char = lines[i][j]
+        if char == ".":
+            continue
+        antennas.setdefault(char, list())
+        antennas[char].append((i, j))
+
+antinodes_locations: set[tuple[int, int]] = set()
+for char, char_antennas in antennas.items():
+    print(f"25 {char} {char_antennas}")
+    for ant_a in char_antennas:
+        for ant_b in char_antennas:
+            if ant_a == ant_b:
+                continue
+            i, j = 2 * ant_b[0] - ant_a[0], 2 * ant_b[1] - ant_a[1]
+            antinode_loc = i, j
+            print(f"30 {antinode_loc}")
+            if i not in range(height) or j not in range(width):
+                continue
+            print(f"kept")
+            antinodes_locations.add(antinode_loc)
+
+print(len(antinodes_locations))
diff --git a/2024/8/two.py b/2024/8/two.py
new file mode 100644
index 0000000..0f147dc
--- /dev/null
+++ b/2024/8/two.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
+
+antennas: dict[str, list[tuple[int, int]]] = dict()
+
+viz = [["."] * width for _ in range(height)]
+
+for i in range(height):
+    for j in range(width):
+        char = lines[i][j]
+        if char == ".":
+            continue
+        antennas.setdefault(char, list())
+        antennas[char].append((i, j))
+        viz[i][j] = char
+
+antinodes_locations: set[tuple[int, int]] = set()
+for char, char_antennas in antennas.items():
+    print(f"25 {char} {char_antennas}")
+    for ant_a in char_antennas:
+        for ant_b in char_antennas:
+            if ant_a == ant_b:
+                continue
+            m = 0
+            while True:
+                i, j = ant_b[0] + m * (ant_b[0] - ant_a[0]), ant_b[1] + m * (
+                    ant_b[1] - ant_a[1]
+                )
+                antinode_loc = i, j
+                print(f"30 {antinode_loc}")
+                if i not in range(height) or j not in range(width):
+                    break
+                print("kept")
+                antinodes_locations.add(antinode_loc)
+                viz[i][j] = "#"
+                m += 1
+for vline in viz:
+    print("".join(vline))
+
+print(len(antinodes_locations))
diff --git a/2024/9/one.py b/2024/9/one.py
new file mode 100644
index 0000000..8ac3129
--- /dev/null
+++ b/2024/9/one.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+line = lines[0]
+
+disk: list[int | None] = list()
+
+fileno = 0
+isfile = True
+for char in line:
+    n = int(char)
+    if isfile:
+        disk += [fileno] * n
+        fileno += 1
+    else:
+        disk += [None] * n
+    isfile = not isfile
+
+beg = 0
+end = len(disk) - 1
+
+
+def print_disk() -> None:
+    return
+    print("".join(str(c) if c is not None else "." for c in disk))
+
+
+print_disk()
+moving = False
+while beg < end:
+    if moving:
+        if disk[end] is not None:
+            disk[beg] = disk[end]
+            disk[end] = None
+            moving = False
+            print_disk()
+        end -= 1
+    else:
+        if disk[beg] is None:
+            moving = True
+        else:
+            beg += 1
+
+checksum = 0
+for c, cid in enumerate(disk):
+    if cid is not None:
+        checksum += c * cid
+
+print(checksum)
diff --git a/2024/9/two.py b/2024/9/two.py
new file mode 100644
index 0000000..d3d3192
--- /dev/null
+++ b/2024/9/two.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+line = lines[0]
+
+disk: list[int | None] = list()
+# position, size
+index: list[tuple[int, int]] = list()
+
+fileno = 0
+isfile = True
+for char in line:
+    n = int(char)
+    if isfile:
+        index.append((len(disk), n))
+        disk += [fileno] * n
+        fileno += 1
+    else:
+        disk += [None] * n
+    isfile = not isfile
+
+beg = 0
+end = len(disk) - 1
+
+
+def print_disk() -> None:
+    return
+    print("".join(str(c) if c is not None else "." for c in disk))
+
+
+print_disk()
+
+for pos, size in index[::-1]:
+    hole_start = None
+    for c, cid in enumerate(disk + [-1]):
+        if c > pos + size:
+            break
+        if cid is None:
+            if hole_start is None:
+                hole_start = c
+        else:
+            if hole_start is not None:
+                hole_size = c - hole_start
+                if hole_size < size:
+                    hole_start = None
+                    continue
+                for i in range(size):
+                    disk[hole_start + i] = disk[pos + i]
+                    disk[pos + i] = None
+                print_disk()
+                break
+
+checksum = 0
+for c, cid in enumerate(disk):
+    if cid is not None:
+        checksum += c * cid
+
+print(checksum)
diff --git a/2024/X/one.py b/2024/X/one.py
new file mode 100644
index 0000000..df5aa86
--- /dev/null
+++ b/2024/X/one.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python3
+
+import sys
+
+input_file = sys.argv[1]
+
+with open(input_file) as fd:
+    lines = [line.rstrip() for line in fd.readlines()]
+
+height = len(lines)
+width = len(lines[0])
diff --git a/2024/X/two.py b/2024/X/two.py
new file mode 100644
index 0000000..e69de29
diff --git a/2024/times.escape b/2024/times.escape
new file mode 100644
index 0000000..e21a332
--- /dev/null
+++ b/2024/times.escape
@@ -0,0 +1,33 @@
+                                                          Advent of Code times                                                          
+┏━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
+┃ Puzzle date ┃ Puzzle title           ┃ Opened date         ┃ P1 time  ┃ P1 attempts ┃ P2 time        ┃ P2 attempts ┃ Total time      ┃
+┡━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
+│ 2024-12-01  │ Historian Hysteria     │ 2024-12-01 11:37:35 │ 0:11:06  │ 2           │ 0:03:35        │ 1           │ 0:14:41         │
+│ 2024-12-02  │ Red-Nosed Reports      │ 2024-12-02 10:17:11 │ 0:07:09  │ 1           │ 0:03:52        │ 1           │ 0:11:01         │
+│ 2024-12-03  │ Mull It Over           │ 2024-12-03 18:25:16 │ 0:04:36  │ 1           │ 0:05:15        │ 1           │ 0:09:51         │
+│ 2024-12-04  │ Ceres Search           │ 2024-12-04 11:04:12 │ 0:24:23  │ 1           │ 0:07:14        │ 1           │ 0:31:37         │
+│ 2024-12-05  │ Print Queue            │ 2024-12-05 10:08:48 │ 0:11:41  │ 1           │ 0:12:48        │ 1           │ 0:24:29         │
+│ 2024-12-06  │ Guard Gallivant        │ 2024-12-06 10:48:54 │ 0:14:23  │ 1           │ 0:19:23        │ 1           │ 0:33:46         │
+│ 2024-12-07  │ Bridge Repair          │ 2024-12-07 11:48:38 │ 0:17:17  │ 2           │ 0:01:42        │ 1           │ 0:18:59         │
+│ 2024-12-08  │ Resonant Collinearity  │ 2024-12-08 12:24:23 │ 0:18:25  │ 1           │ 0:07:19        │ 1           │ 0:25:44         │
+│ 2024-12-09  │ Disk Fragmenter        │ 2024-12-09 17:05:00 │ 0:17:50  │ 1           │ 0:13:38        │ 1           │ 0:31:28         │
+│ 2024-12-10  │ Hoof It                │ 2024-12-10 16:26:17 │ 0:29:16  │ 1           │ 0:01:03        │ 1           │ 0:30:19         │
+│ 2024-12-11  │ Plutonian Pebbles      │ 2024-12-11 10:54:43 │ 0:08:16  │ 1           │ 1:38:37        │ 1           │ 1:46:53         │
+│ 2024-12-12  │ Garden Groups          │ 2024-12-12 09:44:54 │ 0:36:57  │ 1           │ 0:22:54        │ 1           │ 0:59:51         │
+│ 2024-12-13  │ Claw Contraption       │ 2024-12-13 11:18:09 │ 0:46:35  │ 1           │ 4:53:18        │ 1           │ 5:39:53         │
+│ 2024-12-14  │ Restroom Redoubt       │ 2024-12-14 12:13:09 │ 0:30:06  │ 1           │ 0:06:04        │ 1           │ 0:36:10         │
+│ 2024-12-15  │ Warehouse Woes         │ 2024-12-15 10:41:57 │ 0:30:00  │ 1           │ 0:34:19        │ 1           │ 1:04:19         │
+│ 2024-12-16  │ Reindeer Maze          │ 2024-12-16 15:18:12 │ 0:59:02  │ 1           │ 0:31:45        │ 1           │ 1:30:47         │
+│ 2024-12-17  │ Chronospatial Computer │ 2024-12-17 14:59:30 │ 0:18:45  │ 1           │ 2:00:37        │ 1           │ 2:19:22         │
+│ 2024-12-18  │ RAM Run                │ 2024-12-18 09:05:58 │ 0:36:16  │ 1           │ 0:05:46        │ 1           │ 0:42:02         │
+│ 2024-12-19  │ Linen Layout           │ 2024-12-19 10:41:30 │ 0:08:00  │ 1           │ 0:08:01        │ 1           │ 0:16:01         │
+│ 2024-12-20  │ Race Condition         │ 2024-12-20 09:18:09 │ 1:26:24  │ 2           │ 1:51:20        │ 2           │ 3:17:44         │
+│ 2024-12-21  │ Keypad Conundrum       │ 2024-12-21 09:42:01 │ 1:23:38  │ 1           │ 15:30:41       │ 1           │ 16:54:19        │
+│ 2024-12-22  │ Monkey Market          │ 2024-12-22 12:31:22 │ 0:19:56  │ 1           │ 2:07:21        │ 2           │ 2:27:17         │
+│ 2024-12-23  │ LAN Party              │ 2024-12-23 12:43:58 │ 0:16:08  │ 2           │ 0:33:30        │ 1           │ 0:49:38         │
+│ 2024-12-24  │ Crossed Wires          │ 2024-12-24 10:11:36 │ 0:15:34  │ 1           │ 1:52:13        │ 1           │ 2:07:47         │
+│ 2024-12-25  │ Code Chronicle         │ 2024-12-25 10:26:05 │ 0:14:59  │ 1           │ 0:01:24        │ 1           │ 0:16:23         │
+├─────────────┼────────────────────────┼─────────────────────┼──────────┼─────────────┼────────────────┼─────────────┼─────────────────┤
+│ Total       │ 25                     │                     │ 11:06:42 │ 29          │ 1 day, 9:33:39 │ 27          │ 1 day, 20:40:21 │
+└─────────────┴────────────────────────┴─────────────────────┴──────────┴─────────────┴────────────────┴─────────────┴─────────────────┘
+                  Bold puzzle date: weekend; Bold time: worst part; Underline time: worst 25%; Underline attempts: >1                   
diff --git a/LICENSE.EUPLv1.2 b/LICENSE.EUPLv1.2
new file mode 100644
index 0000000..a0fa10b
--- /dev/null
+++ b/LICENSE.EUPLv1.2
@@ -0,0 +1,153 @@
+EUROPEAN UNION PUBLIC LICENCE v. 1.2
+EUPL © the European Union 2007, 2016
+
+This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such use is covered by a right of the copyright holder of the Work).
+
+The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following notice immediately following the copyright notice for the Work:
+
+Licensed under the EUPL
+
+or has expressed by any other means his willingness to license under the EUPL.
+
+1. Definitions
+In this Licence, the following terms have the following meaning:
+
+— ‘The Licence’: this Licence.
+
+— ‘The Original Work’: the work or software distributed or communicated by the Licensor under this Licence, available as Source Code and also as Executable Code as the case may be.
+
+— ‘Derivative Works’: the works or software that could be created by the Licensee, based upon the Original Work or modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in the country mentioned in Article 15.
+
+— ‘The Work’: the Original Work or its Derivative Works.
+
+— ‘The Source Code’: the human-readable form of the Work which is the most convenient for people to study and modify.
+
+— ‘The Executable Code’: any code which has generally been compiled and which is meant to be interpreted by a computer as a program.
+
+— ‘The Licensor’: the natural or legal person that distributes or communicates the Work under the Licence.
+
+— ‘Contributor(s)’: any natural or legal person who modifies the Work under the Licence, or otherwise contributes to the creation of a Derivative Work.
+
+— ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of the Work under the terms of the Licence.
+
+— ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, renting, distributing, communicating, transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential functionalities at the disposal of any other natural or legal person.
+
+2. Scope of the rights granted by the Licence
+The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for the duration of copyright vested in the Original Work:
+
+— use the Work in any circumstance and for all usage,
+
+— reproduce the Work,
+
+— modify the Work, and make Derivative Works based upon the Work,
+
+— communicate to the public, including the right to make available or display the Work or copies thereof to the public and perform publicly, as the case may be, the Work,
+
+— distribute the Work or copies thereof,
+
+— lend and rent the Work or copies thereof,
+
+— sublicense rights in the Work or copies thereof.
+
+Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the applicable law permits so.
+
+In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed by law in order to make effective the licence of the economic rights here above listed.
+
+The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the extent necessary to make use of the rights granted on the Work under this Licence.
+
+3. Communication of the Source Code
+The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to distribute or communicate the Work.
+
+4. Limitations on copyright
+Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations thereto.
+
+5. Obligations of the Licensee
+The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those obligations are the following:
+
+Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work to carry prominent notices stating that the Work has been modified and the date of modification.
+Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless the Original Work is expressly distributed only under this version of the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the Work or Derivative Work that alter or restrict the terms of the Licence.
+Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail.
+Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available for as long as the Licensee continues to distribute or communicate the Work.
+Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the copyright notice.
+6. Chain of Authorship
+The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence.
+
+Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence.
+
+Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contri­ butions to the Work, under the terms of this Licence.
+
+7. Disclaimer of Warranty
+The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work and may therefore contain defects or ‘bugs’ inherent to this type of development.
+
+For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this Licence.
+
+This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work.
+
+8. Disclaimer of Liability
+Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However, the Licensor will be liable under statutory product liability laws as far such laws apply to the Work.
+
+9. Additional agreements
+While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by the fact You have accepted any warranty or additional liability.
+
+10. Acceptance of the Licence
+The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms and conditions.
+
+Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution or Communication by You of the Work or copies thereof.
+
+11. Information to the public
+In case of any Distribution or Communication of the Work by means of electronic communication by You (for example, by offering to download the Work from a remote location) the distribution channel or media (for example, a website) must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence and the way it may be accessible, concluded, stored and reproduced by the Licensee.
+
+12. Termination of the Licence
+The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms of the Licence.
+
+Such a termination will not terminate the licences of any person who has received the Work from the Licensee under the Licence, provided such persons remain in full compliance with the Licence.
+
+13. Miscellaneous
+Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the Work.
+
+If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid and enforceable.
+
+The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence. New versions of the Licence will be published with a unique version number.
+
+All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take advantage of the linguistic version of their choice.
+
+14. Jurisdiction
+Without prejudice to specific agreement between parties,
+
+— any litigation resulting from the interpretation of this License, arising between the European Union institutions, bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union,
+
+— any litigation arising between other parties and resulting from the interpretation of this License, will be subject to the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business.
+
+15. Applicable Law
+Without prejudice to specific agreement between parties,
+
+— this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat, resides or has his registered office,
+
+— this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside a European Union Member State.
+
+Appendix
+‘Compatible Licences’ according to Article 5 EUPL are:
+
+— GNU General Public License (GPL) v. 2, v. 3
+
+— GNU Affero General Public License (AGPL) v. 3
+
+— Open Software License (OSL) v. 2.1, v. 3.0
+
+— Eclipse Public License (EPL) v. 1.0
+
+— CeCILL v. 2.0, v. 2.1
+
+— Mozilla Public Licence (MPL) v. 2
+
+— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
+
+— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software
+
+— European Union Public Licence (EUPL) v. 1.1, v. 1.2
+
+— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+)
+
+The European Commission may update this Appendix to later versions of the above licences without producing a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the covered Source Code from exclusive appropriation.
+
+All other changes or additions to this Appendix require the production of a new EUPL version.
diff --git a/LICENSE.Unlicense b/LICENSE.Unlicense
new file mode 100644
index 0000000..fdddb29
--- /dev/null
+++ b/LICENSE.Unlicense
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <https://unlicense.org>
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..16085fe
--- /dev/null
+++ b/README.md
@@ -0,0 +1,32 @@
+# Advent of Code
+
+[Advent of Code](https://adventofcode.com/) is a yearly coding competition.
+In this repo are the solutions and notes I wrote for it.
+
+**Content warning:** Some profanity.
+
+Unless indicated otherwise in a README, `one.py` and `two.py` are the files that generated the solution that got me the answer.
+So they WILL contain unreadable code and broken solutions, as I'm optimizing for answer speed.
+
+Other codes are either solutions that went so bad I wrote a backup in a different file (otherwise I usually rewrite/comment out the things that don't work),
+or puzzles I revisited because someone posted a part 3, I wanted to try a different approach, or I wanted to figure out how the hell the code that got me the solution worked in the first place.
+
+## Times display
+
+Since there's no way I was going to get up at 6 AM but I still wanted to keep track of the time I took to get to the right answer,
+the `times.py` script display a table of the times between I opened the puzzle in my browser, and the time I submitted the solution.
+It has quite a few limitations but they're documented.
+Either run it with Lix/Nix, or manually install the dependencies that are stated on the 3rd line.
+
+## License
+
+I'm not 100% sure it can work like this but here goes:
+
+- `times.py` is licensed under the "European Union Public License v1.2". The idea is that if you modify the script and use it to share its output, you have to share your modifications as well, so the community can also enjoy it.
+- Puzzle solutions, test inputs I wrote are licensed under "The Unlicense". It's basically public domain, so it can be used as teaching material.
+- Inputs copied from Reddit (identified with a "reddit" somewhere in the filename) are... I don't know, and I'm not reading the Reddit TOS to figure it out.
+
+
+
+
+
diff --git a/times.csv b/times.csv
new file mode 100644
index 0000000..bc766b9
--- /dev/null
+++ b/times.csv
@@ -0,0 +1,30 @@
+puzzle,opened,part1_attempts,part1_finished,part2_attempts,part2_finished
+2023-12-01,2023-12-02 12:02:03+01:00,1,2023-12-02 12:14:04+01:00,1,2023-12-02 12:33:28+01:00
+2024-12-01,2024-12-01 11:37:35+01:00,2,2024-12-01 11:48:41+01:00,1,2024-12-01 11:52:16+01:00
+2023-12-02,2024-12-01 22:31:10+01:00,3,2024-12-01 22:47:42+01:00,1,2024-12-01 22:50:34+01:00
+2023-12-03,2024-12-01 22:50:40+01:00,1,2024-12-01 23:13:57+01:00,1,2024-12-01 23:18:33+01:00
+2023-12-24,2024-12-01 23:18:47+01:00,0,None,0,None
+2024-12-02,2024-12-02 10:17:11+01:00,1,2024-12-02 10:24:20+01:00,1,2024-12-02 10:28:12+01:00
+2024-12-03,2024-12-03 18:25:16+01:00,1,2024-12-03 18:29:52+01:00,1,2024-12-03 18:35:07+01:00
+2024-12-04,2024-12-04 11:04:12+01:00,1,2024-12-04 11:28:35+01:00,1,2024-12-04 11:35:49+01:00
+2024-12-05,2024-12-05 10:08:48+01:00,1,2024-12-05 10:20:29+01:00,1,2024-12-05 10:33:17+01:00
+2024-12-06,2024-12-06 10:48:54+01:00,1,2024-12-06 11:03:17+01:00,1,2024-12-06 11:22:40+01:00
+2024-12-07,2024-12-07 11:48:38+01:00,2,2024-12-07 12:05:55+01:00,1,2024-12-07 12:07:37+01:00
+2024-12-08,2024-12-08 12:24:23+01:00,1,2024-12-08 12:42:48+01:00,1,2024-12-08 12:50:07+01:00
+2024-12-09,2024-12-09 17:05:00+01:00,1,2024-12-09 17:22:50+01:00,1,2024-12-09 17:36:28+01:00
+2024-12-10,2024-12-10 16:26:17+01:00,1,2024-12-10 16:55:33+01:00,1,2024-12-10 16:56:36+01:00
+2024-12-11,2024-12-11 10:54:43+01:00,1,2024-12-11 11:02:59+01:00,1,2024-12-11 12:41:36+01:00
+2024-12-12,2024-12-12 09:44:54+01:00,1,2024-12-12 10:21:51+01:00,1,2024-12-12 10:44:45+01:00
+2024-12-13,2024-12-13 11:18:09+01:00,1,2024-12-13 12:04:44+01:00,1,2024-12-13 16:58:02+01:00
+2024-12-14,2024-12-14 12:13:09+01:00,1,2024-12-14 12:43:15+01:00,1,2024-12-14 12:49:19+01:00
+2024-12-15,2024-12-15 10:41:57+01:00,1,2024-12-15 11:11:57+01:00,1,2024-12-15 11:46:16+01:00
+2024-12-16,2024-12-16 15:18:12+01:00,1,2024-12-16 16:17:14+01:00,1,2024-12-16 16:48:59+01:00
+2024-12-17,2024-12-17 14:59:30+01:00,1,2024-12-17 15:18:15+01:00,1,2024-12-17 17:18:52+01:00
+2024-12-18,2024-12-18 09:05:58+01:00,1,2024-12-18 09:42:14+01:00,1,2024-12-18 09:48:00+01:00
+2024-12-19,2024-12-19 10:41:30+01:00,1,2024-12-19 10:49:30+01:00,1,2024-12-19 10:57:31+01:00
+2024-12-20,2024-12-20 09:18:09+01:00,2,2024-12-20 10:44:33+01:00,2,2024-12-20 12:35:53+01:00
+2024-12-21,2024-12-21 09:42:01+01:00,1,2024-12-21 11:05:39+01:00,1,2024-12-22 02:36:20+01:00
+2024-12-22,2024-12-22 12:31:22+01:00,1,2024-12-22 12:51:18+01:00,2,2024-12-22 14:58:39+01:00
+2024-12-23,2024-12-23 12:43:58+01:00,2,2024-12-23 13:00:06+01:00,1,2024-12-23 13:33:36+01:00
+2024-12-24,2024-12-24 10:11:36+01:00,1,2024-12-24 10:27:10+01:00,1,2024-12-24 12:19:23+01:00
+2024-12-25,2024-12-25 10:26:05+01:00,1,2024-12-25 10:41:04+01:00,1,2024-12-25 10:42:28+01:00
diff --git a/times.py b/times.py
new file mode 100755
index 0000000..5b3ac6b
--- /dev/null
+++ b/times.py
@@ -0,0 +1,308 @@
+#!/usr/bin/env cached-nix-shell
+#! nix-shell -i python3
+#! nix-shell -p python3 python3Packages.rich python3Packages.typer aoc-cli
+# vim: filetype=python
+
+"""
+Display Advent of Code puzzle solving times based on web browser history.
+
+Assumptions/Limitations:
+- You opened your and submitted your inputs with the qutebrowser web browser
+- The last attempt you made at a puzzle was the correct one
+- aoc-cli is configured
+- There will be a YYYY/MM folder relative to $PWD if you attempted the puzzle
+- This script will add a prompt file in above directory (to cache the titles)
+"""
+
+import collections
+import csv
+import datetime
+import enum
+import os
+import pathlib
+import re
+import sqlite3
+import statistics
+import subprocess
+import typing
+import urllib.parse
+import zoneinfo
+
+import rich.console
+import rich.table
+import typer
+
+app = typer.Typer()
+
+
+def get_qutebrowser_urls() -> (
+    typing.Generator[tuple[datetime.datetime, urllib.parse.ParseResult], None, None]
+):
+    qutebrowser_history_path = os.path.expanduser(
+        "~/.local/share/qutebrowser/history.sqlite"
+    )
+    db = sqlite3.connect(qutebrowser_history_path)
+
+    cur = db.cursor()
+    for row in cur.execute(
+        "SELECT atime,url FROM History"
+        " WHERE url LIKE 'https://adventofcode.com/%/day/%'"
+        " ORDER BY atime ASC"
+    ):
+        tim = datetime.datetime.fromtimestamp(row[0]).astimezone()
+        url = urllib.parse.urlparse(row[1])
+        yield tim, url
+
+
+# opened, p1 attempts, p1 last answer, p2 attempts, p2 last answer
+PuzzleTimesAttempts = tuple[
+    datetime.datetime, int, datetime.datetime | None, int, datetime.datetime | None
+]
+
+
+def get_puzzle_folder(date: datetime.date) -> pathlib.Path | None:
+    path = pathlib.Path(f"{date.year}/{date.day}")
+    if path.is_dir():
+        return path
+    else:
+        return None
+
+
+def get_time_attempts(year: int | None) -> dict[datetime.date, PuzzleTimesAttempts]:
+    starts: dict[datetime.date, datetime.datetime] = dict()
+    part1a: dict[datetime.date, int] = collections.Counter()
+    part1t: dict[datetime.date, datetime.datetime] = dict()
+    got_part2: set[datetime.date] = set()
+    part2a: dict[datetime.date, int] = collections.Counter()
+    part2t: dict[datetime.date, datetime.datetime] = dict()
+
+    for tim, url in get_qutebrowser_urls():
+        match = re.match(
+            r"^/(?P<year>[0-9]{4})/day/(?P<day>[0-9]{1,2})(?P<answer>/answer)?$",
+            url.path,
+        )
+        if not match:
+            continue  # Not an interesting URL
+
+        date = datetime.date(int(match["year"]), 12, int(match["day"]))
+        if year is not None and date.year != year:
+            continue  # Not the wanted year
+
+        answer = bool(match["answer"])
+        part2 = url.fragment == "part2"
+
+        if date not in starts:
+            starts[date] = tim
+
+        if answer:
+            if date in got_part2:
+                part2a[date] += 1
+                part2t[date] = tim
+            else:
+                part1a[date] += 1
+                part1t[date] = tim
+        elif part2:
+            got_part2.add(date)
+
+    time_attempts = dict()
+    for date in starts:
+        if get_puzzle_folder(date) is None:
+            continue  # Not attempted
+
+        time_attempts[date] = (
+            starts[date],
+            part1a.get(date, 0),
+            part1t.get(date),
+            part2a.get(date, 0),
+            part2t.get(date),
+        )
+    return time_attempts
+
+
+def get_puzzle_prompt(date: datetime.date) -> str:
+    path = get_puzzle_folder(date)
+    assert path
+    filepath = path / "prompt"
+    if not filepath.is_file():
+        r = subprocess.run(
+            ["aoc", "read", "--year", str(date.year), "--day", str(date.day)],
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+        r.check_returncode()
+        with open(filepath, "w") as fd:
+            fd.write(r.stdout.decode())
+    with open(filepath) as fd:
+        return fd.read()
+
+
+def get_puzzle_title(date: datetime.date) -> str:
+    prompt = get_puzzle_prompt(date)
+    for line in prompt.splitlines():
+        match = re.match(f"^--- Day {date.day}: (.+) ---$", line)
+        if match:
+            return match[1]
+    raise ValueError("Could not find title in prompt")
+
+
+def bold_time_threshold(times: list[datetime.timedelta]) -> datetime.timedelta:
+    times_s = [round(t.total_seconds(), 1) for t in times]
+    quantiles = statistics.quantiles(times_s)
+    return datetime.timedelta(seconds=quantiles[-1])
+
+
+def format_puzzle(date: datetime.date) -> str:
+    string = str(date)
+    if date.weekday() in (5, 6):
+        string = f"[bold]{string}"
+    return string
+
+
+def format_time(
+    time: datetime.timedelta | None,
+    bold_threshold: datetime.timedelta,
+    compare: datetime.timedelta | None = None,
+) -> str:
+    if time is None:
+        return "[bright_black]-"
+    string = str(time)
+    if time >= bold_threshold:
+        string = f"[underline]{string}"
+    if compare and time > compare:
+        string = f"[bold]{string}"
+    return string
+
+
+def format_attempts(
+    attempts: int,
+) -> str:
+    if attempts == 0:
+        return "[bright_black]-"
+    string = str(attempts)
+    if attempts > 1:
+        string = f"[underline]{string}"
+    return string
+
+
+class SortMethod(str, enum.Enum):
+    puzzle = "puzzle"
+    opened = "opened"
+    part1_time = "part1_time"
+    part2_time = "part2_time"
+    total_time = "total_time"
+
+
+@app.command("csv")
+def write_csv(file: typer.FileTextWrite, year: int | None = None) -> None:
+    writer = csv.writer(file)
+    writer.writerow(
+        (
+            "puzzle",
+            "opened",
+            "part1_attempts",
+            "part1_finished",
+            "part2_attempts",
+            "part2_finished",
+        )
+    )
+    for puzzle, times_attempts in get_time_attempts(year).items():
+        all_info = (puzzle,) + times_attempts
+        writer.writerow(str(i) for i in all_info)
+
+
+@app.command("table")
+def print_table(
+    year: int | None = None,
+    assume_early_bird: bool = False,
+    show_opened: bool = True,
+    sort: SortMethod = SortMethod.opened,
+) -> None:
+
+    if assume_early_bird:
+        show_opened = False
+
+    puzzles = get_time_attempts(year)
+    part1_times = dict()
+    part2_times = dict()
+    total_times = dict()
+    for date, time_attempts in puzzles.items():
+        start, _, part1_date, _, part2_date = time_attempts
+        if part1_date:
+            if assume_early_bird:
+                start = datetime.datetime(
+                    date.year, date.month, date.day, tzinfo=zoneinfo.ZoneInfo("EST")
+                )
+            part1_times[date] = part1_date - start
+            if part2_date:
+                part2_times[date] = part2_date - part1_date
+                total_times[date] = part2_date - start
+
+    part1_bold_time_threshold = bold_time_threshold(list(part1_times.values()))
+    part2_bold_time_threshold = bold_time_threshold(list(part2_times.values()))
+    total_bold_time_threshold = bold_time_threshold(list(total_times.values()))
+
+    part1_total = sum(part1_times.values(), start=datetime.timedelta())
+    part1_attempts = sum(puzzle[1] for puzzle in puzzles.values())
+    part2_total = sum(part2_times.values(), start=datetime.timedelta())
+    part2_attempts = sum(puzzle[3] for puzzle in puzzles.values())
+    total_total = sum(total_times.values(), start=datetime.timedelta())
+
+    table = rich.table.Table(
+        title="Advent of Code times",
+        caption="Bold puzzle date: weekend; "
+        "Bold time: worst part; "
+        "Underline time: worst 25%; "
+        "Underline attempts: >1",
+        show_footer=True,
+    )
+    table.add_column("Puzzle date", "Total", style="blue")
+    table.add_column("Puzzle title", str(len(puzzles)), style="blue")
+    if show_opened:
+        table.add_column("Opened date", style="magenta")
+    table.add_column("P1 time", str(part1_total), style="green")
+    table.add_column("P1 attempts", str(part1_attempts), style="cyan")
+    table.add_column("P2 time", str(part2_total), style="green")
+    table.add_column("P2 attempts", str(part2_attempts), style="cyan")
+    table.add_column("Total time", str(total_total), style="yellow")
+
+    dates = list(puzzles.keys())
+    if sort == SortMethod.puzzle:
+        dates.sort()
+    elif sort == SortMethod.opened:
+        pass
+    elif sort == SortMethod.part1_time:
+        dates.sort(key=lambda d: part1_times.get(d, datetime.timedelta.max))
+    elif sort == SortMethod.part2_time:
+        dates.sort(key=lambda d: part2_times.get(d, datetime.timedelta.max))
+    elif sort == SortMethod.total_time:
+        dates.sort(key=lambda d: total_times.get(d, datetime.timedelta.max))
+    else:
+        assert False
+
+    for date in dates:
+        start, part1_attempts, _, part2_attempts, _ = puzzles[date]
+        part1_time = part1_times.get(date)
+        part2_time = part2_times.get(date)
+        total_time = total_times.get(date)
+        row = (
+            (
+                format_puzzle(date),
+                get_puzzle_title(date),
+            )
+            + ((str(start),) if show_opened else tuple())
+            + (
+                format_time(part1_time, part1_bold_time_threshold, part2_time),
+                format_attempts(part1_attempts),
+                format_time(part2_time, part2_bold_time_threshold, part1_time),
+                format_attempts(part2_attempts),
+                format_time(total_time, total_bold_time_threshold),
+            )
+        )
+        table.add_row(*row)
+
+    console = rich.console.Console()
+    console.print(table)
+
+
+if __name__ == "__main__":
+    app()