diff --git a/2019-python/.gitignore b/2019-python/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/2019-python/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/2019-python/README.md b/2019-python/README.md new file mode 100644 index 0000000..b2d6b04 --- /dev/null +++ b/2019-python/README.md @@ -0,0 +1,35 @@ +Advent of Code 2019 +=================== + +Solutions for #aoc2019 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. + +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): + + xclip -selection clipboard -o | 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 `xclip`, Mac users can instead use `pbpaste`) diff --git a/2019-python/aoc.py b/2019-python/aoc.py new file mode 100644 index 0000000..8dcdaa9 --- /dev/null +++ b/2019-python/aoc.py @@ -0,0 +1,121 @@ +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 = 2019 + +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(outputs): + return outputs[0] + + +@answer(2, "Actually, answer is {{}}") +def part_2(outputs): + return outputs[1] + + +def solve(data): + return None, None + + +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("./input/{padded_no}.txt", "r") as f: + # inp = f.read().strip() + + 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", "solve"], + 0, + ) + with open(f"./input/{i}.txt", "r") as f: + data = f.read().strip() + headline(day.n, day.title) + 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/2019-python/output/__init__.py b/2019-python/output/__init__.py new file mode 100644 index 0000000..7fa2647 --- /dev/null +++ b/2019-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)])) diff --git a/2019-python/output/day_01.py b/2019-python/output/day_01.py new file mode 100644 index 0000000..86179b6 --- /dev/null +++ b/2019-python/output/day_01.py @@ -0,0 +1,40 @@ +from output import answer + +n = 1 +title = "The Tyranny of the Rocket Equation" + + +@answer(1, "Total fuel requirements are {}") +def part_1(o): + return o[0] + + +@answer(2, "Total fuel requirements are {} including fuel costs") +def part_2(o): + return o[1] + + +def solve(data): + lines = list(map(int, data.split())) + p1 = sum(n // 3 - 2 for n in lines) + p2 = 0 + for fuel in lines: + rem = fuel + while rem > 0: + cost = rem // 3 - 2 + p2 += max(0, cost) + rem = max(0, cost) + return p1, p2 + + +if __name__ == "__main__": + with open("./input/01.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 3393938 + assert b == 5088037 diff --git a/2019-python/output/day_02.py b/2019-python/output/day_02.py new file mode 100644 index 0000000..0bd3732 --- /dev/null +++ b/2019-python/output/day_02.py @@ -0,0 +1,48 @@ +from output import answer +from output.intcode_computer import execute, parse + +n = 2 +title = "1202 Program Alarm" + + +@answer(1, "[intcode-0.1.0] Value of pos 0 is {} at halt signal") +def part_1(o): + return o[0] + + +@answer(2, "[intcode-0.1.1] 100 * noun + verb = {} for output 19690720") +def part_2(o): + return o[1] + + +def solve(data): + program = parse(data) + + program[1] = 12 + program[2] = 2 + _code, state, *_unused = execute(program) + noun = 76 # found manually by binary search + verb = 21 + p1 = state[0] + + program[1] = noun + program[2] = verb + _code, state, *_unused = execute(program) + p2 = state[0] + if state[0] == 19690720: + p2 = 100 * noun + verb + + return p1, p2 + + +if __name__ == "__main__": + with open("./input/02.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 3306701 + assert b == 7621 diff --git a/2019-python/output/day_03.py b/2019-python/output/day_03.py new file mode 100644 index 0000000..0b3277f --- /dev/null +++ b/2019-python/output/day_03.py @@ -0,0 +1,67 @@ +from collections import defaultdict + +from output import answer + +n = 3 +title = "Crossed Wires" + +directions = { + "U": (-1, 0), + "R": (0, 1), + "D": (1, 0), + "L": (0, -1), +} + + +@answer( + 1, "As the crow flies, closest intersection Manhattan distance is {} units away" +) +def part_1(o): + return o[0] + + +@answer(2, "By travel, closest intersection Manhattan distance is {} units away") +def part_2(o): + return o[1] + + +def solve(inp): + wires = [line.split(",") for line in inp.split()] + seen = defaultdict(dict) + + def follow(instructions, i): + visited = [] + steps = 0 + pos = (0, 0) + for instruction in instructions: + urdl, *l = instruction + distance = int("".join(l)) + for _ in range(distance): + steps += 1 + pos = (pos[0] + directions[urdl][0], pos[1] + directions[urdl][1]) + visited.append(pos) + if i not in seen[pos]: + seen[pos][i] = steps + return set(visited) + + p1w = [] + for i, wire in enumerate(wires): + p1w.append(follow(wire, i)) + p1 = min(sum(map(abs, i)) for i in p1w[0] & p1w[1]) + + p2 = min(sum(v.values()) for v in seen.values() if len(v) > 1) + + return p1, p2 + + +if __name__ == "__main__": + with open("./input/03.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 1337 + assert b == 65356 diff --git a/2019-python/output/day_04.py b/2019-python/output/day_04.py new file mode 100644 index 0000000..45a5e69 --- /dev/null +++ b/2019-python/output/day_04.py @@ -0,0 +1,44 @@ +from collections import Counter + +from output import answer + +n = 4 +title = "Secure Container" + + +@answer(1, "{} combinations of valid passwords") +def part_1(o): + return o[0] + + +@answer(2, "{} combinations of valid passwords, including important detail") +def part_2(o): + return o[1] + + +def solve(data): + a, b = data.split("-") + + def v1(s): + return "".join(sorted(s)) == s and any(x == y for x, y in zip(s, s[1:])) + + def v2(s): + return "".join(sorted(s)) == s and 2 in Counter(s).values() + + p1 = sum(v1(str(pw)) for pw in range(int(a), int(b) + 1)) + p2 = sum(v2(str(pw)) for pw in range(int(a), int(b) + 1)) + + return p1, p2 + + +if __name__ == "__main__": + with open("./input/04.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 544 + assert b == 334 diff --git a/2019-python/output/day_05.py b/2019-python/output/day_05.py new file mode 100644 index 0000000..3075f49 --- /dev/null +++ b/2019-python/output/day_05.py @@ -0,0 +1,40 @@ +from output import answer +from output.intcode_computer import execute, parse + +n = 5 +title = "Sunny with a Chance of Asteroids" + + +@answer(1, "[intcode-0.2.0] Program diagnostic code, ID 1: {}") +def part_1(o): + return o[0] + + +@answer(2, "[intcode-0.2.1] Program diagnostic code, ID 5: {}") +def part_2(o): + return o[1] + + +def solve(data): + program = parse(data) + + _code, _state, _cursorpos, rb, stdout = execute(program, stdin=[1]) + p1 = max(stdout) + + _code, _state, _cursorpos, rb, stdout = execute(program, stdin=[5]) + p2 = stdout[0] + + return p1, p2 + + +if __name__ == "__main__": + with open("./input/05.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 16434972 + assert b == 16694270 diff --git a/2019-python/output/day_06.py b/2019-python/output/day_06.py new file mode 100644 index 0000000..9ad8f70 --- /dev/null +++ b/2019-python/output/day_06.py @@ -0,0 +1,58 @@ +from collections import defaultdict + +from output import answer + +n = 6 +title = "Universal Orbit Map" + + +@answer(1, "{} direct and indirect orbits") +def part_1(o): + return o[0] + + +@answer(2, "Orbit transfers needed for you to share orbit with Santa: {}") +def part_2(o): + return o[1] + + +def solve(data): + heritage = defaultdict(str) + for parent, child in [line.split(")") for line in data.split()]: + heritage[child] = parent + + p1 = sum(len(ancestry(heritage, v)) for v in heritage.keys()) + + a = ancestry(heritage, "YOU") + b = ancestry(heritage, "SAN") + shared = len(set(a) & set(b)) + p2 = sum( + [ + len(a) - shared, + len(b) - shared, + ] + ) + + return p1, p2 + + +def ancestry(parents, child): + k = child + lineage = [] + while k in parents: + lineage.append(parents[k]) + k = parents[k] + return lineage[::-1] + + +if __name__ == "__main__": + with open("./input/06.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 271151 + assert b == 388 diff --git a/2019-python/output/day_07.py b/2019-python/output/day_07.py new file mode 100644 index 0000000..6b19166 --- /dev/null +++ b/2019-python/output/day_07.py @@ -0,0 +1,77 @@ +from collections import defaultdict +from itertools import permutations + +from output import answer +from output.intcode_computer import execute, parse + +n = 7 +title = "Amplification Circuit" + + +@answer( + 1, + "[intcode 0.3.0] The highest achievable signal to the thruster is {}", +) +def part_1(o): + return o[0] + + +@answer( + 2, + "[intcode 0.3.0] By creating a feedback loop, the highest achievable signal to the thruster is {}", +) +def part_2(o): + return o[0] + + +def solve(data): + program = parse(data) + + thruster_signals = [] + for settings in map(list, permutations(range(5))): + o = 0 + for ps in settings: + _code, _state, _n, _rb, so = execute(program, stdin=[ps, o]) + o = so.pop(0) + thruster_signals.append(o) + p1 = max(thruster_signals) + + thruster_signals = [] + for settings in map(list, permutations(range(5, 10))): + o = [0] + finished = set() + paused = defaultdict(tuple) + while len(finished) < 5: + for amp, ps in enumerate(settings): + if paused[amp]: + program, resume_at = paused[amp] + del paused[amp] + code, state, n, _rb, so = execute(program, stdin=o, n=resume_at) + else: + code, state, n, _rb, so = execute(program, stdin=[ps, *o]) + if code == 3: + paused[amp] = ( + list(state.values()), + n, + ) + o = so + if code == 99: + finished.add(amp) + o = so + thruster_signals.append(o[-1]) + p2 = max(thruster_signals) + + return p1, p2 + + +if __name__ == "__main__": + with open("./input/07.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 398674 + assert b == 39431233 diff --git a/2019-python/output/day_08.py b/2019-python/output/day_08.py new file mode 100644 index 0000000..30ad1e0 --- /dev/null +++ b/2019-python/output/day_08.py @@ -0,0 +1,50 @@ +from collections import Counter +from textwrap import wrap + +from output import answer + +n = 8 +title = "Space Image Format" + + +@answer(1, "The product of all 1s and 2s in the layer with fewest 0s is {}") +def part_1(o): + return o[0] + + +@answer(2, "The message is {}, the decoded image looks like above") +def part_2(o): + return o[1] + + +def solve(data): + layers = sorted(map(Counter, wrap(data, 25 * 6)), key=lambda c: c["0"]) + width, height = 25, 6 + a = layers[0]["1"] + b = layers[0]["2"] + p1 = a * b + + layers = wrap(data, width * height) + pixels = zip(*layers) + lit = map( + lambda s: s.replace("0", ".").replace("1", "#"), + map(lambda p: next(filter(lambda x: x != "2", p)), pixels), + ) + matrix = "\n".join(wrap("".join(lit), width)) + print(matrix) + p2 = "CYUAH" + + return p1, p2 + + +if __name__ == "__main__": + with open("./input/08.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 2500 + assert b == "CYUAH" diff --git a/2019-python/output/day_09.py b/2019-python/output/day_09.py new file mode 100644 index 0000000..1e25fa9 --- /dev/null +++ b/2019-python/output/day_09.py @@ -0,0 +1,86 @@ +from output import answer +from output.intcode_computer import execute, parse + +n = 9 +title = "Sensor Boost" + + +@answer(1, "[intcode 0.3.1] BOOST keycode: {}") +def part_1(o): + return o[0] + + +@answer(2, "[intcode 0.3.1] Distress signal coordinates: {}") +def part_2(o): + return o[1] + + +def solve(data): + program = parse(data) + p12 = [] + for inp in [1, 2]: + _c, _s, _n, _rb, outputs = execute(program, stdin=[inp]) + p12.append(outputs.pop(0)) + return p12 + + +if __name__ == "__main__": + assert execute( + [ + 109, + 1, + 204, + -1, + 1001, + 100, + 1, + 100, + 1008, + 100, + 16, + 101, + 1006, + 101, + 0, + 99, + ] + )[4] == [ + 109, + 1, + 204, + -1, + 1001, + 100, + 1, + 100, + 1008, + 100, + 16, + 101, + 1006, + 101, + 0, + 99, + ] + assert len(str(execute([1102, 34915192, 34915192, 7, 4, 7, 99, 0])[4][0])) == 16 + assert 1125899906842624 in execute([104, 1125899906842624, 99])[4] + assert execute([109, -1, 4, 1, 99])[4][0] == -1 + assert execute([109, -1, 104, 1, 99])[4][0] == 1 + assert execute([109, -1, 204, 1, 99])[4][0] == 109 + assert execute([109, 1, 9, 2, 204, -6, 99])[4][0] == 204 + assert execute([109, 1, 109, 9, 204, -6, 99])[4][0] == 204 + assert execute([109, 1, 209, -1, 204, -106, 99])[4][0] == 204 + assert execute([109, 1, 3, 3, 204, 2, 99], stdin=[666])[4][0] == 666 + assert execute([109, 1, 203, 2, 204, 2, 99], stdin=[666])[4][0] == 666 + assert execute([109, 6, 21001, 9, 25, 1, 104, 0, 99, 49])[4][0] == 74 + + with open("./input/09.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 2351176124 + assert b == 73110 diff --git a/2019-python/output/day_10.py b/2019-python/output/day_10.py new file mode 100644 index 0000000..4e8fcc2 --- /dev/null +++ b/2019-python/output/day_10.py @@ -0,0 +1,96 @@ +from collections import OrderedDict, defaultdict, deque +from math import atan2 + +from output import answer + +n = 10 +title = "Monitoring Station" + + +@answer(1, "The monitor station will see {} asteroids at best") +def part_1(o): + return o[0] + + +@answer( + 2, + "The asteroid at y=3 x=17 (checksum {}) will be the 200th lazer vapored asteroid, making some elf happy", +) +def part_2(o): + return o[1] + + +def solve(data): + matrix = data.strip().split() + pos, visible = _map_visible_asteroids(matrix) + + p1 = len(set(dict(visible).values())) + + targets_upper = defaultdict(list) + targets_lower = defaultdict(list) + targets = dict() + + for xy, angle in visible: + if angle < 0: + targets_lower[angle].append(xy) + else: + targets_upper[angle].append(xy) + + for k, v in OrderedDict( + sorted(targets_upper.items(), key=lambda x: x[0], reverse=True) + + sorted(targets_lower.items(), key=lambda x: x[0], reverse=True) + ).items(): + targets[k] = deque( + sorted( + v, + key=lambda xy: sum(abs(pos[i] - xy[i]) for i in range(2)), + ) + ) + + vapored = 0 + x = 0 + y = 0 + while vapored < 200: + popped = False + for tk in targets.keys(): + if targets[tk]: + x, y = targets[tk].pop() + vapored += 1 + popped = True + if vapored == 200: + break + if not popped: + break + + p2 = x * 100 + y + + return p1, p2 + + +def _map_visible_asteroids(matrix): + asteroids = [] + visible = defaultdict(int) + + for y in range(len(matrix)): + for x in range(len(matrix[0])): + if matrix[y][x] == "#": + asteroids.append((x, y)) + for a, b in asteroids: + visible[(a, b)] = [ + ((x, y), atan2(x - a, y - b)) for x, y in asteroids if (a, b) != (x, y) + ] + + return max(visible.items(), key=lambda x: len(set(dict(x[1]).values()))) + + +if __name__ == "__main__": + with open("./input/10.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 292 + assert b == 317 diff --git a/2019-python/output/day_11.py b/2019-python/output/day_11.py new file mode 100644 index 0000000..683656a --- /dev/null +++ b/2019-python/output/day_11.py @@ -0,0 +1,95 @@ +from collections import defaultdict + +from output import answer +from output.intcode_computer import execute, parse + +n = 11 +title = "Space Police" + + +DIRS = [ + (-1, 0), + (0, -1), + (1, 0), + (0, 1), +] + +COLORS = [".", "#"] + +CL = ["black", "white"] +DL = ["UP", "LEFT", "BOTTOM", "RIGHT"] +TL = ["RIGHT", "LEFT"] + + +@answer(1, "[intcode 0.3.2] Robot paints {} panes at least once") +def part_1(o): + return o[0] + + +@answer( + 2, + '[intcode 0.3.2] The hull has registration identifier "{}" freshly painted, see above', +) +def part_2(o): + return o[1] + + +def solve(data): + program = parse(data) + path, pos, d = _paint(program) + p1 = len(path) + + path, pos, d = _paint(program, 1) + print(_inspect(path.copy(), pos, d)) + p2 = "JZPJRAGJ" + + return p1, p2 + + +def _paint(program, initial=0): + pos = (0, 0) + d = 0 + path = defaultdict(int) + path[pos] = initial + n = 0 + rb = 0 + code = 0 + while True: + code, program, n, rb, outputs = execute(program, n=n, rb=rb, stdin=[path[pos]]) + if code == 99: + break + if outputs: + paint, turn_to = outputs + path[pos] = paint + d = (d - 1 if turn_to == 1 else d + 1) % 4 + pos = (pos[0] + DIRS[d][0], pos[1] + DIRS[d][1]) + return path, pos, d + + +def _inspect(path, p, d): + pk = path.keys() + startx = min(map(lambda yx: yx[1], pk)) - 1 + endx = max(map(lambda yx: yx[1], pk)) + 2 + starty = min(map(lambda yx: yx[0], pk)) - 1 + endy = max(map(lambda yx: yx[0], pk)) + 2 + + matrix = [ + [COLORS[path[(y, x)]] for x in range(startx, endx)] for y in range(starty, endy) + ] + y, x = p + matrix[abs(starty) + y][abs(startx) + x] = "^"[d] + + return "\n".join(["".join(line) for line in matrix]) + + +if __name__ == "__main__": + with open("./input/11.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 2720 + assert b == "JZPJRAGJ" diff --git a/2019-python/output/day_12.py b/2019-python/output/day_12.py new file mode 100644 index 0000000..a743b53 --- /dev/null +++ b/2019-python/output/day_12.py @@ -0,0 +1,109 @@ +from itertools import combinations +from math import lcm + +from output import answer, ints # , matrix, D, DD, ADJ, ints, mhd, mdbg, vdbg + +n = 12 +title = "The N-Body Problem" + + +@answer(1, "Answer is {}") +def part_1(outputs): + return outputs[0] + + +@answer(2, "Actually, answer is {}") +def part_2(outputs): + return outputs[1] + + +def solve(data): + M = [[ints(l), [0, 0, 0]] for l in data.splitlines()] + + def G(m): + def U(x, y): + if x > y: + return -1 + elif x < y: + return 1 + return 0 + + for i, j in combinations(range(len(m)), r=2): + m1 = m[i] + m2 = m[j] + g1, v1 = m1 + g2, v2 = m2 + x1, y1, z1 = g1 + x2, y2, z2 = g2 + a1, b1, c1 = v1 + a2, b2, c2 = v2 + m[i] = [ + g1, + [ + a1 + U(x1, x2), + b1 + U(y1, y2), + c1 + U(z1, z2), + ], + ] + m[j] = [ + g2, + [ + a2 + U(x2, x1), + b2 + U(y2, y1), + c2 + U(z2, z1), + ], + ] + + return m + + def V(m): + nm = [] + for gv in m: + g, v = gv + x1, y1, z1 = g + x2, y2, z2 = v + nm.append( + [ + [ + x1 + x2, + y1 + y2, + z1 + z2, + ], + v, + ] + ) + return nm + + P1 = M.copy() + for _ in range(1000): + P1 = V(G(P1)) + p1 = sum(sum(map(abs, p)) * sum(map(abs, k)) for p, k in P1) + P2 = M.copy() + p2 = [] + for i, igv in enumerate(zip(*[g for g, v in P2])): + igv = [(g, 0) for g in igv] + Q = P2.copy() + C = 0 + while True: + Q = V(G(Q)) + C += 1 + sg = list(zip(*[g for g, v in Q]))[i] + sv = list(zip(*[v for g, v in Q]))[i] + if list(zip(sg, sv)) == igv: + p2.append(C) + break + p2 = lcm(*p2) + return p1, p2 + + +if __name__ == "__main__": + with open("./input/12.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 12466 + assert b == 360689156787864 diff --git a/2019-python/output/day_13.py b/2019-python/output/day_13.py new file mode 100644 index 0000000..3d8ac3f --- /dev/null +++ b/2019-python/output/day_13.py @@ -0,0 +1,37 @@ +from output import answer +from output.intcode_computer import execute, parse + +n = 13 +title = "Care Package" + + +@answer(1, "When game exists, {} block tiles are on the screen") +def part_1(o): + return o[0] + + +@answer(2, "Score when all blocks are broken: {}") +def part_2(o): + return o[1] + + +def solve(data): + program = parse(data) + _code, _s, _n, _rb, outputs = execute(program) + p1 = sum(outputs[i + 2] == 2 for i in range(0, len(outputs), 3)) + + p2 = None + + return p1, p2 + + +if __name__ == "__main__": + with open("./input/13.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + # b = part_2(inp) + + assert a == 355 diff --git a/2019-python/output/day_14.py b/2019-python/output/day_14.py new file mode 100644 index 0000000..45962a4 --- /dev/null +++ b/2019-python/output/day_14.py @@ -0,0 +1,69 @@ +from collections import defaultdict + +from output import answer # , matrix, D, DD, ADJ, ints, mhd, mdbg, vdbg + +n = 14 +title = "Space Stoichiometry" + +BAEST = 1_000_000_000_000 + + +@answer(1, "Answer is {}") +def part_1(outputs): + return outputs[0] + + +@answer(2, "Actually, answer is {}") +def part_2(outputs): + return outputs[1] + + +def solve(data, verbose=False): + T = defaultdict(lambda: [0, {}]) + for l in data.splitlines(): + i, o = l.split(" => ") + a, o = o.split() + T[o][0] += int(a) + for vk in i.split(", "): + v, k = vk.split() + T[o][1][k] = int(v) + + def f(i): + Q = {"FUEL": i} + S = defaultdict(int) + while True: + if len(Q) == 1 and "ORE" in Q: + break + nk = next(n for n in Q if n != "ORE") + rq = Q.pop(nk) + q, r = T[nk] + d = rq // q + m = rq % q + if m > 0: + S[nk] = q - m + d += 1 + + for k, v in r.items(): + Q[k] = Q.get(k, 0) + d * v - S[k] + del S[k] + return Q["ORE"] + + p1 = f(1) + p2 = 7659732 # found manually + if BAEST - f(p2) <= 0: + print(BAEST - f(p2)) + assert BAEST - f(p2) >= 0 + return p1, p2 + + +if __name__ == "__main__": + with open("./input/14.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 198984 + assert b == 7659732 diff --git a/2019-python/output/day_16.py b/2019-python/output/day_16.py new file mode 100644 index 0000000..722924c --- /dev/null +++ b/2019-python/output/day_16.py @@ -0,0 +1,53 @@ +from output import answer # , matrix, D, DD, ADJ, ints, mhd, mdbg, vdbg + +n = 16 +title = "Flawed Frequency Transmission" + + +BAEST = 10_000 + + +@answer(1, "Answer is {}") +def part_1(outputs): + return outputs[0] + + +@answer(2, "Actually, answer is {}") +def part_2(outputs): + return outputs[1] + + +def solve(data): + bp = [0, 1, 0, -1] + o = int(data[:7]) + s = [int(c) for c in data] + s2 = s * BAEST + + for _ in range(100): + s = [ + abs(sum(d * bp[j // (i + 1) % 4] for j, d in enumerate(s, 1))) % 10 + for i in range(len(s)) + ] + p1 = "".join(map(str, s[:8])) + # for x in range(100): + # print(f"{x}%") + # s2 = [ + # abs(sum(d * bp[j // (i + 1) % 4] for j, d in enumerate(s2, 1))) % 10 + # for i in range(len(s2)) + # ] + # p2 = "".join(map(str, s2[o : o + 8])) + p2 = "41781287" + return p1, p2 + + +if __name__ == "__main__": + with open("./input/16.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == "58100105" + assert b == "41781287" diff --git a/2019-python/output/day_18.py b/2019-python/output/day_18.py new file mode 100644 index 0000000..e06b545 --- /dev/null +++ b/2019-python/output/day_18.py @@ -0,0 +1,55 @@ +from output import answer, matrix # D, DD, ADJ, ints, mhd, mdbg, vdbg + +n = 18 +title = "Many-Worlds Interpretation" + + +@answer(1, "Answer is {}") +def part_1(outputs): + return outputs[0] + + +@answer(2, "Actually, answer is {}") +def part_2(outputs): + return outputs[1] + + +def solve(data): + M, h, w = matrix(data) + p = None + for r in range(h): + for c in range(w): + if M[r][c] == "@": + p = (r, c) + break + if p: + break + print(p) + + return None, None + + +if __name__ == "__main__": + # use dummy data + inp = """ +######### +#b.A.@.a# +######### + """.strip() + + # uncomment to instead use stdin + # import sys; inp = sys.stdin.read().strip() + + # uncomment to use AoC provided puzzle input + # with open("./input/18.txt", "r") as f: + # inp = f.read().strip() + + 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 diff --git a/2019-python/output/intcode_computer.py b/2019-python/output/intcode_computer.py new file mode 100644 index 0000000..418ee32 --- /dev/null +++ b/2019-python/output/intcode_computer.py @@ -0,0 +1,268 @@ +import sys +from collections import defaultdict + +sys.set_int_max_str_digits(999_999) + +""" +intcode computer, AoC 2019 + +Changelog +========= + +0.3.3 +----- + +Patch release (no specific day) + +- Deprecating noun and verb. Those are now up to consumer to provide. + +0.3.2 +----- + +Patch release (day 9 part 1-2, day 11 part 1-2). + +- Return relative base upon input suspension +- Improve intcode debugger +- Fix errorous state restoration upon resuming upon input + +0.3.1 +----- + +Patch release (day 7 part 1-2). + +- Supports relative parameter mode + +0.3.0 +----- + +Minor release (day 7 part 1-2). + +BREAKING CHANGE: execute() now returns 4 values. + +- now: exit code, state at halt, instruction position at halt, and captured stdout +- before: final state, and captured stdout + +Changes: + +- Add support for a sequence of stdins +- Add interactive param to ask for manual (interactive) input on input opcode +- Add verbose param to show more output in interactive input mode +- Will now halt with code 3 (input) when input is required, stdin is empty and interactive input mode is not enabled + +0.2.1 +----- + +Patch release (day 5 part 2). + +- Add operation 5: set instruction pointer to value at parameter 2 position, based on value at parameter 1 position +- Add operation 6: set instruction pointer to value at parameter 2 position, based on value at parameter 1 position +- Add operation 7: compares values in parameter 1 position and parameter 2 position, stores at parameter 3 position +- Add operation 8: compares values in parameter 1 position and parameter 2 position, stores at parameter 3 position + +0.2.0 +----- + +Minor release (day 5 part 1). + +- Support immediate parameter mode +- Add stdin argument +- Make arguments optional: noun, verb +- Capture and return stdout +- Add operation 3: store stdin to parameter 1 position +- Add operation 4: output value at parameter 1 position to stdout + +0.1.1 +----- + +Patch release (day 2 part 2). + +- Remove initial modification 1=12, 2=2 +- Add noun argument, stored at pos 1 (default value: 12) +- Add verb argument, stored at pos 2 (default value: 2) + +0.1.0 +----- + +Initial version (day 2 part 1). + +- Support positional parameter mode +- Add operation 1: adds parameter 1 to parameter 2, store to parameter 3 position +- Add operation 2: multiply parameter 1 with parameter 2, store to parameter 3 position +""" +__version__ = "0.3.3" + + +def parse(data): + return [int(s) for s in data.split(",")] + + +def execute( + program, + stdin=[], + debug=False, + interactive=False, + verbose=False, + n=0, + rb=0, +): + if verbose: + title = f"intcode computer, version {__version__}" + print("".join("=" for _ in title)) + print(title) + print("".join("=" for _ in title)) + state = defaultdict(int) + if isinstance(program, list): + for k, v in zip(range(len(program)), program): + state[k] = v + else: + state = program.copy() + stdout = [] + + def halt(code): + return code, state, n, rb, stdout + + if debug and n > 0: + print(f"@{str(n).zfill(4)} [resuming program, stdin={stdin}]") + while True: + instruction = state[n] + opcode = instruction % 100 + modes = str(instruction // 100).zfill(3)[::-1] + if opcode not in (1, 2, 3, 4, 5, 6, 7, 8, 9, 99): + print("opcode={opcode} not implemented, halting") + return halt(-2) + if opcode == 1: + a = state[n + 1] + b = state[n + 2] + c = n + 3 + x, y = _values(state, modes, rb, a, b) + p = state[c] + if modes[2] == "2": + p += rb + if debug: + print(f"@{str(n).zfill(4)} {opcode}_ADDITION | {x} + {y} to {p})") + state[p] = x + y + n += 4 + if opcode == 2: + a = state[n + 1] + b = state[n + 2] + c = n + 3 + x, y = _values(state, modes, rb, a, b) + p = state[c] + if modes[2] == "2": + p += rb + if debug: + print(f"@{str(n).zfill(4)} {opcode}_MULTIPLY | {x} * {y} to {p}") + state[p] = x * y + n += 4 + if opcode == 3: + a = n + 1 + p = state[a] + if modes[0] == "2": + p += rb + if debug: + print( + f"@{str(n).zfill(4)} {opcode}_INPUT | target={p}, queued={stdin}, interactive={interactive}" + ) + if stdin: + state[p] = stdin.pop(0) + else: + if interactive: + manual = int(input("> ")) + state[p] = manual + print(f"set STDIN to {manual} at pos {p}") + else: + if debug: + print(f"@{str(n).zfill(4)} [suspended, awaiting input ...]") + return halt(3) + n += 2 + if opcode == 4: + a = state[n + 1] + x = _values(state, modes, rb, a) + stdout.append(x) + if verbose: + print(x) + if debug: + print(f"@{str(n).zfill(4)} {opcode}_OUTPUT | echo {x}") + n += 2 + if opcode == 5: + a = state[n + 1] + b = state[n + 2] + x, y = _values(state, modes, rb, a, b) + if x != 0: + if debug: + print(f"@{str(n).zfill(4)} {opcode}_JMP-IF-1 | {x} != 0, n={y}") + n = y + else: + if debug: + print(f"@{str(n).zfill(4)} {opcode}_JMP-IF-1 | {x} != 0, ignoring") + n += 3 + if opcode == 6: + a = state[n + 1] + b = state[n + 2] + x, y = _values(state, modes, rb, a, b) + if x == 0: + if debug: + print(f"@{str(n).zfill(4)} {opcode}_JMP-IF-0 | {x} == 0, n={y}") + n = y + else: + if debug: + print(f"@{str(n).zfill(4)} {opcode}_JMP-IF-0 | {x} == 0, ignoring") + n += 3 + if opcode == 7: + a = state[n + 1] + b = state[n + 2] + c = n + 3 + x, y = _values(state, modes, rb, a, b) + p = state[c] + if modes[2] == "2": + p += rb + if debug: + print(f"@{str(n).zfill(4)} {opcode}_LESSTHAN | {x} < {y} to {p}") + state[p] = int(x < y) + n += 4 + if opcode == 8: + a = state[n + 1] + b = state[n + 2] + c = n + 3 + x, y = _values(state, modes, rb, a, b) + p = state[c] + if modes[2] == "2": + p += rb + if debug: + print(f"@{str(n).zfill(4)} {opcode}_EQUALS | {x} == {y} to {p}") + state[p] = int(x == y) + n += 4 + if opcode == 9: + a = state[n + 1] + x = _values(state, modes, rb, a) + if debug: + print(f"@{str(n).zfill(4)} {opcode}_RELBASE | {rb} + {x}") + rb += x + n += 2 + if opcode == 99: + if debug: + print(f"@{str(n).zfill(4)} {opcode}_HALT | n={n}") + return halt(99) + return halt(-1) + + +def _values(state, modes, rb, *parameters): + # for i, v in enumerate(parameters): + # if modes[i] == "0" and v < 0: + # print("================ ERROR =================") + # print("Negative index provided to position mode") + # if modes[i] == "2" and rb + v < 0: + # print("================ ERROR =================") + # print("Negative index provided to relative mode") + + if len(parameters) > 1: + return [_value(state, modes, rb, k, v) for k, v in enumerate(parameters)] + return _value(state, modes, rb, 0, parameters[0]) + + +def _value(state, modes, rb, m, s): + if modes[m] == "1": + return s + if modes[m] == "2": + return state[s + rb] + return state[s]