Skip to content

Commit 3501766

Browse files
committed
Initiate multi-year advent-of-code setup
0 parents  commit 3501766

File tree

92 files changed

+4410
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+4410
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.egg-info
2+
input.txt

cli.py

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import click
2+
from datetime import datetime
3+
from aocd import get_data, submit as aocd_submit
4+
from pathlib import Path
5+
import yaml
6+
from importlib import import_module
7+
import sys
8+
from string import Template
9+
10+
sys.path.append(str(Path.cwd()))
11+
12+
13+
TODAY = datetime.now().day
14+
CURRENT_YEAR = datetime.now().year
15+
16+
ENTRYPOINTS_FILE = Path("entrypoints.yaml")
17+
SOLUTION_TEMPLATE_FILE = Path("templates", "solution.template.py")
18+
TEST_TEMPLATE_FILE = Path("templates", "test.template.py")
19+
20+
with open(ENTRYPOINTS_FILE) as f:
21+
ENTRYPOINTS = yaml.safe_load(f) or {}
22+
23+
with open(SOLUTION_TEMPLATE_FILE) as f:
24+
SOLUTION_TEMPLATE = f.read()
25+
26+
with open(TEST_TEMPLATE_FILE) as f:
27+
TEST_TEMPLATE = f.read()
28+
29+
30+
def get_templates():
31+
return SOLUTION_TEMPLATE, Template(TEST_TEMPLATE)
32+
33+
34+
def update_entrypoints():
35+
with open(ENTRYPOINTS_FILE, "w") as f:
36+
yaml.safe_dump(ENTRYPOINTS, f)
37+
38+
39+
def get_day_path(year: int, day: int):
40+
return Path(f"y{year}", f"d{day:02d}")
41+
42+
43+
def get_day_module(year: int, day: int, module_name: str, test=False):
44+
return f"y{year}.d{day:02d}.{'test_' if test else ''}{module_name}"
45+
46+
47+
def get_module(year: int, day: int, test=False):
48+
module_name = ENTRYPOINTS.get(year, {}).get(day)
49+
50+
if not module_name:
51+
click.BadParameter(f"Year {year} day {day} is not initialized yet")
52+
53+
module = get_day_module(year, day, module_name, test)
54+
55+
return import_module(module)
56+
57+
58+
def get_solution(year: int, day: int, part: int):
59+
m = get_module(year, day)
60+
solve_fn = getattr(m, f"part_{part}")
61+
62+
input_path = get_day_path(year, day) / "input.txt"
63+
64+
if not input_path.is_file():
65+
input_string = get_data(year=year, day=day)
66+
67+
with open(input_path, "w") as f:
68+
f.write(input_string)
69+
else:
70+
with open(input_path) as f:
71+
input_string = f.read()
72+
73+
return solve_fn(input_string)
74+
75+
76+
def find_puzzle(selector: list[str]):
77+
year = CURRENT_YEAR
78+
day = TODAY
79+
part = "a"
80+
81+
match len(selector):
82+
case 3:
83+
year, day, part = selector
84+
case 2:
85+
if selector[1] in ["a", "b"]:
86+
day, part = selector
87+
else:
88+
year, day = selector
89+
case 1:
90+
day = selector[0]
91+
92+
return int(year), int(day), part
93+
94+
95+
def test_solution(year: int, day: int, part: int):
96+
m = get_module(year, day, test=True)
97+
98+
test_input = m.TEST_INPUT
99+
solve_fn = getattr(m, f"part_{part}")
100+
101+
click.echo(solve_fn(test_input))
102+
103+
104+
arg_module_name = click.argument("module_name")
105+
arg_selector = click.argument("selector", nargs=-1)
106+
107+
108+
@click.command()
109+
@arg_module_name
110+
@arg_selector
111+
def prepare(module_name, selector):
112+
year, day, _ = find_puzzle(selector)
113+
114+
input_data = get_data(year=year, day=day)
115+
solution_template, test_template = get_templates()
116+
117+
puzzle_path = get_day_path(year, day)
118+
puzzle_path.mkdir(parents=True, exist_ok=True)
119+
120+
with open(puzzle_path / "input.txt", "w") as f:
121+
f.write(input_data)
122+
123+
with open(puzzle_path / f"{module_name}.py", "w") as f:
124+
f.write(solution_template)
125+
126+
with open(puzzle_path / f"test_{module_name}.py", "w") as f:
127+
f.write(test_template.substitute(module=get_day_module(year, day, module_name)))
128+
129+
ENTRYPOINTS[year] = ENTRYPOINTS.get(year, {})
130+
ENTRYPOINTS[year][day] = module_name
131+
update_entrypoints()
132+
133+
134+
@click.command()
135+
@arg_selector
136+
def test(selector):
137+
year, day, part = find_puzzle(selector)
138+
test_solution(year, day, part)
139+
140+
141+
@click.command()
142+
@arg_selector
143+
def solve(selector):
144+
year, day, part = find_puzzle(selector)
145+
146+
click.echo(get_solution(year, day, part))
147+
148+
149+
@click.command()
150+
@arg_selector
151+
def submit(selector):
152+
year, day, part = find_puzzle(selector)
153+
154+
solution = get_solution(year, day, part)
155+
156+
if not solution:
157+
raise click.ClickException("Nothing to submit")
158+
159+
aocd_submit(solution, year=year, day=day, part=part)

entrypoints.yaml

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
2019:
2+
1: rocket_equation
3+
2: program_alarm
4+
3: crossed_wires
5+
4: secure_container
6+
2022:
7+
1: calorie_counting
8+
2: rock_paper_scissors
9+
3: rucksack_reorganization
10+
4: camp_cleanup
11+
5: supply_stacks
12+
6: tuning_trouble
13+
7: no_space
14+
8: treetop_tree_house
15+
9: rope_bridge
16+
10: cathode_ray_tube
17+
11: monkey_in_the_middle
18+
12: hill_climbing
19+
13: distress_signal
20+
14: regolith_reservoir
21+
2023:
22+
1: trebuchet
23+
2: cube_conundrum
24+
3: gear_ratios
25+
4: scratchcards
26+
5: fertilizer
27+
6: wait_for_it
28+
7: camel_cards
29+
8: haunted_wasteland
30+
9: mirage_maintainance
31+
10: pipe_maze
32+
11: cosmic_expansion
33+
12: hot_springs
34+
13: point_of_incidence
35+
14: parabolic_reflector_dish
36+
15: lens_library
37+
16: lava_floor
38+
17: clumsy_crubicle
39+
18: lavaduct_lagoon

lib/array.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from bisect import bisect_left, insort
2+
3+
4+
def split_list(obj: list, index: int) -> tuple[list, list]:
5+
"""Splits list into two at given index."""
6+
return obj[:index], obj[index:]
7+
8+
9+
def product(obj: list[int]):
10+
prod = 1
11+
12+
for item in obj:
13+
prod *= item
14+
15+
return prod
16+
17+
18+
class SortedList(object):
19+
def __init__(self, lst, key=None):
20+
self._lst = sorted(lst, key=key)
21+
self._key = key
22+
23+
def __str__(self):
24+
return str(self._lst)
25+
26+
def __repr__(self):
27+
return f"<SortedList {repr(self._lst)}>"
28+
29+
def pop(self, index):
30+
return self._lst.pop(index)
31+
32+
def add(self, item):
33+
insort(self._lst, item, key=self._key)
34+
35+
def find(self, item):
36+
lookup_value = self._key(item) if self._key else item
37+
index = bisect_left(self._lst, lookup_value, key=self._key)
38+
39+
while self._lst[index] != item:
40+
index += 1
41+
if self._key(item) != lookup_value:
42+
return -1
43+
44+
return index

lib/dijkstra.py

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from functools import total_ordering
2+
from typing import TypeVar
3+
from tqdm import tqdm
4+
5+
from lib.array import SortedList
6+
7+
Node = TypeVar("Node")
8+
9+
10+
@total_ordering
11+
class DistanceEntry(object):
12+
def __init__(self, distance: int, prev_node: Node):
13+
self.distance = distance
14+
self.prev_node = prev_node
15+
16+
def __eq__(self, other):
17+
return self.prev_node == other.prev_node and self.distance == other.distance
18+
19+
def __lt__(self, other):
20+
return self.distance < other.distance
21+
22+
def __repr__(self):
23+
return f"({repr(self.prev_node)}, {repr(self.distance)})"
24+
25+
26+
class DistanceMap(object):
27+
def __init__(self, start_node: Node):
28+
self.distance_map = {start_node: DistanceEntry(0, None)}
29+
30+
def get_distance(self, node):
31+
distance_entry = self.distance_map.get(node)
32+
33+
if not distance_entry:
34+
raise KeyError("No record exist of this node")
35+
36+
return distance_entry.distance
37+
38+
def nodes(self):
39+
return self.distance_map.keys()
40+
41+
def add(self, node_from: Node, node_to: Node, distance: int):
42+
self.distance_map[node_to] = DistanceEntry(distance, node_from)
43+
44+
def backtrace_path(self, node):
45+
current_node = node
46+
while current_node != None:
47+
yield current_node
48+
current_node = self.distance_map[current_node].prev_node
49+
50+
51+
class NoPossiblePath(Exception):
52+
pass
53+
54+
55+
def dijkstra(start_node: Node, end_node, get_neighbours, total_nodes: int = None):
56+
distance_map = DistanceMap(start_node)
57+
visited = {start_node}
58+
unvisited = SortedList([start_node], key=distance_map.get_distance)
59+
60+
is_end_node = (
61+
lambda node: end_node(node) if callable(end_node) else end_node == node
62+
)
63+
64+
current_node = start_node
65+
with tqdm(total=total_nodes, desc="Performing Dijkstra's algorithm") as pbar:
66+
while not is_end_node(current_node):
67+
try:
68+
current_node = unvisited.pop(0)
69+
except IndexError:
70+
raise NoPossiblePath("No path possible between start and end node")
71+
72+
for neighbour, distance in get_neighbours(current_node):
73+
current_distance = distance_map.get_distance(current_node) + distance
74+
75+
if neighbour in distance_map.nodes():
76+
existing_distance = distance_map.get_distance(neighbour)
77+
78+
if existing_distance > current_distance:
79+
neighbour_index = unvisited.find(neighbour)
80+
unvisited.pop(neighbour_index)
81+
else:
82+
continue
83+
84+
distance_map.add(current_node, neighbour, current_distance)
85+
unvisited.add(neighbour)
86+
87+
visited.add(current_node)
88+
pbar.update()
89+
90+
if callable(end_node):
91+
return distance_map, current_node
92+
93+
return distance_map

0 commit comments

Comments
 (0)