diff --git a/2023-python/.gitignore b/2023-python/.gitignore new file mode 100644 index 0000000..fa878cc --- /dev/null +++ b/2023-python/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +input \ No newline at end of file diff --git a/2023-python/README.md b/2023-python/README.md new file mode 100644 index 0000000..2edcd21 --- /dev/null +++ b/2023-python/README.md @@ -0,0 +1,38 @@ +Advent of Code 2023 +=================== + +Solutions for #aoc2023 in Python 3 (3.11.5). + +Help scripts +------------ + +Display all solved puzzles: + + python aoc.py + +To bootstrap a new puzzle (creates `input/.txt` and `output/day_.py`): + + python aoc.py + +Manually copy the puzzle input from https://adventofcode.com and paste it in `input/.txt` +to start coding. + + wl-paste > input/.txt + +Solve separate puzzle (replace `XX` with the puzzle number): + + python -m output.day_XX + +Solve separate puzzle using stdin (replace `XX` with the puzzle number): + + wl-paste | python -m output.day_XX + cat tmpfile | python -m output.day_XX + +Execute separate puzzle on file save (replace `XX` with the puzzle number): + + ls output/*.py | entr -c -s 'xclip -selection clipboard -o | python -m output.day_XX' + ls output/*.py | entr -c -s 'cat tmpfile | python -m output.day_XX' + ls output/*.py | entr -c -r python -m output.day_XX + +(requires `entr` and `wl-paste`, Mac users can instead use `pbpaste`. If you +prefer X at Linux, use `xclip -selection clipboard -o`). diff --git a/2023-python/aoc.py b/2023-python/aoc.py new file mode 100644 index 0000000..ae49a50 --- /dev/null +++ b/2023-python/aoc.py @@ -0,0 +1,127 @@ +import os +import sys + + +def headline(n, title): + """Print day number and name, followed by a ruler. Used by the answer decorator""" + print(f"\n--- Day {n}: {title} ---\n") + + +year = 2023 + +try: + _, day_no, *name = sys.argv +except ValueError: + day_no = None + name = None + +print( + f"\nAdvent of Code {year}" "\n###################" "\n\nby Anders Englöf Ytterström" +) + +if day_no and name: + name = " ".join(name) + padded_no = day_no.zfill(2) + print(f"\n- creating output/day_{padded_no}.py") + with open("output/day_{}.py".format(padded_no), "w") as s: + s.write( + f""" +from output import answer # , matrix, D, DD, ADJ, ints, mhd, mdbg, vdbg + +n = {day_no} +title = "{name}" + + +@answer(1, "Answer is {{}}") +def part_1(presolved): + return presolved[0] + + +@answer(2, "Actually, answer is {{}}") +def part_2(presolved): + return presolved[1] + + +def solve(data): + return 1, 2 + + +if __name__ == "__main__": + # use dummy data + inp = \"\"\" + replace me + \"\"\".strip() + + # uncomment to instead use stdin + # import sys; inp = sys.stdin.read().strip() + + # uncomment to use AoC provided puzzle input + # with open(f"./input/{padded_no}.txt", "r") as f: + # inp = f.read() + + # uncomment to do initial data processing shared by part 1-2 + inp = solve(inp) + + a = part_1(inp) + # b = part_2(inp) + + # uncomment and replace 0 with actual output to refactor code + # and ensure nonbreaking changes + # assert a == 0 + # assert b == 0 +""".strip() + + "\n" + ) + print("- making sure input dir exists") + if not os.path.exists("input"): + os.makedirs("input") + + print( + f""" +Done! start coding. + +Puzzle link: +https://adventofcode.com/{year}/day/{day_no} + +Puzzle input (copy and paste to input/{day_no.zfill(2)}.txt): +https://adventofcode.com/{year}/day/{day_no}/input + """ + ) + exit(0) + + +stars = 0 +for i in [str(n).zfill(2) for n in range(1, 26)]: + if not day_no or day_no.zfill(2) == i: + try: + day = __import__( + "output.day_{}".format(i), + globals(), + locals(), + ["n", "title", "part_1", "part_2"], + 0, + ) + filepath = f"./input/{str(n).zfill(2)}.txt" + with open(filepath, "r") as f: + data = f.read() + headline(day.n, day.title) + try: + data = day.presolve(data) + except AttributeError: + pass + try: + data = day.solve(data) + except AttributeError: + pass + if day.part_1(data, decorate=True): + stars += 1 + if day.part_2(data, decorate=True): + stars += 1 + except IOError: + pass + except ImportError: + pass +if not day_no: + print(f"\nStars: {stars}") + print("".join("*" if n < stars else "•" for n in range(50))) +print("") diff --git a/2023-python/output/__init__.py b/2023-python/output/__init__.py new file mode 100644 index 0000000..3a034d6 --- /dev/null +++ b/2023-python/output/__init__.py @@ -0,0 +1,82 @@ +import functools +import re + +# Directions/Adjacents for 2D matrices, in the order UP, RIGHT, DOWN, LEFT +D = [ + (-1, 0), + (0, 1), + (1, 0), + (0, -1), +] + +# Directions for 2D matrices, as a dict with keys U, R, D, L +DD = { + "U": (-1, 0), + "R": (0, 1), + "D": (1, 0), + "L": (0, -1), +} + +# Adjacent relative positions including diagonals for 2D matrices, in the order NW, N, NE, W, E, SW, S, SE +ADJ = [ + (-1, -1), + (-1, 0), + (1, -1), + (0, -1), + (0, 1), + (1, 1), + (1, 0), + (1, -1), +] + + +def answer(part_index, fmt_string): + """Decorator to present a solution in a human readable format""" + + def decorator_aoc(func): + @functools.wraps(func) + def wrapper_aoc(*args, **kwargs): + decorate = kwargs.get("decorate", False) + if decorate: + del kwargs["decorate"] + answer = func(*args, **kwargs) + if not decorate: + print(answer) + else: + formatted = fmt_string.format(answer) + print(f" {part_index}) {formatted}") + return answer + + return wrapper_aoc + + return decorator_aoc + + +def ints(s): + """Extract all integers from a string""" + return [int(n) for n in re.findall(r"\d+", s)] + + +def mhd(a, b): + """Calculates the Manhattan distance between 2 positions in the format (y, x) or (x, y)""" + ar, ac = a + br, bc = b + return abs(ar - br) + abs(ac - bc) + + +def matrix(d): + """Transform a string into an iterable matrix. Returns the matrix, row count and col count""" + m = [tuple(r) for r in d.split()] + return m, len(m), len(m[0]) + + +def mdbg(m): + """Print-debug a matrix""" + for r in m: + print("".join(r)) + + +def vdbg(seen, h, w): + """Print-debug visited positions of a matrix""" + for r in range(h): + print("".join(["#" if (r, c) in seen else "." for c in range(w)]))