diff --git a/2024/src/aoc/days/day21.py b/2024/src/aoc/days/day21.py new file mode 100644 index 0000000..4f1f5b6 --- /dev/null +++ b/2024/src/aoc/days/day21.py @@ -0,0 +1,132 @@ +import functools + +from . import SeparateRunner + +NUMPAD = { + "A": (3, 2), + "0": (3, 1), + "1": (2, 0), + "2": (2, 1), + "3": (2, 2), + "4": (1, 0), + "5": (1, 1), + "6": (1, 2), + "7": (0, 0), + "8": (0, 1), + "9": (0, 2), +} + +DIRPAD = { + "A": (0, 2), + "^": (0, 1), + "<": (1, 0), + "v": (1, 1), + ">": (1, 2), +} + + +@functools.cache +def shortest_numpad(from_: str, to: str) -> list[str]: + inverse = set(NUMPAD.values()) + ay, ax = NUMPAD[from_] + by, bx = NUMPAD[to] + + dx, dy = bx - ax, by - ay + + sx = "<" if dx < 0 else ">" + sy = "^" if dy < 0 else "v" + + if dx > 0 and (by, ax) in inverse or (ay, bx) not in inverse: + return abs(dy) * sy + abs(dx) * sx + "A" + else: + return abs(dx) * sx + abs(dy) * sy + "A" + + +@functools.cache +def shortest_dirpad(from_: str, to: str) -> str: + inverse = set(DIRPAD.values()) + ay, ax = DIRPAD[from_] + by, bx = DIRPAD[to] + + dx, dy = bx - ax, by - ay + sx = "<" if dx < 0 else ">" + sy = "^" if dy < 0 else "v" + + if dx > 0 and (by, ax) in inverse or (ay, bx) not in inverse: + return abs(dy) * sy + abs(dx) * sx + "A" + else: + return abs(dx) * sx + abs(dy) * sy + "A" + + +def encode_shortest_numpad(code: str) -> str: + pos = "A" + + res = "" + + for c in code: + res += shortest_numpad(pos, c) + # print(c, res) + pos = c + + return res + + +def encode_shortest_dirpad(code: str) -> str: + pos = "A" + + res = "" + + for c in code: + if pos != c: + res += shortest_dirpad(pos, c) + else: + res += "A" + pos = c + + return res + + +def decode(code: str, pad: dict[str, tuple[int, int]]) -> str: + result = "" + inverse = {v: k for k, v in pad.items()} + + y, x = pad["A"] + + for i, c in enumerate(code): + match c: + case "A": + result += inverse[y, x] + case "^": + y -= 1 + case "v": + y += 1 + case "<": + x -= 1 + case ">": + x += 1 + + if (y, x) not in inverse: + raise ValueError( + f"""Moved off the board {x, y}, after processing {c}. + Path so far: {result} (from {code[:i]})""" + ) + + return result + + +class DayRunner(SeparateRunner): + @classmethod + def part1(cls, input: str) -> int: + result = 0 + for code in input.strip().split("\n"): + numpad = encode_shortest_numpad(code) + robot1 = encode_shortest_dirpad(numpad) + robot2 = encode_shortest_dirpad(robot1) + + result += int(code[:-1]) * len(robot2) + + return result + + @classmethod + def part2(cls, input: str) -> int: + pass diff --git a/2024/tests/samples/21.txt b/2024/tests/samples/21.txt new file mode 100644 index 0000000..4cf0c29 --- /dev/null +++ b/2024/tests/samples/21.txt @@ -0,0 +1,5 @@ +029A +980A +179A +456A +379A diff --git a/2024/tests/test_day21.py b/2024/tests/test_day21.py new file mode 100644 index 0000000..eaddd02 --- /dev/null +++ b/2024/tests/test_day21.py @@ -0,0 +1,51 @@ +import pytest + +from aoc.days.day21 import ( + DayRunner, + encode_shortest_dirpad, + encode_shortest_numpad, +) + +from . import get_data + + +def test_encode_shortest_numpad() -> None: + assert encode_shortest_numpad("029A") in ( + "^^AvvvA", + "^AvvvA", + "AvvvA", + ) + + +def test_encode_shortest_dirpad() -> None: + numpad_encoded = encode_shortest_numpad("029A") + assert len(encode_shortest_dirpad(numpad_encoded)) == len( + "v<>^AAvA<^AA>A^A" + ) + + +@pytest.mark.parametrize( + "code,answer", + [ + ( + "029A", + ">^AvAA<^A>A>^AvA^A^A^A>AAvA^AA>^AAAvA<^A>A", + ), + ("980A", ">^AAAvA^A>^AvAA<^A>AA>^AAAvA<^A>A^AA"), + ( + "179A", + ">^A>^AAvAA<^A>A>^AAvA^A^AAAA>^AAAvA<^A>A", + ), + ("456A", ">^AA>^AAvAA<^A>A^AA^AAA>^AAvA<^A>A"), + ("379A", ">^AvA^A>^AAvA<^A>AAvA^A^AAAA>^AAAvA<^A>A"), + ], +) +def test_encode_shortest_dirpad_twice(code: str, answer: str) -> None: + numpad_encoded = encode_shortest_numpad(code) + robot1 = encode_shortest_dirpad(numpad_encoded) + robot2 = encode_shortest_dirpad(robot1) + assert len(robot2) == len(answer) + + +def test_sample_part1() -> None: + assert DayRunner.part1(get_data(21)) == 126384