diff --git a/.gitignore b/.gitignore index b41a722..3708721 100644 --- a/.gitignore +++ b/.gitignore @@ -253,4 +253,7 @@ dmypy.json # End of https://www.toptal.com/developers/gitignore/api/go,python,jetbrains+all input.txt -input-loader.json \ No newline at end of file +credentials.json +flamegraph.pl +frames/ +knowngood.py diff --git a/README.md b/README.md index 3a2cd37..dd14c3c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Advent of Code 🎄 -Jump to: [2020](challenges/2020) - [2021](challenges/2021) - [2022](challenges/2022) +Jump to: [2020](challenges/2020) - [2021](challenges/2021) - [2022](challenges/2022) - [2023](challenges/2023) - [2024](challenges/2024) Solutions to [Advent of Code](https://adventofcode.com) challenges. @@ -12,17 +12,28 @@ Puzzle inputs and descriptions are not included in this repository. You'll have The method of running solutions varies by year. -### All years other than 2020 +
From 2023 onwards -Solutions to other years' solutions are run via the runner program contained in [`./runtime`](./runtime). +Use the `./aoc` script and provide it with the filename of the file you would like to run. For example, `./aoc run challenges/01-trebuchet/main.py`. + +Alternatively, run the code directly. Provide it with an input via stdin and set the first command line argument to be either `1` or `2` to run either part 1 or 2 respectively. This should go for all languages. + +
+ +
2021 and 2022 + +In order to run solutions from 2021 and 2022, you should switch to the `2022` branch first. + +Solutions to 2021's and 2022's solutions are run via the runner program contained in [`./runtime`](./runtime). To run a solution, run `go run github.com/codemicro/adventOfCode/runtime` and follow the on-screen prompts. Configurisation options can be seen by running with the `--help` flag. A benchmark graph can be generated using [`generate-benchmark-graph.py`](./generate-benchmark-graph.py) as follows: `python3 generate-benchmark-graph.py `. For example, to generate a graph for the 2021 benchmarks and save it to `challenges/2021/running-times.png`, you can run `python3 generate-benchmark-graph.py challenges/2021/running-times.png 2021`. +
-### 2020 +
2020 In 2020, all solutions are in Python and/or Go. @@ -32,4 +43,6 @@ In 2020, all solutions are in Python and/or Go. * For Python, run `python3 ./py` * For Go, run `go run ./go` -Dependencies for 2020 challenges are not neatly defined anywhere, so determing and installing the correct ones is an exercise for the reader. \ No newline at end of file +Dependencies for 2020 challenges are not neatly defined anywhere, so determing and installing the correct ones is an exercise for the reader. + +
diff --git a/aoc b/aoc new file mode 100644 index 0000000..25e8e5e --- /dev/null +++ b/aoc @@ -0,0 +1,404 @@ +#!/usr/bin/env python3 +import fire +import os +from pathlib import Path +import re +import requests +import sys +import json +import subprocess +from io import BufferedReader +from typing import Optional, Callable +from glob import glob +import time +from tqdm import tqdm + + +CHALLENGES_DIR = "challenges" +SAMPLE_TEST_JSON = "{}" +RUNNERS = { + "py": (None, ["./runners/py.sh"]), + "go": (["./runners/buildGo.sh"], None), + "kt": (["./runners/buildKotlin.sh"], ["./runners/jar.sh"]), +} + + +def convert_to_camel_case(inp: str) -> str: + parts = list( + map(lambda x: x.lower(), filter(lambda x: len(x) != 0, inp.split(" "))) + ) + for i, st in enumerate(parts): + if i == 0: + continue + parts[i] = st[0].upper() + st[1:] + return "".join(parts) + + +def filter_for_filename(inp: str) -> str: + return "".join(filter(lambda x: x.isalpha() or x.isdigit() or x == "-", inp)) + + +def load_credentials() -> dict[str, str]: + with open("credentials.json") as f: + return json.load(f) + + +def set_terminal_colour(*colours: str): + colcodes = { + "bold": "1", + "italic": "3", + "red": "31", + "green": "32", + "grey": "37", + "reset": "0", + } + for colour in colours: + if colour not in colcodes: + continue + sys.stdout.write(f"\033[{colcodes[colour]}m") + sys.stdout.flush() + + +known_runs = {} + + +def time_command(args: list[str], stdin=None) -> tuple[int, float]: + kwargs = {} + + if type(stdin) == str: + kwargs["input"] = stdin.encode() + else: + kwargs["stdin"] = stdin + + st = time.time() + proc = subprocess.run(args, stdout=subprocess.PIPE, **kwargs) + dur = time.time() - st + + return proc.returncode, dur + + +def run_command(args: list[str], stdin=None, cache=True) -> tuple[int, str]: + if cache: + ah = hash("".join(args)) + sh = hash(stdin) + + if (ah, sh) in known_runs: + return known_runs[(ah, sh)] + + kwargs = {} + + if type(stdin) == str: + kwargs["input"] = stdin.encode() + else: + kwargs["stdin"] = stdin + + proc = subprocess.run(args, stdout=subprocess.PIPE, **kwargs) + + if cache: + known_runs[((ah, sh))] = (proc.returncode, proc.stdout) + + return proc.returncode, proc.stdout + + +def format_result(v: str) -> str: + if len(v) == len( + "".join(filter(lambda x: x.isalpha() or x.isdigit() or x == "-", v)) + ): + return v + return repr(v) + + +def run_part(command: list[str], label: str, spec: dict[str, str | BufferedReader]): + set_terminal_colour("grey") + exit_status, buf_cont = run_command(command, stdin=spec.get("input")) + set_terminal_colour("reset") + + print(f"{label}: ", end="") + + if exit_status != 0: + set_terminal_colour("red") + print(f"exited with a non-zero status code ({exit_status})") + set_terminal_colour("reset") + return + + result_str = buf_cont.decode().strip() + formatted_result_str = format_result(result_str) + + if result_str == "": + set_terminal_colour("red") + print("nothing outputted") + set_terminal_colour("reset") + return + else: + if expected := spec.get("is"): + if expected == result_str: + set_terminal_colour("green") + print(f"pass", end="") + set_terminal_colour("grey") + print(f" ({formatted_result_str})") + else: + set_terminal_colour("red") + print(f"fail", end="") + set_terminal_colour("grey") + print( + f" (got {formatted_result_str}, expected {format_result(expected)})" + ) + set_terminal_colour("reset") + else: + print(formatted_result_str) + + +def get_runner_command( + file_name: str, +) -> tuple[list[str], Optional[Callable[[], None]]]: + """ + Builds a solution using `command` then returns a path to the executable. + """ + file_extension = file_name.split(".")[-1].lower() + + if file_extension not in RUNNERS: + print("No compatible runner found", file=sys.stderr) + raise SystemExit(1) + + (runner_build, runner_run) = RUNNERS[file_extension] + + if runner_build is None: + if runner_run is not None: + return runner_run + [file_name], None + print(f"No build or run command specified for runner {file_extension}") + raise SystemExit(1) + + # if runner_run is not None and runner_build is not None: + # print( + # f"Build command and run command specified for {file_extension} - cannot determine path forwards." + # ) + # raise SystemExit(1) + + command = runner_build + [file_name] + set_terminal_colour("grey") + print("Building...", end="\r") + set_terminal_colour("reset") + exit_code, fpath = run_command(command, cache=False) + if exit_code != 0: + print(f"Failed to build: `{command}` returned exit code {exit_code}") + raise SystemExit(1) + fpstr = fpath.decode().strip() + + if runner_run is None: + return [fpstr], lambda: os.unlink(fpstr) + + return runner_run + [fpstr], lambda: os.unlink(fpstr) + + +class CLI(object): + @staticmethod + def init(year: int, day: int): + """ + Initialise a day's AoC challenge + """ + + # Load day's page to verify that it has been released and to get the + # challenge title + + day_url = f"https://adventofcode.com/{year}/day/{day}" + + page_resp = requests.get(day_url) + if page_resp.status_code == 404: + print( + "Challenge page not found: has that day been released yet?", + file=sys.stderr, + ) + raise SystemExit(1) + + page_resp.raise_for_status() + + matches = re.findall( + r"--- Day \d{1,2}: ([\w\- \?!]+) ---", page_resp.content.decode() + ) + assert len(matches) >= 1, "must be able to discover at least one day title" + day_title = matches[0].replace("-", " ") + + # Work out the challenge's directory. + + p = Path(CHALLENGES_DIR) + p /= str(year) + p /= ( + str(day).zfill(2) + + "-" + + filter_for_filename(convert_to_camel_case(day_title)) + ) + + os.makedirs(p) + + # Drop a basic README and tests file + + with open(p / "README.md", "w") as f: + f.write(f"# [Day {day}: {day_title}]({day_url})\n") + + with open(p / "tests.json", "w") as f: + f.write(SAMPLE_TEST_JSON) + + # Download input and drop it in the challenge's directory + + creds = load_credentials() + input_resp = requests.get( + day_url + "/input", + cookies={"session": creds["session"]}, + headers={"User-Agent": creds["userAgent"]}, + ) + input_resp.raise_for_status() + + with open(p / "input.txt", "wb") as f: + f.write(input_resp.content) + + # Output the challenge's directory + + print(p) + + @staticmethod + def run( + fpath: str, + test_only: bool = False, + no_test: bool = False, + select_part: Optional[int] = None, + ): + """ + Execute a day's code + """ + + if test_only and no_test: + print( + f"Conflicting arguments (test-only and no-test both set)", + file=sys.stderr, + ) + raise SystemExit(1) + + if select_part: + select_part = str(select_part) + + try: + os.stat(fpath) + except FileNotFoundError: + print(f"Could not stat {fpath}", file=sys.stderr) + raise SystemExit(1) + + cmd, cleanup = get_runner_command(fpath) + + challenge_dir = Path(os.path.dirname(fpath)) + input_file = open(challenge_dir / "input.txt", "rb") + + if test_only or not no_test: + test_specs = json.load(open(challenge_dir / "tests.json")) + + for part in ["1", "2"]: + if select_part and select_part != part: + continue + for i, spec in enumerate(test_specs.get(part, [])): + run_part(cmd + [part], f"Test {part}.{i+1}", spec) + + if no_test or not test_only: + if (select_part and select_part == "1") or not select_part: + run_part(cmd + ["1"], "Run 1", {"input": input_file}) + input_file.seek(0) + if (select_part and select_part == "2") or not select_part: + run_part(cmd + ["2"], "Run 2", {"input": input_file}) + + input_file.close() + + if cleanup is not None: + cleanup() + + @staticmethod + def bench(fpath: str, n: int = 100): + try: + os.stat(fpath) + except FileNotFoundError: + print(f"Could not stat {fpath}", file=sys.stderr) + raise SystemExit(1) + + file_extension = fpath.split(".")[-1].lower() + + challenge_dir = Path(os.path.dirname(fpath)) + input_file = open(challenge_dir / "input.txt", "rb") + + cmd, cleanup = get_runner_command(fpath) + + benchmark_file = ( + Path(CHALLENGES_DIR) / challenge_dir.parts[1] / "benchmarks.jsonl" + ) + benchmark_fd = open(benchmark_file, "a") + + for part in ["1", "2"]: + durs = [] + r_c = cmd + [part] + for _ in tqdm(range(n), ncols=0, leave=False, desc=f"Part {part}"): + exit_status, run_duration = time_command(r_c, stdin=input_file) + if exit_status != 0: + set_terminal_colour("red") + print(f"Exited with a non-zero status code ({exit_status})") + set_terminal_colour("reset") + return + input_file.seek(0) + durs.append(run_duration) + + mi, mx, avg = min(durs), max(durs), sum(durs) / len(durs) + + json.dump( + { + "day": int(challenge_dir.parts[-1].split("-")[0]), + "part": int(part), + "runner": file_extension, + "min": mi, + "max": mx, + "avg": avg, + "n": n, + }, + benchmark_fd, + ) + benchmark_fd.write("\n") + + print( + f"Part {part}: min {round(mi, 4)} seconds, max {round(mx, 4)} seconds, avg {round(avg, 4)}" + ) + + benchmark_fd.close() + input_file.close() + + if cleanup is not None: + cleanup() + + @staticmethod + def addtest(year: int, day: int, part: int, output: str): + """ + Add a test to a challenge's test case file + """ + + p = Path(CHALLENGES_DIR) + p /= str(year) + p /= glob(str(day).zfill(2) + "-*", root_dir=p)[0] + p /= "tests.json" + + existing_tests = {} + + try: + with open(p) as f: + existing_tests = json.load(f) + except FileNotFoundError: + pass + + if (sp := str(part)) not in existing_tests: + existing_tests[sp] = [] + + existing_tests[sp].append( + { + "is": str(output), + "input": sys.stdin.read(), + } + ) + + with open(p, "w") as f: + json.dump(existing_tests, f, indent=" ") + + +if __name__ == "__main__": + fire.Fire(CLI) diff --git a/challenges/2023/01-trebuchet/README.md b/challenges/2023/01-trebuchet/README.md new file mode 100644 index 0000000..74425f0 --- /dev/null +++ b/challenges/2023/01-trebuchet/README.md @@ -0,0 +1,3 @@ +# [Day 1: Trebuchet?!](https://adventofcode.com/2023/day/1) + +The Haskell implementation included here is only partial, mostly because I ran out of patience with it after taking an age to implement the CLI required. diff --git a/challenges/2023/01-trebuchet/main.hs b/challenges/2023/01-trebuchet/main.hs new file mode 100644 index 0000000..70e6718 --- /dev/null +++ b/challenges/2023/01-trebuchet/main.hs @@ -0,0 +1,42 @@ +import System.Environment +import System.Exit +import System.IO +import Data.Maybe +import Data.Char +import Control.Exception + +type ChallengeReturn = Int + +parse :: String -> [String] +parse = lines + +one :: String -> ChallengeReturn +one inp = foldl + (\acc line -> + let digits = map (\x -> ord x - ord '0') (filter isDigit line) in + assert ((length digits) /= 0) ( + acc + ((digits!!0) * 10) + (digits!!((length digits) - 1)) + ) + ) + 0 + (parse inp) + +two :: String -> ChallengeReturn +two inp = undefined + +main :: IO () +main = do args <- getArgs + inp <- getContents + _runFn (_selectFn args) inp + +_selectFn :: [String] -> Maybe (String -> ChallengeReturn) +_selectFn ["1"] = Just one +_selectFn ["2"] = Just two +_selectFn _ = Nothing + +_runFn :: Maybe (String -> ChallengeReturn) -> String -> IO () +_runFn Nothing _ = _debug "Missing or invalid day argument" >> exitWith (ExitFailure 1) +_runFn (Just fn) inp = putStrLn (show (fn inp)) >> exitWith ExitSuccess + +_debug :: String -> IO () +_debug x = do hPutStrLn stderr x diff --git a/challenges/2023/01-trebuchet/main.py b/challenges/2023/01-trebuchet/main.py new file mode 100644 index 0000000..6cf30c0 --- /dev/null +++ b/challenges/2023/01-trebuchet/main.py @@ -0,0 +1,83 @@ +import sys +from typing import Optional + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +def parse(inp: str) -> list[str]: + return inp.splitlines() + + +def one(inp: str) -> int: + parsed = parse(inp) + acc = 0 + for line in parsed: + digits = list(filter(lambda x: x.isdigit(), line)) + assert len(digits) != 0, f"must have at least one digit per line: {line}" + val = digits[0] + digits[-1] + acc += int(val) + return acc + + +TRANSFORMATIONS = { + "one": "1", + "two": "2", + "three": "3", + "four": "4", + "five": "5", + "six": "6", + "seven": "7", + "eight": "8", + "nine": "9", +} + + +def find_first(inp: str, opts: list[str]) -> Optional[str]: + candidates = [] + for opt in opts: + if (p := inp.find(opt)) != -1: + candidates.append((p, opt)) + li = sorted(candidates, key=lambda x: x[0]) + if len(li) == 0: + return None + return li[0][1] + + +def two(inp: str) -> int: + parsed = parse(inp) + acc = 0 + + search_values = list(map(str, range(0, 10))) + list(TRANSFORMATIONS.keys()) + reversed_search_values = ["".join(reversed(x)) for x in search_values] + + for line in parsed: + first_digit = find_first(line, search_values) + second_digit = find_first("".join(reversed(line)), reversed_search_values) + assert ( + first_digit is not None and second_digit is not None + ), f"must have at least one digit per line: {line}" + + # second digit will be the reversed form + second_digit = "".join(reversed(second_digit)) + + first_digit = TRANSFORMATIONS.get(first_digit, first_digit) + second_digit = TRANSFORMATIONS.get(second_digit, second_digit) + + val = first_digit + second_digit + acc += int(val) + + return acc + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/01-trebuchet/tests.json b/challenges/2023/01-trebuchet/tests.json new file mode 100644 index 0000000..3447287 --- /dev/null +++ b/challenges/2023/01-trebuchet/tests.json @@ -0,0 +1,10 @@ +{ + "1": [ + {"input": "1abc2\npqr3stu8vwx\na1b2c3d4e5f\ntreb7uchet", "is": "142"} + ], + "2": [ + {"input": "two1nine\neightwothree\nabcone2threexyz\nxtwone3four\n4nineeightseven2\nzoneight234\n7pqrstsixteen", "is": "281"}, + {"input": "eighthree", "is": "83"}, + {"input": "sevenine", "is": "79"} + ] +} diff --git a/challenges/2023/02-cubeConundrum/README.md b/challenges/2023/02-cubeConundrum/README.md new file mode 100644 index 0000000..dffb5c3 --- /dev/null +++ b/challenges/2023/02-cubeConundrum/README.md @@ -0,0 +1 @@ +# [Day 2: Cube Conundrum](https://adventofcode.com/2023/day/2) diff --git a/challenges/2023/02-cubeConundrum/main.py b/challenges/2023/02-cubeConundrum/main.py new file mode 100644 index 0000000..863d713 --- /dev/null +++ b/challenges/2023/02-cubeConundrum/main.py @@ -0,0 +1,89 @@ +import sys +from dataclasses import dataclass + + +@dataclass(init=False) +class Game: + id: int + hands: list[dict[str, int]] + + def __init__(self, id: int): + self.id = id + self.hands = [] + + +def parse(inp: str) -> list[Game]: + res = [] + for line in inp.splitlines(): + game_decl, game_hands = line.split(": ") + + game = Game(int(game_decl.lstrip("Game "))) + + for hand in game_hands.split(";"): + hand = hand.strip() + counts = {} + for part in hand.split(", "): + n, colour = part.split(" ") + counts[colour] = int(n) + game.hands.append(counts) + + res.append(game) + + return res + + +COLOUR_MAXVALS = {"red": 12, "green": 13, "blue": 14} + + +def one(inp: str): + games = parse(inp) + + acc = 0 + + for game in games: + ok = True + + for hand in game.hands: + for key in COLOUR_MAXVALS: + if COLOUR_MAXVALS[key] < hand.get(key, 0): + ok = False + break + + if not ok: + break + + if ok: + acc += game.id + + return acc + + +def two(inp: str): + games = parse(inp) + acc = 0 + + for game in games: + x = 1 + + for colour in COLOUR_MAXVALS.keys(): + x *= max(map(lambda x: x.get(colour, 0), game.hands)) + + acc += x + + return acc + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/02-cubeConundrum/tests.json b/challenges/2023/02-cubeConundrum/tests.json new file mode 100644 index 0000000..19b9ca8 --- /dev/null +++ b/challenges/2023/02-cubeConundrum/tests.json @@ -0,0 +1,8 @@ +{ + "1": [ + {"input": "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green\nGame 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue\nGame 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red\nGame 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red\nGame 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green", "is": "8"} + ], + "2": [ + {"input": "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green\nGame 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue\nGame 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red\nGame 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red\nGame 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green", "is": "2286"} + ] +} diff --git a/challenges/2023/03-gearRatios/README.md b/challenges/2023/03-gearRatios/README.md new file mode 100644 index 0000000..46c8392 --- /dev/null +++ b/challenges/2023/03-gearRatios/README.md @@ -0,0 +1 @@ +# [Day 3: Gear Ratios](https://adventofcode.com/2023/day/3) diff --git a/challenges/2023/03-gearRatios/main.py b/challenges/2023/03-gearRatios/main.py new file mode 100644 index 0000000..4719b9c --- /dev/null +++ b/challenges/2023/03-gearRatios/main.py @@ -0,0 +1,134 @@ +import sys + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +Coordinate = tuple[int, int] +Schematic = dict[Coordinate, str] + + +def parse(instr: str) -> Schematic: + res = {} + + lines = instr.splitlines() + max_x = len(lines[0]) + + for row_n, row in enumerate(lines): + assert len(row) == max_x + for col_n, char in enumerate(row): + res[(col_n, row_n)] = char + + return res + + +DIRECTIONS = [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)] + + +def apply_coord_delta(c: Coordinate, d: Coordinate) -> Coordinate: + a, b = c + e, f = d + return a + e, b + f + + +def seek_digits( + sc: Schematic, start: Coordinate, delta: Coordinate +) -> tuple[str, set[Coordinate]]: + digits = "" + coords = set() + + cursor = start + while True: + cursor = apply_coord_delta(cursor, delta) + val = sc.get(cursor, ".") + if not val.isdigit(): + break + coords.add(cursor) + digits += val + + return digits, coords + + +def collect_digits_around( + sc: Schematic, start: Coordinate +) -> tuple[int, set[Coordinate]]: + backward_digits, backward_coords = seek_digits(sc, start, (-1, 0)) + forward_digits, forward_coords = seek_digits(sc, start, (1, 0)) + + return ( + int("".join(reversed(backward_digits)) + sc[start] + forward_digits), + backward_coords | forward_coords | set((start,)), + ) + + +def one(inp: str): + schematic = parse(inp) + + consumed_numbers = set() + acc = 0 + + for coord in schematic: + if coord in consumed_numbers: + continue + + char = schematic[coord] + + if not char.isdigit(): + continue + + is_part_number = False + for delta in DIRECTIONS: + target = schematic.get(apply_coord_delta(coord, delta), ".") + if not (target.isdigit() or target == "."): + is_part_number = True + break + + if is_part_number: + n, used_coords = collect_digits_around(schematic, coord) + consumed_numbers = consumed_numbers | used_coords + acc += int(n) + + return acc + + +def two(inp: str): + schematic = parse(inp) + acc = 0 + + for coord in schematic: + char = schematic[coord] + + if char != "*": + continue + + consumed_numbers = set() + numbers = [] + + for delta in DIRECTIONS: + test_coord = apply_coord_delta(coord, delta) + if test_coord in consumed_numbers: + continue + if schematic.get(test_coord, ".").isdigit(): + n, c = collect_digits_around(schematic, test_coord) + consumed_numbers = consumed_numbers | c + numbers.append(n) + + if len(numbers) == 2: + # is gear! + x, y = numbers + acc += x * y + + return acc + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/03-gearRatios/tests.json b/challenges/2023/03-gearRatios/tests.json new file mode 100644 index 0000000..026add2 --- /dev/null +++ b/challenges/2023/03-gearRatios/tests.json @@ -0,0 +1,8 @@ +{ + "1": [ + {"is": "4361", "input": "467..114..\n...*......\n..35..633.\n......#...\n617*......\n.....+.58.\n..592.....\n......755.\n...$.*....\n.664.598.."} + ], + "2": [ + {"is": "467835", "input": "467..114..\n...*......\n..35..633.\n......#...\n617*......\n.....+.58.\n..592.....\n......755.\n...$.*....\n.664.598.."} + ] +} \ No newline at end of file diff --git a/challenges/2023/04-scratchcards/README.md b/challenges/2023/04-scratchcards/README.md new file mode 100644 index 0000000..f3d5aad --- /dev/null +++ b/challenges/2023/04-scratchcards/README.md @@ -0,0 +1 @@ +# [Day 4: Scratchcards](https://adventofcode.com/2023/day/4) diff --git a/challenges/2023/04-scratchcards/main.py b/challenges/2023/04-scratchcards/main.py new file mode 100644 index 0000000..7cc9b5d --- /dev/null +++ b/challenges/2023/04-scratchcards/main.py @@ -0,0 +1,95 @@ +import sys +from dataclasses import dataclass +import functools +from typing import Callable + + +@dataclass() +class Card: + id: int + winning: set[int] + present: set[int] + + def __init__(self, id: int): + self.id = id + self.winning = set() + self.present = set() + + def __hash__(self) -> int: + return hash(self.id) + + def get_winning_numbers(self) -> set[int]: + return self.winning & self.present + + def calculate_score(self) -> int: + n = len(self.get_winning_numbers()) + score = 0 if n == 0 else 2 ** (n - 1) + return score + + def get_prize_copies(self) -> set[int]: + return set( + map(lambda x: self.id + x + 1, range(len(self.get_winning_numbers()))) + ) + + +def make_card_counter(all_cards: list[Card]) -> Callable[[Card], int]: + @functools.cache + def fn(c: Card) -> int: + acc = 1 + won = c.get_prize_copies() + + for won_card in won: + acc += fn(all_cards[won_card - 1]) + + return acc + + return fn + + +def parse(instr: str) -> list[Card]: + res = [] + + for line in instr.splitlines(): + card_decl, numbers_sect = line.split(": ") + + card = Card(int(card_decl.lstrip("Card "))) + + winning, present = numbers_sect.split("|") + card.winning = set([int(x) for x in winning.split(" ") if x != ""]) + card.present = set([int(x) for x in present.split(" ") if x != ""]) + + res.append(card) + + return res + + +def one(instr: str) -> int: + cards = parse(instr) + return sum(x.calculate_score() for x in cards) + + +def two(instr: str) -> int: + cards = parse(instr) + acc = 0 + + count_fn = make_card_counter(cards) + + for card in cards: + acc += count_fn(card) + return acc + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/04-scratchcards/tests.json b/challenges/2023/04-scratchcards/tests.json new file mode 100644 index 0000000..70d5aaf --- /dev/null +++ b/challenges/2023/04-scratchcards/tests.json @@ -0,0 +1,8 @@ +{ + "1": [ + {"is": "13", "input": "Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53\nCard 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19\nCard 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1\nCard 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83\nCard 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36\nCard 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11\n"} + ], + "2": [ + {"is": "30", "input": "Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53\nCard 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19\nCard 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1\nCard 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83\nCard 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36\nCard 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11\n"} + ] +} diff --git a/challenges/2023/05-ifYouGiveASeedAFertilizer/README.md b/challenges/2023/05-ifYouGiveASeedAFertilizer/README.md new file mode 100644 index 0000000..2506b81 --- /dev/null +++ b/challenges/2023/05-ifYouGiveASeedAFertilizer/README.md @@ -0,0 +1 @@ +# [Day 5: If You Give A Seed A Fertilizer](https://adventofcode.com/2023/day/5) diff --git a/challenges/2023/05-ifYouGiveASeedAFertilizer/main.py b/challenges/2023/05-ifYouGiveASeedAFertilizer/main.py new file mode 100644 index 0000000..3de17c3 --- /dev/null +++ b/challenges/2023/05-ifYouGiveASeedAFertilizer/main.py @@ -0,0 +1,158 @@ +import sys +import re +from typing import Optional +from dataclasses import dataclass +import math +from functools import reduce + + +@dataclass +class Transition: + dest_start: int + src_start: int + n: int + + def __init__(self, dest_start: int, src_start: int, n: int): + self.dest_start = dest_start + self.src_start = src_start + self.n = n + + def get_delta(self) -> int: + return self.dest_start - self.src_start + + def get_src_end(self) -> int: + return self.src_start + self.n - 1 + + def get_dest_end(self) -> int: + return self.dest_start + self.n - 1 + + def is_applicable_to(self, x: int) -> bool: + return self.src_start <= x <= self.get_src_end() + + def is_inverse_applicable_to(self, x: int) -> bool: + return self.dest_start <= x <= self.get_dest_end() + + def get_next_value(self, x: int) -> int: + return (x - self.src_start) + self.dest_start + + def get_previous_value(self, x: int) -> int: + return x + self.src_start - self.dest_start + + +def parse(instr: str) -> tuple[list[int], dict[str, str], dict[str, list[Transition]]]: + state_transitions = {} + transition_functions = {} + seeds = [] + + blocks = instr.split("\n\n") + for block in blocks: + if block.startswith("seeds: "): + seeds = [int(x) for x in block.lstrip("seeds: ").strip().split(" ")] + else: + lines = block.splitlines() + m = re.match(r"([a-z]+)-to-([a-z]+) map:", lines[0]) + assert m, f"invalid block: {lines[0]=}" + + from_type, to_type = m.groups() + + state_transitions[from_type] = to_type + + li = [] + for number_line in lines[1:]: + sp = number_line.split(" ") + assert len(sp) == 3 + li.append(Transition(*[int(x) for x in sp])) + + transition_functions[from_type] = li + + return seeds, state_transitions, transition_functions + + +def resolve(x: int, level: str, state_transitions, transition_functions) -> int: + while level != "location": + for transition in transition_functions[level]: + if transition.is_applicable_to(x): + x = transition.get_next_value(x) + break + level = state_transitions[level] + + return x + + +def apply_level( + transition_functions: dict[str, list[Transition]], level: str, val: int +) -> int: + for transition in transition_functions[level]: + if transition.is_applicable_to(val): + return transition.get_next_value(val) + return val + + +def apply_transitions( + seeds: list[int], + state_transitions: dict[str, str], + transition_functions: dict[str, list[Transition]], +) -> list[int]: + res = [] + + for item_id in seeds: + item_type = "seed" + while item_type != "location": + item_id = apply_level(transition_functions, item_type, item_id) + item_type = state_transitions[item_type] + + res.append(item_id) + + return res + + +def one(instr: str) -> int: + return min(apply_transitions(*parse(instr))) + + +def two(instr: str): + seeds, state_transitions, transition_functions = parse(instr) + assert len(seeds) % 2 == 0 + + inverted_transitions = { + (k := tuple(reversed(x)))[0]: k[1] for x in state_transitions.items() + } + + end_stop = max(seeds[i - 1] + seeds[i] for i in range(1, len(seeds), 2)) + + for i in range(0, end_stop): + c = i + level = "location" + while level != "seed": + level = inverted_transitions[level] + for fn in transition_functions[level]: + if fn.is_inverse_applicable_to(c): + c = fn.get_previous_value(c) + break + + for x in range(0, len(seeds), 2): + start = seeds[x] + end = seeds[x + 1] + start - 1 + + if start <= c <= end: + return i + + i += 1 + + return -1 + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/05-ifYouGiveASeedAFertilizer/tests.json b/challenges/2023/05-ifYouGiveASeedAFertilizer/tests.json new file mode 100644 index 0000000..60d32d6 --- /dev/null +++ b/challenges/2023/05-ifYouGiveASeedAFertilizer/tests.json @@ -0,0 +1,8 @@ +{ + "1": [ + {"is": "35", "input": "seeds: 79 14 55 13\n\nseed-to-soil map:\n50 98 2\n52 50 48\n\nsoil-to-fertilizer map:\n0 15 37\n37 52 2\n39 0 15\n\nfertilizer-to-water map:\n49 53 8\n0 11 42\n42 0 7\n57 7 4\n\nwater-to-light map:\n88 18 7\n18 25 70\n\nlight-to-temperature map:\n45 77 23\n81 45 19\n68 64 13\n\ntemperature-to-humidity map:\n0 69 1\n1 0 69\n\nhumidity-to-location map:\n60 56 37\n56 93 4"} + ], + "2": [ + {"is": "46", "input": "seeds: 79 14 55 13\n\nseed-to-soil map:\n50 98 2\n52 50 48\n\nsoil-to-fertilizer map:\n0 15 37\n37 52 2\n39 0 15\n\nfertilizer-to-water map:\n49 53 8\n0 11 42\n42 0 7\n57 7 4\n\nwater-to-light map:\n88 18 7\n18 25 70\n\nlight-to-temperature map:\n45 77 23\n81 45 19\n68 64 13\n\ntemperature-to-humidity map:\n0 69 1\n1 0 69\n\nhumidity-to-location map:\n60 56 37\n56 93 4"} + ] +} diff --git a/challenges/2023/06-waitForIt/README.md b/challenges/2023/06-waitForIt/README.md new file mode 100644 index 0000000..6b8928d --- /dev/null +++ b/challenges/2023/06-waitForIt/README.md @@ -0,0 +1 @@ +# [Day 6: Wait For It](https://adventofcode.com/2023/day/6) diff --git a/challenges/2023/06-waitForIt/main.py b/challenges/2023/06-waitForIt/main.py new file mode 100644 index 0000000..fc3b71e --- /dev/null +++ b/challenges/2023/06-waitForIt/main.py @@ -0,0 +1,66 @@ +import sys +from functools import reduce +import math + + +# time then record dist +Race = tuple[int, int] + + +def parse(instr: str) -> list[Race]: + times, distances = [ + [int(y) for y in x.split(":")[1].split(" ") if y != ""] + for x in instr.splitlines() + ] + return list(zip(times, distances)) + + +def solve_quadratic(a: int, b: int, c: int) -> list[float]: + # This doesn't handle less than 2 solutions because we're assuming that AoC + # isn't (completely) evil + res = [] + res.append((-b + math.sqrt((b**2) - (4 * a * c))) / 2 * a) + res.append((-b - math.sqrt((b**2) - (4 * a * c))) / 2 * a) + return res + + +def solve_races(races: list[Race]) -> int: + acc = 1 + + for (duration, record) in races: + roots = solve_quadratic(1, -duration, record) + a, b = list(sorted(roots)) + a = math.floor(a) + 1 + b = math.ceil(b) - 1 + acc *= b - a + 1 + + return acc + + +def one(instr: str): + return solve_races(parse(instr)) + + +def two(instr: str): + races = parse(instr) + race = tuple( + int(reduce(lambda x, y: x + str(y), [race[i] for race in races], "")) + for i in range(2) + ) + return solve_races([race]) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/06-waitForIt/tests.json b/challenges/2023/06-waitForIt/tests.json new file mode 100644 index 0000000..53aa161 --- /dev/null +++ b/challenges/2023/06-waitForIt/tests.json @@ -0,0 +1,8 @@ +{ + "1": [ + {"is": "288", "input": "Time: 7 15 30\nDistance: 9 40 200\n"} + ], + "2": [ + {"is": "71503", "input": "Time: 7 15 30\nDistance: 9 40 200\n"} + ] +} diff --git a/challenges/2023/07-camelCards/README.md b/challenges/2023/07-camelCards/README.md new file mode 100644 index 0000000..e4760f3 --- /dev/null +++ b/challenges/2023/07-camelCards/README.md @@ -0,0 +1 @@ +# [Day 7: Camel Cards](https://adventofcode.com/2023/day/7) diff --git a/challenges/2023/07-camelCards/main.py b/challenges/2023/07-camelCards/main.py new file mode 100644 index 0000000..6420e3d --- /dev/null +++ b/challenges/2023/07-camelCards/main.py @@ -0,0 +1,146 @@ +import sys +from enum import IntEnum +import functools +from typing import Callable, Optional + + +def parse(instr: str) -> list[tuple[str, int]]: + return [((sp := x.split(" "))[0], int(sp[1])) for x in instr.splitlines()] + + +def count_chars(x: str) -> dict[str, int]: + res = {} + for ch in x: + res[ch] = res.get(ch, 0) + 1 + return res + + +class HandType(IntEnum): + HighCard = 1 + OnePair = 2 + TwoPair = 3 + ThreeOfAKind = 4 + FullHouse = 5 + FourOfAKind = 6 + FiveOfAKind = 7 + + +def get_hand_type(hand: str, counts: Optional[dict[str, int]] = None) -> HandType: + if counts is None: + counts = count_chars(hand) + num_unique_chars = len(counts) + + if num_unique_chars == 5: + return HandType.HighCard + + if num_unique_chars == 4: + return HandType.OnePair + + if num_unique_chars == 1: + return HandType.FiveOfAKind + + sorted_counts = list(sorted(counts.values())) + + if num_unique_chars == 3: + if sorted_counts == [1, 2, 2]: + return HandType.TwoPair + return HandType.ThreeOfAKind + + if num_unique_chars == 2: + if sorted_counts == [1, 4]: + return HandType.FourOfAKind + return HandType.FullHouse + + raise ValueError(f"bad hand {hand}") + + +def get_best_hand_type(hand: str) -> HandType: + counts = count_chars(hand) + + if "J" in counts: + num_jokers = counts["J"] + + if num_jokers == 5: + return HandType.FiveOfAKind + + del counts["J"] + most_frequent_card = max(counts, key=lambda x: counts[x]) + counts[most_frequent_card] = counts[most_frequent_card] + num_jokers + + return get_hand_type(hand, counts=counts) + + +def compare_card(a: str, b: str, ranking_sequence: list[str]) -> int: + ca, cb = ranking_sequence.index(a), ranking_sequence.index(b) + + if ca > cb: + return 1 + + if ca < cb: + return -1 + + return 0 + + +def compare_hands( + a: str, b: str, ranking_sequence: list[str], hand_type: Callable[[str], HandType] +) -> int: + hta, htb = hand_type(a), hand_type(b) + + if hta > htb: + return 1 + + if hta < htb: + return -1 + + for (ca, cb) in zip(a, b): + if ca != cb: + return compare_card(ca, cb, ranking_sequence) + + return 0 + + +def run( + instr: str, sequence: list[str], hand_type_fn: Callable[[str], HandType] +) -> int: + @functools.cmp_to_key + def cmp_fn(a: tuple[str, any], b: tuple[str, any]) -> int: + return compare_hands(a[0], b[0], sequence, hand_type_fn) + + plays = parse(instr) + acc = 0 + for i, (_, bid) in enumerate(sorted(plays, key=cmp_fn)): + acc += bid * (i + 1) + return acc + + +def one(instr: str): + return run( + instr, + ["2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K", "A"], + get_hand_type, + ) + + +def two(instr: str): + return run( + instr, + ["J", "2", "3", "4", "5", "6", "7", "8", "9", "T", "Q", "K", "A"], + get_best_hand_type, + ) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/07-camelCards/tests.json b/challenges/2023/07-camelCards/tests.json new file mode 100644 index 0000000..765c499 --- /dev/null +++ b/challenges/2023/07-camelCards/tests.json @@ -0,0 +1,8 @@ +{ + "1": [ + {"is": "6440", "input": "32T3K 765\nT55J5 684\nKK677 28\nKTJJT 220\nQQQJA 483"} + ], + "2": [ + {"is": "5905", "input": "32T3K 765\nT55J5 684\nKK677 28\nKTJJT 220\nQQQJA 483"} + ] +} diff --git a/challenges/2023/08-hauntedWasteland/README.md b/challenges/2023/08-hauntedWasteland/README.md new file mode 100644 index 0000000..adcd447 --- /dev/null +++ b/challenges/2023/08-hauntedWasteland/README.md @@ -0,0 +1,3 @@ +# [Day 8: Haunted Wasteland](https://adventofcode.com/2023/day/8) + +A generic solution is implemented in commit 208114cb6222064c1a174b69e737c7cd051defa2 \ No newline at end of file diff --git a/challenges/2023/08-hauntedWasteland/main.py b/challenges/2023/08-hauntedWasteland/main.py new file mode 100644 index 0000000..0fe1512 --- /dev/null +++ b/challenges/2023/08-hauntedWasteland/main.py @@ -0,0 +1,81 @@ +import sys +import re +from functools import reduce +import math + + +def parse(instr: str) -> tuple[str, dict[str, tuple[str, str]]]: + instructions, raw_graph = instr.split("\n\n") + graph = {} + + for (this_node, left, right) in re.findall( + r"([A-Z\d]{3}) = \(([A-Z\d]{3}), ([A-Z\d]{3})\)", raw_graph + ): + graph[this_node] = (left, right) + + return instructions, graph + + +def one(instr: str): + instructions, graph = parse(instr) + + cursor = "AAA" + i = 0 + while cursor != "ZZZ": + instruction = instructions[i % len(instructions)] + cursor = graph[cursor][0 if instruction == "L" else 1] + i += 1 + + return i + + +def two(instr: str): + instructions, graph = parse(instr) + + cursors = [] + for key in graph: + if key[-1] == "A": + cursors.append(key) + + loop_lengths = [] + + for start in cursors: + cursor = start + instruction_step = 0 + + history = {} + i = 0 + + while (cursor, instruction_step) not in history: + instruction = instructions[instruction_step] + history[(cursor, instruction_step)] = i + cursor = graph[cursor][0 if instruction == "L" else 1] + i += 1 + instruction_step = i % len(instructions) + + assert reduce( + lambda acc, v: acc or v[0][-1] == "Z", history, False + ), f"{start} has no end condition" + + loop_start_key = (cursor, instruction_step) + loop_starts_at = history[loop_start_key] + + loop_lengths.append((max(history.values()) - loop_starts_at) + 1) + + return math.lcm(*loop_lengths) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/08-hauntedWasteland/tests.json b/challenges/2023/08-hauntedWasteland/tests.json new file mode 100644 index 0000000..fcdf668 --- /dev/null +++ b/challenges/2023/08-hauntedWasteland/tests.json @@ -0,0 +1,9 @@ +{ + "1": [ + {"is": "2", "input": "RL\n\nAAA = (BBB, CCC)\nBBB = (DDD, EEE)\nCCC = (ZZZ, GGG)\nDDD = (DDD, DDD)\nEEE = (EEE, EEE)\nGGG = (GGG, GGG)\nZZZ = (ZZZ, ZZZ)\n"}, + {"is": "6", "input": "LLR\n\nAAA = (BBB, BBB)\nBBB = (AAA, ZZZ)\nZZZ = (ZZZ, ZZZ)\n"} + ], + "2": [ + {"is": "6", "input": "LR\n\n11A = (11B, XXX)\n11B = (XXX, 11Z)\n11Z = (11B, XXX)\n22A = (22B, XXX)\n22B = (22C, 22C)\n22C = (22Z, 22Z)\n22Z = (22B, 22B)\nXXX = (XXX, XXX)\n"} + ] +} diff --git a/challenges/2023/09-mirageMaintenance/README.md b/challenges/2023/09-mirageMaintenance/README.md new file mode 100644 index 0000000..f9c4819 --- /dev/null +++ b/challenges/2023/09-mirageMaintenance/README.md @@ -0,0 +1 @@ +# [Day 9: Mirage Maintenance](https://adventofcode.com/2023/day/9) diff --git a/challenges/2023/09-mirageMaintenance/main.py b/challenges/2023/09-mirageMaintenance/main.py new file mode 100644 index 0000000..351cf59 --- /dev/null +++ b/challenges/2023/09-mirageMaintenance/main.py @@ -0,0 +1,50 @@ +import sys +from functools import reduce +from typing import Callable + + +def parse(instr: str) -> list[list[int]]: + return [[int(x) for x in line.split(" ")] for line in instr.splitlines()] + + +def get_term(start: list[int], reduction_fn: Callable[[int, list[int]], int]) -> int: + seqs = [start] + while not reduce(lambda acc, v: acc and v == 0, seqs[-1], True): + cs = seqs[-1] + x = [] + for i in range(len(cs) - 1): + x.append(cs[i + 1] - cs[i]) + seqs.append(x) + return reduce(reduction_fn, seqs[-2::-1], 0) + + +def run(instr: str, reduction_fn: Callable[[int, list[int]], int]): + sequences = parse(instr) + acc = 0 + for sq in sequences: + acc += get_term(sq, reduction_fn) + return acc + + +def one(instr: str): + return run(instr, lambda acc, x: acc + x[-1]) + + +def two(instr: str): + return run(instr, lambda acc, x: x[0] - acc) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/09-mirageMaintenance/tests.json b/challenges/2023/09-mirageMaintenance/tests.json new file mode 100644 index 0000000..8e3986c --- /dev/null +++ b/challenges/2023/09-mirageMaintenance/tests.json @@ -0,0 +1,10 @@ +{ + "1": [ + {"is": "114", "input": "0 3 6 9 12 15\n1 3 6 10 15 21\n10 13 16 21 30 45\n"}, + {"is": "36", "input": "30 26 24 24 26 30"} + ], + "2": [ + {"is": "2", "input": "0 3 6 9 12 15\n1 3 6 10 15 21\n10 13 16 21 30 45\n"}, + {"is": "36", "input": "30 26 24 24 26 30"} + ] +} diff --git a/challenges/2023/10-pipeMaze/README.md b/challenges/2023/10-pipeMaze/README.md new file mode 100644 index 0000000..62b4944 --- /dev/null +++ b/challenges/2023/10-pipeMaze/README.md @@ -0,0 +1 @@ +# [Day 10: Pipe Maze](https://adventofcode.com/2023/day/10) diff --git a/challenges/2023/10-pipeMaze/main.py b/challenges/2023/10-pipeMaze/main.py new file mode 100644 index 0000000..6ba459e --- /dev/null +++ b/challenges/2023/10-pipeMaze/main.py @@ -0,0 +1,156 @@ +import sys +import time + + +Coordinate = tuple[int, int] + + +def parse(instr: str) -> tuple[dict[Coordinate, str], Coordinate]: + s_loc = None + res = {} + + for y, line in enumerate(instr.splitlines()): + for x, char in enumerate(line): + coord = (x, y) + + if char == "S": + s_loc = coord + + res[coord] = char + + assert s_loc is not None + return res, s_loc + + +acceptable_moves = { + "-": { + (-1, 0): ["L", "F", "-"], + (1, 0): ["7", "J", "-"], + }, + "|": { + (0, -1): ["7", "F", "|"], + (0, 1): ["J", "L", "|"], + }, +} + +acceptable_moves["L"] = { + (c := (0, -1)): acceptable_moves["|"][c], + (c := (1, 0)): acceptable_moves["-"][c], +} + +acceptable_moves["J"] = { + (c := (0, -1)): acceptable_moves["|"][c], + (c := (-1, 0)): acceptable_moves["-"][c], +} + +acceptable_moves["F"] = { + (c := (0, 1)): acceptable_moves["|"][c], + (c := (1, 0)): acceptable_moves["-"][c], +} + +acceptable_moves["7"] = { + (c := (0, 1)): acceptable_moves["|"][c], + (c := (-1, 0)): acceptable_moves["-"][c], +} + +acceptable_moves["S"] = {**acceptable_moves["|"], **acceptable_moves["-"]} + + +def apply_coord_delta(a: Coordinate, b: Coordinate) -> Coordinate: + aa, ab = a + ba, bb = b + return aa + ba, ab + bb + + +def check_coord( + grid: dict[Coordinate, str], coord: Coordinate, vals: list[str] +) -> bool: + v = grid.get(coord) + if v is None: + return False + + if v in vals: + return True + + return False + + +def get_loop_boundary( + grid: dict[Coordinate, str], start: Coordinate +) -> dict[Coordinate, int]: + visited = {start: 0} + frontier = [start] + + while frontier: + coord = frontier.pop(0) + char = grid[coord] + + for c in acceptable_moves.get(char, {}): + c_must_be = acceptable_moves[char][c] + c = apply_coord_delta(coord, c) + if c not in grid or c in visited: + continue + if check_coord(grid, c, c_must_be): + frontier.append(c) + visited[c] = visited[coord] + 1 + + return visited + + +def one(instr: str): + loop_boundary = get_loop_boundary(*parse(instr)) + return max(loop_boundary.values()) + + +def area(p): + # https://stackoverflow.com/a/451482 + # HMM. I BARELY UNDERSTAND THIS. + return 0.5 * abs( + sum(x0 * y1 - x1 * y0 for ((x0, y0), (x1, y1)) in zip(p, p[1:] + [p[0]])) + ) + + +def two(instr: str): + grid, start_coord = parse(instr) + loop_boundary = get_loop_boundary(grid, start_coord) + + # This hilarious thing turns the loop boundary (dict of points with their + # distances away from the origin) and orders them into a contiguous + # sequence of points that represents the geometry of the shape. + + cands = {} + + for key in loop_boundary: + cands[loop_boundary[key]] = cands.get(loop_boundary[key], []) + [key] + + keys = cands.keys() + res = [] + + for key in sorted(keys): + res.append(cands[key][0]) + + for key in reversed(sorted(keys)): + if len(cands[key]) < 2: + continue + res.append(cands[key][1]) + + # The area includes the size of the line itself, which we don't want. And + # an extra one that I don't understand the source of. + + return int(area(res) - max(loop_boundary.values()) + 1) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/10-pipeMaze/tests.json b/challenges/2023/10-pipeMaze/tests.json new file mode 100644 index 0000000..a4389dc --- /dev/null +++ b/challenges/2023/10-pipeMaze/tests.json @@ -0,0 +1,30 @@ +{ + "1": [ + { + "is": "4", + "input": ".....\n.S-7.\n.|.|.\n.L-J.\n.....\n\n" + }, + { + "is": "8", + "input": "..F7.\n.FJ|.\nSJ.L7\n|F--J\nLJ...\n" + } + ], + "2": [ + { + "is": "4", + "input": "..........\n.S------7.\n.|F----7|.\n.||....||.\n.||....||.\n.|L-7F-J|.\n.|..||..|.\n.L--JL--J.\n..........\n" + }, + { + "is": "4", + "input": "...........\n.S-------7.\n.|F-----7|.\n.||.....||.\n.||.....||.\n.|L-7.F-J|.\n.|..|.|..|.\n.L--J.L--J.\n...........\n\n" + }, + { + "is": "10", + "input": "FF7FSF7F7F7F7F7F---7\nL|LJ||||||||||||F--J\nFL-7LJLJ||||||LJL-77\nF--JF--7||LJLJ7F7FJ-\nL---JF-JLJ.||-FJLJJ7\n|F|F-JF---7F7-L7L|7|\n|FFJF7L7F-JF7|JL---7\n7-L-JL7||F7|L7F-7F7|\nL.L7LFJ|||||FJL7||LJ\nL7JLJL-JLJLJL--JLJ.L\n\n" + }, + { + "is": "8", + "input": ".F----7F7F7F7F-7....\n.|F--7||||||||FJ....\n.||.FJ||||||||L7....\nFJL7L7LJLJ||LJ.L-7..\nL--J.L7...LJS7F-7L7.\n....F-J..F7FJ|L7L7L7\n....L7.F7||L7|.L7L7|\n.....|FJLJ|FJ|F7|.LJ\n....FJL-7.||.||||...\n....L---J.LJ.LJLJ...\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/11-cosmicExpansion/README.md b/challenges/2023/11-cosmicExpansion/README.md new file mode 100644 index 0000000..02c86c7 --- /dev/null +++ b/challenges/2023/11-cosmicExpansion/README.md @@ -0,0 +1 @@ +# [Day 11: Cosmic Expansion](https://adventofcode.com/2023/day/11) diff --git a/challenges/2023/11-cosmicExpansion/main.py b/challenges/2023/11-cosmicExpansion/main.py new file mode 100644 index 0000000..02c049d --- /dev/null +++ b/challenges/2023/11-cosmicExpansion/main.py @@ -0,0 +1,87 @@ +import sys +import math + + +Coordinate = tuple[int, int] +Universe = dict[Coordinate, str] + + +def parse(instr: str) -> Universe: + res = {} + for y, line in enumerate(instr.splitlines()): + for x, char in enumerate(line): + if char != ".": + res[(x, y)] = char + return res + + +def expand_universe(universe: Universe, n: int): + used_rows = list(map(lambda x: x[1], universe.keys())) + expand_rows = [i for i in range(max(used_rows)) if i not in used_rows] + + used_cols = list(map(lambda x: x[0], universe.keys())) + expand_cols = [i for i in range(max(used_cols)) if i not in used_cols] + + for src_col_x in reversed(sorted(expand_cols)): + exp = [galaxy for galaxy in universe if galaxy[0] > src_col_x] + for galaxy in exp: + (gx, gy) = galaxy + v = universe[galaxy] + del universe[galaxy] + universe[(gx + n, gy)] = v + + for src_row_y in reversed(sorted(expand_rows)): + exp = [galaxy for galaxy in universe if galaxy[1] > src_row_y] + for galaxy in exp: + (gx, gy) = galaxy + v = universe[galaxy] + del universe[galaxy] + universe[(gx, gy + n)] = v + + +def get_shortest_path_len(start: Coordinate, end: Coordinate) -> int: + (xa, ya) = start + (xb, yb) = end + return abs(xb - xa) + abs(yb - ya) + + +def run(instr: str, expand_to: int) -> int: + universe = parse(instr) + expand_universe(universe, expand_to - 1) + + galaxy_pairs = {} + for g in universe: + for h in universe: + if h == g or (g, h) in galaxy_pairs or (h, g) in galaxy_pairs: + continue + galaxy_pairs[(g, h)] = None + galaxy_pairs = list(galaxy_pairs.keys()) + + acc = 0 + for (a, b) in galaxy_pairs: + acc += get_shortest_path_len(a, b) + return acc + + +def one(instr: str): + return run(instr, 2) + + +def two(instr: str): + return run(instr, 1_000_000) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/11-cosmicExpansion/tests.json b/challenges/2023/11-cosmicExpansion/tests.json new file mode 100644 index 0000000..c639157 --- /dev/null +++ b/challenges/2023/11-cosmicExpansion/tests.json @@ -0,0 +1,8 @@ +{ + "1": [ + { + "is": "374", + "input": "...#......\n.......#..\n#.........\n..........\n......#...\n.#........\n.........#\n..........\n.......#..\n#...#.....\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/12-hotSprings/README.md b/challenges/2023/12-hotSprings/README.md new file mode 100644 index 0000000..5d6ffd4 --- /dev/null +++ b/challenges/2023/12-hotSprings/README.md @@ -0,0 +1,9 @@ +# [Day 12: Hot Springs](https://adventofcode.com/2023/day/12) + +Before optimisation: 17.13 seconds for part 1 + +![Part 1 before optimisation](p1before.svg) + +After optimisation: 0.80 seconds for part 2 + +![Part 2 after optimisation](p2after.svg) \ No newline at end of file diff --git a/challenges/2023/12-hotSprings/main.py b/challenges/2023/12-hotSprings/main.py new file mode 100644 index 0000000..09782f3 --- /dev/null +++ b/challenges/2023/12-hotSprings/main.py @@ -0,0 +1,83 @@ +import sys +from typing import Iterable +from functools import cache, reduce + + +Rule = tuple[str, list[int]] + + +def parse(instr: str) -> list[Rule]: + res = [] + for line in instr.splitlines(): + observations, lengths = line.split(" ") + res.append((observations, tuple(map(int, lengths.split(","))))) + return res + + +def unfold(rule: Rule) -> Rule: + obs, lens = rule + return ((obs + "?") * 5)[:-1], lens * 5 + + +@cache +def solve(observations: str, lengths: list[int]) -> int: + if len(lengths) == 0: + if "#" in observations: + return 0 + return 1 + elif len(observations) == 0: + return 0 + + char = observations[0] + if char == ".": + return solve(observations[1:], lengths) + + if char == "?": + a = solve("." + observations[1:], lengths) + b = solve("#" + observations[1:], lengths) + return a + b + + # assuming char == "#" + + target_len = lengths[0] + if len(observations) < target_len: + return 0 + + if "." in observations[:target_len]: + return 0 + + if target_len + 1 <= len(observations): + if observations[target_len] == "#": + return 0 + if observations[target_len] == "?": + return solve("." + observations[target_len + 1 :], lengths[1:]) + + return solve(observations[target_len:], lengths[1:]) + + +def run(rules: Iterable[Rule]) -> int: + return reduce(lambda acc, x: acc + solve(*x), rules, 0) + + +def one(instr: str): + return run(parse(instr)) + + +def two(instr: str): + return run(map(unfold, parse(instr))) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/12-hotSprings/p1before.svg b/challenges/2023/12-hotSprings/p1before.svg new file mode 100644 index 0000000..d646b24 --- /dev/null +++ b/challenges/2023/12-hotSprings/p1before.svg @@ -0,0 +1,513 @@ + + + + + + + + + + + + + + +Part 1, before optimisation + +Reset Zoom +Search +ic + + + +generate_permutations:main.py:18 (16 samples, 0.53%) + + + +one:main.py:64 (3,006 samples, 99.90%) +one:main.py:64 + + +check_broken_length_constraints:main.py:42 (746 samples, 24.79%) +check_broken_length_constraints:main.py.. + + +stop:~/.local/lib/python3.10/site-packages/pyflame/sampler.py:29 (1 samples, 0.03%) + + + +_wait_for_tstate_lock:/usr/lib/python3.10/threading.py:1102 (1 samples, 0.03%) + + + +wait:/usr/lib/python3.10/threading.py:288 (1 samples, 0.03%) + + + +generate_permutations:main.py:18 (11 samples, 0.37%) + + + +all (3,009 samples, 100%) + + + +generate_permutations:main.py:18 (64 samples, 2.13%) +g.. + + +generate_possible_observations:main.py:27 (2,170 samples, 72.12%) +generate_possible_observations:main.py:27 + + +__init__:~/.local/lib/python3.10/site-packages/pyflame/sampler.py:13 (1 samples, 0.03%) + + + +generate_permutations:main.py:18 (6 samples, 0.20%) + + + +join:/usr/lib/python3.10/threading.py:1064 (1 samples, 0.03%) + + + +start:/usr/lib/python3.10/threading.py:916 (1 samples, 0.03%) + + + +<module>:main.py:1 (3,007 samples, 99.93%) +<module>:main.py:1 + + +generate_permutations:main.py:18 (25 samples, 0.83%) + + + +generate_permutations:main.py:18 (4 samples, 0.13%) + + + +generate_permutations:main.py:18 (2 samples, 0.07%) + + + +wait:/usr/lib/python3.10/threading.py:589 (1 samples, 0.03%) + + + +generate_permutations:main.py:18 (35 samples, 1.16%) + + + + diff --git a/challenges/2023/12-hotSprings/p2after.svg b/challenges/2023/12-hotSprings/p2after.svg new file mode 100644 index 0000000..5404e0a --- /dev/null +++ b/challenges/2023/12-hotSprings/p2after.svg @@ -0,0 +1,1021 @@ + + + + + + + + + + + + + + +Part 2, after optimisation + +Reset Zoom +Search +ic + + + +solve:main.py:22 (5 samples, 3.97%) +solv.. + + +solve:main.py:22 (34 samples, 26.98%) +solve:main.py:22 + + +solve:main.py:22 (13 samples, 10.32%) +solve:main.py:22 + + +solve:main.py:22 (3 samples, 2.38%) +s.. + + +solve:main.py:22 (3 samples, 2.38%) +s.. + + +solve:main.py:22 (34 samples, 26.98%) +solve:main.py:22 + + +solve:main.py:22 (82 samples, 65.08%) +solve:main.py:22 + + +solve:main.py:22 (118 samples, 93.65%) +solve:main.py:22 + + +solve:main.py:22 (110 samples, 87.30%) +solve:main.py:22 + + +solve:main.py:22 (36 samples, 28.57%) +solve:main.py:22 + + +join:/usr/lib/python3.10/threading.py:1064 (1 samples, 0.79%) + + + +solve:main.py:22 (12 samples, 9.52%) +solve:main.py.. + + +solve:main.py:22 (105 samples, 83.33%) +solve:main.py:22 + + +solve:main.py:22 (22 samples, 17.46%) +solve:main.py:22 + + +solve:main.py:22 (98 samples, 77.78%) +solve:main.py:22 + + +solve:main.py:22 (60 samples, 47.62%) +solve:main.py:22 + + +solve:main.py:22 (19 samples, 15.08%) +solve:main.py:22 + + +solve:main.py:22 (84 samples, 66.67%) +solve:main.py:22 + + +solve:main.py:22 (45 samples, 35.71%) +solve:main.py:22 + + +solve:main.py:22 (91 samples, 72.22%) +solve:main.py:22 + + +solve:main.py:22 (3 samples, 2.38%) +s.. + + +solve:main.py:22 (17 samples, 13.49%) +solve:main.py:22 + + +wait:/usr/lib/python3.10/threading.py:589 (1 samples, 0.79%) + + + +solve:main.py:22 (21 samples, 16.67%) +solve:main.py:22 + + +solve:main.py:22 (20 samples, 15.87%) +solve:main.py:22 + + +solve:main.py:22 (78 samples, 61.90%) +solve:main.py:22 + + +solve:main.py:22 (71 samples, 56.35%) +solve:main.py:22 + + +solve:main.py:22 (96 samples, 76.19%) +solve:main.py:22 + + +solve:main.py:22 (12 samples, 9.52%) +solve:main.py.. + + +solve:main.py:22 (2 samples, 1.59%) + + + +solve:main.py:22 (37 samples, 29.37%) +solve:main.py:22 + + +solve:main.py:22 (13 samples, 10.32%) +solve:main.py:22 + + +solve:main.py:22 (97 samples, 76.98%) +solve:main.py:22 + + +solve:main.py:22 (120 samples, 95.24%) +solve:main.py:22 + + +_wait_for_tstate_lock:/usr/lib/python3.10/threading.py:1102 (1 samples, 0.79%) + + + +solve:main.py:22 (38 samples, 30.16%) +solve:main.py:22 + + +solve:main.py:22 (86 samples, 68.25%) +solve:main.py:22 + + +solve:main.py:22 (119 samples, 94.44%) +solve:main.py:22 + + +solve:main.py:22 (63 samples, 50.00%) +solve:main.py:22 + + +solve:main.py:22 (101 samples, 80.16%) +solve:main.py:22 + + +solve:main.py:22 (121 samples, 96.03%) +solve:main.py:22 + + +solve:main.py:22 (97 samples, 76.98%) +solve:main.py:22 + + +solve:main.py:22 (89 samples, 70.63%) +solve:main.py:22 + + +solve:main.py:22 (41 samples, 32.54%) +solve:main.py:22 + + +solve:main.py:22 (3 samples, 2.38%) +s.. + + +all (126 samples, 100%) + + + +start:/usr/lib/python3.10/threading.py:916 (1 samples, 0.79%) + + + +solve:main.py:22 (32 samples, 25.40%) +solve:main.py:22 + + +unfold:main.py:17 (1 samples, 0.79%) + + + +solve:main.py:22 (7 samples, 5.56%) +solve:m.. + + +solve:main.py:22 (111 samples, 88.10%) +solve:main.py:22 + + +solve:main.py:22 (122 samples, 96.83%) +solve:main.py:22 + + +solve:main.py:22 (102 samples, 80.95%) +solve:main.py:22 + + +solve:main.py:22 (23 samples, 18.25%) +solve:main.py:22 + + +solve:main.py:22 (7 samples, 5.56%) +solve:m.. + + +solve:main.py:22 (20 samples, 15.87%) +solve:main.py:22 + + +solve:main.py:22 (40 samples, 31.75%) +solve:main.py:22 + + +solve:main.py:22 (5 samples, 3.97%) +solv.. + + +solve:main.py:22 (5 samples, 3.97%) +solv.. + + +solve:main.py:22 (18 samples, 14.29%) +solve:main.py:22 + + +solve:main.py:22 (64 samples, 50.79%) +solve:main.py:22 + + +solve:main.py:22 (3 samples, 2.38%) +s.. + + +solve:main.py:22 (7 samples, 5.56%) +solve:m.. + + +two:main.py:66 (123 samples, 97.62%) +two:main.py:66 + + +<module>:main.py:1 (124 samples, 98.41%) +<module>:main.py:1 + + +solve:main.py:22 (99 samples, 78.57%) +solve:main.py:22 + + +solve:main.py:22 (118 samples, 93.65%) +solve:main.py:22 + + +wait:/usr/lib/python3.10/threading.py:288 (1 samples, 0.79%) + + + +solve:main.py:22 (80 samples, 63.49%) +solve:main.py:22 + + +solve:main.py:22 (35 samples, 27.78%) +solve:main.py:22 + + +solve:main.py:22 (67 samples, 53.17%) +solve:main.py:22 + + +solve:main.py:22 (3 samples, 2.38%) +s.. + + +solve:main.py:22 (116 samples, 92.06%) +solve:main.py:22 + + +solve:main.py:22 (1 samples, 0.79%) + + + +solve:main.py:22 (5 samples, 3.97%) +solv.. + + +solve:main.py:22 (18 samples, 14.29%) +solve:main.py:22 + + +solve:main.py:22 (14 samples, 11.11%) +solve:main.py:22 + + +solve:main.py:22 (42 samples, 33.33%) +solve:main.py:22 + + +solve:main.py:22 (31 samples, 24.60%) +solve:main.py:22 + + +solve:main.py:22 (4 samples, 3.17%) +sol.. + + +solve:main.py:22 (12 samples, 9.52%) +solve:main.py.. + + +solve:main.py:22 (3 samples, 2.38%) +s.. + + +solve:main.py:22 (21 samples, 16.67%) +solve:main.py:22 + + +<lambda>:main.py:60 (122 samples, 96.83%) +<lambda>:main.py:60 + + +solve:main.py:22 (51 samples, 40.48%) +solve:main.py:22 + + +solve:main.py:22 (75 samples, 59.52%) +solve:main.py:22 + + +solve:main.py:22 (103 samples, 81.75%) +solve:main.py:22 + + +solve:main.py:22 (122 samples, 96.83%) +solve:main.py:22 + + +stop:~/.local/lib/python3.10/site-packages/pyflame/sampler.py:29 (1 samples, 0.79%) + + + +solve:main.py:22 (39 samples, 30.95%) +solve:main.py:22 + + +solve:main.py:22 (7 samples, 5.56%) +solve:m.. + + +solve:main.py:22 (109 samples, 86.51%) +solve:main.py:22 + + +solve:main.py:22 (89 samples, 70.63%) +solve:main.py:22 + + +solve:main.py:22 (35 samples, 27.78%) +solve:main.py:22 + + +solve:main.py:22 (86 samples, 68.25%) +solve:main.py:22 + + +solve:main.py:22 (99 samples, 78.57%) +solve:main.py:22 + + +__init__:~/.local/lib/python3.10/site-packages/pyflame/sampler.py:13 (1 samples, 0.79%) + + + +solve:main.py:22 (115 samples, 91.27%) +solve:main.py:22 + + +solve:main.py:22 (9 samples, 7.14%) +solve:mai.. + + +solve:main.py:22 (96 samples, 76.19%) +solve:main.py:22 + + +solve:main.py:22 (109 samples, 86.51%) +solve:main.py:22 + + +solve:main.py:22 (9 samples, 7.14%) +solve:mai.. + + +solve:main.py:22 (68 samples, 53.97%) +solve:main.py:22 + + +solve:main.py:22 (116 samples, 92.06%) +solve:main.py:22 + + +solve:main.py:22 (3 samples, 2.38%) +s.. + + +solve:main.py:22 (3 samples, 2.38%) +s.. + + +solve:main.py:22 (53 samples, 42.06%) +solve:main.py:22 + + +solve:main.py:22 (121 samples, 96.03%) +solve:main.py:22 + + +solve:main.py:22 (72 samples, 57.14%) +solve:main.py:22 + + +solve:main.py:22 (24 samples, 19.05%) +solve:main.py:22 + + +solve:main.py:22 (5 samples, 3.97%) +solv.. + + +run:main.py:59 (123 samples, 97.62%) +run:main.py:59 + + +solve:main.py:22 (2 samples, 1.59%) + + + +solve:main.py:22 (39 samples, 30.95%) +solve:main.py:22 + + +solve:main.py:22 (22 samples, 17.46%) +solve:main.py:22 + + +solve:main.py:22 (16 samples, 12.70%) +solve:main.py:22 + + +solve:main.py:22 (54 samples, 42.86%) +solve:main.py:22 + + +solve:main.py:22 (27 samples, 21.43%) +solve:main.py:22 + + +solve:main.py:22 (72 samples, 57.14%) +solve:main.py:22 + + +solve:main.py:22 (13 samples, 10.32%) +solve:main.py:22 + + +solve:main.py:22 (1 samples, 0.79%) + + + +solve:main.py:22 (34 samples, 26.98%) +solve:main.py:22 + + +solve:main.py:22 (122 samples, 96.83%) +solve:main.py:22 + + +solve:main.py:22 (109 samples, 86.51%) +solve:main.py:22 + + +solve:main.py:22 (86 samples, 68.25%) +solve:main.py:22 + + +solve:main.py:22 (17 samples, 13.49%) +solve:main.py:22 + + +solve:main.py:22 (5 samples, 3.97%) +solv.. + + +solve:main.py:22 (7 samples, 5.56%) +solve:m.. + + +solve:main.py:22 (34 samples, 26.98%) +solve:main.py:22 + + +solve:main.py:22 (21 samples, 16.67%) +solve:main.py:22 + + +solve:main.py:22 (86 samples, 68.25%) +solve:main.py:22 + + +solve:main.py:22 (118 samples, 93.65%) +solve:main.py:22 + + +solve:main.py:22 (89 samples, 70.63%) +solve:main.py:22 + + +solve:main.py:22 (14 samples, 11.11%) +solve:main.py:22 + + +solve:main.py:22 (122 samples, 96.83%) +solve:main.py:22 + + +solve:main.py:22 (107 samples, 84.92%) +solve:main.py:22 + + +solve:main.py:22 (120 samples, 95.24%) +solve:main.py:22 + + +solve:main.py:22 (93 samples, 73.81%) +solve:main.py:22 + + +solve:main.py:22 (10 samples, 7.94%) +solve:main... + + +solve:main.py:22 (76 samples, 60.32%) +solve:main.py:22 + + +solve:main.py:22 (2 samples, 1.59%) + + + +solve:main.py:22 (100 samples, 79.37%) +solve:main.py:22 + + +solve:main.py:22 (29 samples, 23.02%) +solve:main.py:22 + + +solve:main.py:22 (93 samples, 73.81%) +solve:main.py:22 + + +solve:main.py:22 (2 samples, 1.59%) + + + +solve:main.py:22 (15 samples, 11.90%) +solve:main.py:22 + + +solve:main.py:22 (47 samples, 37.30%) +solve:main.py:22 + + + diff --git a/challenges/2023/12-hotSprings/tests.json b/challenges/2023/12-hotSprings/tests.json new file mode 100644 index 0000000..959fe14 --- /dev/null +++ b/challenges/2023/12-hotSprings/tests.json @@ -0,0 +1,38 @@ +{ + "1": [ + { + "is": "1", + "input": "???.### 1,1,3" + }, + { + "is": "4", + "input": ".??..??...?##. 1,1,3" + }, + { + "is": "1", + "input": "?#?#?#?#?#?#?#? 1,3,1,6" + }, + { + "is": "1", + "input": "????.#...#... 4,1,1" + }, + { + "is": "4", + "input": "????.######..#####. 1,6,5" + }, + { + "is": "10", + "input": "?###???????? 3,2,1" + }, + { + "is": "0", + "input": "#?? 2,1" + } + ], + "2": [ + { + "is": "525152", + "input": "???.### 1,1,3\n.??..??...?##. 1,1,3\n?#?#?#?#?#?#?#? 1,3,1,6\n????.#...#... 4,1,1\n????.######..#####. 1,6,5\n?###???????? 3,2,1\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/13-pointOfIncidence/README.md b/challenges/2023/13-pointOfIncidence/README.md new file mode 100644 index 0000000..59e23e8 --- /dev/null +++ b/challenges/2023/13-pointOfIncidence/README.md @@ -0,0 +1,9 @@ +# [Day 13: Point of Incidence](https://adventofcode.com/2023/day/13) + +I've got a function that checks vertical lines of symmetry, and to check horizontal ones I just swap the `(x, y)` coords in the grid to `(y, x)` then run it again. + +For each candidate line of symmetry, work out a range that values we need to check (based on which side goes out of bounds first), then for each row in the full height of the grid, count how many rocks are `x` steps away from the line (eg. `.#.|.#.` has 2 rocks 1 step away, assuming the pipe is the line we're checking around). + +For part 1, we just check that no single row has any step within it that doesn't have 0 or 2 rocks. + +For part 2, we make the same check but allow any grid where only a single row fouls that condition. This gives us multiple candidate lines of symmetry for each grid, so we diff it with the as-is case from part 1 and use whatever is new. \ No newline at end of file diff --git a/challenges/2023/13-pointOfIncidence/main.py b/challenges/2023/13-pointOfIncidence/main.py new file mode 100644 index 0000000..c106ecd --- /dev/null +++ b/challenges/2023/13-pointOfIncidence/main.py @@ -0,0 +1,152 @@ +import sys +import gridutil.grid as gu +from collections import defaultdict +from typing import Optional, Callable, Iterable + + +def parse(instr: str) -> list[gu.Grid]: + res = [] + for block in instr.split("\n\n"): + res.append(gu.parse(block, filter_fn=lambda x: x != ".")) + return res + + +Acceptor = Callable[[Iterable[int]], bool] + + +def find_vertical_reflections( + grid: gu.Grid, make_accept_fn: Callable[[], Acceptor] +) -> set[int]: + max_x = gu.get_max_x(grid) + max_y = gu.get_max_y(grid) + + res = set() + + for base_x in range(max_x): + acceptable = True + + accept_fn = make_accept_fn() + + diff = min(max_x - base_x, base_x + 1) + range_from = max(0, base_x + 1 - diff) + range_to = min(max_x, base_x + diff) + 1 + + for y in range(max_y + 1): + dists = defaultdict(lambda: 0) + + for x in range(range_from, range_to): + if grid.get((x, y)) is None: + continue + + if x > base_x: + key = x - base_x - 1 + else: + key = base_x - x + + dists[key] = dists[key] + 1 + + if not accept_fn(dists.values()): + acceptable = False + break + + if acceptable: + res.add(base_x) + + return res + + +def make_standard_acceptor() -> Acceptor: + def inner(x: Iterable[int]) -> bool: + for v in x: + if v != 2: + return False + return True + + return inner + + +def make_smudge_acceptor() -> Acceptor: + n = 0 + + def inner(x: Iterable[int]) -> bool: + nonlocal n + for v in x: + if v != 2: + if n == 1: + return False + n += 1 + return True + + return inner + + +def swap_x_y(grid: gu.Grid) -> gu.Grid: + res = {} + for (x, y) in grid: + res[(y, x)] = grid[(x, y)] + return res + + +def set_to_option(x: set[int]) -> Optional[int]: + assert len(x) <= 1 + if len(x) == 0: + return None + return x.pop() + + +def one(instr: str): + grids = parse(instr) + + acc = 0 + + for grid in grids: + v = find_vertical_reflections(grid, make_standard_acceptor) + h = find_vertical_reflections(swap_x_y(grid), make_standard_acceptor) + + if (v := set_to_option(v)) is not None: + acc += v + 1 + + if (h := set_to_option(h)) is not None: + acc += (h + 1) * 100 + + return acc + + +def two(instr: str): + grids = parse(instr) + + acc = 0 + + for grid in grids: + v = find_vertical_reflections( + grid, make_smudge_acceptor + ) - find_vertical_reflections(grid, make_standard_acceptor) + + sgrid = swap_x_y(grid) + h = find_vertical_reflections( + sgrid, make_smudge_acceptor + ) - find_vertical_reflections(sgrid, make_standard_acceptor) + + if (v := set_to_option(v)) is not None: + acc += v + 1 + + if (h := set_to_option(h)) is not None: + acc += (h + 1) * 100 + + return acc + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/13-pointOfIncidence/tests.json b/challenges/2023/13-pointOfIncidence/tests.json new file mode 100644 index 0000000..e2f27d4 --- /dev/null +++ b/challenges/2023/13-pointOfIncidence/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "405", + "input": "#.##..##.\n..#.##.#.\n##......#\n##......#\n..#.##.#.\n..##..##.\n#.#.##.#.\n\n#...##..#\n#....#..#\n..##..###\n#####.##.\n#####.##.\n..##..###\n#....#..#\n" + } + ], + "2": [ + { + "is": "400", + "input": "#.##..##.\n..#.##.#.\n##......#\n##......#\n..#.##.#.\n..##..##.\n#.#.##.#.\n\n#...##..#\n#....#..#\n..##..###\n#####.##.\n#####.##.\n..##..###\n#....#..#\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/14-parabolicReflectorDish/README.md b/challenges/2023/14-parabolicReflectorDish/README.md new file mode 100644 index 0000000..bbae9f4 --- /dev/null +++ b/challenges/2023/14-parabolicReflectorDish/README.md @@ -0,0 +1,3 @@ +# [Day 14: Parabolic Reflector Dish](https://adventofcode.com/2023/day/14) + +Visualisation exists as `out.mp4` \ No newline at end of file diff --git a/challenges/2023/14-parabolicReflectorDish/digits.txt b/challenges/2023/14-parabolicReflectorDish/digits.txt new file mode 100644 index 0000000..518a22d --- /dev/null +++ b/challenges/2023/14-parabolicReflectorDish/digits.txt @@ -0,0 +1,79 @@ + xxx +x x +x x +x x +x x +x x + xxx + + x +xxx + x + x + x + x +xxxxx + + xxx +x x + x + x + x + x +xxxxx + + xxx +x x + x + xx + x +x x + xxx + + x + xx + x x + x x +xxxxx + x + x + +xxxxx +x +x +xxxx + x +x x + xxx + + xxx +x x +x +xxxx +x x +x x + xxx + +xxxxx + x + x + x + x + x + x + + xxx +x x +x x + xxx +x x +x x + xxx + + xxx +x x +x x + xxxx + x +x x + xxx diff --git a/challenges/2023/14-parabolicReflectorDish/generate-vis.sh b/challenges/2023/14-parabolicReflectorDish/generate-vis.sh new file mode 100644 index 0000000..8ea6576 --- /dev/null +++ b/challenges/2023/14-parabolicReflectorDish/generate-vis.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -ex + +cat input.txt | python3 vis.py +ffmpeg -y -framerate 30 -pattern_type glob -i 'frames/*.png' -c:v libx264 -pix_fmt yuv420p out.mp4 diff --git a/challenges/2023/14-parabolicReflectorDish/main.py b/challenges/2023/14-parabolicReflectorDish/main.py new file mode 100644 index 0000000..e244546 --- /dev/null +++ b/challenges/2023/14-parabolicReflectorDish/main.py @@ -0,0 +1,105 @@ +import sys +from enum import Enum, auto + + +def parse(instr: str) -> tuple[str]: + return tuple(instr.splitlines()) + + +class TiltDirection(Enum): + North = auto() + East = auto() + South = auto() + West = auto() + + +def flip_platform(p: tuple[str]) -> tuple[str]: + return tuple(map(lambda x: "".join(x), zip(*p))) + + +def tilt_platform(platform: tuple[str], direction: TiltDirection) -> tuple[str]: + if direction == TiltDirection.North or direction == TiltDirection.South: + platform = flip_platform(platform) + + if direction == TiltDirection.North or direction == TiltDirection.West: + transformation = lambda x: x.replace(".O", "O.") + else: + transformation = lambda x: x.replace("O.", ".O") + + res = [] + + for line in platform: + prev = "" + l = line + while prev != l: + prev = l + l = transformation(l) + + res.append(l) + + if direction == TiltDirection.North or direction == TiltDirection.South: + res = flip_platform(res) + + return tuple(res) + + +def calc_north_load(platform: tuple[str]) -> int: + acc = 0 + for y, line in enumerate(platform): + for x, char in enumerate(line): + if char != "O": + continue + acc += len(platform) - y + return acc + + +def one(instr: str): + platform = parse(instr) + platform = tilt_platform(platform, TiltDirection.North) + return calc_north_load(platform) + + +def two(instr: str): + platform = parse(instr) + + ITERS = 1_000_000_000 + + i = 0 + known = {} + jumped = False + while i < ITERS: + for direction in [ + TiltDirection.North, + TiltDirection.West, + TiltDirection.South, + TiltDirection.East, + ]: + platform = tilt_platform(platform, direction) + + if not jumped: + if platform in known: + last_occurrence = known[platform] + period = i - last_occurrence + i += ((ITERS - i) // period) * period + jumped = True + else: + known[platform] = i + i += 1 + + return calc_north_load(platform) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/14-parabolicReflectorDish/out.mp4 b/challenges/2023/14-parabolicReflectorDish/out.mp4 new file mode 100644 index 0000000..199efb9 Binary files /dev/null and b/challenges/2023/14-parabolicReflectorDish/out.mp4 differ diff --git a/challenges/2023/14-parabolicReflectorDish/tests.json b/challenges/2023/14-parabolicReflectorDish/tests.json new file mode 100644 index 0000000..f63b4de --- /dev/null +++ b/challenges/2023/14-parabolicReflectorDish/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "136", + "input": "O....#....\nO.OO#....#\n.....##...\nOO.#O....O\n.O.....O#.\nO.#..O.#.#\n..O..#O..O\n.......O..\n#....###..\n#OO..#....\n\n" + } + ], + "2": [ + { + "is": "64", + "input": "O....#....\nO.OO#....#\n.....##...\nOO.#O....O\n.O.....O#.\nO.#..O.#.#\n..O..#O..O\n.......O..\n#....###..\n#OO..#....\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/14-parabolicReflectorDish/vis.py b/challenges/2023/14-parabolicReflectorDish/vis.py new file mode 100644 index 0000000..40f1367 --- /dev/null +++ b/challenges/2023/14-parabolicReflectorDish/vis.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 + +import main +import os +from pathlib import Path +from PIL import Image +import sys +from typing import Optional + + +DIGIT_FONT = [ + [[(True if char == "x" else False) for char in line] for line in block.splitlines()] + for block in open("digits.txt").read().split("\n\n") +] + +background_colour = (0, 0, 0) # black +stationary_colour = (190, 52, 58) # red +falling_colour = (50, 49, 49) # grey +scan_colour = (52, 190, 58) # green +alt_scan_colour = (24, 77, 191) # blue +letter_colour = (255, 255, 255) # white + +frame_dir = Path("frames") +os.mkdir(frame_dir) + +counter = 0 +frame_number = 0 +scale_factor = 4 + + +def draw_frame( + platform: tuple[str], + highlight_y: Optional[int] = None, + allow_skip: bool = True, + number: Optional[int] = None, +): + global frame_number, counter + + counter += 1 + if not highlight_y and allow_skip: + if counter % 21 != 0: + return + + y = len(platform) + x = len(platform[0]) + + img = Image.new("RGB", (x, y), color=background_colour) + + for y, line in enumerate(platform): + for x, char in enumerate(line): + c = background_colour + + if char == "#": + c = stationary_colour + if char == "O": + c = falling_colour + if highlight_y is not None and y == highlight_y: + if char == "O": + c = alt_scan_colour + else: + c = scan_colour + + img.putpixel((x, y), c) + + if number is not None: + pos_x = 5 + pos_y = 5 + + for digit in str(number): + digit = int(digit) + for yd, line in enumerate(DIGIT_FONT[digit]): + for xd, putpix in enumerate(line): + if putpix: + img.putpixel((pos_x + xd, pos_y + yd), letter_colour) + pos_x += 7 # 5 pixel wide font + 2 pixel gap + + img = img.resize((x * scale_factor, y * scale_factor), resample=Image.NEAREST) + img.save(frame_dir / f"{str(frame_number).zfill(8)}.png") + frame_number += 1 + + +def modtilt( + platform: tuple[str], direction: main.TiltDirection, allow_skip=True, partial=True +) -> tuple[str]: + needs_flip = ( + direction == main.TiltDirection.North or direction == main.TiltDirection.South + ) + + if direction == main.TiltDirection.North or direction == main.TiltDirection.South: + platform = main.flip_platform(platform) + + if direction == main.TiltDirection.North or direction == main.TiltDirection.West: + transformation = lambda x: x.replace(".O", "O.") + else: + transformation = lambda x: x.replace("O.", ".O") + + res = list(platform) + + changes = True + + while changes: + changes = False + for i in range(len(res)): + after = transformation(res[i]) + if res[i] != after: + changes = True + res[i] = after + if (partial and not changes) or not partial: + draw_frame( + res if not needs_flip else main.flip_platform(res), + allow_skip=allow_skip, + ) + + if direction == main.TiltDirection.North or direction == main.TiltDirection.South: + res = main.flip_platform(res) + + return tuple(res) + + +platform = main.parse(sys.stdin.read().strip()) + +draw_frame(platform) +platform = modtilt(platform, main.TiltDirection.North, allow_skip=False, partial=False) + +acc = 0 +for y, line in enumerate(platform): + for x, char in enumerate(line): + if char != "O": + continue + acc += len(platform) - y + draw_frame(platform, highlight_y=y, number=acc) + +for i in range(20): + draw_frame(platform, number=acc, allow_skip=False) + +platform = modtilt(platform, main.TiltDirection.West) +platform = modtilt(platform, main.TiltDirection.North) +platform = modtilt(platform, main.TiltDirection.East) + +ITERS = 1_000_000_000 - 1 + +i = 0 +known = {} +jumped = False +while i < ITERS: + print(f"{i}/{ITERS}", end="\r") + for direction in [ + main.TiltDirection.North, + main.TiltDirection.West, + main.TiltDirection.South, + main.TiltDirection.East, + ]: + platform = modtilt(platform, direction) + + if not jumped: + if platform in known: + last_occurrence = known[platform] + period = i - last_occurrence + i += ((ITERS - i) // period) * period + jumped = True + else: + known[platform] = i + i += 1 + +acc = 0 +for y, line in enumerate(platform): + for x, char in enumerate(line): + if char != "O": + continue + acc += len(platform) - y + draw_frame(platform, highlight_y=y, number=acc) + +for i in range(20): + draw_frame(platform, number=acc, allow_skip=False) diff --git a/challenges/2023/15-lensLibrary/README.md b/challenges/2023/15-lensLibrary/README.md new file mode 100644 index 0000000..b6321cb --- /dev/null +++ b/challenges/2023/15-lensLibrary/README.md @@ -0,0 +1,3 @@ +# [Day 15: Lens Library](https://adventofcode.com/2023/day/15) + +* 2 not 315986 \ No newline at end of file diff --git a/challenges/2023/15-lensLibrary/main.go b/challenges/2023/15-lensLibrary/main.go new file mode 100644 index 0000000..49c2796 --- /dev/null +++ b/challenges/2023/15-lensLibrary/main.go @@ -0,0 +1,146 @@ +package main + +import ( + "fmt" + "io" + "os" + "strconv" + "strings" +) + +func hash(x string) int { + var res int + for _, char := range x { + res += int(char) + res *= 17 + res = res % 256 + } + return res +} + +func parse(instr string) []string { + return strings.Split(instr, ",") +} + +func one(instr string) int { + var ( + sequence = parse(instr) + acc int + ) + + for _, section := range sequence { + acc += hash(section) + } + + return acc +} + +type lens struct { + Label string + FocalLength int +} + +func parseLens(x string) (*lens, rune) { + var ( + op rune + labelRunes []rune + foundFocalLength bool + focalLengthRunes []rune + ) + + for _, char := range x { + if char == '-' { + op = char + break + } + + if char == '=' { + op = char + foundFocalLength = true + continue + } + + if foundFocalLength { + focalLengthRunes = append(focalLengthRunes, char) + } else { + labelRunes = append(labelRunes, char) + } + } + + res := new(lens) + + if foundFocalLength { + var err error + res.FocalLength, err = strconv.Atoi(string(focalLengthRunes)) + if err != nil { + panic(err) + } + } + + res.Label = string(labelRunes) + + return res, op +} + +func two(instr string) int { + sequence := parse(instr) + + boxes := make([][]*lens, 256) + + for _, part := range sequence { + var ( + lens, op = parseLens(part) + labelHash = hash(lens.Label) + added bool + ) + + var n int + for _, x := range boxes[labelHash] { + if labelMatches := x.Label == lens.Label; labelMatches && op == '=' { + added = true + boxes[labelHash][n] = lens + n += 1 + } else if !labelMatches { + boxes[labelHash][n] = x + n += 1 + } + } + boxes[labelHash] = boxes[labelHash][:n] + + if !added && op == '=' { + boxes[labelHash] = append(boxes[labelHash], lens) + } + } + + var acc int + for i, box := range boxes { + for j, lens := range box { + acc += (i + 1) * (j + 1) * lens.FocalLength + } + } + return acc +} + +func main() { + if len(os.Args) < 2 || !(os.Args[1] == "1" || os.Args[1] == "2") { + debug("Missing day argument") + os.Exit(1) + } + + inp, err := io.ReadAll(os.Stdin) + if err != nil { + panic(err) + } + inpStr := strings.TrimSpace(string(inp)) + + switch os.Args[1] { + case "1": + fmt.Println(one(inpStr)) + case "2": + fmt.Println(two(inpStr)) + } +} + +func debug(f string, sub ...any) { + fmt.Fprintf(os.Stderr, f, sub...) +} diff --git a/challenges/2023/15-lensLibrary/tests.json b/challenges/2023/15-lensLibrary/tests.json new file mode 100644 index 0000000..6be358e --- /dev/null +++ b/challenges/2023/15-lensLibrary/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "1320", + "input": "rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7\n" + } + ], + "2": [ + { + "is": "145", + "input": "rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/16-theFloorWillBeLava/README.md b/challenges/2023/16-theFloorWillBeLava/README.md new file mode 100644 index 0000000..44102dc --- /dev/null +++ b/challenges/2023/16-theFloorWillBeLava/README.md @@ -0,0 +1 @@ +# [Day 16: The Floor Will Be Lava](https://adventofcode.com/2023/day/16) diff --git a/challenges/2023/16-theFloorWillBeLava/main.py b/challenges/2023/16-theFloorWillBeLava/main.py new file mode 100644 index 0000000..c721524 --- /dev/null +++ b/challenges/2023/16-theFloorWillBeLava/main.py @@ -0,0 +1,137 @@ +import gridutil.grid as gu +import gridutil.coord as cu +from enum import Enum, auto +import sys +from collections import defaultdict + + +class Direction(Enum): + Up = auto() + Down = auto() + Left = auto() + Right = auto() + + +DIRECTION_DELTAS = { + Direction.Up: (0, -1), + Direction.Down: (0, 1), + Direction.Left: (-1, 0), + Direction.Right: (1, 0), +} + + +def parse(instr: str) -> gu.Grid: + return gu.parse(instr) + + +def get_n_energised( + box: gu.Grid, start_pos: cu.Coordinate, start_directon: Direction +) -> int: + energised = defaultdict(lambda: []) + beams = [(start_pos, start_directon)] + + while beams: + (pos, direction) = beams.pop(0) + + while True: + if pos not in box: + break + + if pos in energised and direction in energised[pos]: + break + else: + energised[pos].append(direction) + + at_pos = box[pos] + + match at_pos: + case "/": + match direction: + case Direction.Up: + direction = Direction.Right + case Direction.Down: + direction = Direction.Left + case Direction.Left: + direction = Direction.Down + case Direction.Right: + direction = Direction.Up + + case "\\": + match direction: + case Direction.Up: + direction = Direction.Left + case Direction.Down: + direction = Direction.Right + case Direction.Left: + direction = Direction.Up + case Direction.Right: + direction = Direction.Down + + case "-": + if not ( + direction == Direction.Left or direction == direction.Right + ): + beams.append( + ( + (cu.add(pos, DIRECTION_DELTAS[Direction.Left])), + Direction.Left, + ) + ) + direction = Direction.Right + + case "|": + if not (direction == Direction.Up or direction == direction.Down): + beams.append( + ( + (cu.add(pos, DIRECTION_DELTAS[Direction.Up])), + Direction.Up, + ) + ) + direction = Direction.Down + + pos = cu.add(pos, DIRECTION_DELTAS[direction]) + + return len(energised) + + +def one(instr: str): + box = parse(instr) + return get_n_energised(box, (0, 0), Direction.Right) + + +def two(instr: str): + box = parse(instr) + + max_x = gu.get_max_x(box) + max_y = gu.get_max_y(box) + + starts = [(0, y) for y in range(max_y + 1)] + starts += [(max_x, y) for y in range(max_y + 1)] + starts += [(x, 0) for x in range(1, max_x)] + starts += [(x, max_y) for x in range(1, max_x)] + + max_energised = 0 + + for start in starts: + for direction in Direction: + x = get_n_energised(box, start, direction) + if x > max_energised: + max_energised = x + + return max_energised + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/16-theFloorWillBeLava/test.txt b/challenges/2023/16-theFloorWillBeLava/test.txt new file mode 100644 index 0000000..d6805ce --- /dev/null +++ b/challenges/2023/16-theFloorWillBeLava/test.txt @@ -0,0 +1,10 @@ +.|...\.... +|.-.\..... +.....|-... +........|. +.......... +.........\ +..../.\\.. +.-.-/..|.. +.|....-|.\ +..//.|.... diff --git a/challenges/2023/16-theFloorWillBeLava/tests.json b/challenges/2023/16-theFloorWillBeLava/tests.json new file mode 100644 index 0000000..e0b7536 --- /dev/null +++ b/challenges/2023/16-theFloorWillBeLava/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "46", + "input": ".|...\\....\n|.-.\\.....\n.....|-...\n........|.\n..........\n.........\\\n..../.\\\\..\n.-.-/..|..\n.|....-|.\\\n..//.|....\n" + } + ], + "2": [ + { + "is": "51", + "input": ".|...\\....\n|.-.\\.....\n.....|-...\n........|.\n..........\n.........\\\n..../.\\\\..\n.-.-/..|..\n.|....-|.\\\n..//.|....\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/17-clumsyCrucible/README.md b/challenges/2023/17-clumsyCrucible/README.md new file mode 100644 index 0000000..cc53407 --- /dev/null +++ b/challenges/2023/17-clumsyCrucible/README.md @@ -0,0 +1 @@ +# [Day 17: Clumsy Crucible](https://adventofcode.com/2023/day/17) diff --git a/challenges/2023/17-clumsyCrucible/main.py b/challenges/2023/17-clumsyCrucible/main.py new file mode 100644 index 0000000..b11ba2d --- /dev/null +++ b/challenges/2023/17-clumsyCrucible/main.py @@ -0,0 +1,139 @@ +import sys +import gridutil.grid as gu +import gridutil.coord as cu +from collections.abc import Callable, Iterator +from collections import namedtuple +from queue import PriorityQueue + + +def parse(instr: str) -> gu.Grid: + g = gu.parse(instr) + return {k: int(g[k]) for k in g} + + +State = namedtuple("State", ["pos", "steps_taken", "direction"]) + + +def djikstra( + start: State, + end: Callable[[State], bool], + neighbours: Callable[[State], Iterator[State, None, None]], + cost: Callable[[State, State], int], +) -> list[State]: + dq = PriorityQueue() + dq.put((0, start)) + d = {start: 0} + p = {} + + endState = None + while endState is None: + w = dq.get()[1] + + for u in neighbours(w): + c = d[w] + cost(w, u) + if u not in d or c < d[u]: + d[u] = c + dq.put((c, u)) + p[u] = w + + if end(u): + endState = u + break + + res = [endState] + cursor = p[endState] + while cursor is not None: + res.append(cursor) + cursor = p.get(cursor) + return list(reversed(res)) + + +def one(instr: str): + grid = parse(instr) + end = (gu.get_max_x(grid), gu.get_max_y(grid)) + + def neighbours(node: State) -> Iterator[State, None, None]: + for direction in cu.Direction: + if ( + direction == node.direction and node.steps_taken == 3 + ) or node.direction == direction.opposite(): + continue + + nc = cu.add(node.pos, direction.delta()) + if nc not in grid: + continue + + yield State( + nc, + (node.steps_taken + 1) if direction == node.direction else 1, + direction, + ) + + path = djikstra( + State((0, 0), 0, cu.Direction.Right), + lambda x: x.pos == end, + neighbours, + lambda _, x: grid[x.pos], + )[1:] + + acc = 0 + for node in path: + acc += grid[node.pos] + return acc + + +def two(instr: str): + grid = parse(instr) + end = (gu.get_max_x(grid), gu.get_max_y(grid)) + + for x in range(end[0] - 3, end[0]): + for y in range(end[1] - 3, end[1]): + del grid[(x, y)] + + def neighbours(node: State) -> Iterator[State, None, None]: + for direction in cu.Direction: + if 0 < node.steps_taken < 4 and direction != node.direction: + continue + + if ( + direction == node.direction and node.steps_taken == 10 + ) or node.direction == direction.opposite(): + continue + + nc = cu.add(node.pos, direction.delta()) + if nc not in grid: + continue + + yield State( + nc, + (node.steps_taken + 1) if direction == node.direction else 1, + direction, + ) + + path = djikstra( + State((0, 0), 0, cu.Direction.Right), + lambda x: x.pos == end, + neighbours, + lambda _, x: grid[x.pos], + )[1:] + + acc = 0 + for node in path: + acc += grid[node.pos] + return acc + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/17-clumsyCrucible/tests.json b/challenges/2023/17-clumsyCrucible/tests.json new file mode 100644 index 0000000..356b032 --- /dev/null +++ b/challenges/2023/17-clumsyCrucible/tests.json @@ -0,0 +1,18 @@ +{ + "1": [ + { + "is": "102", + "input": "2413432311323\n3215453535623\n3255245654254\n3446585845452\n4546657867536\n1438598798454\n4457876987766\n3637877979653\n4654967986887\n4564679986453\n1224686865563\n2546548887735\n4322674655533\n\n" + } + ], + "2": [ + { + "is": "94", + "input": "2413432311323\n3215453535623\n3255245654254\n3446585845452\n4546657867536\n1438598798454\n4457876987766\n3637877979653\n4654967986887\n4564679986453\n1224686865563\n2546548887735\n4322674655533\n\n" + }, + { + "is": "71", + "input": "111111111111\n999999999991\n999999999991\n999999999991\n999999999991\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/18-lavaductLagoon/README.md b/challenges/2023/18-lavaductLagoon/README.md new file mode 100644 index 0000000..c0320e5 --- /dev/null +++ b/challenges/2023/18-lavaductLagoon/README.md @@ -0,0 +1 @@ +# [Day 18: Lavaduct Lagoon](https://adventofcode.com/2023/day/18) diff --git a/challenges/2023/18-lavaductLagoon/main.py b/challenges/2023/18-lavaductLagoon/main.py new file mode 100644 index 0000000..b58752e --- /dev/null +++ b/challenges/2023/18-lavaductLagoon/main.py @@ -0,0 +1,88 @@ +import sys +import re +from collections import namedtuple +import gridutil.coord as cu +import gridutil.grid as gu + + +Instruction = namedtuple("Instruction", ["direction", "dist", "colour"]) +PARSE_RE = re.compile(r"([RDUL]) (\d+) \(#([a-f\d]{6})\)") +DIRECTION_TRANSFORMATION = { + "R": cu.Direction.Right, + "L": cu.Direction.Left, + "U": cu.Direction.Up, + "D": cu.Direction.Down, + "0": cu.Direction.Right, + "1": cu.Direction.Down, + "2": cu.Direction.Left, + "3": cu.Direction.Up, +} + + +def parse(instr: str) -> list[Instruction]: + res = [] + for line in instr.splitlines(): + m = PARSE_RE.match(line) + assert m is not None + + raw_dir, dist, colour = m.groups() + parsed_dir = DIRECTION_TRANSFORMATION[raw_dir] + assert parsed_dir is not None + + res.append(Instruction(parsed_dir, int(dist), colour)) + return res + + +def run(instructions: list[Instruction]) -> int: + perimeter = 0 + vertices = [cu.Coordinate(0, 0)] + for instruction in instructions: + perimeter += instruction.dist + vertices.append( + cu.add( + vertices[-1], cu.mult(instruction.direction.delta(), instruction.dist) + ) + ) + + vertices = vertices[:-1] + + area = cu.area(vertices) + + # This is Pick's theorem. + # Normally, we'd want to just get the internal area, which the Shoelace formula would do. + # But since we want the area including walls that we assume are a single + # unit thick, we apply Pick's theorem as this counts all coordinates that + # the walls pass through, which in this case is effectively the same thing. + return int(area + perimeter / 2) + 1 + + +def one(instr: str): + instructions = parse(instr) + return run(instructions) + + +def two(instr: str): + instructions = parse(instr) + for i, instruction in enumerate(instructions): + instructions[i] = Instruction( + DIRECTION_TRANSFORMATION[instruction.colour[-1]], + int(instruction.colour[:5], base=16), + "", + ) + return run(instructions) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/18-lavaductLagoon/tests.json b/challenges/2023/18-lavaductLagoon/tests.json new file mode 100644 index 0000000..6870dea --- /dev/null +++ b/challenges/2023/18-lavaductLagoon/tests.json @@ -0,0 +1,18 @@ +{ + "1": [ + { + "is": "62", + "input": "R 6 (#70c710)\nD 5 (#0dc571)\nL 2 (#5713f0)\nD 2 (#d2c081)\nR 2 (#59c680)\nD 2 (#411b91)\nL 5 (#8ceee2)\nU 2 (#caa173)\nL 1 (#1b58a2)\nU 2 (#caa171)\nR 2 (#7807d2)\nU 3 (#a77fa3)\nL 2 (#015232)\nU 2 (#7a21e3)\n\n" + }, + { + "is": "20", + "input": "R 2 (#000000)\nD 1 (#000000)\nR 2 (#000000)\nU 1 (#000000)\nR 2 (#000000)\nD 2 (#000000)\nL 6 (#000000)\nU 2 (#000000)\n" + } + ], + "2": [ + { + "is": "952408144115", + "input": "R 6 (#70c710)\nD 5 (#0dc571)\nL 2 (#5713f0)\nD 2 (#d2c081)\nR 2 (#59c680)\nD 2 (#411b91)\nL 5 (#8ceee2)\nU 2 (#caa173)\nL 1 (#1b58a2)\nU 2 (#caa171)\nR 2 (#7807d2)\nU 3 (#a77fa3)\nL 2 (#015232)\nU 2 (#7a21e3)\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/19-aplenty/README.md b/challenges/2023/19-aplenty/README.md new file mode 100644 index 0000000..5ea58a1 --- /dev/null +++ b/challenges/2023/19-aplenty/README.md @@ -0,0 +1,3 @@ +# [Day 19: Aplenty](https://adventofcode.com/2023/day/19) + +This is not a problem I expected to be able to first-try part 2 for. \ No newline at end of file diff --git a/challenges/2023/19-aplenty/main.py b/challenges/2023/19-aplenty/main.py new file mode 100644 index 0000000..96ace69 --- /dev/null +++ b/challenges/2023/19-aplenty/main.py @@ -0,0 +1,162 @@ +import sys +from collections import namedtuple +from functools import reduce + + +ConditionalRule = namedtuple("ConditionalRule", ["field", "op", "value", "next"]) +UnconditionalRule = namedtuple("UnconditionalRule", ["next"]) +Rule = ConditionalRule | UnconditionalRule +Workflows = dict[str, list[Rule]] + +Part = namedtuple("Part", ["x", "m", "a", "s"]) + + +def parse(instr: str) -> tuple[Workflows, list[Part]]: + raw_workflows, raw_parts = instr.split("\n\n") + + workflows: Workflows = {} + + for line in raw_workflows.splitlines(): + bracket_start = line.find("{") + workflow_name = line[0:bracket_start] + rules = [] + + for raw_rule in line[bracket_start + 1 : -1].split(","): + colon_pos = raw_rule.find(":") + + if colon_pos == -1: + rules.append(UnconditionalRule(raw_rule)) + continue + + field = raw_rule[0] + op = raw_rule[1] + value = int(raw_rule[2:colon_pos]) + next_workflow = raw_rule[colon_pos + 1 :] + + rules.append(ConditionalRule(field, op, value, next_workflow)) + + workflows[workflow_name] = rules + + parts: list[Part] = [] + for line in raw_parts.splitlines(): + line = line[1:-1] + sp = [x.split("=") for x in line.split(",")] + assert "".join(map(lambda x: x[0], sp)) == "xmas" + parts.append(Part(*map(lambda x: int(x[1]), sp))) + + return workflows, parts + + +def test_rule(r: ConditionalRule, p: Part) -> bool: + test_value = p.__getattribute__(r.field) + match r.op: + case ">": + return test_value > r.value + case "<": + return test_value < r.value + case _: + raise ValueError(f"unknown operation {r.op}") + + +def is_acceptable(w: Workflows, p: Part) -> bool: + cursor = "in" + while not (cursor == "R" or cursor == "A"): + for rule in w[cursor]: + if (type(rule) == ConditionalRule and test_rule(rule, p)) or type( + rule + ) == UnconditionalRule: + cursor = rule.next + break + return cursor == "A" + + +def one(instr: str): + workflows, parts = parse(instr) + + acc = 0 + for part in parts: + if is_acceptable(workflows, part): + acc += sum(part) + return acc + + +Range = tuple[int, int] + + +def split_range(rng: Range, rule: ConditionalRule) -> tuple[Range | None, Range | None]: + # First range is the matching one, second range is the non-matching one. + (lower, upper) = rng + match rule.op: + case "<": + if upper < rule.value: + return rng, None + if lower >= rule.value: + return None, rng + return ((lower, rule.value - 1), (rule.value, upper)) + case ">": + if lower > rule.value: + return rng, None + if upper <= rule.value: + return None, rng + return ((rule.value + 1, upper), (lower, rule.value)) + case _: + raise ValueError(f"unknown operation {rule.op}") + + +def get_acceptable_ranges( + workflows: Workflows, workflow_name: str, ranges: dict[str, Range] +) -> list[dict[str, Range]]: + if workflow_name == "A": + return [ranges] + if workflow_name == "R": + return [] + + res = [] + + for rule in workflows[workflow_name]: + if type(rule) == UnconditionalRule: + res += get_acceptable_ranges(workflows, rule.next, ranges) + continue + + matches, not_matches = split_range(ranges[rule.field], rule) + + if matches is not None: + x = ranges.copy() + x[rule.field] = matches + res += get_acceptable_ranges(workflows, rule.next, x) + + if not_matches is not None: + ranges[rule.field] = not_matches + + return res + + +def get_range_len(r: Range) -> int: + (start, end) = r + return (end - start) + 1 + + +def two(instr: str): + workflows, _ = parse(instr) + acc = 0 + for ranges in get_acceptable_ranges( + workflows, "in", {c: [1, 4000] for c in "xmas"} + ): + acc += reduce(lambda acc, x: acc * get_range_len(x), ranges.values(), 1) + return acc + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/19-aplenty/tests.json b/challenges/2023/19-aplenty/tests.json new file mode 100644 index 0000000..bbad668 --- /dev/null +++ b/challenges/2023/19-aplenty/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "19114", + "input": "px{a<2006:qkq,m>2090:A,rfg}\npv{a>1716:R,A}\nlnx{m>1548:A,A}\nrfg{s<537:gd,x>2440:R,A}\nqs{s>3448:A,lnx}\nqkq{x<1416:A,crn}\ncrn{x>2662:A,R}\nin{s<1351:px,qqz}\nqqz{s>2770:qs,m<1801:hdj,R}\ngd{a>3333:R,R}\nhdj{m>838:A,pv}\n\n{x=787,m=2655,a=1222,s=2876}\n{x=1679,m=44,a=2067,s=496}\n{x=2036,m=264,a=79,s=2244}\n{x=2461,m=1339,a=466,s=291}\n{x=2127,m=1623,a=2188,s=1013}\n\n" + } + ], + "2": [ + { + "is": "167409079868000", + "input": "px{a<2006:qkq,m>2090:A,rfg}\npv{a>1716:R,A}\nlnx{m>1548:A,A}\nrfg{s<537:gd,x>2440:R,A}\nqs{s>3448:A,lnx}\nqkq{x<1416:A,crn}\ncrn{x>2662:A,R}\nin{s<1351:px,qqz}\nqqz{s>2770:qs,m<1801:hdj,R}\ngd{a>3333:R,R}\nhdj{m>838:A,pv}\n\n{x=787,m=2655,a=1222,s=2876}\n{x=1679,m=44,a=2067,s=496}\n{x=2036,m=264,a=79,s=2244}\n{x=2461,m=1339,a=466,s=291}\n{x=2127,m=1623,a=2188,s=1013}\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/20-pulsePropagation/README.md b/challenges/2023/20-pulsePropagation/README.md new file mode 100644 index 0000000..1882c60 --- /dev/null +++ b/challenges/2023/20-pulsePropagation/README.md @@ -0,0 +1 @@ +# [Day 20: Pulse Propagation](https://adventofcode.com/2023/day/20) diff --git a/challenges/2023/20-pulsePropagation/main.py b/challenges/2023/20-pulsePropagation/main.py new file mode 100644 index 0000000..c1d9d02 --- /dev/null +++ b/challenges/2023/20-pulsePropagation/main.py @@ -0,0 +1,77 @@ +import sys +from collections import namedtuple +from enum import IntEnum + + +Module = namedtuple("Module", ["type", "name", "outputs"]) +Pulse = namedtuple("Pulse", ["from", "to", "value"]) + + +class PulseValue(IntEnum): + LOW = 0 + HIGH = 1 + + +def parse(instr: str) -> dict[str, Module]: + x = {} + + for line in instr.splitlines(): + name, outputs = line.split(" -> ") + module_type = None + if not name[0].isalpha(): + module_type = name[0] + name = name[1:] + else: + module_type = name + + x[name] = Module(module_type, name, outputs.split(", ")) + + return x + + +def tick(modules: dict[str, Module], state: dict[str, any], pulses: list[Pulse]): + next_pulses = [] + + for pulse in pulses: + dest_module = modules[pulse.to] + + match dest_module.type: + case "broadcaster": + for output in dest_module.outputs: + next_pulses.append(Pulse(dest_module.name, output, pulse.value)) + case "%": + if pulse.value == PulseValue.LOW: + toggled = state.get(dest_module.name, False) + next_pulse_val = PulseValue.HIGH if toggled else PulseValue.LOW + for output in dest_module.outputs: + next_pulses.append(Pulse(dest_module.name, output, next_pulse_val)) + state[dest_module.name] = not toggled + case "&": + state[dest_module.name] = state[dest_module.name] + [pulse] + + + +def one(instr: str): + modules = parse(instr) + _debug(modules) + return + + +def two(instr: str): + return + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) \ No newline at end of file diff --git a/challenges/2023/20-pulsePropagation/tests.json b/challenges/2023/20-pulsePropagation/tests.json new file mode 100644 index 0000000..f2dd02d --- /dev/null +++ b/challenges/2023/20-pulsePropagation/tests.json @@ -0,0 +1,12 @@ +{ + "1": [ + { + "is": "32000000", + "input": "broadcaster -> a, b, c\n%a -> b\n%b -> c\n%c -> inv\n&inv -> a\n\n" + }, + { + "is": "11687500", + "input": "broadcaster -> a\n%a -> inv, con\n&inv -> b\n%b -> con\n&con -> output\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/21-stepCounter/README.md b/challenges/2023/21-stepCounter/README.md new file mode 100644 index 0000000..4c544f2 --- /dev/null +++ b/challenges/2023/21-stepCounter/README.md @@ -0,0 +1 @@ +# [Day 21: Step Counter](https://adventofcode.com/2023/day/21) diff --git a/challenges/2023/21-stepCounter/main.py b/challenges/2023/21-stepCounter/main.py new file mode 100644 index 0000000..925b3f8 --- /dev/null +++ b/challenges/2023/21-stepCounter/main.py @@ -0,0 +1,86 @@ +import sys +import gridutil.grid as gu +import gridutil.coord as cu +from collections.abc import Callable +from functools import reduce + + +def parse(instr: str) -> gu.Grid: + return gu.parse(instr) + + +def find_n_end_points(grid: gu.Grid, start_pos: cu.Coordinate, n_steps: int, infinite: bool = False) -> int: + max_x = gu.get_max_x(grid) + max_y = gu.get_max_y(grid) + + locations = set([start_pos]) + for _ in range(n_steps): + new_locations = set([]) + for loc in locations: + for direction in cu.Direction: + next_pos = cu.add(loc, direction.delta()) + adjusted_next_pos = next_pos + + if infinite: + if not (0 < adjusted_next_pos.x < max_x): + adjusted_next_pos = cu.Coordinate(abs(adjusted_next_pos.x) % (max_x + 1), adjusted_next_pos.y) + + if not (0 < adjusted_next_pos.y < max_y): + adjusted_next_pos = cu.Coordinate(adjusted_next_pos.x, abs(adjusted_next_pos.y) % (max_y + 1)) + + if adjusted_next_pos not in grid or grid[adjusted_next_pos] == "#": + continue + new_locations.add(next_pos) + + locations = new_locations + return len(locations) + + +def find_start_point(grid: gu.Grid) -> cu.Coordinate: + for k in grid: + if grid[k] == "S": + return k + raise ValueError("No start position found") + + +def one(instr: str): + grid = parse(instr) + start_pos = find_start_point(grid) + res = find_n_end_points(grid, start_pos, 64) + return res + + +def two(instr: str): + grid = parse(instr) + start_pos = find_start_point(grid) + + TARGET_STEPS = 26501365 + WIDTH = int(gu.get_max_x(grid)) + 1 + a, b = TARGET_STEPS // WIDTH, TARGET_STEPS % WIDTH + + _debug(WIDTH, a, b) + + r = [] + for i in range(3): + _debug((WIDTH * i) + b) + r.append(find_n_end_points(grid, start_pos, (WIDTH * i) + b, infinite=True)) + + _debug(r) + x, y, z = r + return x+a*(y-z+(a-1)*(z-(2*y)+x)//2) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) \ No newline at end of file diff --git a/challenges/2023/21-stepCounter/tests.json b/challenges/2023/21-stepCounter/tests.json new file mode 100644 index 0000000..1764331 --- /dev/null +++ b/challenges/2023/21-stepCounter/tests.json @@ -0,0 +1,8 @@ +{ + "1": [ + { + "is": "16", + "input": "...........\n.....###.#.\n.###.##..#.\n..#.#...#..\n....#.#....\n.##..S####.\n.##..#...#.\n.......##..\n.##.#.####.\n.##..##.##.\n...........\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/22-sandSlabs/README.md b/challenges/2023/22-sandSlabs/README.md new file mode 100644 index 0000000..2cd6d89 --- /dev/null +++ b/challenges/2023/22-sandSlabs/README.md @@ -0,0 +1 @@ +# [Day 22: Sand Slabs](https://adventofcode.com/2023/day/22) diff --git a/challenges/2023/22-sandSlabs/main.py b/challenges/2023/22-sandSlabs/main.py new file mode 100644 index 0000000..357cc1f --- /dev/null +++ b/challenges/2023/22-sandSlabs/main.py @@ -0,0 +1,146 @@ +import sys +import gridutil.coord as cu +from tqdm import tqdm + + +Brick = tuple[cu.Coordinate3, cu.Coordinate3] + + +def parse(instr: str) -> set[Brick]: + # (sorry) + return set( + sorted( + [ + tuple( + sorted( + map( + lambda x: cu.Coordinate3(*map(int, x.split(","))), + line.split("~"), + ) + ) + ) + for line in instr.splitlines() + ], + key=lambda x: x[0].z, + ) + ) + + +def are_bricks_overlapping(a: Brick, b: Brick) -> bool: + x_overlaps = a[0].x <= b[1].x and b[0].x <= a[1].x + y_overlaps = a[0].y <= b[1].y and b[0].y <= a[1].y + return x_overlaps and y_overlaps + + +def lower_bricks( + bricks: set[Brick], start_at: int = 2, return_early: bool = False +) -> int: + max_z = max([x[1].z for x in bricks]) + 1 + n = 0 + for level in range(start_at, max_z if not return_early else start_at + 1): + present_below = [] + on_this_level = [] + + for brick in bricks: + if brick[1].z == level - 1: + present_below.append(brick) + elif brick[0].z == level: + on_this_level.append(brick) + + for brick in on_this_level: + overlaps = False + for lower_brick in present_below: + if are_bricks_overlapping(brick, lower_brick): + overlaps = True + break + + if not overlaps: + n += 1 + # This is what lowering a brick looks like + bricks.remove(brick) + bricks.add( + ( + cu.Coordinate3(brick[0].x, brick[0].y, brick[0].z - 1), + cu.Coordinate3(brick[1].x, brick[1].y, brick[1].z - 1), + ) + ) + + return n + + +def collapse_tower(bricks: set[Brick]): + changed = 1 + while changed != 0: + changed = lower_bricks(bricks) + + +def generate_openscad(bricks: set[Brick]) -> str: + """ + This was for debugging + """ + lines = [] + for brick in bricks: + lines.append( + "translate([" + + (", ".join(map(str, brick[0]))) + + "]) { cube([" + + ( + ", ".join( + map(str, map(lambda x: 1 + brick[1][x] - brick[0][x], range(3))) + ) + ) + + "], center=false); };" + ) + return "\n".join(lines) + + +def one(instr: str): + bricks = parse(instr) + + _debug("Building initial state", end="\r") + + collapse_tower(bricks) + + acc = 0 + for brick in tqdm(bricks, desc="Testing individual bricks", file=sys.stderr, leave=False): + bc = bricks.copy() + bc.remove(brick) + v = lower_bricks(bc, start_at=brick[1].z + 1, return_early=True) + if v == 0: + acc += 1 + + return acc + + +def two(instr: str): + bricks = parse(instr) + + _debug("Building initial state", end="\r") + + collapse_tower(bricks) + + acc = 0 + for brick in tqdm(bricks, desc="Testing individual bricks", file=sys.stderr, leave=False): + bc = bricks.copy() + bc.remove(brick) + v = lower_bricks(bc, start_at=brick[1].z + 1) + if v != 0: + acc += v + + return acc + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/22-sandSlabs/tests.json b/challenges/2023/22-sandSlabs/tests.json new file mode 100644 index 0000000..39ba35e --- /dev/null +++ b/challenges/2023/22-sandSlabs/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "5", + "input": "1,0,1~1,2,1\n0,0,2~2,0,2\n0,2,3~2,2,3\n0,0,4~0,2,4\n2,0,5~2,2,5\n0,1,6~2,1,6\n1,1,8~1,1,9\n\n" + } + ], + "2": [ + { + "is": "7", + "input": "1,0,1~1,2,1\n0,0,2~2,0,2\n0,2,3~2,2,3\n0,0,4~0,2,4\n2,0,5~2,2,5\n0,1,6~2,1,6\n1,1,8~1,1,9\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/23-aLongWalk/README.md b/challenges/2023/23-aLongWalk/README.md new file mode 100644 index 0000000..6b64953 --- /dev/null +++ b/challenges/2023/23-aLongWalk/README.md @@ -0,0 +1,6 @@ +# [Day 23: A Long Walk](https://adventofcode.com/2023/day/23) + +Part 2 is *not*: +* 4717 (too low) +* 4826 (too low) +* 5350 (too low) \ No newline at end of file diff --git a/challenges/2023/23-aLongWalk/main.py b/challenges/2023/23-aLongWalk/main.py new file mode 100644 index 0000000..805c258 --- /dev/null +++ b/challenges/2023/23-aLongWalk/main.py @@ -0,0 +1,222 @@ +import sys +import gridutil.grid as gu +import gridutil.coord as cu +from collections import namedtuple +from collections.abc import Generator + + +def parse(instr: str) -> gu.Grid: + return gu.parse(instr) + + +def get_terminal_points(forest: gu.Grid) -> tuple[cu.Coordinate, cu.Coordinate]: + start_pos = None + end_pos = None + max_y = gu.get_max_y(forest) + for x in range(gu.get_max_x(forest) + 1): + if forest[(x, 0)] == "." and start_pos is None: + start_pos = cu.Coordinate(x, 0) + if forest[(x, max_y)] == "." and end_pos is None: + end_pos = cu.Coordinate(x, max_y) + + if start_pos is None: + raise ValueError("No open cell found on the first row of the forest") + + if end_pos is None: + raise ValueError("No open cell found on the last row of the forest") + + return start_pos, end_pos + + +def trace_path( + forest: gu.Grid, + start_pos: cu.Coordinate, + end_pos: cu.Coordinate, + history: set[cu.Coordinate] | None = None, + n: int = 0, + disallowed_moves: dict[str, cu.Direction] | None = None, +) -> int: + if history is None: + history = set() + + if disallowed_moves is None: + disallowed_moves = {} + + cursor = start_pos + while cursor != end_pos: + history.add(cursor) + allowable = [] + for direction in cu.Direction: + target_pos = cu.add(cursor, direction.delta()) + + if target_pos not in forest or target_pos in history: + continue + + val = forest[target_pos] + + if val == "#" or direction == disallowed_moves.get(val): + continue + + allowable.append(target_pos) + + n += 1 + + match len(allowable): + case 0: + return -1 + case 1: + cursor = allowable[0] + case _: + highest = max( + trace_path( + forest, + x, + end_pos, + history=history.copy(), + n=n, + disallowed_moves=disallowed_moves, + ) + for x in allowable + ) + return highest + + return n + + +def one(instr: str): + forest = parse(instr) + start_pos, end_pos = get_terminal_points(forest) + return trace_path( + forest, + start_pos, + end_pos, + disallowed_moves={ + "^": cu.Direction.Down, + "v": cu.Direction.Up, + ">": cu.Direction.Left, + "<": cu.Direction.Right, + }, + ) + + +DistanceTo: tuple[cu.Coordinate, int] = namedtuple( + "DistanceTo", ["coordinate", "distance"] +) + + +def neighbours( + forest: gu.Grid, pos: cu.Coordinate +) -> Generator[cu.Coordinate, None, None]: + for direction in cu.Direction: + target = cu.add(pos, direction.delta()) + if target not in forest or forest[target] == "#": + continue + yield target + + +def build_graph( + forest: gu.Grid, start_pos: cu.Coordinate +) -> dict[cu.Coordinate, list[DistanceTo]]: + junctions = [start_pos] + graph = {} + + while junctions: + st = junctions.pop(0) + + possible_next = list(neighbours(forest, st)) + routes = [] + + for start_point in possible_next: + n = 0 + cursor = start_point + history = set([start_point, st]) + get_next_steps = lambda: list( + filter( + lambda x: x not in history, + neighbours(forest, cursor), + ) + ) + next_steps = get_next_steps() + + while len(next_steps) == 1: + n += 1 + cursor = next_steps[0] + history.add(cursor) + next_steps = get_next_steps() + + routes.append(DistanceTo(cursor, n + 1)) + if cursor not in graph: + junctions.append(cursor) + + graph[st] = routes + + return graph + + +def display_graph(graph: dict[cu.Coordinate, list[DistanceTo]]): + import networkx as nx + import matplotlib.pyplot as plt + + G = nx.Graph() + + for node in graph: + for (end, length) in graph[node]: + G.add_edge(node, end, weight=length) + + nx.draw(G, with_labels=True, font_weight="bold") + plt.show() + + +def trace_graph_path( + graph: dict[cu.Coordinate, list[DistanceTo]], + start_pos: cu.Coordinate, + end_pos: cu.Coordinate, + history: set[cu.Coordinate] | None = None, + n: int = 0, +) -> int: + if history is None: + history = set() + + if start_pos == end_pos: + return n + + history.add(start_pos) + adjacent = list(filter(lambda x: x.coordinate not in history, graph[start_pos])) + + if len(adjacent) == 0: + return -1 + + for edge in adjacent: + if edge.coordinate == end_pos: + return edge.distance + n + + return max( + trace_graph_path( + graph, p.coordinate, end_pos, history=history.copy(), n=n + p.distance + ) + for p in adjacent + ) + + +def two(instr: str): + forest = parse(instr) + start_pos, end_pos = get_terminal_points(forest) + graph = build_graph(forest, start_pos) + # display_graph(graph) + return trace_graph_path(graph, start_pos, end_pos) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/23-aLongWalk/tests.json b/challenges/2023/23-aLongWalk/tests.json new file mode 100644 index 0000000..71c7bb5 --- /dev/null +++ b/challenges/2023/23-aLongWalk/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "94", + "input": "#.#####################\n#.......#########...###\n#######.#########.#.###\n###.....#.>.>.###.#.###\n###v#####.#v#.###.#.###\n###.>...#.#.#.....#...#\n###v###.#.#.#########.#\n###...#.#.#.......#...#\n#####.#.#.#######.#.###\n#.....#.#.#.......#...#\n#.#####.#.#.#########v#\n#.#...#...#...###...>.#\n#.#.#v#######v###.###v#\n#...#.>.#...>.>.#.###.#\n#####v#.#.###v#.#.###.#\n#.....#...#...#.#.#...#\n#.#########.###.#.#.###\n#...###...#...#...#.###\n###.###.#.###v#####v###\n#...#...#.#.>.>.#.>.###\n#.###.###.#.###.#.#v###\n#.....###...###...#...#\n#####################.#\n\n" + } + ], + "2": [ + { + "is": "154", + "input": "#.#####################\n#.......#########...###\n#######.#########.#.###\n###.....#.>.>.###.#.###\n###v#####.#v#.###.#.###\n###.>...#.#.#.....#...#\n###v###.#.#.#########.#\n###...#.#.#.......#...#\n#####.#.#.#######.#.###\n#.....#.#.#.......#...#\n#.#####.#.#.#########v#\n#.#...#...#...###...>.#\n#.#.#v#######v###.###v#\n#...#.>.#...>.>.#.###.#\n#####v#.#.###v#.#.###.#\n#.....#...#...#.#.#...#\n#.#########.###.#.#.###\n#...###...#...#...#.###\n###.###.#.###v#####v###\n#...#...#.#.>.>.#.>.###\n#.###.###.#.###.#.#v###\n#.....###...###...#...#\n#####################.#\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/24-neverTellMeTheOdds/README.md b/challenges/2023/24-neverTellMeTheOdds/README.md new file mode 100644 index 0000000..58ad75c --- /dev/null +++ b/challenges/2023/24-neverTellMeTheOdds/README.md @@ -0,0 +1,7 @@ +# [Day 24: Never Tell Me The Odds](https://adventofcode.com/2023/day/24) + +Part 1 is not: +* 1344 (too low) +* 48384 (too high) + +Running this solution requires `z3-solver` to be installed. \ No newline at end of file diff --git a/challenges/2023/24-neverTellMeTheOdds/main.py b/challenges/2023/24-neverTellMeTheOdds/main.py new file mode 100644 index 0000000..f8aada7 --- /dev/null +++ b/challenges/2023/24-neverTellMeTheOdds/main.py @@ -0,0 +1,112 @@ +import sys +from collections import namedtuple +from numbers import Number +import gridutil.coord as cu +from functools import reduce +import math +import z3 + + +Hailstone = namedtuple("Hailstone", ["position", "velocity"]) + + +def parse(instr: str) -> list[Hailstone]: + return [ + Hailstone( + *map(lambda x: cu.Coordinate3(*map(int, x.split(", "))), line.split(" @ ")) + ) + for line in instr.splitlines() + ] + + +def get_2d_line(stone: Hailstone) -> tuple[Number, Number]: + m = stone.velocity.y / stone.velocity.x + c = stone.position.y - (m * stone.position.x) + return m, c + + +def get_2d_intersection(a: Hailstone, b: Hailstone) -> cu.Coordinate | None: + m1, c1 = get_2d_line(a) + m2, c2 = get_2d_line(b) + + if math.isclose(m1, m2): + return None + + x = (c2 - c1) / (m1 - m2) + y = (m1 * x) + c1 + return cu.Coordinate(x, y) + + +def one(instr: str): + hailstones = parse(instr) + + MIN, MAX = (7, 27) if len(hailstones) == 5 else (200000000000000, 400000000000000) + + acc = 0 + for i, stone1 in enumerate(hailstones[:-1]): + for stone2 in hailstones[i + 1 :]: + if stone1 == stone2: + continue + + p = get_2d_intersection(stone1, stone2) + if p is None or not reduce(lambda acc, x: acc and MIN <= x <= MAX, p, True): + continue + + if not ( + (p.x > stone1.position.x) == (stone1.velocity.x > 0) + and (p.x > stone2.position.x) == (stone2.velocity.x > 0) + ): + continue + acc += 1 + + return acc + + +def two(instr: str): + hailstones = parse(instr) + + # for our thrown rock + # initial position: x, y, z + # velocity: vx, vy, vz + + # for each of 3 hailstones + # time of collision with that hailstone: t_i + # intial position: hx, hy, hz + # velocity: hvx, hvy, hvz + + x, y, z = (z3.Int(x) for x in ["x", "y", "z"]) + vx, vy, vz = (z3.Int(x) for x in ["vx", "vy", "vz"]) + + solver = z3.Solver() + + for i in range(3): + hailstone = hailstones[i] + t = z3.Int(f"t_{i}") + solver.add( + t >= 0, + x + (vx * t) == hailstone.position.x + (hailstone.velocity.x * t), + y + (vy * t) == hailstone.position.y + (hailstone.velocity.y * t), + z + (vz * t) == hailstone.position.z + (hailstone.velocity.z * t), + ) + + _debug("Solving...", end="", flush=True) + assert solver.check() == z3.sat + model = solver.model() + _debug("\r \r" + str(model)) + return sum(model[var].as_long() for var in [x, y, z]) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/24-neverTellMeTheOdds/tests.json b/challenges/2023/24-neverTellMeTheOdds/tests.json new file mode 100644 index 0000000..5ea8b97 --- /dev/null +++ b/challenges/2023/24-neverTellMeTheOdds/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "2", + "input": "19, 13, 30 @ -2, 1, -2\n18, 19, 22 @ -1, -1, -2\n20, 25, 34 @ -2, -2, -4\n12, 31, 28 @ -1, -2, -1\n20, 19, 15 @ 1, -5, -3\n\n" + } + ], + "2": [ + { + "is": "47", + "input": "19, 13, 30 @ -2, 1, -2\n18, 19, 22 @ -1, -1, -2\n20, 25, 34 @ -2, -2, -4\n12, 31, 28 @ -1, -2, -1\n20, 19, 15 @ 1, -5, -3\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/25-snowverload/README.md b/challenges/2023/25-snowverload/README.md new file mode 100644 index 0000000..7e440a8 --- /dev/null +++ b/challenges/2023/25-snowverload/README.md @@ -0,0 +1 @@ +# [Day 25: Snowverload](https://adventofcode.com/2023/day/25) diff --git a/challenges/2023/25-snowverload/main.py b/challenges/2023/25-snowverload/main.py new file mode 100644 index 0000000..d977877 --- /dev/null +++ b/challenges/2023/25-snowverload/main.py @@ -0,0 +1,54 @@ +import sys +import networkx as nx +import random +from collections import defaultdict +from functools import reduce + + +def parse(instr: str) -> nx.Graph: + g = nx.Graph() + for line in instr.splitlines(): + node, next_nodes = line.split(": ") + next_nodes = next_nodes.split(" ") + for x in zip([node] * len(next_nodes), next_nodes): + g.add_nodes_from(x) + g.add_edge(*x, capacity=1) + return g + + +def one(instr: str): + g = parse(instr) + nodes = list(g.nodes()) + + cut = 0 + split_nodes = None + + while cut != 3: + x = random.choices(nodes, k=2) + if x[0] == x[1]: + continue + + cut, nodes = nx.minimum_cut(g, *x, flow_func=nx.algorithms.flow.shortest_augmenting_path) + split_nodes = nodes + + assert split_nodes is not None + return reduce(lambda acc, x: acc * len(x), split_nodes, 1) + + +def two(_: str): + return "Merry Christmas!" + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) \ No newline at end of file diff --git a/challenges/2023/25-snowverload/tests.json b/challenges/2023/25-snowverload/tests.json new file mode 100644 index 0000000..b90f46a --- /dev/null +++ b/challenges/2023/25-snowverload/tests.json @@ -0,0 +1,8 @@ +{ + "1": [ + { + "is": "54", + "input": "jqt: rhn xhk nvd\nrsh: frs pzl lsr\nxhk: hfx\ncmg: qnr nvd lhk bvb\nrhn: xhk bvb hfx\nbvb: xhk hfx\npzl: lsr hfx nvd\nqnr: nvd\nntq: jqt hfx bvb xhk\nnvd: lhk\nlsr: lhk\nrzs: qnr cmg lsr rsh\nfrs: qnr lhk lsr\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/README.md b/challenges/2023/README.md new file mode 100644 index 0000000..b1e0162 --- /dev/null +++ b/challenges/2023/README.md @@ -0,0 +1,41 @@ +# Advent of Code 2023 + +Solutions to the [2023 Advent of Code](https://adventofcode.com/2023). + +--- + +Total stars: **46 ★** + +![Benchmark graph](./benchmark-graph.png) + + + +A day denoted with a star means it has a visualisation. + +| Day | Status | Solutions | Notes | +|-----|--------|-----------|-------| +| 01 - Trebuchet?! | ★ ★ | Python | I never knew detecting numbers could be so confusingly tricky | +| 02 - Cube Conundrum | ★ ★ | Python | Pleasingly straightforwards, though seems like it would be well suited to Haskell | +| 03 - Gear Ratios | ★ ★ | Python | First coordinate grid of the year! | +| 04 - Scratchcards | ★ ★ | Python | First flawed initial grok of the year | +| 05 - If You Give A Seed A Fertilizer | ★ ★ | Python | Gave up on the smart solution and brute-forced it backwards after 5 days of on-off trying. | +| 06 - Wait For It | ★ ★ | Python | Easy, GCSE-level maths :) | +| 07 - Camel Cards | ★ ★ | Python | Pedantic problem statements will be my downfall | +| 08 - Haunted Wasteland | ★ ★ | Python | I'm not sure any feasible generic solution exists - but I did work out the infeasible one! | +| 09 - Mirage Maintenance | ★ ★ | Python | GCSE maths and the n-th term coming in clutch right here | +| 10 - Pipe Maze | ★ ★ | Python | Thoroughly barked up two wrong trees then Googled an algo based on a thought and oops it just works? | +| 11 - Cosmic Expansion | ★ ★ | Python | Djikstra's and A* are the wrong way to do this (I tried both before engaging my brain) and then had to optimise various things for part 2 but nothing was horrendous. | +| 12 - Hot Springs | ★ ★ | Python | Some hints from the subreddit were needed but they got me well on my way to completing this. Memoisation is king, long live memoisation. | +| 13 - Point of Incidence | ★ ★ | Python | This one made me feel slightly intelligent. | +| 14* - Parabolic Reflector Dish | ★ ★ | Python | Why do I always overcomplicate cycle detection?! | +| 15 - Lens Library | ★ ★ | Go | Still took some brainpower but this time the brainpower was needed to work out what the problem was, *not* to work out how to solve the problem. | +| 16 - The Floor Will Be Lava | ★ ★ | Python | Pathfinding, sort of, but also brute forceable?? | +| 17 - Clumsy Crucible | ★ ★ | Python | This taught me quite a lot about how to meddle with Djikstra's | +| 18 - Ladaduct Lagoon | ★ ★ | Python | Nothing quite like a problem that I thought I knew the solution to showing up my lack of mathematical knowledge. | +| 19 - Aplenty | ★ ★ | Python | So maybe I *can* do range maths? | +| 20 - Pulse Propagation | ☆ ☆ | Python | Too much reading. | +| 21 - Step Counter | ★ ☆ | Python | ??? | +| 22 - Sand Slabs | ★ ★ | Python | I maintain that OpenSCAD is the best AoC 3D debugging tool | +| 23 - A Long Walk | ★ ★ | Python | Both parts here could theorietcially be done with the same implementation but I couldn't be bothered to rework the part 2 solution to work for part 1 as well. | +| 24 - Never Tell Me The Odds | ★ ★ | Python | Z3 seems like an *incredible* useful tool but also not a satisfying puzzle at all :( | +| 25 - Snowverload | ★ ☆ | Python | This... sometimes works? If it fails, re-run it and tadah and then it works. Definitely is user error. | \ No newline at end of file diff --git a/challenges/2023/benchmark-graph.png b/challenges/2023/benchmark-graph.png new file mode 100644 index 0000000..e334f20 Binary files /dev/null and b/challenges/2023/benchmark-graph.png differ diff --git a/challenges/2023/benchmarks.jsonl b/challenges/2023/benchmarks.jsonl new file mode 100644 index 0000000..8786015 --- /dev/null +++ b/challenges/2023/benchmarks.jsonl @@ -0,0 +1,46 @@ +{"day": 1, "part": 1, "runner": "py", "min": 0.02806234359741211, "max": 0.0957028865814209, "avg": 0.033700079917907716, "n": 100} +{"day": 1, "part": 2, "runner": "py", "min": 0.036818742752075195, "max": 0.08628964424133301, "avg": 0.05282108783721924, "n": 100} +{"day": 2, "part": 1, "runner": "py", "min": 0.035996198654174805, "max": 0.16872835159301758, "avg": 0.05545815944671631, "n": 100} +{"day": 2, "part": 2, "runner": "py", "min": 0.03395867347717285, "max": 0.04913759231567383, "avg": 0.04006660461425781, "n": 100} +{"day": 3, "part": 1, "runner": "py", "min": 0.0647578239440918, "max": 0.18926548957824707, "avg": 0.08141623735427857, "n": 100} +{"day": 3, "part": 2, "runner": "py", "min": 0.029996156692504883, "max": 0.04504847526550293, "avg": 0.034812936782836916, "n": 100} +{"day": 4, "part": 1, "runner": "py", "min": 0.041077375411987305, "max": 0.11529421806335449, "avg": 0.06057122230529785, "n": 100} +{"day": 4, "part": 2, "runner": "py", "min": 0.040993690490722656, "max": 0.06064128875732422, "avg": 0.04680403709411621, "n": 100} +{"day": 5, "part": 1, "runner": "py", "min": 0.03789997100830078, "max": 0.17576885223388672, "avg": 0.046078598499298094, "n": 100} +{"day": 6, "part": 1, "runner": "py", "min": 0.01985001564025879, "max": 0.044942378997802734, "avg": 0.028468074798583983, "n": 100} +{"day": 6, "part": 2, "runner": "py", "min": 0.019793033599853516, "max": 0.03257584571838379, "avg": 0.02442594528198242, "n": 100} +{"day": 7, "part": 1, "runner": "py", "min": 0.04590344429016113, "max": 0.13384461402893066, "avg": 0.062185251712799074, "n": 100} +{"day": 7, "part": 2, "runner": "py", "min": 0.05640268325805664, "max": 0.09029102325439453, "avg": 0.07284079790115357, "n": 100} +{"day": 8, "part": 1, "runner": "py", "min": 0.02858424186706543, "max": 0.06758975982666016, "avg": 0.04282158613204956, "n": 100} +{"day": 8, "part": 2, "runner": "py", "min": 0.07455253601074219, "max": 0.1345658302307129, "avg": 0.09417223215103149, "n": 100} +{"day": 9, "part": 1, "runner": "py", "min": 0.032199859619140625, "max": 0.12543773651123047, "avg": 0.044357788562774655, "n": 100} +{"day": 9, "part": 2, "runner": "py", "min": 0.03330564498901367, "max": 0.07211709022521973, "avg": 0.04614929676055908, "n": 100} +{"day": 10, "part": 1, "runner": "py", "min": 0.04725289344787598, "max": 0.08723211288452148, "avg": 0.06157275438308716, "n": 100} +{"day": 10, "part": 2, "runner": "py", "min": 0.05262422561645508, "max": 0.09373903274536133, "avg": 0.06505710601806641, "n": 100} +{"day": 11, "part": 1, "runner": "py", "min": 0.1369001865386963, "max": 0.48844385147094727, "avg": 0.18774306058883666, "n": 100} +{"day": 11, "part": 2, "runner": "py", "min": 0.1357133388519287, "max": 0.24532866477966309, "avg": 0.176527099609375, "n": 100} +{"day": 12, "part": 1, "runner": "py", "min": 0.05989861488342285, "max": 0.13327932357788086, "avg": 0.07568852424621582, "n": 100} +{"day": 12, "part": 2, "runner": "py", "min": 0.904543399810791, "max": 2.621077299118042, "avg": 1.316542136669159, "n": 100} +{"day": 13, "part": 1, "runner": "py", "min": 0.04506516456604004, "max": 0.1567397117614746, "avg": 0.059773361682891844, "n": 100} +{"day": 13, "part": 2, "runner": "py", "min": 0.06211209297180176, "max": 0.12923550605773926, "avg": 0.07918766498565674, "n": 100} +{"day": 14, "part": 1, "runner": "py", "min": 0.01713418960571289, "max": 0.02086782455444336, "avg": 0.01824690341949463, "n": 50} +{"day": 14, "part": 2, "runner": "py", "min": 0.48926782608032227, "max": 0.6927425861358643, "avg": 0.5653452634811401, "n": 50} +{"day": 15, "part": 1, "runner": "go", "min": 0.0005846023559570312, "max": 0.0011608600616455078, "avg": 0.0007052874565124512, "n": 100} +{"day": 15, "part": 2, "runner": "go", "min": 0.0012998580932617188, "max": 0.0028612613677978516, "avg": 0.0014359498023986817, "n": 100} +{"day": 16, "part": 1, "runner": "py", "min": 0.030805587768554688, "max": 0.03831839561462402, "avg": 0.03250444730122884, "n": 15} +{"day": 16, "part": 2, "runner": "py", "min": 2.7863943576812744, "max": 4.14529013633728, "avg": 3.1346225261688234, "n": 15} +{"day": 17, "part": 1, "runner": "py", "min": 5.36311674118042, "max": 5.36311674118042, "avg": 5.36311674118042, "n": 1} +{"day": 17, "part": 2, "runner": "py", "min": 26.201914072036743, "max": 26.201914072036743, "avg": 26.201914072036743, "n": 1} +{"day": 18, "part": 1, "runner": "py", "min": 0.02330160140991211, "max": 0.03203868865966797, "avg": 0.024628419876098633, "n": 100} +{"day": 18, "part": 2, "runner": "py", "min": 0.023529052734375, "max": 0.030207157135009766, "avg": 0.02483478546142578, "n": 100} +{"day": 19, "part": 1, "runner": "py", "min": 0.023938894271850586, "max": 0.05737614631652832, "avg": 0.027661228179931642, "n": 100} +{"day": 19, "part": 2, "runner": "py", "min": 0.026041030883789062, "max": 0.03458356857299805, "avg": 0.028042428493499756, "n": 100} +{"day": 19, "part": 1, "runner": "py", "min": 0.023969173431396484, "max": 0.03136777877807617, "avg": 0.026349050998687742, "n": 100} +{"day": 19, "part": 2, "runner": "py", "min": 0.024805068969726562, "max": 0.03318047523498535, "avg": 0.027637341022491456, "n": 100} +{"day": 22, "part": 1, "runner": "py", "min": 7.400631666183472, "max": 7.400631666183472, "avg": 7.400631666183472, "n": 1} +{"day": 22, "part": 2, "runner": "py", "min": 30.386138439178467, "max": 30.386138439178467, "avg": 30.386138439178467, "n": 1} +{"day": 23, "part": 1, "runner": "py", "min": 2.006169557571411, "max": 2.006169557571411, "avg": 2.006169557571411, "n": 1} +{"day": 23, "part": 2, "runner": "py", "min": 33.72923398017883, "max": 33.72923398017883, "avg": 33.72923398017883, "n": 1} +{"day": 24, "part": 1, "runner": "py", "min": 0.12593984603881836, "max": 0.1335890293121338, "avg": 0.13133821487426758, "n": 5} +{"day": 24, "part": 2, "runner": "py", "min": 4.988582134246826, "max": 6.0756916999816895, "avg": 5.414014768600464, "n": 5} +{"day": 25, "part": 1, "runner": "py", "min": 0.3831920623779297, "max": 0.38574719429016113, "avg": 0.3844696283340454, "n": 2} diff --git a/challenges/2024/01-historianHysteria/README.md b/challenges/2024/01-historianHysteria/README.md new file mode 100644 index 0000000..9860443 --- /dev/null +++ b/challenges/2024/01-historianHysteria/README.md @@ -0,0 +1 @@ +# [Day 1: Historian Hysteria](https://adventofcode.com/2024/day/1) diff --git a/challenges/2024/01-historianHysteria/main.py b/challenges/2024/01-historianHysteria/main.py new file mode 100644 index 0000000..cda4b5f --- /dev/null +++ b/challenges/2024/01-historianHysteria/main.py @@ -0,0 +1,56 @@ +import sys +from collections import defaultdict + + +def parse(instr: str) -> tuple[list[int], list[int]]: + a, b = [], [] + + for line in instr.splitlines(): + ai, bi = line.split(" ") + a.append(int(ai)) + b.append(int(bi)) + + return a, b + + +def one(instr: str) -> int: + a, b = parse(instr) + + a = sorted(a) + b = sorted(b) + + acc = 0 + for (x, y) in zip(a, b): + acc += abs(y - x) + + return acc + + +def two(instr: str): + a, b = parse(instr) + + counts = defaultdict(lambda: 0) + for val in b: + counts[val] = counts[val] + 1 + + acc = 0 + for val in a: + acc += counts[val] * val + + return acc + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2024/01-historianHysteria/tests.json b/challenges/2024/01-historianHysteria/tests.json new file mode 100644 index 0000000..304ada4 --- /dev/null +++ b/challenges/2024/01-historianHysteria/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "11", + "input": "3 4\n4 3\n2 5\n1 3\n3 9\n3 3\n\n" + } + ], + "2": [ + { + "is": "31", + "input": "3 4\n4 3\n2 5\n1 3\n3 9\n3 3\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2024/02-redNosedReports/README.md b/challenges/2024/02-redNosedReports/README.md new file mode 100644 index 0000000..af62b55 --- /dev/null +++ b/challenges/2024/02-redNosedReports/README.md @@ -0,0 +1 @@ +# [Day 2: Red Nosed Reports](https://adventofcode.com/2024/day/2) diff --git a/challenges/2024/02-redNosedReports/main.py b/challenges/2024/02-redNosedReports/main.py new file mode 100644 index 0000000..88ef61a --- /dev/null +++ b/challenges/2024/02-redNosedReports/main.py @@ -0,0 +1,87 @@ +import sys +from typing import Optional + + +def parse(instr: str) -> list[list[int]]: + res = [] + for line in instr.splitlines(): + res.append(list(map(int, line.split(" ")))) + return res + + +def test_pair( + a: int, b: int, sequence_is_negative: Optional[bool] +) -> tuple[bool, bool]: + diff = b - a + this_is_negative = diff < 0 + + if sequence_is_negative is not None and this_is_negative != sequence_is_negative: + return False, this_is_negative + + return 1 <= abs(diff) <= 3, this_is_negative + + +def test_report(rep: list[int]) -> bool: + should_be_negative: Optional[bool] = None + ok = False + + for i, v in enumerate(rep[:-1]): + w = rep[i + 1] + + ok, neg = test_pair(v, w, should_be_negative) + if should_be_negative is None: + should_be_negative = neg + + if not ok: + break + + return ok + + +def one(instr: str): + reports = parse(instr) + + n = 0 + for rep in reports: + if test_report(rep): + n += 1 + + return n + + +def two(instr: str): + reports = parse(instr) + + n = 0 + for rep in reports: + if test_report(rep): + n += 1 + else: + ok = False + for i in range(len(rep)): + r = rep.copy() + r.pop(i) + if test_report(r): + ok = True + break + + if ok: + n += 1 + + return n + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2024/02-redNosedReports/tests.json b/challenges/2024/02-redNosedReports/tests.json new file mode 100644 index 0000000..54d412d --- /dev/null +++ b/challenges/2024/02-redNosedReports/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "2", + "input": "7 6 4 2 1\n1 2 7 8 9\n9 7 6 2 1\n1 3 2 4 5\n8 6 4 4 1\n1 3 6 7 9\n" + } + ], + "2": [ + { + "is": "4", + "input": "7 6 4 2 1\n1 2 7 8 9\n9 7 6 2 1\n1 3 2 4 5\n8 6 4 4 1\n1 3 6 7 9\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2024/03-mullItOver/README.md b/challenges/2024/03-mullItOver/README.md new file mode 100644 index 0000000..95586d4 --- /dev/null +++ b/challenges/2024/03-mullItOver/README.md @@ -0,0 +1 @@ +# [Day 3: Mull It Over](https://adventofcode.com/2024/day/3) diff --git a/challenges/2024/03-mullItOver/main.py b/challenges/2024/03-mullItOver/main.py new file mode 100644 index 0000000..2406f44 --- /dev/null +++ b/challenges/2024/03-mullItOver/main.py @@ -0,0 +1,61 @@ +import sys +import re + + +Instruction = tuple[str, int, int] + + +def parse(instr: str) -> list[Instruction]: + r = re.compile(r"(mul)\((\d{1,3}),(\d{1,3})\)|(do)\(\)|(don't)\(\)") + res = [] + for m in r.findall(instr): + if m[0]: + res.append(("mul", int(m[1]), int(m[2]))) + elif m[3]: + res.append(("do", 0, 0)) + elif m[4]: + res.append(("don't", 0, 0)) + return res + + +def one(instr: str): + instructions = parse(instr) + + acc = 0 + for (op, a, b) in instructions: + if op == "mul": + acc += a * b + + return acc + + +def two(instr: str): + instructions = parse(instr) + + acc = 0 + on = True + for (op, a, b) in instructions: + if op == "mul" and on: + acc += a * b + elif op == "do": + on = True + elif op == "don't": + on = False + + return acc + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2024/03-mullItOver/tests.json b/challenges/2024/03-mullItOver/tests.json new file mode 100644 index 0000000..582693e --- /dev/null +++ b/challenges/2024/03-mullItOver/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "161", + "input": "xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))\n" + } + ], + "2": [ + { + "is": "48", + "input": "xmul(2,4)&mul[3,7]rundon't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2024/04-ceresSearch/README.md b/challenges/2024/04-ceresSearch/README.md new file mode 100644 index 0000000..5d91aad --- /dev/null +++ b/challenges/2024/04-ceresSearch/README.md @@ -0,0 +1,11 @@ +# [Day 4: Ceres Search](https://adventofcode.com/2024/day/4) + +Part two is: +* less than 1976 +* also, somewhat obviously, less than 1978 + +Heatmap of the most overlapped locations + +| Part 1 | Part 2 | +|--------|--------| +| ![heatmap 1](heatmap-1.png) | ![heatmap 2](heatmap-2.png) | \ No newline at end of file diff --git a/challenges/2024/04-ceresSearch/generate-vis.sh b/challenges/2024/04-ceresSearch/generate-vis.sh new file mode 100644 index 0000000..aa7b474 --- /dev/null +++ b/challenges/2024/04-ceresSearch/generate-vis.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -ex + +cat input.txt | PYTHONPATH=../../../ python3 vis.py diff --git a/challenges/2024/04-ceresSearch/heatmap-1.png b/challenges/2024/04-ceresSearch/heatmap-1.png new file mode 100644 index 0000000..c7e3f58 Binary files /dev/null and b/challenges/2024/04-ceresSearch/heatmap-1.png differ diff --git a/challenges/2024/04-ceresSearch/heatmap-2.png b/challenges/2024/04-ceresSearch/heatmap-2.png new file mode 100644 index 0000000..23dd4d0 Binary files /dev/null and b/challenges/2024/04-ceresSearch/heatmap-2.png differ diff --git a/challenges/2024/04-ceresSearch/main.py b/challenges/2024/04-ceresSearch/main.py new file mode 100644 index 0000000..aa456e2 --- /dev/null +++ b/challenges/2024/04-ceresSearch/main.py @@ -0,0 +1,81 @@ +import sys +import gridutil.grid as grid +import gridutil.coord as coord + + +def parse(instr: str) -> grid.Grid: + return grid.parse(instr.upper()) + + +def one(instr: str): + wordsearch = parse(instr) + + seq_starts = list( + map(lambda x: x[0], filter(lambda x: x[1] == "X", wordsearch.items())) + ) + detected_sequences = set() + + for start_pos in seq_starts: + for xdir in [-1, 0, 1]: + for ydir in [-1, 0, 1]: + + if xdir == 0 and ydir == 0: + continue + + delta = coord.Coordinate(xdir, ydir) + + ok = True + for i, v in enumerate("XMAS"): + if not ok: + break + + g = wordsearch.get(coord.add(start_pos, coord.mult(delta, i)), "-") + ok = g == v + + if ok: + detected_sequences.add((start_pos, delta)) + + return len(detected_sequences) + + +def check_cross_adjacents(s: str) -> bool: + return s == "SM" or s == "MS" + + +def two(instr: str): + wordsearch = parse(instr) + + seq_starts = list( + map(lambda x: x[0], filter(lambda x: x[1] == "A", wordsearch.items())) + ) + detected_sequences = set() + + for start_pos in seq_starts: + + a = wordsearch.get(coord.add(start_pos, (-1, -1)), "") + wordsearch.get( + coord.add(start_pos, (1, 1)), "" + ) + b = wordsearch.get(coord.add(start_pos, (-1, 1)), "") + wordsearch.get( + coord.add(start_pos, (1, -1)), "" + ) + + if check_cross_adjacents(a) and check_cross_adjacents(b): + detected_sequences.add(start_pos) + + return len(detected_sequences) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2024/04-ceresSearch/tests.json b/challenges/2024/04-ceresSearch/tests.json new file mode 100644 index 0000000..9c92e6f --- /dev/null +++ b/challenges/2024/04-ceresSearch/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "18", + "input": "MMMSXXMASM\nMSAMXMSMSA\nAMXSXMAAMM\nMSAMASMSMX\nXMASAMXAMM\nXXAMMXXAMA\nSMSMSASXSS\nSAXAMASAAA\nMAMMMXMMMM\nMXMXAXMASX\n\n" + } + ], + "2": [ + { + "is": "9", + "input": "MMMSXXMASM\nMSAMXMSMSA\nAMXSXMAAMM\nMSAMASMSMX\nXMASAMXAMM\nXXAMMXXAMA\nSMSMSASXSS\nSAXAMASAAA\nMAMMMXMMMM\nMXMXAXMASX\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2024/04-ceresSearch/vis.py b/challenges/2024/04-ceresSearch/vis.py new file mode 100644 index 0000000..0d26325 --- /dev/null +++ b/challenges/2024/04-ceresSearch/vis.py @@ -0,0 +1,122 @@ +import sys +import gridutil.grid as grid +import gridutil.coord as coord +from collections import defaultdict +import os +from pathlib import Path +from PIL import Image +from tqdm import tqdm + + +def parse(instr: str) -> grid.Grid: + return grid.parse(instr.upper()) + +def one(instr: str): + wordsearch = parse(instr) + + seq_starts = list( + map(lambda x: x[0], filter(lambda x: x[1] == "X", wordsearch.items())) + ) + detected_sequences = set() + + for start_pos in seq_starts: + for xdir in [-1, 0, 1]: + for ydir in [-1, 0, 1]: + + if xdir == 0 and ydir == 0: + continue + + delta = coord.Coordinate(xdir, ydir) + + ok = True + b = [] + for i, v in enumerate("XMAS"): + if not ok: + break + + x = coord.add(start_pos, coord.mult(delta, i)) + g = wordsearch.get(x, "-") + ok = g == v + b.append(x) + + if ok: + detected_sequences.add(tuple(b)) + + return detected_sequences + + +def check_cross_adjacents(s: str) -> bool: + return s == "SM" or s == "MS" + + +def two(instr: str): + wordsearch = parse(instr) + + seq_starts = list( + map(lambda x: x[0], filter(lambda x: x[1] == "A", wordsearch.items())) + ) + detected_sequences = set() + + for start_pos in seq_starts: + + a = wordsearch.get(coord.add(start_pos, (-1, -1)), "") + wordsearch.get( + coord.add(start_pos, (1, 1)), "" + ) + b = wordsearch.get(coord.add(start_pos, (-1, 1)), "") + wordsearch.get( + coord.add(start_pos, (1, -1)), "" + ) + + if check_cross_adjacents(a) and check_cross_adjacents(b): + detected_sequences.add(start_pos) + + return detected_sequences + + +lowest_colour = (255, 245, 237) +highest_colour = (255, 159, 45) +colour_diffs = tuple(map(lambda x: x[1] - x[0], zip(highest_colour, lowest_colour))) + + +def get_colour_for(n): + return tuple( + map(int, map(lambda x: x[0] - x[1], zip(lowest_colour, map(lambda x: x * n, colour_diffs)))) + ) + + +scale_factor = 4 + +def generate_frame(path: str, wordsearch: grid.Grid, counts: dict[coord.Coordinate, int]): + max_val = max(counts.values()) + + maxx, maxy = grid.get_max_x(wordsearch), grid.get_max_y(wordsearch) + + img = Image.new("RGB", (maxx+1, maxy+1)) + + for x in range(maxx+1): + for y in range(maxy+1): + img.putpixel((x, y), get_colour_for(counts[(x, y)]/max_val)) + + img = img.resize((maxx * scale_factor, maxy * scale_factor), resample=Image.NEAREST) + img.save(path) + + +def main(): + inp = sys.stdin.read().strip() + wordsearch = parse(inp) + + j = defaultdict(lambda: 0) + for state in one(inp): + for s in state: + j[s] = j[s] + 1 + generate_frame("heatmap-1.png", wordsearch, j) + + j = defaultdict(lambda: 0) + for state in two(inp): + for dir in [(0, 0), (-1, -1), (-1, 1), (1, 1), (1, -1)]: + s = coord.add(state, dir) + j[s] = j[s] + 1 + generate_frame("heatmap-2.png", wordsearch, j) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/challenges/2024/05-printQueue/README.md b/challenges/2024/05-printQueue/README.md new file mode 100644 index 0000000..e2b215a --- /dev/null +++ b/challenges/2024/05-printQueue/README.md @@ -0,0 +1 @@ +# [Day 5: Print Queue](https://adventofcode.com/2024/day/5) diff --git a/challenges/2024/05-printQueue/main.py b/challenges/2024/05-printQueue/main.py new file mode 100644 index 0000000..dc31774 --- /dev/null +++ b/challenges/2024/05-printQueue/main.py @@ -0,0 +1,91 @@ +import sys +from collections import namedtuple, defaultdict +import math + + +OrderingRule = namedtuple("OrderingRule", ["page", "goes_before"]) + + +def parse(instr: str) -> tuple[list[OrderingRule], list[list[int]]]: + rules, sets = instr.split("\n\n") + + return ( + [OrderingRule(*map(int, line.split("|"))) for line in rules.splitlines()], + [list(map(int, line.split(","))) for line in sets.splitlines()], + ) + + +def generate_rule_map(rules: list[OrderingRule]) -> dict[int, list[int]]: + rule_map = defaultdict(lambda: []) + for rule in rules: + rule_map[rule.page].append(rule.goes_before) + return rule_map + + +def is_pageset_valid(rule_map: dict[int, list[int]], pageset: list[int]) -> bool: + for i, v in enumerate(pageset): + before = pageset[:i] + + for following_number in rule_map[v]: + if following_number in before: + return False + return True + + +def get_middle_number(x: list[int]) -> int: + assert len(x) % 2 == 1, f"{x} has no nice middle point" + return x[int((len(x) - 1) / 2)] + + +def one(instr: str): + rules, pagesets = parse(instr) + rule_map = generate_rule_map(rules) # for each item, these items should be after it + + acc = 0 + for pageset in pagesets: + if is_pageset_valid(rule_map, pageset): + acc += get_middle_number(pageset) + + return acc + + +def two(instr: str): + rules, pagesets = parse(instr) + rule_map = generate_rule_map(rules) + + inverse_rule_map = defaultdict( + lambda: [] + ) # for each item, these items should be before it + for rule in rules: + inverse_rule_map[rule.goes_before].append(rule.page) + + acc = 0 + for pageset in filter(lambda x: not is_pageset_valid(rule_map, x), pagesets): + while not is_pageset_valid(rule_map, pageset): + for i in range(len(pageset)): + for j in range(i + 1, len(pageset)): + iv = pageset[i] + jv = pageset[j] + + if jv in inverse_rule_map[iv] and i < j: + pageset[i], pageset[j] = pageset[j], pageset[i] + + acc += get_middle_number(pageset) + + return acc + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2024/05-printQueue/tests.json b/challenges/2024/05-printQueue/tests.json new file mode 100644 index 0000000..3041331 --- /dev/null +++ b/challenges/2024/05-printQueue/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "143", + "input": "47|53\n97|13\n97|61\n97|47\n75|29\n61|13\n75|53\n29|13\n97|29\n53|29\n61|53\n97|53\n61|29\n47|13\n75|47\n97|75\n47|61\n75|61\n47|29\n75|13\n53|13\n\n75,47,61,53,29\n97,61,53,29,13\n75,29,13\n75,97,47,61,53\n61,13,29\n97,13,75,29,47\n\n" + } + ], + "2": [ + { + "is": "123", + "input": "47|53\n97|13\n97|61\n97|47\n75|29\n61|13\n75|53\n29|13\n97|29\n53|29\n61|53\n97|53\n61|29\n47|13\n75|47\n97|75\n47|61\n75|61\n47|29\n75|13\n53|13\n\n75,47,61,53,29\n97,61,53,29,13\n75,29,13\n75,97,47,61,53\n61,13,29\n97,13,75,29,47\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2024/06-guardGallivant/README.md b/challenges/2024/06-guardGallivant/README.md new file mode 100644 index 0000000..b15914b --- /dev/null +++ b/challenges/2024/06-guardGallivant/README.md @@ -0,0 +1,30 @@ +# [Day 6: Guard Gallivant](https://adventofcode.com/2024/day/6) + +Part 2 is: +* higher than 456 +* less than 1689 + +The first implementation of part 2 took 15 minutes to run. + +* Turning `visited_sequence` in `trace` into a set instead of a list cut this to 60 seconds +* Starting immediately in front of the new obstacle when testing positions to put said new obstacle in cut this to 20 seconds +* Removing a load of maps and type wranging to turn the fully generic `gridutil.add` (and counterparts) into a more limited but much faster function took this to ~16 seconds + +Before: + +```py +def _coordmap(a: AnyCoordinate, b: AnyCoordinate, fn: Callable) -> AnyCoordinate: + at = type(a) + return at(*map(fn, zip(a, b))) + +def add(a: AnyCoordinate, b: AnyCoordinate) -> AnyCoordinate: + return _coordmap(a, b, lambda x: x[0] + x[1]) +``` + +After: + + +```py +def add(a: Coordinate, b: Coordinate) -> Coordinate: + return Coordinate(a.x + b.x, a.y + b.y) +``` \ No newline at end of file diff --git a/challenges/2024/06-guardGallivant/main.py b/challenges/2024/06-guardGallivant/main.py new file mode 100644 index 0000000..8ef7093 --- /dev/null +++ b/challenges/2024/06-guardGallivant/main.py @@ -0,0 +1,90 @@ +import sys +from gridutil import grid, coord +from tqdm import tqdm + + +def parse(instr: str) -> grid.Grid: + return grid.parse(instr) + + +def find_start(g: grid.Grid) -> coord.Coordinate: + for pos in g: + if g[pos] == "^": + return pos + assert False, "no start point found" + + +def modplus(x: int) -> int: + return (x + 1) % 4 + + +dirs = [ + coord.Direction.Up, + coord.Direction.Right, + coord.Direction.Down, + coord.Direction.Left, +] + + +class LoopEncounteredException(Exception): + pass + + +def trace( + g: grid.Grid, guard_pos: coord.Coordinate, guard_direction: int +) -> set[tuple[coord.Coordinate, int]]: + visited_sequence = set() + + while guard_pos in g: + if (guard_pos, guard_direction) in visited_sequence: + raise LoopEncounteredException + + visited_sequence.add((guard_pos, guard_direction)) + + nc = coord.add(guard_pos, dirs[guard_direction % 4].delta()) + if nc in g and g[nc] == "#": + guard_direction = modplus(guard_direction) + else: + guard_pos = nc + + return visited_sequence + + +def one(instr: str) -> int: + g = parse(instr) + return len(set(map(lambda x: x[0], trace(g, find_start(g), 0)))) + + +def two(instr: str) -> int: + g = parse(instr) + + start_pos = find_start(g) + seq = trace(g, start_pos, 0) + known_blocks = set() + + for (pos, dir) in tqdm(seq, file=sys.stderr): + assert pos in g, "pos off the rails" + g[pos] = "#" + try: + trace(g, coord.add(pos, coord.mult(dirs[dir].delta(), -1)), dir) + except LoopEncounteredException: + known_blocks.add(pos) + g[pos] = "." + + return len(known_blocks) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2024/06-guardGallivant/tests.json b/challenges/2024/06-guardGallivant/tests.json new file mode 100644 index 0000000..a9d142e --- /dev/null +++ b/challenges/2024/06-guardGallivant/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "41", + "input": "....#.....\n.........#\n..........\n..#.......\n.......#..\n..........\n.#..^.....\n........#.\n#.........\n......#...\n\n" + } + ], + "2": [ + { + "is": "6", + "input": "....#.....\n.........#\n..........\n..#.......\n.......#..\n..........\n.#..^.....\n........#.\n#.........\n......#...\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2024/07-bridgeRepair/README.md b/challenges/2024/07-bridgeRepair/README.md new file mode 100644 index 0000000..434d0fc --- /dev/null +++ b/challenges/2024/07-bridgeRepair/README.md @@ -0,0 +1,22 @@ +# [Day 7: Bridge Repair](https://adventofcode.com/2024/day/7) + +Part 1 is: +* greater than 450054910499 + +Before optimisation (pregenerating then testing operator combiantions using `itertools.product("*+|", length=n)`), run time looks something like this: + +``` +Part 1: min 0.2671 seconds, max 0.2818 seconds, avg 0.2755 +Part 2: min 23.2387 seconds, max 24.8753 seconds, avg 23.8805 +``` + +It also appeared that using string concatenation as opposed to pure mathematical functions for the concatenation operator was marginally faster in Python. + +After optimisation (recursive solves working backwards through the numbers), the run time looks something like this: + +``` +Part 1: min 0.0214 seconds, max 0.041 seconds, avg 0.0233 +Part 2: min 0.0215 seconds, max 0.0273 seconds, avg 0.0229 +``` + +The intial version of this solve is in commit `b2fa4b7`. \ No newline at end of file diff --git a/challenges/2024/07-bridgeRepair/main.py b/challenges/2024/07-bridgeRepair/main.py new file mode 100644 index 0000000..f1e9881 --- /dev/null +++ b/challenges/2024/07-bridgeRepair/main.py @@ -0,0 +1,75 @@ +import sys +import math + + +def parse(instr: str) -> list[tuple[int, list[int]]]: + res = [] + for line in instr.splitlines(): + spa, spb = line.split(": ") + res.append( + (int(spa), list(map(int, spb.split(" ")))), + ) + return res + + +def ends_with(x: int, y: int) -> bool: + ycard = int(math.log10(y)) + 1 + return (x - y) * (10**-ycard) == int(x * (10**-ycard)) + + +def trim_int(x: int, y: int) -> int: + ycard = int(math.log10(y)) + 1 + return int((x - y) * (10**-ycard)) + + +def solve(target: int, ns: list[int], use_concat: bool = False) -> bool: + v = ns[-1] + rest = ns[:-1] + + if len(rest) == 0: + return target == v + + if target % v == 0: + # this represents a possible multiplication + if solve(int(target / v), rest, use_concat): + return True + + if use_concat and ends_with(target, v): + # this is a possible concatenation + if solve(trim_int(target, v), rest, use_concat): + return True + + # last resort, addition + return solve(target - v, rest, use_concat) + + +def do(instr: str, use_concat: bool = False) -> int: + cases = parse(instr) + return sum( + target if solve(target, numbers, use_concat) else 0 + for (target, numbers) in cases + ) + + +def one(instr: str): + return do(instr) + + +def two(instr: str): + return do(instr, use_concat=True) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2024/07-bridgeRepair/tests.json b/challenges/2024/07-bridgeRepair/tests.json new file mode 100644 index 0000000..7cb94e7 --- /dev/null +++ b/challenges/2024/07-bridgeRepair/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "3749", + "input": "190: 10 19\n3267: 81 40 27\n83: 17 5\n156: 15 6\n7290: 6 8 6 15\n161011: 16 10 13\n192: 17 8 14\n21037: 9 7 18 13\n292: 11 6 16 20\n" + } + ], + "2": [ + { + "is": "11387", + "input": "190: 10 19\n3267: 81 40 27\n83: 17 5\n156: 15 6\n7290: 6 8 6 15\n161011: 16 10 13\n192: 17 8 14\n21037: 9 7 18 13\n292: 11 6 16 20\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2024/08-resonantCollinearity/README.md b/challenges/2024/08-resonantCollinearity/README.md new file mode 100644 index 0000000..4c91cab --- /dev/null +++ b/challenges/2024/08-resonantCollinearity/README.md @@ -0,0 +1,8 @@ +# [Day 8: Resonant Collinearity](https://adventofcode.com/2024/day/8) + +Part 1 is: +* less than 298 + +This is not a fully generic solution as, in a situation where the differences between x and y dimensions of pairs of coordinates are not coprime, this implementation would skip steps. + +Visualisation exists as `out.mp4` diff --git a/challenges/2024/08-resonantCollinearity/generate-vis.sh b/challenges/2024/08-resonantCollinearity/generate-vis.sh new file mode 100644 index 0000000..c9a9f8b --- /dev/null +++ b/challenges/2024/08-resonantCollinearity/generate-vis.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -ex + +cat input.txt | PYTHONPATH=../../../ python3 vis.py +ffmpeg -y -framerate 30 -pattern_type glob -i 'frames/*.png' -c:v libx264 -pix_fmt yuv420p out.mp4 diff --git a/challenges/2024/08-resonantCollinearity/main.py b/challenges/2024/08-resonantCollinearity/main.py new file mode 100644 index 0000000..baf2a4c --- /dev/null +++ b/challenges/2024/08-resonantCollinearity/main.py @@ -0,0 +1,74 @@ +import sys +from gridutil import grid, coord +from collections import defaultdict +import itertools +from fractions import Fraction + + +def parse(instr: str) -> tuple[dict[str, list[coord.Coordinate]], tuple[int, int]]: + g = grid.parse(instr) + antenna_by_type = defaultdict(list) + for key in g: + if g[key] == ".": + continue + antenna_by_type[g[key]].append(key) + return antenna_by_type, (grid.get_max_x(g), grid.get_max_y(g)) + + +def one(instr: str): + (antenna_by_type, (max_x, max_y)) = parse(instr) + + pos = set() + for antenna_type in antenna_by_type: + for (a, b) in itertools.permutations(antenna_by_type[antenna_type], 2): + diff = coord.sub(b, a) + c = coord.add(a, coord.mult(diff, 2)) + if 0 <= c.x <= max_x and 0 <= c.y <= max_y: + pos.add(c) + + return len(pos) + + +def two(instr: str): + (antenna_by_type, (max_x, max_y)) = parse(instr) + + pos = set() + for antenna_type in antenna_by_type: + for (a, b) in itertools.permutations(antenna_by_type[antenna_type], 2): + if ( + a.x > b.x + ): # filter out (most) duplicate pairs eg ((1, 2), (2, 1)) will only be calculated as ((2, 1), (1, 2)) will be filtered. This also prevents diff.x from being negative (useful for the mod operation) + continue + + diff = coord.sub(b, a) + + m = Fraction( + diff.y, diff.x + ) # equiv of diff.y / diff.x but without the 26.9999999999996 issue + c = a.y - (m * a.x) + + x_cursor = a.x % diff.x + y_cursor = int((x_cursor * m) + c) + while x_cursor <= max_x: + if 0 <= y_cursor <= max_y: + pos.add((x_cursor, y_cursor)) + x_cursor += diff.x + y_cursor += diff.y + + return len(pos) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2024/08-resonantCollinearity/out.mp4 b/challenges/2024/08-resonantCollinearity/out.mp4 new file mode 100644 index 0000000..03fa74a Binary files /dev/null and b/challenges/2024/08-resonantCollinearity/out.mp4 differ diff --git a/challenges/2024/08-resonantCollinearity/tests.json b/challenges/2024/08-resonantCollinearity/tests.json new file mode 100644 index 0000000..3aaf3a8 --- /dev/null +++ b/challenges/2024/08-resonantCollinearity/tests.json @@ -0,0 +1,26 @@ +{ + "1": [ + { + "is": "14", + "input": "............\n........0...\n.....0......\n.......0....\n....0.......\n......A.....\n............\n............\n........A...\n.........A..\n............\n............\n\n" + }, + { + "is": "2", + "input": "..........\n..........\n..........\n....a.....\n..........\n.....a....\n..........\n..........\n..........\n..........\n\n" + }, + { + "is": "4", + "input": "..........\n..........\n..........\n....a.....\n........a.\n.....a....\n..........\n..........\n..........\n..........\n" + } + ], + "2": [ + { + "is": "34", + "input": "............\n........0...\n.....0......\n.......0....\n....0.......\n......A.....\n............\n............\n........A...\n.........A..\n............\n............\n\n" + }, + { + "is": "9", + "input": "T.........\n...T......\n.T........\n..........\n..........\n..........\n..........\n..........\n..........\n..........\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2024/08-resonantCollinearity/vis.py b/challenges/2024/08-resonantCollinearity/vis.py new file mode 100644 index 0000000..31f54a3 --- /dev/null +++ b/challenges/2024/08-resonantCollinearity/vis.py @@ -0,0 +1,116 @@ +import sys +from gridutil import grid, coord +from collections import defaultdict +import itertools +from fractions import Fraction +import os +from pathlib import Path +from PIL import Image +from tqdm import tqdm +from colorsys import hsv_to_rgb + + +def parse(instr: str) -> tuple[dict[str, list[coord.Coordinate]], tuple[int, int]]: + g = grid.parse(instr) + antenna_by_type = defaultdict(list) + for key in g: + if g[key] == ".": + continue + antenna_by_type[g[key]].append(key) + return antenna_by_type, (grid.get_max_x(g), grid.get_max_y(g)) + + +lowest_colour = (255, 245, 237) +highest_colour = (255, 159, 45) +highlight_colour = (255, 47, 47) +shadow_colour = (226, 218, 211) +colour_diffs = tuple(map(lambda x: x[1] - x[0], zip(highest_colour, lowest_colour))) + + +def get_colour_for(n): + return tuple( + map( + int, + map( + lambda x: x[0] - x[1], + zip(lowest_colour, map(lambda x: x * n, colour_diffs)), + ), + ) + ) + + +scale_factor = 8 + + +def generate_frame(i, base_img, highlight_locs, sequence) -> int: + for n in range(len(sequence)): + s = sequence[: n + 1] + img = base_img.copy() + + sl = len(s) + 1 + for j, p in enumerate(s): + img.putpixel(p, get_colour_for((j + 1) / sl)) + + for h in highlight_locs: + img.putpixel(h, highlight_colour) + + maxx, maxy = img.size + img = img.resize( + (maxx * scale_factor, maxy * scale_factor), resample=Image.NEAREST + ) + img.save(f"frames/{str(i).zfill(5)}.png") + i += 1 + return i + + +def update_base(base_img, add): + for v in add: + base_img.putpixel(v, shadow_colour) + + +if __name__ == "__main__": + inp = sys.stdin.read().strip() + (antenna_by_type, (max_x, max_y)) = parse(inp) + + ns = list(sorted(antenna_by_type.keys())) + nns = len(ns) + + try: + os.makedirs("frames") + except FileExistsError: + pass + + base_img = Image.new("RGB", (max_x + 1, max_y + 1), color=lowest_colour) + + i = 0 + for antenna_type in tqdm(antenna_by_type): + for (a, b) in itertools.permutations(antenna_by_type[antenna_type], 2): + if ( + a.x > b.x + ): # filter out (most) duplicate pairs eg ((1, 2), (2, 1)) will only be calculated as ((2, 1), (1, 2)) will be filtered. This also prevents diff.x from being negative (useful for the mod operation) + continue + + this_iter = [] + + diff = coord.sub(b, a) + + m = Fraction( + diff.y, diff.x + ) # equiv of diff.y / diff.x but without the 26.9999999999996 issue + c = a.y - (m * a.x) + + x_cursor = a.x % diff.x + y_cursor = int((x_cursor * m) + c) + while x_cursor <= max_x: + if 0 <= y_cursor <= max_y: + this_iter.append((x_cursor, y_cursor)) + x_cursor += diff.x + y_cursor += diff.y + + i = generate_frame( + i, + base_img, + (a, b), + this_iter, + ) + update_base(base_img, this_iter) diff --git a/challenges/2024/09-diskFragmenter/README.md b/challenges/2024/09-diskFragmenter/README.md new file mode 100644 index 0000000..c9be906 --- /dev/null +++ b/challenges/2024/09-diskFragmenter/README.md @@ -0,0 +1,5 @@ +# [Day 9: Disk Fragmenter](https://adventofcode.com/2024/day/9) + +Part 2 is: +* less than 6277851188082 + 6272188244509 \ No newline at end of file diff --git a/challenges/2024/09-diskFragmenter/main.py b/challenges/2024/09-diskFragmenter/main.py new file mode 100644 index 0000000..f807bd9 --- /dev/null +++ b/challenges/2024/09-diskFragmenter/main.py @@ -0,0 +1,129 @@ +import sys +from collections import namedtuple, defaultdict + + +Segment = namedtuple("Segment", ["id", "len"]) + + +def parse(instr: str) -> list[Segment]: + res = [] + next_id = 0 + + for i, v in enumerate(instr): + v = int(v) + if i % 2 == 0: + # this is a file + res.append(Segment(next_id, v)) + next_id += 1 + else: + # this is a gap + res.append(Segment(None, v)) + + if res[-1].id is None: + # random gap at the end? no thanks + res = res[:-1] + + return res + + +def calc_checksum(files: list[Segment]) -> int: + acc = 0 + pos = 0 + for file in files: + if file.id is not None: + # sum of sequence of n consecutive integers is (n / 2)(first + last) + acc += int((file.len / 2) * ((pos * 2) + file.len - 1) * file.id) + pos += file.len + return acc + + +def one(instr: str): + files = parse(instr) + + i = 0 + while i < len(files) - 1: + f = files[i] + last_file = files[-1] + + if last_file.id is None: + files = files[:-1] + continue + + if f.id is None: + # _debug(i, files[i]) + last_file = files[-1] + + assert last_file.id is not None + if last_file.len > f.len: + files[-1] = Segment(last_file.id, last_file.len - f.len) + files[i] = Segment(last_file.id, f.len) + elif last_file.len == f.len: + # we're gonna move all of this so just delete it + files = files[:-1] + files[i] = last_file + else: + # TODO: when we haven't got enough in this last file so we need to split f into two + files = files[:-1] + files[i] = last_file + assert f.len - last_file.len != 0 + files.insert(i+1, Segment(None, f.len - last_file.len)) + + i += 1 + + return calc_checksum(files) + + +def two(instr: str): + files = parse(instr) + + gaps = defaultdict(list) + + for i, file in enumerate(files): + if file.id is None and file.len != 0: + gaps[file.len].append(i) + + moved = set() + + i = len(files) - 1 + while i > 0: + f = files[i] + + if f.id is not None and f.id not in moved: + gap_loc = None + for j, v in enumerate(files): + if j >= i: + break + + if v.id is None and v.len >= f.len: + gap_loc = j + break + + if gap_loc is not None: + v = files[gap_loc] + if v.len == f.len: + files[i], files[gap_loc] = files[gap_loc], files[i] + else: + files[i] = Segment(None, f.len) + files[gap_loc] = f + files.insert(gap_loc + 1, Segment(None, v.len - f.len)) + moved.add(f.id) + + i -= 1 + + return calc_checksum(files) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) \ No newline at end of file diff --git a/challenges/2024/09-diskFragmenter/tests.json b/challenges/2024/09-diskFragmenter/tests.json new file mode 100644 index 0000000..737ab31 --- /dev/null +++ b/challenges/2024/09-diskFragmenter/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "1928", + "input": "2333133121414131402\n" + } + ], + "2": [ + { + "is": "2858", + "input": "2333133121414131402\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2024/README.md b/challenges/2024/README.md new file mode 100644 index 0000000..fe7a44e --- /dev/null +++ b/challenges/2024/README.md @@ -0,0 +1,25 @@ +# Advent of Code 2024 + +Solutions to the [2024 Advent of Code](https://adventofcode.com/2024)! + +--- + +Total stars: **16 ★** + +![Benchmark graph](./benchmark-graph.png) + + + +A day denoted with an asterisk means it has a visualisation. + +| Day | Status | Solutions | Notes | +|-------------------------------------|--------|----------------------|-------| +| 01 - Historian Hysteria | ★ ★ | Python | The reading comprehension was the hardest part of this. | +| 02 - Red-Nosed Reindeer | ★ ★ | Python || +| 03 - Mull It Over | ★ ★ | Python | The first instance of Advent of Parsing this year! | +| 04* - Ceres Search | ★ ★ | Python | When it says a cross, it does not mean a plus. | +| 05 - Print Queue | ★ ★ | Python | Before you dismiss and idea as being "too simple", make sure you check that it doesn't work. | +| 06 - Guard Gallivant | ★ ★ | Python | oh dear runtime (also I knew what I wanted to do for so long it just took me 3 hours to implement it properly) | +| 07 - Bridge Repair | ★ ★ | Python | Maths? Backwards?? | +| 08* - Resonant Collinearity | ★ ★ | Python | `Fraction` saving us all from the curse of a computer's inability to do floating point arithmetic | +| 09 - Disk Fragmenter | ★ ★ | Python | Many cursed strategies were attempted before I landed on the final implementation. Part 1 could also be sped up. | \ No newline at end of file diff --git a/challenges/2024/benchmark-graph.png b/challenges/2024/benchmark-graph.png new file mode 100644 index 0000000..4b35c8f Binary files /dev/null and b/challenges/2024/benchmark-graph.png differ diff --git a/challenges/2024/benchmarks.jsonl b/challenges/2024/benchmarks.jsonl new file mode 100644 index 0000000..ee808c7 --- /dev/null +++ b/challenges/2024/benchmarks.jsonl @@ -0,0 +1,18 @@ +{"day": 1, "part": 1, "runner": "py", "min": 0.016046524047851562, "max": 0.025307178497314453, "avg": 0.016975646018981935, "n": 100} +{"day": 1, "part": 2, "runner": "py", "min": 0.0161440372467041, "max": 0.019031047821044922, "avg": 0.016828114986419677, "n": 100} +{"day": 2, "part": 1, "runner": "py", "min": 0.021907567977905273, "max": 0.02720332145690918, "avg": 0.023289167881011964, "n": 100} +{"day": 2, "part": 2, "runner": "py", "min": 0.02489948272705078, "max": 0.0319674015045166, "avg": 0.026371052265167238, "n": 100} +{"day": 3, "part": 1, "runner": "py", "min": 0.02116227149963379, "max": 0.03153491020202637, "avg": 0.022634525299072266, "n": 100} +{"day": 3, "part": 2, "runner": "py", "min": 0.02115607261657715, "max": 0.03121805191040039, "avg": 0.022722084522247315, "n": 100} +{"day": 4, "part": 1, "runner": "py", "min": 0.17342424392700195, "max": 0.3778045177459717, "avg": 0.18848238706588746, "n": 100} +{"day": 4, "part": 2, "runner": "py", "min": 0.05280470848083496, "max": 0.06299543380737305, "avg": 0.05627016305923462, "n": 100} +{"day": 5, "part": 1, "runner": "py", "min": 0.02001357078552246, "max": 0.030559301376342773, "avg": 0.02152919292449951, "n": 100} +{"day": 5, "part": 2, "runner": "py", "min": 0.02507805824279785, "max": 0.03197765350341797, "avg": 0.027084295749664308, "n": 100} +{"day": 6, "part": 1, "runner": "py", "min": 0.05790352821350098, "max": 0.06762170791625977, "avg": 0.061776439348856606, "n": 6} +{"day": 6, "part": 2, "runner": "py", "min": 15.881408452987671, "max": 17.086341857910156, "avg": 16.64130985736847, "n": 6} +{"day": 7, "part": 1, "runner": "py", "min": 0.020526885986328125, "max": 0.07849740982055664, "avg": 0.022122257232666014, "n": 500} +{"day": 7, "part": 2, "runner": "py", "min": 0.02680826187133789, "max": 0.04485297203063965, "avg": 0.029925800323486327, "n": 500} +{"day": 8, "part": 1, "runner": "py", "min": 0.025803089141845703, "max": 0.036757469177246094, "avg": 0.02743640899658203, "n": 200} +{"day": 8, "part": 2, "runner": "py", "min": 0.027710437774658203, "max": 0.035851240158081055, "avg": 0.029560294151306152, "n": 200} +{"day": 9, "part": 1, "runner": "py", "min": 0.5576984882354736, "max": 0.5812969207763672, "avg": 0.5654887676239013, "n": 5} +{"day": 9, "part": 2, "runner": "py", "min": 3.876859188079834, "max": 4.176096677780151, "avg": 4.0687650680542, "n": 5} diff --git a/generate-benchmark-graph.py b/generate-benchmark-graph.py index 810bd36..9412df0 100644 --- a/generate-benchmark-graph.py +++ b/generate-benchmark-graph.py @@ -5,49 +5,32 @@ import matplotlib.patches as patches import os import sys -import re +from pathlib import Path OUTPUT_FILE = sys.argv[1] -YEAR = sys.argv[2] +BENCHMARK_FILE = Path(sys.argv[2]) -COLOURS = {"Golang": "#00ADD8", "Python": "#3572A5", "Nim": "#ffc200"} +YEAR = BENCHMARK_FILE.parts[1] -MAX_Y_VALUE = 1 - -challenge_dir_regex = re.compile("""(?m)^(\d{2})-([a-zA-Z]+)$""") +COLOURS = {"Python": "#3572A5", "Go": "#00ADD8"} -directories = [] -path = os.path.join("challenges", YEAR) -for filename in os.listdir(path): - if os.path.isdir(os.path.join(path, filename)) and challenge_dir_regex.match( - filename - ): - directories.append(filename) +MAX_Y_VALUE = 1 -files = [os.path.join(x, "benchmark.json") for x in directories] +runner_translation = { + "py": "Python", + "go": "Go", +} benchmark_data = { "Python": {}, - "Golang": {}, - "Nim": {}, + "Go": {}, } # adding dicts here sets the order of points being plotted -for filename in files: - fpath = os.path.join(path, filename) - try: - f = open(fpath) - except FileNotFoundError: - print(f"Warning: missing file {fpath}") - continue - - data = json.load(f) - f.close() - - for language in data["implementations"]: - x = benchmark_data.get(language, {}) - x[str(data["day"]) + ".1"] = data["implementations"][language]["part.1.avg"] - x[str(data["day"]) + ".2"] = data["implementations"][language]["part.2.avg"] - benchmark_data[language] = x +with open(BENCHMARK_FILE) as f: + for line in f.readlines(): + d = json.loads(line) + rn = runner_translation[d["runner"]] + benchmark_data[rn][f"{d['day']}.{d['part']}"] = d["avg"] all_days = set() @@ -66,38 +49,48 @@ for i, language in enumerate(benchmark_data): data = benchmark_data[language] + part_one_times = [] part_two_times = [] - days = [] + + p1days = [] + p2days = [] for key in data: day = int(key.split(".", 1)[0]) - if day not in days: - days.append(day) if key.endswith(".1"): + if day not in p1days: + p1days.append(day) part_one_times.append(data[key]) if key.endswith(".2"): + if day not in p2days: + p2days.append(day) part_two_times.append(data[key]) colour = COLOURS.get(language) - p1 = axp1.scatter(days, part_one_times, color=colour) - p2 = axp2.scatter(days, part_two_times, color=colour) + p1 = axp1.scatter(p1days, part_one_times, color=colour) + p2 = axp2.scatter(p2days, part_two_times, color=colour) - for i, day in enumerate(days): - if i + 1 >= len(days): + for i, day in enumerate(p1days): + if i + 1 >= len(p1days): continue - if days[i + 1] == day + 1: + if p1days[i + 1] == day + 1: axp1.plot( - (day, days[i + 1]), + (day, p1days[i + 1]), (part_one_times[i], part_one_times[i + 1]), "-", color=colour, ) + + for i, day in enumerate(p2days): + if i + 1 >= len(p2days): + continue + if p2days[i + 1] == day + 1: axp2.plot( - (day, days[i + 1]), + (day, p2days[i + 1]), (part_two_times[i], part_two_times[i + 1]), "-", color=colour, @@ -111,11 +104,11 @@ def do_auxillary_parts(axis): plt.sca(axis) plt.xticks(list(all_days), [str(y) for y in all_days]) - plt.ylabel("Running time (seconds)") + plt.ylabel("Running time (log seconds)") plt.yscale("log") plt.xlabel("Day") plt.legend( - handles=[patches.Patch(color=COLOURS[label], label=label) for label in COLOURS] + handles=[patches.Patch(color=COLOURS[label], label=label) for label in COLOURS if len(benchmark_data[label]) > 0] ) # plt.ylim([0, MAX_Y_VALUE]) # plt.legend(legends) diff --git a/get-input.py b/get-input.py deleted file mode 100644 index 7d175bf..0000000 --- a/get-input.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 - -import datetime -import requests -import json -import os -import sys -import argparse - -script_dir = os.path.dirname(os.path.realpath(__file__)) -default_config_file_name = os.path.join(script_dir, "input-loader.json") - -today = datetime.date.today() - -parser = argparse.ArgumentParser(description="Process some integers.") -parser.add_argument( - "--config-file", - dest="config_file", - default=default_config_file_name, - help="config file name", -) -parser.add_argument("--day", default=today.day, help="day to get input for") -parser.add_argument("--year", default=today.year, help="year to get input for") -parser.add_argument( - "--stdout", - default=False, - action="store_true", - help="send the input to stdout instead of a file", -) - -args = parser.parse_args() - -with open(args.config_file) as f: - config_data = json.load(f) - -r = requests.get( - f"https://adventofcode.com/{args.year}/day/{args.day}/input", - cookies={"session": config_data["session"]}, - headers={"User-Agent": config_data["userAgent"]}, -) - -if args.stdout: - sys.stdout.write(r.text + "\n") - sys.stderr.write("OK\n") -else: - year_dir = os.path.join("challenges", str(args.year)) - directory_name = None - for entry in os.listdir(year_dir): - if entry.startswith(f"{args.day}-"): - directory_name = entry - break - assert directory_name is not None, "challenge directory must already exist" - path = os.path.join("challenges", str(args.year), directory_name, "input.txt") - with open(path, "w") as f: - f.write(r.text + "\n") - - sys.stderr.write(f"Written to {path}\n") diff --git a/go.mod b/go.mod deleted file mode 100644 index 32132ec..0000000 --- a/go.mod +++ /dev/null @@ -1,27 +0,0 @@ -module github.com/codemicro/adventOfCode - -go 1.19 - -require ( - github.com/AlecAivazis/survey/v2 v2.3.2 - github.com/alexflint/go-arg v1.4.2 - github.com/deckarep/golang-set v1.8.0 - github.com/fatih/color v1.13.0 - github.com/logrusorgru/aurora v2.0.3+incompatible - github.com/schollz/progressbar/v3 v3.8.3 -) - -require ( - github.com/alexflint/go-scalar v1.0.0 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/mattn/go-colorable v0.1.9 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect - github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect - github.com/rivo/uniseg v0.2.0 // indirect - golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect - golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.3.6 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index f3191d4..0000000 --- a/go.sum +++ /dev/null @@ -1,78 +0,0 @@ -github.com/AlecAivazis/survey/v2 v2.3.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO4gCnU8= -github.com/AlecAivazis/survey/v2 v2.3.2/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg= -github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= -github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= -github.com/alexflint/go-arg v1.4.2 h1:lDWZAXxpAnZUq4qwb86p/3rIJJ2Li81EoMbTMujhVa0= -github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM= -github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70= -github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= -github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= -github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= -github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ= -github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= -github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/schollz/progressbar/v3 v3.8.3 h1:FnLGl3ewlDUP+YdSwveXBaXs053Mem/du+wr7XSYKl8= -github.com/schollz/progressbar/v3 v3.8.3/go.mod h1:pWnVCjSBZsT2X3nx9HfRdnCDrpbevliMeoEVhStwHko= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= -golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/gridutil/__init__.py b/gridutil/__init__.py new file mode 100644 index 0000000..0494dd6 --- /dev/null +++ b/gridutil/__init__.py @@ -0,0 +1,2 @@ +import gridutil.grid +import gridutil.coord diff --git a/gridutil/coord.py b/gridutil/coord.py new file mode 100644 index 0000000..9b4d7f2 --- /dev/null +++ b/gridutil/coord.py @@ -0,0 +1,79 @@ +from enum import Enum, auto +from collections import namedtuple +from numbers import Number +from typing import TypeVar, Callable, Union + + +Coordinate = namedtuple("Coordinate", ["x", "y"]) +Coordinate3 = namedtuple("Coordinate3", ["x", "y", "z"]) +AnyCoordinate = Coordinate | Coordinate3 + + +def add(a: Coordinate, b: Coordinate) -> Coordinate: + return Coordinate(a.x + b.x, a.y + b.y) + + +def sub(a: Coordinate, b: Coordinate) -> Coordinate: + return Coordinate(a.x - b.x, a.y - b.y) + + +def mult(a: Coordinate, b: Number) -> Coordinate: + return Coordinate(a.x * b, a.y * b) + + +def manhattan_dist(a: AnyCoordinate, b: AnyCoordinate) -> Number: + return sum(map(abs, sub(b, a))) + + +def area(x: list[Coordinate]) -> Number: + """ + Finds the area of a closed polygon. + + https://en.wikipedia.org/wiki/Shoelace_formula + """ + acc = 0 + for ((ax, ay), (bx, by)) in zip(x, x[1:] + [x[0]]): + acc += (ax * by) - (bx * ay) + return acc / 2 + + +class Direction(Enum): + Up = auto() + Down = auto() + Left = auto() + Right = auto() + + def delta(self) -> Coordinate: + match self: + case Direction.Up: + return Coordinate(0, -1) + case Direction.Down: + return Coordinate(0, 1) + case Direction.Left: + return Coordinate(-1, 0) + case Direction.Right: + return Coordinate(1, 0) + + def opposite(self): + match self: + case Direction.Down: + return Direction.Up + case Direction.Up: + return Direction.Down + case Direction.Left: + return Direction.Right + case Direction.Right: + return Direction.Left + + def __lt__(self, x): + return False + + def __eq__(self, x): + if type(x) != Direction: + return False + return self.value == x.value + + def __hash__(self): + return hash(self.value) + +directions = [Direction.Up, Direction.Down, Direction.Left, Direction.Right] diff --git a/gridutil/grid.py b/gridutil/grid.py new file mode 100644 index 0000000..e71c478 --- /dev/null +++ b/gridutil/grid.py @@ -0,0 +1,44 @@ +from typing import TypeVar, Callable, Optional + +import gridutil.coord as coord + +T = TypeVar("T") +Grid = dict[coord.Coordinate, T] + + +def parse(instr: str, filter_fn: Optional[Callable[[str], bool]] = None) -> Grid: + if filter_fn is None: + filter_fn = lambda _: True + + res = {} + for y, line in enumerate(instr.splitlines()): + for x, char in enumerate(line): + if filter_fn(char): + res[coord.Coordinate(x, y)] = char + + return res + + +def _get_max( + grid: Grid, idx: str, filter_fn: Optional[Callable[[T], bool]] = None +) -> int: + g = grid + if filter is not None: + g = filter(filter_fn, grid) + return max(map(lambda x: x[idx], g)) + + +def get_max_x(grid: Grid, filter_fn: Optional[Callable[[T], bool]] = None) -> int: + return _get_max(grid, 0, filter_fn=filter_fn) + + +def get_max_y(grid: Grid, filter_fn: Optional[Callable[[T], bool]] = None) -> int: + return _get_max(grid, 1, filter_fn=filter_fn) + + +def print_grid(grid: Grid, **kwargs): + for y in range(min(map(lambda x: x[1], grid)), get_max_y(grid) + 1): + for x in range(min(map(lambda x: x[0], grid)), get_max_x(grid) + 1): + v = grid.get((x, y), " ") + print(v, end="", **kwargs) + print(**kwargs) diff --git a/lib/aocgo/aocgo.go b/lib/aocgo/aocgo.go deleted file mode 100644 index 707af12..0000000 --- a/lib/aocgo/aocgo.go +++ /dev/null @@ -1,62 +0,0 @@ -package aocgo - -import "errors" - -type BaseChallenge struct{} - -func (b BaseChallenge) One(instr string) (interface{}, error) { - return nil, errors.New("not implemented") -} - -func (b BaseChallenge) Two(instr string) (interface{}, error) { - return nil, errors.New("not implemented") -} - -func (b BaseChallenge) Vis(instr string, outdir string) error { - return errors.New("not implemented") -} - -func IntPermutations(arr []int) [][]int { - var helper func([]int, int) - res := [][]int{} - - helper = func(arr []int, n int) { - if n == 1 { - tmp := make([]int, len(arr)) - copy(tmp, arr) - res = append(res, tmp) - } else { - for i := 0; i < n; i++ { - helper(arr, n-1) - if n%2 == 1 { - tmp := arr[i] - arr[i] = arr[n-1] - arr[n-1] = tmp - } else { - tmp := arr[0] - arr[0] = arr[n-1] - arr[n-1] = tmp - } - } - } - } - helper(arr, len(arr)) - return res -} - -func StringPermutations(x string) []string { - var asInts []int - for _, char := range x { - asInts = append(asInts, int(char)) - } - ip := IntPermutations(asInts) - var o []string - for _, x := range ip { - var b string - for _, y := range x { - b += string(rune(y)) - } - o = append(o, b) - } - return o -} \ No newline at end of file diff --git a/lib/aocgo/set.go b/lib/aocgo/set.go deleted file mode 100644 index fe01d3e..0000000 --- a/lib/aocgo/set.go +++ /dev/null @@ -1,34 +0,0 @@ -package aocgo - -type Set []interface{} - -func NewSet() *Set { - return new(Set) -} - -func (s *Set) Contains(x interface{}) bool { - for _, item := range *s { - if item == x { - return true - } - } - return false -} - -func (s *Set) Add(x interface{}) { - if !s.Contains(x) { - *s = append(*s, x) - } -} - -func (s *Set) Union(t *Set) { - for _, item := range *t { - s.Add(item) - } -} - -func (s *Set) ShallowCopy() *Set { - ns := NewSet() - *ns = append(*ns, *s...) - return ns -} \ No newline at end of file diff --git a/lib/aocpy/__init__.py b/lib/aocpy/__init__.py deleted file mode 100644 index c42cdf4..0000000 --- a/lib/aocpy/__init__.py +++ /dev/null @@ -1,135 +0,0 @@ -from __future__ import annotations -from typing import * -from collections.abc import Sequence - - -class BaseChallenge: - @staticmethod - def one(instr: str) -> Any: - raise NotImplementedError - - @staticmethod - def two(instr: str) -> Any: - raise NotImplementedError - - @staticmethod - def vis(instr: str, outputDir: str) -> Any: - raise NotImplementedError - - -T = TypeVar("T") -U = TypeVar("U") - - -def foldl(p: Callable[[U, T], U], i: Iterable[T], start: U) -> U: - res = start - for item in i: - res = p(res, item) - return res - - -def foldr(p: Callable[[U, T], U], i: Iterable[T], start: U) -> U: - return foldl(p, reversed(i), start) - - -def min_max(x: Iterable[int]) -> Tuple[int, int]: - mini, maxi = None, 0 - - for item in x: - if item > maxi: - maxi = item - if mini is None or item < mini: - mini = item - - if mini is None: - raise ValueError("empty set") - - return mini, maxi - - -class Vector: - x: int - y: int - - def __init__(self, *args): - if len(args) == 1 and Vector._is_vector_tuple(args[0]): - x, y = args[0] - elif len(args) != 2: - raise ValueError("expected integer tuple or pair of integers") - else: - x, y = args - - self.x = int(x) - self.y = int(y) - - @staticmethod - def _is_vector_tuple(o: Any) -> bool: - return ( - type(o) == tuple and len(o) == 2 and type(o[0]) == int and type(o[1]) == int - ) - - def manhattan_distance(self, o: Vector) -> int: - return abs(self.x - o.x) + abs(self.y - o.y) - - @property - def tuple(self) -> Tuple[int, int]: - return self.x, self.y - - def __add__(self, o: Any) -> Vector: - if Vector._is_vector_tuple(o): - return Vector(self.x + o[0], self.y + o[1]) - elif type(o) == Vector: - return Vector(self.x + o.x, self.y + o.y) - else: - raise ValueError(f"cannot add Vector and {type(o)}") - - def __sub__(self, o: Any) -> Vector: - if Vector._is_vector_tuple(o): - return Vector(self.x - o[0], self.y - o[1]) - elif type(o) == Vector: - return Vector(self.x - o.x, self.y - o.y) - else: - raise ValueError(f"cannot subtract Vector and {type(o)}") - - def __eq__(self, o: Any) -> bool: - if Vector._is_vector_tuple(o): - return self.x == o[0] and self.y == o[1] - elif type(o) == Vector: - return self.x == o.x and self.y == o.y - else: - raise ValueError(f"cannot equate Vector and {type(o)}") - - def __repr__(self) -> str: - # return f"Vector(x={self.x}, y={self.y})" - return f"({self.x}, {self.y})" - - def __hash__(self): - return hash((self.x, self.y)) - - -class Consumer: - x: Sequence[T] - i: int - - def __init__(self, x: Sequence[T]): - self.x = x - self.i = 0 - - def take(self) -> T: - self.i += 1 - return self.x[self.i - 1] - - def undo(self): - self.i -= 1 - - -class RepeatingConsumer(Consumer): - def take(self) -> T: - val = super().take() - self.i = self.i % len(self.x) - return val - - def undo(self): - super().undo() - if self.i < 0: - self.i += len(self.x) diff --git a/lib/aocpy/vis.py b/lib/aocpy/vis.py deleted file mode 100644 index c568bca..0000000 --- a/lib/aocpy/vis.py +++ /dev/null @@ -1,11 +0,0 @@ -import os.path - - -class SaveManager: - def __init__(self, d): - self.dir = d - self.current_n = 0 - - def save(self, im): - im.save(os.path.join(self.dir, f"frame_{str(self.current_n).zfill(4)}.png")) - self.current_n += 1 diff --git a/runners/buildGo.sh b/runners/buildGo.sh new file mode 100644 index 0000000..8cc2564 --- /dev/null +++ b/runners/buildGo.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +TEMPFILE=$(mktemp) +go build -o "$TEMPFILE" "$1" +echo $TEMPFILE diff --git a/runners/buildKotlin.sh b/runners/buildKotlin.sh new file mode 100644 index 0000000..d49ada7 --- /dev/null +++ b/runners/buildKotlin.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +TEMPDIR="$(mktemp -d)" +FNAME=$(basename $1 | sed 's/\.kt$/.jar/') +FULLPATH="$TEMPDIR/$FNAME" +kotlinc "$1" -include-runtime -d "$FULLPATH" +echo $FULLPATH diff --git a/runners/jar.sh b/runners/jar.sh new file mode 100644 index 0000000..857fab3 --- /dev/null +++ b/runners/jar.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -e + +java -jar $@ \ No newline at end of file diff --git a/runners/py.sh b/runners/py.sh new file mode 100644 index 0000000..1327cad --- /dev/null +++ b/runners/py.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -e + +PYTHONPATH=. python3 $@ diff --git a/runtime/README.md b/runtime/README.md deleted file mode 100644 index fbd4238..0000000 --- a/runtime/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# AoC runtime - -In brief: the runtime indexes available challenge implementations in the current working directory, loads inputs for a given challenge from disk and runs those inputs against a variety of available implementations for that challenge. - -## Challenge discovery - -The `./challenges` directory is indexed. Every subdirectory of `./challenges` is considered a "year". Every subdirectory within a given year that matches the regular expression `^(\d{2})-([a-zA-Z]+)$` (in practise, if it looks something like `05-somethingOrOther`) is considered a "challenge". - -## Challenge data - -Within every challenge, there should be a `info.json` file. This file should contain the filename of the main challenge input relative to the challenge directory and any test cases that can be run against a solution. An example is as follows: - -```json -{ - "inputFile": "input.txt", - "testCases": { - "one": [ - { - "input": "8A004A801A8002F478", - "expected": "16" - }, - { - "input": "620080001611562C8802118E34", - "expected": "12" - } - ], - "two": [ - { - "input": "C200B40A82", - "expected": "3" - }, - { - "input": "04005AC33890", - "expected": "54" - }, - { - "input": "880086C3E88112", - "expected": "7" - } - ] - } -} -``` - -## Challenge implementations - -Any subdirectory in a challenge that has a name equal to one of the map keys defined in `Available` in [`./runners/runners.go`](./runners/runners.go) is considered a "challenge implementation". - -## Running a challenge implementation - -Once a challenge and implemetation has been selected, it is run by instantiating a runner inside the challenge directory. The type of runner is dependent on the challenge implementation selected. - -Each runner will wrap the challenge code in some language specific wrapper code, then run that wrapper. - -## Communicating with running challenges - -Running challenge implemnentations recieve their inputs in JSON format, via `stdin`. A sample input might look like this: - -```json -{"task_id": "test.1.0", "part": 1, "input": "8A004A801A8002F478"} -``` - -The running challenge implementation then processes the input, and returns a result via `stdout`, which might look soemthing like this: - -```json -{"task_id": "test.1.0", "ok": true, "output": "16", "duration": 9.131431579589844e-05} -``` - -The format of both the input and output JSON is defined in [`./runners/comm.go`](./runners/comm.go) as the `Task` and `Result` structs. - -The prefix of a task ID can be used within the wrapper to determine the type of task being run. - -* A `test` prefix indicates a test is being run. -* A `main` prefix indicates that the main input is being used. -* A `vis` prefix indicates that a visualisation is being run. -* A `benchmark` prefix indicates that a given task is part of a benchmark. - -**A running challenge implementation must return results in the same order they were returned in.** - -## Debugging output - -If anything is sent by a running challenge implementation via `stdout` that is not valid JSON, it will be passed through to the `stdout` of the runtime program. - -## Stopping a running challenge implementation - -There is no way for the runtime to communicate to a running implementation that it needs to shut down. Instead, the runtime forcibly kills the running implementation. \ No newline at end of file diff --git a/runtime/benchmark/benchmark.go b/runtime/benchmark/benchmark.go deleted file mode 100644 index a996235..0000000 --- a/runtime/benchmark/benchmark.go +++ /dev/null @@ -1,182 +0,0 @@ -package benchmark - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "path/filepath" - "strings" - - "github.com/codemicro/adventOfCode/runtime/challenge" - "github.com/codemicro/adventOfCode/runtime/runners" - "github.com/schollz/progressbar/v3" -) - -func makeBenchmarkID(part runners.Part, number int) string { - if number == -1 { - return fmt.Sprintf("benchmark.part.%d", part) - } - return fmt.Sprintf("benchmark.part.%d.%d", part, number) -} - -func meanFloatSlice(arr []float64) float64 { - var sum float64 - for _, v := range arr { - sum += v - } - return sum / float64(len(arr)) -} - -func minFloatSlice(arr []float64) float64 { - min := arr[0] - for _, v := range arr { - if v < min { - min = v - } - } - return min -} - -func maxFloatSlice(arr []float64) float64 { - max := arr[0] - for _, v := range arr { - if v > max { - max = v - } - } - return max -} - -func Run(selectedChallenge *challenge.Challenge, input string, numberRuns int) error { - - implementations, err := selectedChallenge.GetImplementations() - if err != nil { - return err - } - - var valueSets []*values - - for _, implementation := range implementations { - v, err := benchmarkImplementation(implementation, selectedChallenge.Dir, input, numberRuns) - if err != nil { - return err - } - valueSets = append(valueSets, v) - } - - // make file - jdata := make(map[string]interface{}) - jdata["day"] = selectedChallenge.Number - jdata["dir"] = selectedChallenge.Dir - jdata["numRuns"] = numberRuns - jdata["implementations"] = make(map[string]interface{}) - - for _, vs := range valueSets { - x := make(map[string]interface{}) - for _, v := range vs.values { - x[v.key] = v.value - } - (jdata["implementations"].(map[string]interface{}))[vs.implementation] = x - } - - fpath := filepath.Join(selectedChallenge.Dir, "benchmark.json") - - fmt.Println("Writing results to", fpath) - - jBytes, err := json.MarshalIndent(jdata, "", " ") - if err != nil { - return err - } - - return ioutil.WriteFile( - fpath, - jBytes, - 0644, - ) -} - -type values struct { - implementation string - values []kv -} - -type kv struct { - key string - value float64 -} - -func benchmarkImplementation(implementation string, dir string, inputString string, numberRuns int) (*values, error) { - - var ( - tasks []*runners.Task - results []*runners.Result - ) - - runner := runners.Available[implementation](dir) - for i := 0; i < numberRuns; i++ { - - tasks = append(tasks, &runners.Task{ - TaskID: makeBenchmarkID(runners.PartOne, i), - Part: runners.PartOne, - Input: inputString, - }, &runners.Task{ - TaskID: makeBenchmarkID(runners.PartTwo, i), - Part: runners.PartTwo, - Input: inputString, - }) - - } - - pb := progressbar.NewOptions( - numberRuns * 2, // two parts means 2x the number of runs - progressbar.OptionSetDescription( - fmt.Sprintf("Running %s benchmarks", runners.RunnerNames[implementation]), - ), - ) - - if err := runner.Start(); err != nil { - return nil, err - } - defer func() { - _ = runner.Stop() - _ = runner.Cleanup() - }() - - for _, task := range tasks { - res, err := runner.Run(task) - if err != nil { - _ = pb.Close() - return nil, err - } - results = append(results, res) - _ = pb.Add(1) - } - - fmt.Println() - - var ( - p1, p2 []float64 - p1id = makeBenchmarkID(runners.PartOne, -1) - p2id = makeBenchmarkID(runners.PartTwo, -1) - ) - - for _, result := range results { - if strings.HasPrefix(result.TaskID, p1id) { - p1 = append(p1, result.Duration) - } else if strings.HasPrefix(result.TaskID, p2id) { - p2 = append(p2, result.Duration) - } - } - - return &values{ - implementation: runners.RunnerNames[implementation], - values: []kv{ - {"part.1.avg", meanFloatSlice(p1)}, - {"part.1.min", minFloatSlice(p1)}, - {"part.1.max", maxFloatSlice(p1)}, - {"part.2.avg", meanFloatSlice(p2)}, - {"part.2.min", minFloatSlice(p2)}, - {"part.2.max", maxFloatSlice(p2)}, - }, - }, nil -} \ No newline at end of file diff --git a/runtime/challenge/challenge.go b/runtime/challenge/challenge.go deleted file mode 100644 index 5350690..0000000 --- a/runtime/challenge/challenge.go +++ /dev/null @@ -1,53 +0,0 @@ -package challenge - -import ( - "fmt" - "os" - "path/filepath" - "regexp" - "strconv" - "strings" - - "github.com/codemicro/adventOfCode/runtime/util" -) - -type Challenge struct { - Number int - Name string - Dir string -} - -func (c *Challenge) String() string { - return fmt.Sprintf("%d - %s", c.Number, c.Name) -} - -var challengeDirRegexp = regexp.MustCompile(`(?m)^(\d{2})-([a-zA-Z]+)$`) - -func ListingFromDir(sourceDir string) ([]*Challenge, error) { - - dirEntries, err := os.ReadDir(sourceDir) - if err != nil { - return nil, err - } - - var o []*Challenge - for _, entry := range dirEntries { - - if entry.IsDir() && challengeDirRegexp.MatchString(entry.Name()) { - dir := entry.Name() - - x := strings.Split(dir, "-") - dayInt, _ := strconv.Atoi(x[0]) // error ignored because regex should have ensured this is ok - dayTitle := util.CamelToTitle(x[1]) - o = append(o, &Challenge{ - Number: dayInt, - Name: dayTitle, - Dir: filepath.Join(sourceDir, dir), - }) - - } - - } - - return o, nil -} diff --git a/runtime/challenge/challengeInfo.go b/runtime/challenge/challengeInfo.go deleted file mode 100644 index 0ad523b..0000000 --- a/runtime/challenge/challengeInfo.go +++ /dev/null @@ -1,35 +0,0 @@ -package challenge - -import ( - "encoding/json" - "io/ioutil" -) - -type Info struct { - InputFile string `json:"inputFile"` - TestCases struct { - One []*TestCase `json:"one"` - Two []*TestCase `json:"two"` - } `json:"testCases"` -} - -type TestCase struct { - Input string `json:"input"` - Expected string `json:"expected"` -} - -func LoadChallengeInfo(fname string) (*Info, error) { - - fcont, err := ioutil.ReadFile(fname) - if err != nil { - return nil, err - } - - c := new(Info) - err = json.Unmarshal(fcont, c) - if err != nil { - return nil, err - } - - return c, nil -} diff --git a/runtime/challenge/implementations.go b/runtime/challenge/implementations.go deleted file mode 100644 index b3f5880..0000000 --- a/runtime/challenge/implementations.go +++ /dev/null @@ -1,26 +0,0 @@ -package challenge - -import ( - "github.com/codemicro/adventOfCode/runtime/runners" - "os" - "strings" -) - -func (c *Challenge) GetImplementations() ([]string, error) { - dirEntries, err := os.ReadDir(c.Dir) - if err != nil { - return nil, err - } - - var o []string - for _, de := range dirEntries { - if !de.IsDir() { - continue - } - if _, ok := runners.Available[strings.ToLower(de.Name())]; ok { - o = append(o, de.Name()) - } - } - - return o, nil -} \ No newline at end of file diff --git a/runtime/main.go b/runtime/main.go deleted file mode 100644 index 74f571f..0000000 --- a/runtime/main.go +++ /dev/null @@ -1,147 +0,0 @@ -package main - -import ( - "fmt" - "io/ioutil" - "path/filepath" - "strings" - - "github.com/codemicro/adventOfCode/runtime/benchmark" - au "github.com/logrusorgru/aurora" - - "github.com/alexflint/go-arg" - "github.com/codemicro/adventOfCode/runtime/challenge" - "github.com/codemicro/adventOfCode/runtime/runners" -) - -const ( - challengeDir = "challenges" - challengeInfoFile = "info.json" -) - -var args struct { - Year string `arg:"-y,--year" help:"AoC year to use"` - ChallengeDay *int `arg:"-d,--day" help:"challenge day number to run"` - Implementation string `arg:"-i,--implementation" help:"implementation to use"` - Benchmark bool `arg:"-b,--benchmark" help:"benchmark a day's implementations'"` - BenchmarkN int `arg:"-n,--benchmark-n" help:"Number of iterations to run for benchmarking" default:"1000"` - TestOnly bool `arg:"-t,--test-only" help:"Only run test inputs"` - NoTest bool `arg:"-x,--no-test" help:"Do not run test inputs"` - Visualise bool `arg:"-g,--visualise" help:"Run visualisation generation"` -} - -func run() error { - - arg.MustParse(&args) - - // List and select year - selectedYear, err := selectYear(challengeDir) - if err != nil { - return err - } - - // List and select challenges - selectedChallenge, err := selectChallenge(selectedYear) - if err != nil { - return err - } - - // Load info.json file - challengeInfo, err := challenge.LoadChallengeInfo(filepath.Join(selectedChallenge.Dir, challengeInfoFile)) - if err != nil { - return err - } - - // Load challenge input - challengeInput, err := ioutil.ReadFile(filepath.Join(selectedChallenge.Dir, challengeInfo.InputFile)) - if err != nil { - return err - } - challengeInputString := string(challengeInput) - - if args.Benchmark { - return benchmark.Run(selectedChallenge, challengeInputString, args.BenchmarkN) - } - - // List and select implementations - selectedImplementation, err := selectImplementation(selectedChallenge) - if err != nil { - return err - } - - fmt.Print( - au.Bold( - fmt.Sprintf( - "%s-%d %s (%s)\n\n", - strings.TrimPrefix(selectedYear, "challenges/"), - selectedChallenge.Number, - selectedChallenge.Name, - runners.RunnerNames[selectedImplementation], - ), - ), - ) - - runner := runners.Available[selectedImplementation](selectedChallenge.Dir) - if err := runner.Start(); err != nil { - return err - } - defer func() { - _ = runner.Stop() - _ = runner.Cleanup() - }() - - if args.Visualise { - id := "vis" - r, err := runner.Run(&runners.Task{ - TaskID: id, - Part: runners.Visualise, - Input: challengeInputString, - OutputDir: ".", // directory the runner is run in, which is the challenge directory - }) - if err != nil { - return err - } - - fmt.Print(au.Bold("Visualisation: ")) - - var status string - var followUpText string - if !r.Ok { - status = incompleteLabel - followUpText = "saying \"" + r.Output + "\"" - } else { - status = passLabel - } - - if followUpText == "" { - followUpText = fmt.Sprintf("in %.4f seconds", r.Duration) - } - - fmt.Print(status) - fmt.Println(au.Gray(10, " "+followUpText)) - - } else { - - fmt.Print("Running...\n\n") - - if !args.NoTest { - if err := runTests(runner, challengeInfo); err != nil { - return err - } - } - - if !args.TestOnly { - if err := runMainTasks(runner, challengeInputString); err != nil { - return err - } - } - } - - return nil -} - -func main() { - if err := run(); err != nil { - panic(err) - } -} diff --git a/runtime/runners/comm.go b/runtime/runners/comm.go deleted file mode 100644 index 588f148..0000000 --- a/runtime/runners/comm.go +++ /dev/null @@ -1,108 +0,0 @@ -package runners - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "os/exec" - "strings" - "sync" - "time" - - au "github.com/logrusorgru/aurora" -) - -type Task struct { - TaskID string `json:"task_id"` - Part Part `json:"part"` - Input string `json:"input"` - OutputDir string `json:"output_dir,omitempty"` -} - -type Result struct { - TaskID string `json:"task_id"` - Ok bool `json:"ok"` - Output string `json:"output"` - Duration float64 `json:"duration"` -} - -type customWriter struct { - pending []byte - entries [][]byte - mux sync.Mutex -} - -func (c *customWriter) Write(b []byte) (int, error) { - var n int - - c.mux.Lock() - for _, x := range b { - if x == '\n' { - c.entries = append(c.entries, c.pending) - c.pending = nil - } else { - c.pending = append(c.pending, x) - } - n += 1 - } - c.mux.Unlock() - - return n, nil -} - -func (c *customWriter) GetEntry() ([]byte, error) { - c.mux.Lock() - defer c.mux.Unlock() - if len(c.entries) == 0 { - return nil, errors.New("no entries") - } - var x []byte - x, c.entries = c.entries[0], c.entries[1:] - return x, nil -} - -func setupBuffers(cmd *exec.Cmd) (io.WriteCloser, error) { - stdoutWriter := &customWriter{} - cmd.Stdout = stdoutWriter - cmd.Stderr = new(bytes.Buffer) - return cmd.StdinPipe() -} - -func checkWait(cmd *exec.Cmd) ([]byte, error) { - c := cmd.Stdout.(*customWriter) - for { - e, err := c.GetEntry() - if err == nil { - return e, nil - } - - if cmd.ProcessState != nil { - // this is only populated after program exit - we have an issue - return nil, fmt.Errorf("run failed with exit code %d: %s", cmd.ProcessState.ExitCode(), cmd.Stderr.(*bytes.Buffer).String()) - } - - time.Sleep(time.Millisecond * 10) - } -} - -func readJSONFromCommand(res interface{}, cmd *exec.Cmd) error { - - for { - inp, err := checkWait(cmd) - if err != nil { - return err - } - - err = json.Unmarshal(inp, res) - if err != nil { - // echo anything that won't parse to stdout (this lets us add debug print statements) - fmt.Printf("[%s] %v\n", au.BrightRed("DBG"), strings.TrimSpace(string(inp))) - } else { - break - } - } - - return nil -} diff --git a/runtime/runners/golangRunner.go b/runtime/runners/golangRunner.go deleted file mode 100644 index 9f1bdda..0000000 --- a/runtime/runners/golangRunner.go +++ /dev/null @@ -1,128 +0,0 @@ -package runners - -import ( - "bytes" - _ "embed" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "text/template" -) - -const ( - golangInstallation = "go" - golangWrapperFilename = "runtime-wrapper.go" - golangWrapperExecutableFilename = "runtime-wrapper" -) - -type golangRunner struct { - dir string - cmd *exec.Cmd - wrapperFilepath string - executableFilepath string - stdin io.WriteCloser -} - -func newGolangRunner(dir string) Runner { - return &golangRunner{ - dir: dir, - } -} - -//go:embed interface/go.go -var golangInterface []byte - -func (g *golangRunner) Start() error { - g.wrapperFilepath = filepath.Join(g.dir, golangWrapperFilename) - g.executableFilepath = filepath.Join(g.dir, golangWrapperExecutableFilename) - - // determine package import path - buildPath := fmt.Sprintf("github.com/codemicro/adventOfCode/challenges/%s/%s", filepath.Base(filepath.Dir(g.dir)), filepath.Base(g.dir)) - importPath := buildPath + "/go" - - // generate code - var wrapperContent []byte - { - tpl := template.Must(template.New("").Parse(string(golangInterface))) - b := new(bytes.Buffer) - err := tpl.Execute(b, struct { - ImportPath string - }{importPath}) - if err != nil { - return err - } - wrapperContent = b.Bytes() - } - - // save interaction code - if err := ioutil.WriteFile(g.wrapperFilepath, wrapperContent, 0644); err != nil { - return err - } - - // compile executable - stderrBuffer := new(bytes.Buffer) - - cmd := exec.Command(golangInstallation, "build", "-tags", "runtime", "-o", g.executableFilepath, buildPath) - cmd.Stderr = stderrBuffer - if err := cmd.Run(); err != nil { - return fmt.Errorf("compilation failed: %s: %s", err, stderrBuffer.String()) - } - - if !cmd.ProcessState.Success() { - return errors.New("compilation failed, hence cannot continue") - } - - // now we run! - absExecPath, err := filepath.Abs(g.executableFilepath) - if err != nil { - return err - } - - // run executable - g.cmd = exec.Command(absExecPath) - cmd.Dir = g.dir - - if stdin, err := setupBuffers(g.cmd); err != nil { - return err - } else { - g.stdin = stdin - } - - return g.cmd.Start() -} - -func (g *golangRunner) Stop() error { - if g.cmd == nil || g.cmd.Process == nil { - return nil - } - return g.cmd.Process.Kill() -} - -func (g *golangRunner) Cleanup() error { - if g.wrapperFilepath != "" { - _ = os.Remove(g.wrapperFilepath) - } - if g.executableFilepath != "" { - _ = os.Remove(g.executableFilepath) - } - return nil -} - -func (g *golangRunner) Run(task *Task) (*Result, error) { - taskJSON, err := json.Marshal(task) - if err != nil { - return nil, err - } - _, _ = g.stdin.Write(append(taskJSON, '\n')) - - res := new(Result) - if err := readJSONFromCommand(res, g.cmd); err != nil { - return nil, err - } - return res, nil -} diff --git a/runtime/runners/interface/go.go b/runtime/runners/interface/go.go deleted file mode 100644 index 2fd55d2..0000000 --- a/runtime/runners/interface/go.go +++ /dev/null @@ -1,80 +0,0 @@ -//+build runtime - -package main - -import ( - "bufio" - "encoding/json" - "fmt" - "github.com/codemicro/adventOfCode/runtime/runners" - "os" - "time" - chcode "{{ .ImportPath }}" -) - -func sendResult(taskID string, ok bool, output string, duration float64) { - x := runners.Result{ - TaskID: taskID, - Ok: ok, - Output: output, - Duration: duration, - } - dat, err := json.Marshal(&x) - if err != nil { - panic(err) - } - fmt.Println(string(dat)) -} - -func run() error { - reader := bufio.NewReader(os.Stdin) - for { - task := new(runners.Task) - taskBytes, err := reader.ReadBytes('\n') - if err != nil { - return err - } - if err := json.Unmarshal(taskBytes, task); err != nil { - return err - } - - var run func() (interface{}, error) - - switch task.Part { - case runners.PartOne: - run = func() (interface{}, error) { - return (chcode.Challenge{}).One(task.Input) - } - case runners.PartTwo: - run = func() (interface{}, error) { - return (chcode.Challenge{}).Two(task.Input) - } - case runners.Visualise: - run = func() (interface{}, error) { - return "", (chcode.Challenge{}).Vis(task.Input, task.OutputDir) - } - } - - startTime := time.Now() - res, err := run() - endTIme := time.Now() - - runningTime := endTIme.Sub(startTime).Seconds() - - if err != nil { - sendResult(task.TaskID, false, err.Error(), runningTime) - } else { - sendResult(task.TaskID, true, fmt.Sprintf("%v", res), runningTime) - } - - } - - return nil - -} - -func main() { - if err := run(); err != nil { - panic(err) - } -} diff --git a/runtime/runners/interface/nim.nim b/runtime/runners/interface/nim.nim deleted file mode 100644 index 8e16c7f..0000000 --- a/runtime/runners/interface/nim.nim +++ /dev/null @@ -1,58 +0,0 @@ -import std/strutils -import std/options -import std/json -import std/monotimes -import std/times - -from nim/challenge as solutions import nil - -proc sendResult(taskID: string, ok: bool, output: string, duration: float64) = - let jobj = %* { - "task_id": taskID, - "ok": ok, - "output": output, - "duration": duration, - } - echo $jobj - -type - Task = ref object - task_id: string - part: int - input: string - output_dir: Option[string] - -while true: - - let - taskString = readLine(stdin) - task = to(parseJson(taskString), Task) - - var runProc: proc(): string - - case task.part - of 1: - runProc = proc(): string = $(solutions.partOne(task.input)) - of 2: - runProc = proc(): string = $(solutions.partTwo(task.input)) - else: - sendResult(task.task_id, false, "unknown task part", 0.0) - - var - result: string - error: string - - let startTime = getMonoTime() - try: - result = runProc() - except: - error = getCurrentExceptionMsg() - let endTime = getMonoTime() - - let runningTime = endTime - startTime - let runningTimeSeconds = float(inNanoseconds(runningTime)) / float(1000000000) - - if error != "": - sendResult(task.task_id, false, error, runningTimeSeconds) - else: - sendResult(task.task_id, true, result, runningTimeSeconds) \ No newline at end of file diff --git a/runtime/runners/interface/python.py b/runtime/runners/interface/python.py deleted file mode 100644 index 82c5a02..0000000 --- a/runtime/runners/interface/python.py +++ /dev/null @@ -1,58 +0,0 @@ -from py import Challenge - -import time -import json - -# TASKS_STR = input() -# TASKS = json.loads(TASKS_STR) - - -def send_result(task_id, ok, output, duration): - print( - json.dumps( - { - "task_id": task_id, - "ok": ok, - "output": str(output) if output is not None else "", - "duration": float(duration), - } - ), - flush=True, - ) - - -while True: - task = json.loads(input()) - taskPart = task["part"] - task_id = task["task_id"] - - run = None - - if taskPart == 1: - run = lambda: Challenge.one(task["input"]) - elif taskPart == 2: - run = lambda: Challenge.two(task["input"]) - elif taskPart == 3: - run = lambda: Challenge.vis(task["input"], task["output_dir"]) - else: - send_result(task_id, False, "unknown task part", 0) - continue - - start_time = time.time() - res = None - err = None - try: - res = run() - except Exception as e: - err = f"{type(e)}: {e}" - import traceback - - err = f"{type(e)}: {e}\n{''.join(traceback.format_tb(e.__traceback__))}" - end_time = time.time() - - running_time = end_time - start_time - - if err is not None: - send_result(task_id, False, err, running_time) - else: - send_result(task_id, True, res, running_time) diff --git a/runtime/runners/nimRunner.go b/runtime/runners/nimRunner.go deleted file mode 100644 index e6c27ee..0000000 --- a/runtime/runners/nimRunner.go +++ /dev/null @@ -1,109 +0,0 @@ -package runners - -import ( - "bytes" - _ "embed" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "os/exec" - "path/filepath" -) - -const ( - nimInstallation = "nim" - nimWrapperFilename = "runtimeWrapper.nim" - nimWrapperExecutableFilename = "runtimeWrapper" -) - -type nimRunner struct { - dir string - cmd *exec.Cmd - wrapperFilepath string - executableFilepath string - stdin io.WriteCloser -} - -func newNimRunner(dir string) Runner { - return &nimRunner{ - dir: dir, - } -} - -//go:embed interface/nim.nim -var nimInterface []byte - -func (n *nimRunner) Start() error { - n.wrapperFilepath = filepath.Join(n.dir, nimWrapperFilename) - n.executableFilepath = filepath.Join(n.dir, nimWrapperExecutableFilename) - - // save interaction code - err := ioutil.WriteFile(n.wrapperFilepath, nimInterface, 0644) - if err != nil { - return err - } - - // compile - stderrBuffer := new(bytes.Buffer) - cmd := exec.Command(nimInstallation, "compile", "-o:"+n.executableFilepath, "-d:release", n.wrapperFilepath) - cmd.Stderr = stderrBuffer - err = cmd.Run() - if err != nil { - return fmt.Errorf("compilation failed: %s: %s", err, stderrBuffer.String()) - } - - if !cmd.ProcessState.Success() { - return errors.New("compilation failed, hence cannot continue") - } - - // now we run! - absExecPath, err := filepath.Abs(n.executableFilepath) - if err != nil { - return err - } - - n.cmd = exec.Command(absExecPath) - n.cmd.Dir = n.dir - - if stdin, err := setupBuffers(n.cmd); err != nil { - return err - } else { - n.stdin = stdin - } - - return n.cmd.Start() -} - -func (n *nimRunner) Stop() error { - if n.cmd == nil || n.cmd.Process == nil { - return nil - } - return n.cmd.Process.Kill() -} - -func (n *nimRunner) Cleanup() error { - if n.wrapperFilepath != "" { - _ = os.Remove(n.wrapperFilepath) - } - if n.executableFilepath != "" { - _ = os.Remove(n.executableFilepath) - } - return nil -} - -func (n *nimRunner) Run(task *Task) (*Result, error) { - taskJSON, err := json.Marshal(task) - if err != nil { - return nil, err - } - _, _ = n.stdin.Write(append(taskJSON, '\n')) - - res := new(Result) - if err := readJSONFromCommand(res, n.cmd); err != nil { - return nil, err - } - return res, nil -} diff --git a/runtime/runners/pythonRunner.go b/runtime/runners/pythonRunner.go deleted file mode 100644 index 56fb161..0000000 --- a/runtime/runners/pythonRunner.go +++ /dev/null @@ -1,99 +0,0 @@ -package runners - -import ( - _ "embed" - "encoding/json" - "io" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strings" -) - -const ( - python3Installation = "python3" - pythonWrapperFilename = "runtime-wrapper.py" -) - -type pythonRunner struct { - dir string - cmd *exec.Cmd - wrapperFilepath string - stdin io.WriteCloser -} - -func newPythonRunner(dir string) Runner { - return &pythonRunner{ - dir: dir, - } -} - -//go:embed interface/python.py -var pythonInterface []byte - -func (p *pythonRunner) Start() error { - p.wrapperFilepath = filepath.Join(p.dir, pythonWrapperFilename) - - // Save interaction code - if err := ioutil.WriteFile(p.wrapperFilepath, pythonInterface, 0644); err != nil { - return err - } - - // Sort out PYTHONPATH - cwd, err := os.Getwd() - if err != nil { - return err - } - - absDir, err := filepath.Abs(p.dir) - if err != nil { - return err - } - - pythonPathVar := strings.Join([]string{ - filepath.Join(cwd, "lib"), // so we can use aocpy - filepath.Join(absDir, "py"), // so we can import stuff in the challenge directory - }, ":") - - p.cmd = exec.Command(python3Installation, "-B", pythonWrapperFilename) // -B prevents .pyc files from being written - p.cmd.Env = append(p.cmd.Env, "PYTHONPATH="+pythonPathVar) - p.cmd.Dir = p.dir - - if stdin, err := setupBuffers(p.cmd); err != nil { - return err - } else { - p.stdin = stdin - } - - return p.cmd.Start() -} - -func (p *pythonRunner) Stop() error { - if p.cmd == nil || p.cmd.Process == nil { - return nil - } - return p.cmd.Process.Kill() -} - -func (p *pythonRunner) Cleanup() error { - if p.wrapperFilepath == "" { - return nil - } - _ = os.Remove(p.wrapperFilepath) - return nil -} - -func (p *pythonRunner) Run(task *Task) (*Result, error) { - taskJSON, err := json.Marshal(task) - if err != nil { - return nil, err - } - _, _ = p.stdin.Write(append(taskJSON, '\n')) - - res := new(Result) - if err := readJSONFromCommand(res, p.cmd); err != nil { - return nil, err - } - return res, nil -} diff --git a/runtime/runners/runners.go b/runtime/runners/runners.go deleted file mode 100644 index 5ab52df..0000000 --- a/runtime/runners/runners.go +++ /dev/null @@ -1,35 +0,0 @@ -package runners - -type Part uint8 - -const ( - PartOne Part = iota + 1 - PartTwo - Visualise -) - -type Runner interface { - Start() error - Stop() error - Cleanup() error - Run(task *Task) (*Result, error) -} - -type ResultOrError struct { - Result *Result - Error error -} - -type RunnerCreator func(dir string) Runner - -var Available = map[string]RunnerCreator{ - "py": newPythonRunner, - "go": newGolangRunner, - "nim": newNimRunner, -} - -var RunnerNames = map[string]string{ - "py": "Python", - "go": "Golang", - "nim": "Nim", -} \ No newline at end of file diff --git a/runtime/selections.go b/runtime/selections.go deleted file mode 100644 index c0c27f0..0000000 --- a/runtime/selections.go +++ /dev/null @@ -1,154 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "github.com/AlecAivazis/survey/v2" - "github.com/codemicro/adventOfCode/runtime/challenge" - "github.com/codemicro/adventOfCode/runtime/runners" - "os" - "path/filepath" - "strings" -) - -func userSelect(question string, choices []string) (int, error) { - var o string - prompt := &survey.Select{ - Message: question, - Options: choices, - } - //err := survey.AskOne(prompt, &o, survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)) - err := survey.AskOne(prompt, &o) - if err != nil { - return 0, err - } - - for i, x := range choices { - if x == o { - return i, nil - } - } - - return -1, nil -} - -func selectYear(dir string) (string, error) { - - var opts []string - - entries, err := os.ReadDir(dir) - if err != nil { - return "", err - } - - for _, entry := range entries { - if !entry.IsDir() { - continue - } - opts = append(opts, entry.Name()) - } - - if len(opts) == 0 { - return "", errors.New("no years to use") - } - - if args.Year != "" { - for _, x := range opts { - if x == args.Year { - return filepath.Join(dir, x), nil - } - } - fmt.Printf("Could not locate year %s\n", args.Year) - } - - var selectedYearIndex int - - if x := len(opts); x == 1 { - selectedYearIndex = 0 - } else { - selectedYearIndex, err = userSelect("Which year do you want to use?", opts) - if err != nil { - return "", err - } - } - - return filepath.Join(dir, opts[selectedYearIndex]), nil -} - -func selectChallenge(dir string) (*challenge.Challenge, error) { - - challenges, err := challenge.ListingFromDir(dir) - if err != nil { - return nil, err - } - - if len(challenges) == 0 { - return nil, errors.New("no challenges to run") - } - - if args.ChallengeDay != nil { - for _, ch := range challenges { - if ch.Number == *args.ChallengeDay { - return ch, nil - } - } - fmt.Printf("Could not locate day %d\n", *args.ChallengeDay) - } - - var selectedChallengeIndex int - - if x := len(challenges); x == 1 { - selectedChallengeIndex = 0 - } else { - var opts []string - for _, c := range challenges { - opts = append(opts, c.String()) - } - - selectedChallengeIndex, err = userSelect("Which challenge do you want to run?", opts) - if err != nil { - return nil, err - } - } - - return challenges[selectedChallengeIndex], nil -} - -func selectImplementation(ch *challenge.Challenge) (string, error) { - - implementations, err := ch.GetImplementations() - if err != nil { - return "", err - } - - if len(implementations) == 0 { - return "", errors.New("no implementations to use") - } - - if args.Implementation != "" { - for _, im := range implementations { - if strings.EqualFold(im, args.Implementation) { - return im, nil - } - } - fmt.Printf("Could not locate implementation %#v\n", args.Implementation) - } - - var selectedImplementationIndex int - - if x := len(implementations); x == 1 { - selectedImplementationIndex = 0 - } else { - var opts []string - for _, i := range implementations { - opts = append(opts, runners.RunnerNames[i]) - } - - selectedImplementationIndex, err = userSelect("Which implementation do you want to use?", opts) - if err != nil { - return "", err - } - } - - return implementations[selectedImplementationIndex], nil -} diff --git a/runtime/tasks.go b/runtime/tasks.go deleted file mode 100644 index 653bc9e..0000000 --- a/runtime/tasks.go +++ /dev/null @@ -1,132 +0,0 @@ -package main - -import ( - "fmt" - "strconv" - "strings" - - "github.com/codemicro/adventOfCode/runtime/challenge" - "github.com/codemicro/adventOfCode/runtime/runners" - au "github.com/logrusorgru/aurora" -) - -var ( - passLabel = au.BrightGreen("pass").String() - failLabel = au.BrightRed("fail").String() - incompleteLabel = au.BgBrightRed("did not complete").String() -) - -func makeTestID(part runners.Part, n int) string { - return fmt.Sprintf("test.%d.%d", part, n) -} - -func parseTestID(x string) (runners.Part, int) { - y := strings.Split(x, ".") - p, _ := strconv.Atoi(y[1]) - n, _ := strconv.Atoi(y[2]) - return runners.Part(p), n -} - -func makeMainID(part runners.Part) string { - return fmt.Sprintf("main.%d", part) -} - -func parseMainID(x string) runners.Part { - y := strings.Split(x, ".") - p, _ := strconv.Atoi(y[1]) - return runners.Part(p) -} - -func runTests(runner runners.Runner, info *challenge.Info) error { - for i, testCase := range info.TestCases.One { - id := makeTestID(runners.PartOne, i) - result, err := runner.Run(&runners.Task{ - TaskID: id, - Part: runners.PartOne, - Input: testCase.Input, - }) - if err != nil { - return err - } - - handleTestResult(result, testCase) - } - - for i, testCase := range info.TestCases.Two { - id := makeTestID(runners.PartTwo, i) - result, err := runner.Run(&runners.Task{ - TaskID: id, - Part: runners.PartTwo, - Input: testCase.Input, - }) - if err != nil { - return err - } - - handleTestResult(result, testCase) - } - - return nil -} - -func handleTestResult(r *runners.Result, testCase *challenge.TestCase) { - part, n := parseTestID(r.TaskID) - - fmt.Print(au.Bold(fmt.Sprintf("Test %s: ", - au.BrightBlue(fmt.Sprintf("%d.%d", part, n)), - ))) - - passed := r.Output == testCase.Expected - - var status string - var followUpText string - if !r.Ok { - status = incompleteLabel - followUpText = "saying \"" + r.Output + "\"" - } else if passed { - status = passLabel - } else { - status = failLabel - } - - if followUpText == "" { - followUpText = fmt.Sprintf("in %.4f seconds", r.Duration) - } - - fmt.Print(status) - fmt.Println(au.Gray(10, " "+followUpText)) - - if !passed && r.Ok { - fmt.Printf(" └ Expected %s, got %s\n", au.BrightBlue(testCase.Expected), au.BrightBlue(r.Output)) - } -} - -func runMainTasks(runner runners.Runner, input string) error { - for part := runners.PartOne; part <= runners.PartTwo; part += 1 { - id := makeMainID(part) - result, err := runner.Run(&runners.Task{ - TaskID: id, - Part: part, - Input: input, - }) - if err != nil { - return err - } - handleMainResult(result) - } - return nil -} - -func handleMainResult(r *runners.Result) { - part := parseMainID(r.TaskID) - - fmt.Print(au.Bold(fmt.Sprintf("Part %d: ", au.Yellow(part)))) - - if !r.Ok { - fmt.Print(incompleteLabel) - fmt.Println(au.Gray(10, " saying \""+r.Output+"\"")) - } else { - fmt.Print(au.BrightBlue(r.Output)) - fmt.Println(au.Gray(10, fmt.Sprintf(" in %.4f seconds", r.Duration))) - } -} \ No newline at end of file diff --git a/runtime/util/util.go b/runtime/util/util.go deleted file mode 100644 index 4f96af5..0000000 --- a/runtime/util/util.go +++ /dev/null @@ -1,19 +0,0 @@ -package util - -import ( - "unicode" -) - -func CamelToTitle(x string) string { - var o string - for i, char := range x { - if i == 0 { - o += string(unicode.ToUpper(char)) - } else if unicode.IsUpper(char) { - o += " " + string(char) - } else { - o += string(char) - } - } - return o -} \ No newline at end of file diff --git a/template/cookiecutter.json b/template/cookiecutter.json deleted file mode 100644 index e666ea4..0000000 --- a/template/cookiecutter.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "_extensions": ["local_extensions.camel_case", "local_extensions.current_year", "local_extensions.current_day"], - - "year": "{{ 0 | current_year }}", - "dayNumber": "{{ 0 | current_day }}", - "challengeTitle": null, - - "__formattedTitle": "{{ cookiecutter.challengeTitle | title }}", - "__camelTitle": "{{ cookiecutter.challengeTitle | camel_case }}", - "__formattedDayNumber": "{{ '%02d' | format(cookiecutter.dayNumber|int) }}" -} - diff --git a/template/local_extensions.py b/template/local_extensions.py deleted file mode 100644 index b03c4e4..0000000 --- a/template/local_extensions.py +++ /dev/null @@ -1,25 +0,0 @@ -import datetime -from cookiecutter.utils import simple_filter - - -@simple_filter -def camel_case(v): - res = v.split(" ") - for i in range(len(res)): - f = lambda x: x.upper() - if i == 0: - f = lambda x: x.lower() - - res[i] = f(res[i][0]) + res[i][1:] - - return "".join(res) - - -@simple_filter -def current_year(_): - return datetime.datetime.now().year - - -@simple_filter -def current_day(_): - return datetime.datetime.now().day diff --git a/template/{{cookiecutter.__formattedDayNumber}}-{{cookiecutter.__camelTitle}}/README.md b/template/{{cookiecutter.__formattedDayNumber}}-{{cookiecutter.__camelTitle}}/README.md deleted file mode 100644 index 2ea9750..0000000 --- a/template/{{cookiecutter.__formattedDayNumber}}-{{cookiecutter.__camelTitle}}/README.md +++ /dev/null @@ -1 +0,0 @@ -# [Day {{ cookiecutter.dayNumber }}: {{ cookiecutter.__formattedTitle }}](https://adventofcode.com/{{ cookiecutter.year }}/day/{{ cookiecutter.dayNumber }}) diff --git a/template/{{cookiecutter.__formattedDayNumber}}-{{cookiecutter.__camelTitle}}/go/challenge.go b/template/{{cookiecutter.__formattedDayNumber}}-{{cookiecutter.__camelTitle}}/go/challenge.go deleted file mode 100644 index 105731f..0000000 --- a/template/{{cookiecutter.__formattedDayNumber}}-{{cookiecutter.__camelTitle}}/go/challenge.go +++ /dev/null @@ -1,15 +0,0 @@ -package challenge - -import "github.com/codemicro/adventOfCode/lib/aocgo" - -type Challenge struct { - aocgo.BaseChallenge -} - -func (c Challenge) One(instr string) (any, error) { - return nil, nil -} - -func (c Challenge) Two(instr string) (any, error) { - return nil, nil -} \ No newline at end of file diff --git a/template/{{cookiecutter.__formattedDayNumber}}-{{cookiecutter.__camelTitle}}/info.json b/template/{{cookiecutter.__formattedDayNumber}}-{{cookiecutter.__camelTitle}}/info.json deleted file mode 100644 index 1666e85..0000000 --- a/template/{{cookiecutter.__formattedDayNumber}}-{{cookiecutter.__camelTitle}}/info.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "inputFile": "input.txt", - "testCases": { - "one": [ - { - "input": "", - "expected": "" - } - ], - "two": [ - { - "input": "", - "expected": "" - } - ] - } -} \ No newline at end of file diff --git a/template/{{cookiecutter.__formattedDayNumber}}-{{cookiecutter.__camelTitle}}/nim/challenge.nim b/template/{{cookiecutter.__formattedDayNumber}}-{{cookiecutter.__camelTitle}}/nim/challenge.nim deleted file mode 100644 index 702b398..0000000 --- a/template/{{cookiecutter.__formattedDayNumber}}-{{cookiecutter.__camelTitle}}/nim/challenge.nim +++ /dev/null @@ -1,5 +0,0 @@ -proc partOne*(instr: string): untyped = - raise newException(CatchableError, "not implemented") - -proc partTwo*(instr: string): untyped = - raise newException(CatchableError, "not implemented") \ No newline at end of file diff --git a/template/{{cookiecutter.__formattedDayNumber}}-{{cookiecutter.__camelTitle}}/py/__init__.py b/template/{{cookiecutter.__formattedDayNumber}}-{{cookiecutter.__camelTitle}}/py/__init__.py deleted file mode 100644 index 885d677..0000000 --- a/template/{{cookiecutter.__formattedDayNumber}}-{{cookiecutter.__camelTitle}}/py/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import * -from aocpy import BaseChallenge - - -class Challenge(BaseChallenge): - @staticmethod - def one(instr: str) -> int: - raise NotImplementedError - - @staticmethod - def two(instr: str) -> int: - raise NotImplementedError diff --git a/templates/main.go b/templates/main.go new file mode 100644 index 0000000..7747e24 --- /dev/null +++ b/templates/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "io" + "os" + "strings" +) + +func parse(instr string) any { + return nil +} + +func one(instr string) int { + return -1 +} + +func two(instr string) int { + return -1 +} + +func main() { + if len(os.Args) < 2 || !(os.Args[1] == "1" || os.Args[1] == "2") { + debug("Missing day argument") + os.Exit(1) + } + + inp, err := io.ReadAll(os.Stdin) + if err != nil { + panic(err) + } + inpStr := strings.TrimSpace(string(inp)) + + switch os.Args[1] { + case "1": + fmt.Println(one(inpStr)) + case "2": + fmt.Println(two(inpStr)) + } +} + +func debug(f string, sub ...any) { + fmt.Fprintf(os.Stderr, f, sub...) +} diff --git a/templates/main.hs b/templates/main.hs new file mode 100644 index 0000000..cd5a6b1 --- /dev/null +++ b/templates/main.hs @@ -0,0 +1,29 @@ +import System.Environment +import System.Exit +import System.IO +import Data.Maybe + +type ChallengeReturn = String + +one :: String -> ChallengeReturn +one inp = undefined + +two :: String -> ChallengeReturn +two inp = undefined + +main :: IO () +main = do args <- getArgs + inp <- getContents + _runFn (_selectFn args) inp + +_selectFn :: [String] -> Maybe (String -> ChallengeReturn) +_selectFn ["1"] = Just one +_selectFn ["2"] = Just two +_selectFn _ = Nothing + +_runFn :: Maybe (String -> ChallengeReturn) -> String -> IO () +_runFn Nothing _ = _debug "Missing or invalid day argument" >> exitWith (ExitFailure 1) +_runFn (Just fn) inp = putStrLn (fn inp) >> exitWith ExitSuccess + +_debug :: String -> IO () +_debug x = do hPutStrLn stderr x diff --git a/templates/main.kt b/templates/main.kt new file mode 100644 index 0000000..da9ddf2 --- /dev/null +++ b/templates/main.kt @@ -0,0 +1,33 @@ +import java.lang.System +import kotlin.system.* +import kotlin.sequences.generateSequence +import kotlin.text.trim + +fun parse(instr: String) { + +} + +fun one(instr: String): Int { + return 0 +} + +fun two(instr: String): Int { + return 0 +} + +fun main(args: Array) { + if (args.size < 1 || !(args[0] == "1" || args[0] == "2")) { + debug("Missing or invalid day argument") + exitProcess(1) + } + val inp = generateSequence(::readLine).joinToString("\n").trim() + if (args[0] == "1") { + println("${one(inp)}") + } else { + println("${two(inp)}") + } +} + +fun debug(msg: String) { + System.err.println(msg) +} \ No newline at end of file diff --git a/templates/main.py b/templates/main.py new file mode 100644 index 0000000..fdcc3cb --- /dev/null +++ b/templates/main.py @@ -0,0 +1,29 @@ +import sys + + +def parse(instr: str) -> None: + return + + +def one(instr: str): + return + + +def two(instr: str): + return + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) \ No newline at end of file pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy