aboutsummaryrefslogtreecommitdiffstats
path: root/aoc
diff options
context:
space:
mode:
Diffstat (limited to 'aoc')
-rw-r--r--aoc/__init__.py43
-rw-r--r--aoc/__main__.py72
-rw-r--r--aoc/datastructures.py98
-rw-r--r--aoc/decorators.py29
-rw-r--r--aoc/mixins.py77
-rw-r--r--aoc/template/__init__.py16
-rw-r--r--aoc/template/example.txt0
-rw-r--r--aoc/template/input.txt0
-rw-r--r--aoc/test_init.py8
-rw-r--r--aoc/tests/__init__.py0
-rw-r--r--aoc/tests/test_datastructures.py112
-rw-r--r--aoc/utils.py6
12 files changed, 461 insertions, 0 deletions
diff --git a/aoc/__init__.py b/aoc/__init__.py
new file mode 100644
index 0000000..7a089f6
--- /dev/null
+++ b/aoc/__init__.py
@@ -0,0 +1,43 @@
1# -*- coding: utf-8 -*-
2import os
3from abc import ABC
4from typing import Generator, Any, Iterator, Dict, TypeVar, Generic
5
6T = TypeVar("T")
7I = TypeVar("I")
8
9
10class BaseAssignment(Generic[T, I], ABC):
11 example_result: T = NotImplemented
12 example_kwargs: Dict = {}
13
14 def __init__(self, path):
15 self.path = path
16
17 def __str__(self):
18 return f"{self.__module__}.{self.__class__.__name__}"
19
20 def parse_item(self, item: str) -> I:
21 return item
22
23 @property
24 def part(self) -> int:
25 return 1 if self.__class__.__name__.endswith("One") else 2
26
27 def read_input(self, example=False) -> Iterator[I]:
28 file = f"{self.path}/input.txt"
29
30 if example or not os.path.isfile(file):
31 for file in [
32 f"{self.path}/example_part_{self.part}.txt",
33 f"{self.path}/example.txt",
34 ]:
35 if os.path.exists(file):
36 break
37
38 with open(file, "r") as input_file:
39 for line in input_file.readlines():
40 yield self.parse_item(line.strip("\n"))
41
42 def run(self, input: Iterator[I]) -> T:
43 raise NotImplementedError("Please implement run")
diff --git a/aoc/__main__.py b/aoc/__main__.py
new file mode 100644
index 0000000..596abee
--- /dev/null
+++ b/aoc/__main__.py
@@ -0,0 +1,72 @@
1# -*- coding: utf-8 -*-
2import enum
3import typing
4import importlib
5import os
6from pathlib import Path
7from shutil import copytree
8from time import perf_counter
9from typing import List, Callable
10import typer
11
12app = typer.Typer()
13
14
15class AssignmentPart(str, enum.Enum):
16 One = "1"
17 Two = "2"
18
19
20def day(assignment_part: str) -> str:
21 return AssignmentPart(assignment_part).name
22
23
24def kwargs(kwarg: str) -> List:
25 return kwarg.split("=")
26
27
28@app.command()
29def run(
30 day: str,
31 part: str = typer.Option(
32 "1", "--part", help="Assignment part. Defaults to 'One'.", show_choices=True
33 ),
34 example: bool = typer.Option(False, "--example", help="Use an example input file"),
35 kwargs: List[str] = typer.Argument(None),
36):
37 assignment_day = importlib.import_module(day)
38
39 Assignment = getattr(assignment_day, f"Assignment{AssignmentPart(part).name}")
40 assignment = Assignment(
41 path=os.path.dirname(assignment_day.__file__), **dict(kwargs)
42 )
43
44 start = perf_counter()
45 typer.echo(
46 assignment.run(
47 input=assignment.read_input(example=example),
48 )
49 )
50 end = perf_counter()
51 delta = end - start
52 typer.secho(
53 f'\n{(delta if delta > 1 else delta * 1000):.3f}{"s" if delta > 1 else "ms"}',
54 fg="green",
55 )
56
57
58@app.command()
59def new(day: str):
60 self = importlib.import_module(".", package="aoc")
61 path = Path(self.__file__)
62 template = path.parent.joinpath("template")
63 target = path.parent.parent.joinpath(day)
64
65 try:
66 copytree(template, target)
67 except FileExistsError:
68 typer.secho(f"{day} already exists", fg="red")
69
70
71if __name__ == "__main__":
72 app()
diff --git a/aoc/datastructures.py b/aoc/datastructures.py
new file mode 100644
index 0000000..1f141e2
--- /dev/null
+++ b/aoc/datastructures.py
@@ -0,0 +1,98 @@
1# -*- coding: utf-8 -*-
2from collections import namedtuple
3from typing import Iterator
4
5
6class Coordinate(namedtuple("Coordinate", ["x", "y"])):
7 def __sub__(self, other: "Coordinate") -> "Coordinate":
8 return Coordinate(self.x - other.x, self.y - other.y)
9
10 def __add__(self, other: "Coordinate") -> "Coordinate":
11 return Coordinate(self.x + other.x, self.y + other.y)
12
13 def manhattan_distance(self, other: "Coordinate") -> int:
14 return abs(self.x - other.x) + abs(self.y - other.y)
15
16 @property
17 def polarity(self) -> "Coordinate":
18 try:
19 px = abs(self.x) / self.x
20 except ZeroDivisionError:
21 px = 0
22
23 try:
24 py = abs(self.y) / self.y
25 except ZeroDivisionError:
26 py = 0
27
28 return Coordinate(
29 px,
30 py,
31 )
32
33 def neighbours(self, no_diagonal: bool = False) -> Iterator["Coordinate"]:
34 if no_diagonal:
35 yield self + Coordinate(-1, 0)
36 yield self + Coordinate(1, 0)
37 yield self + Coordinate(0, -1)
38 yield self + Coordinate(0, 1)
39 else:
40 for dy in range(self.y - 1, self.y + 2):
41 for dx in range(self.x - 1, self.x + 2):
42 if dy == self.y and dx == self.x:
43 continue
44
45 yield Coordinate(dx, dy)
46
47
48class Coordinate3(namedtuple("Coordinate3", ["x", "y", "z"])):
49 def __sub__(self, other: "Coordinate3") -> "Coordinate3":
50 return Coordinate3(self.x - other.x, self.y - other.y, self.z - other.z)
51
52 def __add__(self, other: "Coordinate3") -> "Coordinate3":
53 return Coordinate3(self.x + other.x, self.y + other.y, self.z + other.z)
54
55 def manhattan_distance(self, other: "Coordinate3") -> int:
56 return abs(self.x - other.x) + abs(self.y - other.y) + abs(self.z - other.z)
57
58 @property
59 def polarity(self) -> "Coordinate3":
60 try:
61 px = abs(self.x) / self.x
62 except ZeroDivisionError:
63 px = 0
64
65 try:
66 py = abs(self.y) / self.y
67 except ZeroDivisionError:
68 py = 0
69
70 try:
71 pz = abs(self.z) / self.z
72 except ZeroDivisionError:
73 pz = 0
74
75 return Coordinate3(
76 px,
77 py,
78 pz,
79 )
80
81 def neighbours(self, no_diagonal: bool = False) -> Iterator["Coordinate3"]:
82 if no_diagonal:
83 yield self + Coordinate3(-1, 0, 0)
84 yield self + Coordinate3(1, 0, 0)
85 yield self + Coordinate3(0, -1, 0)
86 yield self + Coordinate3(0, 1, 0)
87 yield self + Coordinate3(0, 0, -1)
88 yield self + Coordinate3(0, 0, 1)
89 else:
90 for dz in range(self.z - 1, self.z + 2):
91 for dy in range(self.y - 1, self.y + 2):
92 for dx in range(self.x - 1, self.x + 2):
93 coordinate = Coordinate3(dx, dy, dz)
94
95 if dy == self.y and dx == self.x and dz == self.z:
96 continue
97
98 yield coordinate
diff --git a/aoc/decorators.py b/aoc/decorators.py
new file mode 100644
index 0000000..98246e6
--- /dev/null
+++ b/aoc/decorators.py
@@ -0,0 +1,29 @@
1# -*- coding: utf-8 -*-
2from functools import wraps
3from typing import Iterator, TypeVar, Callable, List
4
5T = TypeVar("T")
6I = TypeVar("I")
7
8AssignmentRun = Callable[[Iterator[I], ...], Iterator[T]]
9AssignmentRunList = Callable[[List[I], ...], Iterator[T]]
10
11
12def infinite_generator(func: AssignmentRun) -> AssignmentRun:
13 @wraps(func)
14 def wrapper(*args, **kwargs):
15 items = list(func(*args, **kwargs))
16
17 while True:
18 for item in items:
19 yield item
20
21 return wrapper
22
23
24def list_input(func: AssignmentRun) -> AssignmentRunList:
25 @wraps(func)
26 def wrapper(self, input: Iterator, *args, **kwargs) -> T:
27 return func(self, list(input), *args, **kwargs)
28
29 return wrapper
diff --git a/aoc/mixins.py b/aoc/mixins.py
new file mode 100644
index 0000000..5986b6e
--- /dev/null
+++ b/aoc/mixins.py
@@ -0,0 +1,77 @@
1# -*- coding: utf-8 -*-
2from math import floor, inf
3from typing import Tuple, TypeVar, Generic, Callable, Iterator, List, Dict
4
5Node = TypeVar("Node")
6
7
8class AStarMixin(Generic[Node]):
9 def _gen_path(self, current: Node, came_from: Dict[Node, Node]) -> List[Node]:
10 print("GENPATH")
11 path = [current]
12
13 while current in came_from:
14 current = came_from[current]
15 path = [current, *path]
16
17 return path
18
19 def a_star(
20 self,
21 start: Node,
22 end: Callable[[Node, Callable[[], List[Node]]], bool],
23 neighbours: Callable[[Node], Iterator[Node]],
24 distance: Callable[[Node, Node], int] = lambda a, b: 1,
25 heuristic: Callable[[Node], int] = lambda a: 0,
26 ) -> List[Node]:
27 open_nodes = {start}
28 came_from = {}
29
30 g_scores = {start: 0}
31
32 f_scores = {start: heuristic(start)}
33
34 while True:
35 try:
36 current = sorted(
37 [item for item in open_nodes],
38 key=lambda item: f_scores.get(item, inf),
39 )[0]
40 except IndexError:
41 raise RuntimeError("No path found")
42
43 if end(current, lambda: self._gen_path(current, came_from)):
44 return self._gen_path(current, came_from)
45
46 open_nodes.remove(current)
47
48 for neighbour in neighbours(current):
49 g_score = g_scores.get(current, inf) + distance(current, neighbour)
50
51 if g_score < g_scores.get(neighbour, inf):
52 came_from[neighbour] = current
53 g_scores[neighbour] = g_score
54 f_scores[neighbour] = g_score + heuristic(neighbour)
55
56 if neighbour not in open_nodes:
57 open_nodes.add(neighbour)
58
59
60class BreathFirstSearchMixin(Generic[Node]):
61 @staticmethod
62 def bfs(
63 start: Node, neighbours: Callable[[Node], Iterator[Node]]
64 ) -> Iterator[Node]:
65 queue = [start]
66 searched = set()
67
68 while len(queue):
69 item = queue.pop(0)
70
71 for neighbour in neighbours(item):
72 if neighbour not in searched and neighbour not in set(queue):
73 queue.append(neighbour)
74
75 searched.add(item)
76
77 yield item
diff --git a/aoc/template/__init__.py b/aoc/template/__init__.py
new file mode 100644
index 0000000..05b2b45
--- /dev/null
+++ b/aoc/template/__init__.py
@@ -0,0 +1,16 @@
1# -*- coding: utf-8 -*-
2from abc import ABC
3
4from aoc import BaseAssignment
5
6
7class Assignment(BaseAssignment, ABC):
8 pass
9
10
11class AssignmentOne(Assignment):
12 pass
13
14
15class AssignmentTwo(Assignment):
16 pass
diff --git a/aoc/template/example.txt b/aoc/template/example.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/aoc/template/example.txt
diff --git a/aoc/template/input.txt b/aoc/template/input.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/aoc/template/input.txt
diff --git a/aoc/test_init.py b/aoc/test_init.py
new file mode 100644
index 0000000..e52daf7
--- /dev/null
+++ b/aoc/test_init.py
@@ -0,0 +1,8 @@
1# -*- coding: utf-8 -*-
2def test_assignment_examples(assignment):
3 assert (
4 assignment.run(
5 input=assignment.read_input(example=True), **assignment.example_kwargs
6 )
7 == assignment.example_result
8 )
diff --git a/aoc/tests/__init__.py b/aoc/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/aoc/tests/__init__.py
diff --git a/aoc/tests/test_datastructures.py b/aoc/tests/test_datastructures.py
new file mode 100644
index 0000000..1c291cc
--- /dev/null
+++ b/aoc/tests/test_datastructures.py
@@ -0,0 +1,112 @@
1# -*- coding: utf-8 -*-
2from aoc.datastructures import Coordinate, Coordinate3
3
4
5class TestCoordinate:
6 def test_addition(self):
7 c1 = Coordinate(1, 1)
8 c2 = Coordinate(2, 2)
9 assert c1 + c2 == Coordinate(3, 3)
10
11 def test_subtraction(self):
12 c1 = Coordinate(2, 2)
13 c2 = Coordinate(1, 1)
14 assert c1 - c2 == Coordinate(1, 1)
15
16 def test_manhattan_distance(self):
17 c1 = Coordinate(1, 1)
18 c2 = Coordinate(3, 3)
19 assert c1.manhattan_distance(c2) == 4
20
21 def test_polarity(self):
22 c1 = Coordinate(2, -3)
23 assert c1.polarity == Coordinate(1, -1)
24
25 def test_neighbours(self):
26 c = Coordinate(0, 0)
27 neighbours = {
28 Coordinate(-1, 0),
29 Coordinate(1, 0),
30 Coordinate(0, -1),
31 Coordinate(0, 1),
32 Coordinate(-1, -1),
33 Coordinate(-1, 1),
34 Coordinate(1, -1),
35 Coordinate(1, 1),
36 }
37 assert set(c.neighbours()) == neighbours
38
39 def test_neighbours_no_diagonal(self):
40 c = Coordinate(0, 0)
41 neighbours_no_diagonal = {
42 Coordinate(-1, 0),
43 Coordinate(1, 0),
44 Coordinate(0, -1),
45 Coordinate(0, 1),
46 }
47 assert set(c.neighbours(no_diagonal=True)) == neighbours_no_diagonal
48
49
50class TestCoordinate3:
51 def test_addition(self):
52 c1 = Coordinate3(1, 1, 1)
53 c2 = Coordinate3(2, 2, 2)
54 assert c1 + c2 == Coordinate3(3, 3, 3)
55
56 def test_subtraction(self):
57 c1 = Coordinate3(2, 2, 2)
58 c2 = Coordinate3(1, 1, 1)
59 assert c1 - c2 == Coordinate3(1, 1, 1)
60
61 def test_manhattan_distance(self):
62 c1 = Coordinate3(1, 1, 1)
63 c2 = Coordinate3(3, 3, 3)
64 assert c1.manhattan_distance(c2) == 6
65
66 def test_polarity(self):
67 c1 = Coordinate3(2, -3, 0)
68 assert c1.polarity == Coordinate3(1, -1, 0)
69
70 def test_neighbours(self):
71 c = Coordinate3(0, 0, 0)
72 neighbours = {
73 Coordinate3(-1, -1, -1),
74 Coordinate3(-1, -1, 0),
75 Coordinate3(-1, -1, 1),
76 Coordinate3(-1, 0, -1),
77 Coordinate3(-1, 0, 0),
78 Coordinate3(-1, 0, 1),
79 Coordinate3(-1, 1, -1),
80 Coordinate3(-1, 1, 0),
81 Coordinate3(-1, 1, 1),
82 Coordinate3(0, -1, -1),
83 Coordinate3(0, -1, 0),
84 Coordinate3(0, -1, 1),
85 Coordinate3(0, 0, -1),
86 Coordinate3(0, 0, 1),
87 Coordinate3(0, 1, -1),
88 Coordinate3(0, 1, 0),
89 Coordinate3(0, 1, 1),
90 Coordinate3(1, -1, -1),
91 Coordinate3(1, -1, 0),
92 Coordinate3(1, -1, 1),
93 Coordinate3(1, 0, -1),
94 Coordinate3(1, 0, 0),
95 Coordinate3(1, 0, 1),
96 Coordinate3(1, 1, -1),
97 Coordinate3(1, 1, 0),
98 Coordinate3(1, 1, 1),
99 }
100 assert set(c.neighbours()) == neighbours
101
102 def test_neighbours_no_diagonal(self):
103 c = Coordinate3(0, 0, 0)
104 neighbours_no_diagonal = {
105 Coordinate3(-1, 0, 0),
106 Coordinate3(1, 0, 0),
107 Coordinate3(0, -1, 0),
108 Coordinate3(0, 1, 0),
109 Coordinate3(0, 0, -1),
110 Coordinate3(0, 0, 1),
111 }
112 assert set(c.neighbours(no_diagonal=True)) == neighbours_no_diagonal
diff --git a/aoc/utils.py b/aoc/utils.py
new file mode 100644
index 0000000..a5de050
--- /dev/null
+++ b/aoc/utils.py
@@ -0,0 +1,6 @@
1# -*- coding: utf-8 -*-
2from typing import Callable
3
4
5def bold(item: str, condition: Callable[[], bool]):
6 return f"\033[36m{item}\033[0m" if condition() else item