From fb30236fd87fc905792d320ccbb320c69172991d Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Fri, 22 Oct 2021 17:35:00 +0200 Subject: [PATCH 001/144] coordinates \o/ --- coordinate.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 coordinate.py diff --git a/coordinate.py b/coordinate.py new file mode 100644 index 0000000..a5bf583 --- /dev/null +++ b/coordinate.py @@ -0,0 +1,62 @@ +from __future__ import annotations +from dataclasses import dataclass +from math import sqrt, inf +from typing import Union + + +@dataclass(frozen=True, order=True) +class Coordinate: + x: int + y: int + + def getDistanceTo(self, target: Coordinate, mode: int = 1, includeDiagonals: bool = False) -> Union[int, float]: + """ + Get distance to target Coordinate + + :param target: + :param mode: Calculation Mode (0 = Manhattan, 1 = Pythagoras) + :param includeDiagonals: in Manhattan Mode specify if diagonal movements are allowed (counts as 1.4) + :return: Distance to Target + """ + assert isinstance(target, Coordinate) + assert mode in [0, 1] + assert isinstance(includeDiagonals, bool) + + if mode == 1: + return sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2) + elif mode == 0: + if not includeDiagonals: + return abs(self.x - target.x) + abs(self.y - target.y) + else: + x_dist = abs(self.x - target.x) + y_dist = abs(self.y - target.y) + o_dist = max(x_dist, y_dist) - min(x_dist, y_dist) + return o_dist + 1.4 * min(x_dist, y_dist) + + def getNeighbours(self, includeDiagonal: bool = True, minX: int = -inf, minY: int = -inf, + maxX: int = inf, maxY: int = inf) -> list[Coordinate]: + """ + Get a list of neighbouring coordinates + + :param includeDiagonal: include diagonal neighbours + :param minX: ignore all neighbours that would have an X value below this + :param minY: ignore all neighbours that would have an Y value below this + :param maxX: ignore all neighbours that would have an X value above this + :param maxY: ignore all neighbours that would have an Y value above this + :return: list of Coordinate + """ + neighbourList = [] + if includeDiagonal: + nb_list = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] + else: + nb_list = [(-1, 0), (1, 0), (0, -1), (0, 1)] + + for dx, dy in nb_list: + tx = self.x + dx + ty = self.y + dy + if tx < minX or tx > maxX or ty < minY or ty > maxY: + continue + + neighbourList.append(Coordinate(tx, ty)) + + return neighbourList From abcc9f32e4f30d3bb879176de887ff5e5f0d14ea Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Fri, 22 Oct 2021 17:35:30 +0200 Subject: [PATCH 002/144] __init__ --- __init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 __init__.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 From 8221403ed0bc88fc921440d0e29817642651e20c Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Fri, 22 Oct 2021 18:46:28 +0200 Subject: [PATCH 003/144] grids \o/ --- grid.py | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 grid.py diff --git a/grid.py b/grid.py new file mode 100644 index 0000000..c2b8fe4 --- /dev/null +++ b/grid.py @@ -0,0 +1,105 @@ +from __future__ import annotations +from .coordinate import Coordinate +from typing import Union + +OFF_STATES = [False, 0, None] +OFF = False +ON = True + + +class Grid: + def __init__(self, default=False): + self.__default = default + self.__grid = {} + self.__minX = 0 + self.__minY = 0 + self.__maxX = 0 + self.__maxY = 0 + + def __trackBoundaries(self, pos: Coordinate): + if pos.x < self.__minX: + self.__minX = pos.x + + if pos.y < self.__minY: + self.__minY = pos.y + + if pos.x > self.__maxX: + self.__maxX = pos.x + + if pos.y > self.__maxY: + self.__maxY = pos.y + + def toggle(self, pos: Coordinate): + if pos in self.__grid: + del self.__grid[pos] + else: + self.__trackBoundaries(pos) + self.__grid[pos] = not self.__default + + def set(self, pos: Coordinate, value: any): + if value in OFF_STATES and pos in self.__grid: + del self.__grid[pos] + elif value not in OFF_STATES: + self.__trackBoundaries(pos) + self.__grid[pos] = value + + def getState(self, pos: Coordinate) -> bool: + if pos not in self.__grid: + return False + else: + return self.__grid[pos] not in OFF_STATES + + def getValue(self, pos: Coordinate) -> any: + if pos not in self.__grid: + return self.__default + else: + return self.__grid[pos] + + def getOnCount(self): + return len(self.__grid) + + def isSet(self, pos: Coordinate) -> bool: + return pos in self.__grid + + def isCorner(self, pos: Coordinate) -> bool: + return pos in [ + Coordinate(self.__minX, self.__minY), + Coordinate(self.__minX, self.__maxY), + Coordinate(self.__maxX, self.__minY), + Coordinate(self.__maxX, self.__maxY), + ] + + def add(self, pos: Coordinate, value: Union[float, int] = 1): + if pos in self.__grid: + self.__grid[pos] += value + else: + self.__trackBoundaries(pos) + self.__grid[pos] = self.__default + value + + def sub(self, pos: Coordinate, value: Union[float, int] = 1): + if pos in self.__grid: + self.__grid[pos] -= value + else: + self.__trackBoundaries(pos) + self.__grid[pos] = self.__default - value + + def getSum(self, includeNegative: bool = True): + grid_sum = 0 + for value in self.__grid.values(): + if includeNegative or value > 0: + grid_sum += value + + return grid_sum + + def getNeighbourSum(self, pos: Coordinate, includeNegative: bool = True, includeDiagonal: bool = True) \ + -> Union[float, int]: + neighbour_sum = 0 + for neighbour in pos.getNeighbours( + includeDiagonal=includeDiagonal, + minX=self.__minX, minY=self.__minY, + maxX=self.__maxX, maxY=self.__maxY): + if neighbour in self.__grid: + if includeNegative or self.__grid[neighbour] > 0: + neighbour_sum += self.__grid[neighbour] + + return neighbour_sum From 78b5f7b6c8af3c68c08c7c3c065067f6d1cf9c3c Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 21 Nov 2021 13:27:49 +0100 Subject: [PATCH 004/144] setup.py --- setup.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..74439f5 --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup + +setup( + name='py-tools', + version='0.1', + packages=[''], + url='', + license='GPLv3', + author='pennywise', + author_email='pennywise@drock.de', + description='Just some small tools to make life easier' +) From 2713c66a34774c5c6db0ef2088d9fe151a0f792b Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Fri, 26 Nov 2021 07:30:06 +0100 Subject: [PATCH 005/144] move aoc thingy from year-by-year to central tool lib --- aoc.py | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 aoc.py diff --git a/aoc.py b/aoc.py new file mode 100644 index 0000000..835743d --- /dev/null +++ b/aoc.py @@ -0,0 +1,149 @@ +import os +from typing import List, Any, Type + +BASE_PATH = os.path.dirname(os.path.dirname(__file__)) +INPUTS_PATH = os.path.join(BASE_PATH, 'inputs') + + +class AOCDay: + day: int + input: List # our input is always a list of str/lines + test_solutions_p1: List + test_solutions_p2: List + + def __init__(self, day: int): + self.day = day + with open(os.path.join(INPUTS_PATH, "input%02d" % day)) as f: + self.input = f.read().splitlines() + + def part1(self) -> Any: + pass + + def part2(self) -> Any: + pass + + def test_part1(self, silent: bool = False) -> bool: + live_input = self.input.copy() + for case, solution in enumerate(self.test_solutions_p1): + with open(os.path.join(INPUTS_PATH, "test_input%02d_1_%d" % (self.day, case))) as f: + self.input = f.read().splitlines() + + check = self.part1() + if not silent: + printSolution(self.day, 1, check, solution, case) + + if check != solution: + if silent: + printSolution(self.day, 1, check, solution, case) + return False + + self.input = live_input + return True + + def test_part2(self, silent: bool = False) -> bool: + live_input = self.input.copy() + for case, solution in enumerate(self.test_solutions_p2): + with open(os.path.join(INPUTS_PATH, "test_input%02d_2_%d" % (self.day, case))) as f: + self.input = f.read().splitlines() + + check = self.part2() + if not silent: + printSolution(self.day, 2, check, solution, case) + + if check != solution: + if silent: + printSolution(self.day, 2, check, solution, case) + return False + + self.input = live_input + return True + + def getInputListAsType(self, return_type: Type) -> List: + """ + get input as list casted to return_type, each line representing one list entry + """ + return [return_type(i) for i in self.input] + + def getMultiLineInputAsArray(self, return_type: Type = None, join_char: str = None) -> List: + """ + get input for day x as 2d array, split by empty lines + """ + lines = self.input + lines.append('') + + return_array = [] + line_array = [] + for line in lines: + if not line: + if join_char: + return_array.append(join_char.join(line_array)) + else: + return_array.append(line_array) + line_array = [] + continue + + if return_type: + line_array.append(return_type(line)) + else: + line_array.append(line) + + return return_array + + def getInputAs2DArray(self, return_type: Type = None) -> List: + """ + get input for day x as 2d-array (a[line][pos]) + """ + if return_type is None: + return self.input # strings already act like a list + else: + return_array = [] + for line in self.input: + return_array.append([return_type(i) for i in line]) + + return return_array + + def getInputAsArraySplit(self, split_char: str = ',', return_type: Type = None) -> List: + """ + get input for day x with the lines split by split_char + if input has only one line, returns a 1d array with the values + if input has multiple lines, returns a 2d array (a[line][values]) + """ + if len(self.input) == 1: + return splitLine(line=self.input[0], split_char=split_char, return_type=return_type) + else: + return_array = [] + for line in self.input: + return_array.append(splitLine(line=line, split_char=split_char, return_type=return_type)) + + return return_array + + +def printSolution(day, part, solution, test=None, test_case=0, exec_time=None): + if exec_time is None: + time_output = "" + else: + units = ['s', 'ms', 'μs', 'ns'] + unit = 0 + while exec_time < 1: + exec_time *= 1000 + unit += 1 + + time_output = " (Average run time: %1.2f%s)" % (exec_time, units[unit]) + + if test is not None: + print( + "(TEST case%d/day%d/part%d) -- got '%s' -- expected '%s' -> %s" + % (test_case, day, part, solution, test, "correct" if test == solution else "WRONG") + ) + else: + print("Solution to day %s, part %s: %s%s" % (day, part, solution, time_output)) + + +def splitLine(line, split_char=',', return_type=None): + if split_char: + line = line.split(split_char) + + if return_type is None: + return line + else: + return [return_type(i) for i in line] diff --git a/setup.py b/setup.py index 74439f5..2aad0e9 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name='py-tools', - version='0.1', + version='0.2', packages=[''], url='', license='GPLv3', From 96dbd7a323486c02b5bd90c47cc3b172ee2bff20 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Fri, 26 Nov 2021 07:33:38 +0100 Subject: [PATCH 006/144] don't treat whole repo as a single package --- grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid.py b/grid.py index c2b8fe4..e217e76 100644 --- a/grid.py +++ b/grid.py @@ -1,5 +1,5 @@ from __future__ import annotations -from .coordinate import Coordinate +from coordinate import Coordinate from typing import Union OFF_STATES = [False, 0, None] From 263b343b24355b2a758928c50b7e49a0d19c1c7b Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Fri, 26 Nov 2021 07:53:56 +0100 Subject: [PATCH 007/144] having fun with paths --- aoc.py | 3 ++- tools.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tools.py diff --git a/aoc.py b/aoc.py index 835743d..d12d219 100644 --- a/aoc.py +++ b/aoc.py @@ -1,7 +1,8 @@ import os +import tools from typing import List, Any, Type -BASE_PATH = os.path.dirname(os.path.dirname(__file__)) +BASE_PATH = tools.get_script_dir() INPUTS_PATH = os.path.join(BASE_PATH, 'inputs') diff --git a/tools.py b/tools.py new file mode 100644 index 0000000..8ee2e30 --- /dev/null +++ b/tools.py @@ -0,0 +1,18 @@ +import inspect +import os.path +import sys + + +def get_script_dir(follow_symlinks=True): + if getattr(sys, 'frozen', False): + path = os.path.abspath(sys.executable) + else: + if '__main__' in sys.modules and hasattr(sys.modules['__main__'], '__file__'): + path = sys.modules['__main__'].__file__ + else: + path = inspect.getabsfile(get_script_dir) + + if follow_symlinks: + path = os.path.realpath(path) + + return os.path.dirname(path) From 6bf5835488aa746e99c8ed9db9e04a7baa8fe22e Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 27 Nov 2021 17:03:42 +0100 Subject: [PATCH 008/144] output reorder --- aoc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aoc.py b/aoc.py index d12d219..9064738 100644 --- a/aoc.py +++ b/aoc.py @@ -133,8 +133,8 @@ def printSolution(day, part, solution, test=None, test_case=0, exec_time=None): if test is not None: print( - "(TEST case%d/day%d/part%d) -- got '%s' -- expected '%s' -> %s" - % (test_case, day, part, solution, test, "correct" if test == solution else "WRONG") + "(TEST day%d/part%d/case%d) -- got '%s' -- expected '%s' -> %s" + % (day, part, test_case, solution, test, "correct" if test == solution else "WRONG") ) else: print("Solution to day %s, part %s: %s%s" % (day, part, solution, time_output)) From fc0178288e34051a0621e60059fd25dd150bc3aa Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 29 Nov 2021 16:40:01 +0100 Subject: [PATCH 009/144] Coordinate.generate(): get list of coordinates from x1,y1 to x2,y2 Grid(): expose boundaries Grid.isWithinBoundaries(): check if Coordinate is within boundaries Grid.getActiveCells(): get all Coordinates within Grid() with a value --- coordinate.py | 8 +++++++- grid.py | 42 ++++++++++++++++++++++++------------------ 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/coordinate.py b/coordinate.py index a5bf583..34140b5 100644 --- a/coordinate.py +++ b/coordinate.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass from math import sqrt, inf -from typing import Union +from typing import Union, List @dataclass(frozen=True, order=True) @@ -60,3 +60,9 @@ class Coordinate: neighbourList.append(Coordinate(tx, ty)) return neighbourList + + @staticmethod + def generate(from_x: int, to_x: int, from_y: int, to_y: int) -> List[Coordinate]: + return [Coordinate(x, y) for x in range(from_x, to_x + 1) for y in range(from_y, to_y + 1)] + + diff --git a/grid.py b/grid.py index e217e76..7e9820c 100644 --- a/grid.py +++ b/grid.py @@ -11,23 +11,23 @@ class Grid: def __init__(self, default=False): self.__default = default self.__grid = {} - self.__minX = 0 - self.__minY = 0 - self.__maxX = 0 - self.__maxY = 0 + self.minX = 0 + self.minY = 0 + self.maxX = 0 + self.maxY = 0 def __trackBoundaries(self, pos: Coordinate): - if pos.x < self.__minX: - self.__minX = pos.x + if pos.x < self.minX: + self.minX = pos.x - if pos.y < self.__minY: - self.__minY = pos.y + if pos.y < self.minY: + self.minY = pos.y - if pos.x > self.__maxX: - self.__maxX = pos.x + if pos.x > self.maxX: + self.maxX = pos.x - if pos.y > self.__maxY: - self.__maxY = pos.y + if pos.y > self.maxY: + self.maxY = pos.y def toggle(self, pos: Coordinate): if pos in self.__grid: @@ -63,12 +63,15 @@ class Grid: def isCorner(self, pos: Coordinate) -> bool: return pos in [ - Coordinate(self.__minX, self.__minY), - Coordinate(self.__minX, self.__maxY), - Coordinate(self.__maxX, self.__minY), - Coordinate(self.__maxX, self.__maxY), + Coordinate(self.minX, self.minY), + Coordinate(self.minX, self.maxY), + Coordinate(self.maxX, self.minY), + Coordinate(self.maxX, self.maxY), ] + def isWithinBoundaries(self, pos: Coordinate) -> bool: + return self.minX <= pos.x <= self.maxX and self.minY <= pos.y <= self.maxY + def add(self, pos: Coordinate, value: Union[float, int] = 1): if pos in self.__grid: self.__grid[pos] += value @@ -83,6 +86,9 @@ class Grid: self.__trackBoundaries(pos) self.__grid[pos] = self.__default - value + def getActiveCells(self): + return [i for i in self.__grid.keys()] + def getSum(self, includeNegative: bool = True): grid_sum = 0 for value in self.__grid.values(): @@ -96,8 +102,8 @@ class Grid: neighbour_sum = 0 for neighbour in pos.getNeighbours( includeDiagonal=includeDiagonal, - minX=self.__minX, minY=self.__minY, - maxX=self.__maxX, maxY=self.__maxY): + minX=self.minX, minY=self.minY, + maxX=self.maxX, maxY=self.maxY): if neighbour in self.__grid: if includeNegative or self.__grid[neighbour] > 0: neighbour_sum += self.__grid[neighbour] From 02b4f869b94f6bc9f9b127ce066c8b3dab3868d4 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 30 Nov 2021 01:02:57 +0100 Subject: [PATCH 010/144] angling away --- coordinate.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/coordinate.py b/coordinate.py index 34140b5..3da267a 100644 --- a/coordinate.py +++ b/coordinate.py @@ -1,6 +1,6 @@ from __future__ import annotations from dataclasses import dataclass -from math import sqrt, inf +from math import sqrt, inf, atan2, degrees from typing import Union, List @@ -61,6 +61,19 @@ class Coordinate: return neighbourList + def getAngleTo(self, target: Coordinate, normalized: bool = False) -> float: + """normalized returns an angle going clockwise with 0 starting in the 'north'""" + dx = target.x - self.x + dy = target.y - self.y + if not normalized: + return degrees(atan2(dy, dx)) + else: + angle = degrees(atan2(dx, dy)) + if dx >= 0: + return 180.0 - angle + else: + return 180.0 + abs(angle) + @staticmethod def generate(from_x: int, to_x: int, from_y: int, to_y: int) -> List[Coordinate]: return [Coordinate(x, y) for x in range(from_x, to_x + 1) for y in range(from_y, to_y + 1)] From 37211190dde01d80fa6ec96db78634f8d0e50cc7 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 30 Nov 2021 12:11:36 +0100 Subject: [PATCH 011/144] add/sub coordinates --- coordinate.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/coordinate.py b/coordinate.py index 3da267a..b949255 100644 --- a/coordinate.py +++ b/coordinate.py @@ -74,6 +74,12 @@ class Coordinate: else: return 180.0 + abs(angle) + def __add__(self, other: Coordinate) -> Coordinate: + return Coordinate(self.x + other.x, self.y + other.y) + + def __sub__(self, other: Coordinate) -> Coordinate: + return Coordinate(self.x - other.x, self.y - other.y) + @staticmethod def generate(from_x: int, to_x: int, from_y: int, to_y: int) -> List[Coordinate]: return [Coordinate(x, y) for x in range(from_x, to_x + 1) for y in range(from_y, to_y + 1)] From 0c0a9a7eb14d95378e0d02a117dea4947b12a32f Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 30 Nov 2021 12:46:25 +0100 Subject: [PATCH 012/144] grid transformations --- grid.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/grid.py b/grid.py index 7e9820c..a124e29 100644 --- a/grid.py +++ b/grid.py @@ -1,12 +1,23 @@ from __future__ import annotations from coordinate import Coordinate -from typing import Union +from enum import Enum +from typing import Union, Any OFF_STATES = [False, 0, None] OFF = False ON = True +class GridTransformation(Enum): + FLIP_HORIZONTALLY = 1 + FLIP_VERTICALLY = 2 + FLIP_DIAGONALLY = 3 + FLIP_DIAGONALLY_REV = 4 + ROTATE_RIGHT = 5 + ROTATE_LEFT = 6 + ROTATE_TWICE = 7 + + class Grid: def __init__(self, default=False): self.__default = default @@ -36,13 +47,19 @@ class Grid: self.__trackBoundaries(pos) self.__grid[pos] = not self.__default - def set(self, pos: Coordinate, value: any): + def set(self, pos: Coordinate, value: Any): if value in OFF_STATES and pos in self.__grid: del self.__grid[pos] elif value not in OFF_STATES: self.__trackBoundaries(pos) self.__grid[pos] = value + def get(self, pos: Coordinate) -> Any: + if pos in self.__grid: + return self.__grid[pos] + else: + return self.__default + def getState(self, pos: Coordinate) -> bool: if pos not in self.__grid: return False @@ -109,3 +126,41 @@ class Grid: neighbour_sum += self.__grid[neighbour] return neighbour_sum + + def flip(self, c1: Coordinate, c2: Coordinate): + buf = self.get(c1) + self.set(c1, self.get(c2)) + self.set(c2, buf) + + def transform(self, mode: GridTransformation): + if mode == GridTransformation.FLIP_HORIZONTALLY: + for x in range(self.minX, (self.maxX - self.minX) // 2 + 1): + for y in range(self.minY, self.maxY + 1): + self.flip(Coordinate(x, y), Coordinate(self.maxX - x, y)) + elif mode == GridTransformation.FLIP_VERTICALLY: + for y in range(self.minY, (self.maxY - self.minY) // 2 + 1): + for x in range(self.minX, self.maxX + 1): + self.flip(Coordinate(x, y), Coordinate(x, self.maxY - y)) + elif mode == GridTransformation.FLIP_DIAGONALLY: + self.transform(GridTransformation.ROTATE_LEFT) + self.transform(GridTransformation.FLIP_HORIZONTALLY) + elif mode == GridTransformation.FLIP_DIAGONALLY_REV: + self.transform(GridTransformation.ROTATE_RIGHT) + self.transform(GridTransformation.FLIP_HORIZONTALLY) + elif mode == GridTransformation.ROTATE_LEFT: + newGrid = Grid() + for x in range(self.maxX, self.minX - 1, -1): + for y in range(self.minY, self.maxY + 1): + newGrid.set(Coordinate(y, self.maxX - x), self.get(Coordinate(x, y))) + + self.__dict__.update(newGrid.__dict__) + elif mode == GridTransformation.ROTATE_RIGHT: + newGrid = Grid() + for x in range(self.minX, self.maxX + 1): + for y in range(self.maxY, self.minY - 1, -1): + newGrid.set(Coordinate(self.maxY - y, x), self.get(Coordinate(x, y))) + + self.__dict__.update(newGrid.__dict__) + elif mode == GridTransformation.ROTATE_TWICE: + self.transform(GridTransformation.ROTATE_RIGHT) + self.transform(GridTransformation.ROTATE_RIGHT) From 91e247732837e2a15a6a303ca1644eb7c9dabfdf Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 30 Nov 2021 13:24:05 +0100 Subject: [PATCH 013/144] welcome to the 3D world --- coordinate.py | 102 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 74 insertions(+), 28 deletions(-) diff --git a/coordinate.py b/coordinate.py index b949255..5b2567b 100644 --- a/coordinate.py +++ b/coordinate.py @@ -1,68 +1,110 @@ from __future__ import annotations from dataclasses import dataclass +from enum import Enum from math import sqrt, inf, atan2, degrees -from typing import Union, List +from typing import Union, List, Optional + + +class DistanceAlgorithm(Enum): + MANHATTAN = 0 + PYTHAGORAS = 1 @dataclass(frozen=True, order=True) class Coordinate: x: int y: int + z: Optional[int] = None - def getDistanceTo(self, target: Coordinate, mode: int = 1, includeDiagonals: bool = False) -> Union[int, float]: + def getDistanceTo(self, target: Coordinate, mode: DistanceAlgorithm = DistanceAlgorithm.PYTHAGORAS, + includeDiagonals: bool = False) -> Union[int, float]: """ Get distance to target Coordinate :param target: :param mode: Calculation Mode (0 = Manhattan, 1 = Pythagoras) - :param includeDiagonals: in Manhattan Mode specify if diagonal movements are allowed (counts as 1.4) + :param includeDiagonals: in Manhattan Mode specify if diagonal + movements are allowed (counts as 1.4 in 2D, 1.7 in 3D) :return: Distance to Target """ assert isinstance(target, Coordinate) - assert mode in [0, 1] + assert isinstance(mode, DistanceAlgorithm) assert isinstance(includeDiagonals, bool) - if mode == 1: - return sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2) - elif mode == 0: - if not includeDiagonals: - return abs(self.x - target.x) + abs(self.y - target.y) + if mode == DistanceAlgorithm.PYTHAGORAS: + if self.z is None: + return sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2) else: - x_dist = abs(self.x - target.x) - y_dist = abs(self.y - target.y) - o_dist = max(x_dist, y_dist) - min(x_dist, y_dist) - return o_dist + 1.4 * min(x_dist, y_dist) + ab = sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2) + return sqrt(ab ** 2 + abs(self.z - target.z) ** 2) + elif mode == DistanceAlgorithm.MANHATTAN: + if not includeDiagonals: + if self.z is None: + return abs(self.x - target.x) + abs(self.y - target.y) + else: + return abs(self.x - target.x) + abs(self.y - target.y) + abs(self.z - target.z) + else: + dist = [abs(self.x - target.x), abs(self.y - target.y)] + if self.z is None: + o_dist = max(dist) - min(dist) + return o_dist + 1.4 * min(dist) + else: + dist.append(abs(self.z - target.z)) + d_steps = min(dist) + dist.remove(min(dist)) + dist = [x - d_steps for x in dist] + o_dist = max(dist) - min(dist) + return 1.7 * d_steps + o_dist + 1.4 * min(dist) def getNeighbours(self, includeDiagonal: bool = True, minX: int = -inf, minY: int = -inf, - maxX: int = inf, maxY: int = inf) -> list[Coordinate]: + maxX: int = inf, maxY: int = inf, minZ: int = -inf, maxZ: int = inf) -> list[Coordinate]: """ Get a list of neighbouring coordinates :param includeDiagonal: include diagonal neighbours :param minX: ignore all neighbours that would have an X value below this :param minY: ignore all neighbours that would have an Y value below this + :param minZ: ignore all neighbours that would have an Z value below this :param maxX: ignore all neighbours that would have an X value above this :param maxY: ignore all neighbours that would have an Y value above this + :param maxZ: ignore all neighbours that would have an Z value above this :return: list of Coordinate """ neighbourList = [] - if includeDiagonal: - nb_list = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] + if self.z is None: + if includeDiagonal: + nb_list = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] + else: + nb_list = [(-1, 0), (1, 0), (0, -1), (0, 1)] + + for dx, dy in nb_list: + tx = self.x + dx + ty = self.y + dy + if minX <= tx <= maxX and minY <= ty <= maxY: + neighbourList.append(Coordinate(tx, ty)) else: - nb_list = [(-1, 0), (1, 0), (0, -1), (0, 1)] + if includeDiagonal: + nb_list = [(x, y, z) for x in [-1, 0, 1] for y in [-1, 0, 1] for z in [-1, 0, 1]] + nb_list.remove((0, 0, 0)) + else: + nb_list = [ + (-1, 0, 0), (0, -1, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (0, 0, -1) + ] - for dx, dy in nb_list: - tx = self.x + dx - ty = self.y + dy - if tx < minX or tx > maxX or ty < minY or ty > maxY: - continue - - neighbourList.append(Coordinate(tx, ty)) + for dx, dy, dz in nb_list: + tx = self.x + dx + ty = self.y + dy + tz = self.z + dz + if minX <= tx <= maxX and minY <= ty <= maxY and minZ <= tz <= maxZ: + neighbourList.append(Coordinate(tx, ty, tz)) return neighbourList def getAngleTo(self, target: Coordinate, normalized: bool = False) -> float: """normalized returns an angle going clockwise with 0 starting in the 'north'""" + if self.z is not None: + raise NotImplementedError() + dx = target.x - self.x dy = target.y - self.y if not normalized: @@ -75,13 +117,17 @@ class Coordinate: return 180.0 + abs(angle) def __add__(self, other: Coordinate) -> Coordinate: - return Coordinate(self.x + other.x, self.y + other.y) + if self.z is None: + return Coordinate(self.x + other.x, self.y + other.y) + else: + return Coordinate(self.x + other.x, self.y + other.y, self.z + other.z) def __sub__(self, other: Coordinate) -> Coordinate: - return Coordinate(self.x - other.x, self.y - other.y) + if self.z is None: + return Coordinate(self.x - other.x, self.y - other.y) + else: + return Coordinate(self.x - other.x, self.y - other.y, self.z - other.z) @staticmethod def generate(from_x: int, to_x: int, from_y: int, to_y: int) -> List[Coordinate]: return [Coordinate(x, y) for x in range(from_x, to_x + 1) for y in range(from_y, to_y + 1)] - - From 70dd7657ecab114dde66ef95e3b9818d1620cd9e Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 1 Dec 2021 02:30:04 +0100 Subject: [PATCH 014/144] tools.compare(): a <> b => -1/0/1 aoc.printSolution(): better readable test output --- aoc.py | 4 ++-- tools.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/aoc.py b/aoc.py index 9064738..cf90202 100644 --- a/aoc.py +++ b/aoc.py @@ -133,8 +133,8 @@ def printSolution(day, part, solution, test=None, test_case=0, exec_time=None): if test is not None: print( - "(TEST day%d/part%d/case%d) -- got '%s' -- expected '%s' -> %s" - % (day, part, test_case, solution, test, "correct" if test == solution else "WRONG") + "%s (TEST day%d/part%d/case%d) -- got '%s' -- expected '%s'" + % ("OK" if test == solution else "FAIL", day, part, test_case, solution, test) ) else: print("Solution to day %s, part %s: %s%s" % (day, part, solution, time_output)) diff --git a/tools.py b/tools.py index 8ee2e30..a11be26 100644 --- a/tools.py +++ b/tools.py @@ -4,6 +4,7 @@ import sys def get_script_dir(follow_symlinks=True): + """return path of the executed script""" if getattr(sys, 'frozen', False): path = os.path.abspath(sys.executable) else: @@ -16,3 +17,13 @@ def get_script_dir(follow_symlinks=True): path = os.path.realpath(path) return os.path.dirname(path) + + +def compare(a: int, b: int) -> int: + """compare to values, return -1 if a is smaller than b, 1 if a is greater than b, 0 is both are equal""" + if a > b: + return -1 + elif b > a: + return 1 + else: + return 0 From 5e3d5d115672cabb9dd76d9c00c56892649555d5 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 1 Dec 2021 02:49:15 +0100 Subject: [PATCH 015/144] streamline input getting --- aoc.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/aoc.py b/aoc.py index cf90202..af9ecb9 100644 --- a/aoc.py +++ b/aoc.py @@ -1,6 +1,6 @@ import os import tools -from typing import List, Any, Type +from typing import List, Any, Type, Union BASE_PATH = tools.get_script_dir() INPUTS_PATH = os.path.join(BASE_PATH, 'inputs') @@ -59,6 +59,12 @@ class AOCDay: self.input = live_input return True + def getInput(self) -> Union[str, List]: + if len(self.input) == 1: + return self.input[0] + else: + return self.input + def getInputListAsType(self, return_type: Type) -> List: """ get input as list casted to return_type, each line representing one list entry @@ -90,19 +96,6 @@ class AOCDay: return return_array - def getInputAs2DArray(self, return_type: Type = None) -> List: - """ - get input for day x as 2d-array (a[line][pos]) - """ - if return_type is None: - return self.input # strings already act like a list - else: - return_array = [] - for line in self.input: - return_array.append([return_type(i) for i in line]) - - return return_array - def getInputAsArraySplit(self, split_char: str = ',', return_type: Type = None) -> List: """ get input for day x with the lines split by split_char From b8e54f51f548693e63ca27b02596a617e2a7f52e Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Thu, 2 Dec 2021 05:17:29 +0100 Subject: [PATCH 016/144] better compare(), especially now it's doing what's expected (and documented) --- tools.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tools.py b/tools.py index a11be26..c47e9cd 100644 --- a/tools.py +++ b/tools.py @@ -1,6 +1,7 @@ import inspect import os.path import sys +from typing import Any def get_script_dir(follow_symlinks=True): @@ -19,11 +20,6 @@ def get_script_dir(follow_symlinks=True): return os.path.dirname(path) -def compare(a: int, b: int) -> int: +def compare(a: Any, b: Any) -> int: """compare to values, return -1 if a is smaller than b, 1 if a is greater than b, 0 is both are equal""" - if a > b: - return -1 - elif b > a: - return 1 - else: - return 0 + return bool(a > b) - bool(a < b) From ef0da77133c033700d150c1c7670e3c286e90839 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Thu, 2 Dec 2021 06:20:34 +0100 Subject: [PATCH 017/144] aoc.splitline(): allow return fields to have different types --- aoc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aoc.py b/aoc.py index af9ecb9..ed2cc38 100644 --- a/aoc.py +++ b/aoc.py @@ -96,7 +96,7 @@ class AOCDay: return return_array - def getInputAsArraySplit(self, split_char: str = ',', return_type: Type = None) -> List: + def getInputAsArraySplit(self, split_char: str = ',', return_type: Union[Type, List[Type]] = None) -> List: """ get input for day x with the lines split by split_char if input has only one line, returns a 1d array with the values @@ -139,5 +139,7 @@ def splitLine(line, split_char=',', return_type=None): if return_type is None: return line + elif isinstance(return_type, list): + return [return_type[x](i) if len(return_type) > x else i for x, i in enumerate(line)] else: return [return_type(i) for i in line] From c7b8c6ead68822ae027285e459ebcf24aa3f327f Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Thu, 2 Dec 2021 06:21:27 +0100 Subject: [PATCH 018/144] type annotations --- aoc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aoc.py b/aoc.py index ed2cc38..db845b0 100644 --- a/aoc.py +++ b/aoc.py @@ -133,7 +133,7 @@ def printSolution(day, part, solution, test=None, test_case=0, exec_time=None): print("Solution to day %s, part %s: %s%s" % (day, part, solution, time_output)) -def splitLine(line, split_char=',', return_type=None): +def splitLine(line, split_char: str = ',', return_type: Union[Type, List[Type]] = None): if split_char: line = line.split(split_char) From d0c5e319fb1ee03098623ced530d7cd8ad08a3ce Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Thu, 2 Dec 2021 06:22:33 +0100 Subject: [PATCH 019/144] more type annotations --- aoc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aoc.py b/aoc.py index db845b0..731f7e9 100644 --- a/aoc.py +++ b/aoc.py @@ -112,7 +112,7 @@ class AOCDay: return return_array -def printSolution(day, part, solution, test=None, test_case=0, exec_time=None): +def printSolution(day: int, part: int, solution: Any, test: bool = None, test_case: int = 0, exec_time: float = None): if exec_time is None: time_output = "" else: From 1c87545892015db469c900245d8d0cd83c0993d4 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Fri, 3 Dec 2021 06:56:17 +0100 Subject: [PATCH 020/144] AOCDay.getInput() always return a copy, never a reference to self.input --- aoc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aoc.py b/aoc.py index 731f7e9..ee3781f 100644 --- a/aoc.py +++ b/aoc.py @@ -63,7 +63,7 @@ class AOCDay: if len(self.input) == 1: return self.input[0] else: - return self.input + return self.input.copy() def getInputListAsType(self, return_type: Type) -> List: """ From da791d3ec5d3e424f1b508288161755ae9f9ca8b Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Fri, 3 Dec 2021 17:01:19 +0100 Subject: [PATCH 021/144] Grid.set() sets pos to True by default --- grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid.py b/grid.py index a124e29..e3f9c89 100644 --- a/grid.py +++ b/grid.py @@ -47,7 +47,7 @@ class Grid: self.__trackBoundaries(pos) self.__grid[pos] = not self.__default - def set(self, pos: Coordinate, value: Any): + def set(self, pos: Coordinate, value: Any = True): if value in OFF_STATES and pos in self.__grid: del self.__grid[pos] elif value not in OFF_STATES: From f6e9cfb4afc3979cc5fc5d3cd04fe6155f751f99 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 05:58:53 +0100 Subject: [PATCH 022/144] AOCDay.getMultilineInputArray(): work with an input-copy ... --- aoc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aoc.py b/aoc.py index ee3781f..b7236ca 100644 --- a/aoc.py +++ b/aoc.py @@ -75,7 +75,7 @@ class AOCDay: """ get input for day x as 2d array, split by empty lines """ - lines = self.input + lines = self.input.copy() lines.append('') return_array = [] From e2d36cbde6bd519a749248ad89d859dc9bec3aec Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 06:45:35 +0100 Subject: [PATCH 023/144] grid.Grid: let add() and sub() use self.set() instead of reimplementing part of it. Also makes sure default values aren't part of the dict anymore. --- grid.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/grid.py b/grid.py index e3f9c89..cac3a98 100644 --- a/grid.py +++ b/grid.py @@ -48,7 +48,7 @@ class Grid: self.__grid[pos] = not self.__default def set(self, pos: Coordinate, value: Any = True): - if value in OFF_STATES and pos in self.__grid: + if (value == self.__default or value in OFF_STATES) and pos in self.__grid: del self.__grid[pos] elif value not in OFF_STATES: self.__trackBoundaries(pos) @@ -90,18 +90,10 @@ class Grid: return self.minX <= pos.x <= self.maxX and self.minY <= pos.y <= self.maxY def add(self, pos: Coordinate, value: Union[float, int] = 1): - if pos in self.__grid: - self.__grid[pos] += value - else: - self.__trackBoundaries(pos) - self.__grid[pos] = self.__default + value + self.set(pos, self.get(pos) + value) def sub(self, pos: Coordinate, value: Union[float, int] = 1): - if pos in self.__grid: - self.__grid[pos] -= value - else: - self.__trackBoundaries(pos) - self.__grid[pos] = self.__default - value + self.set(pos, self.get(pos) - value) def getActiveCells(self): return [i for i in self.__grid.keys()] From 122f1e768e60d40f59b08d00ebc89baf6eee7a96 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 06:46:43 +0100 Subject: [PATCH 024/144] grid.Grid(): let set() not track default values not present in OFF_STATES --- grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid.py b/grid.py index cac3a98..fd3bbd3 100644 --- a/grid.py +++ b/grid.py @@ -50,7 +50,7 @@ class Grid: def set(self, pos: Coordinate, value: Any = True): if (value == self.__default or value in OFF_STATES) and pos in self.__grid: del self.__grid[pos] - elif value not in OFF_STATES: + elif value != self.__default and value not in OFF_STATES: self.__trackBoundaries(pos) self.__grid[pos] = value From 2b19538fb2fbcc948fc82e07034f81b8a04dd72a Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 07:16:40 +0100 Subject: [PATCH 025/144] Coordinate.getLineTo(): return coordinates in a line from self to target --- coordinate.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/coordinate.py b/coordinate.py index 5b2567b..835b6b3 100644 --- a/coordinate.py +++ b/coordinate.py @@ -1,9 +1,11 @@ from __future__ import annotations from dataclasses import dataclass from enum import Enum -from math import sqrt, inf, atan2, degrees +from math import gcd, sqrt, inf, atan2, degrees from typing import Union, List, Optional +from tools import compare + class DistanceAlgorithm(Enum): MANHATTAN = 0 @@ -116,6 +118,14 @@ class Coordinate: else: return 180.0 + abs(angle) + def getLineTo(self, target: Coordinate) -> List[Coordinate]: + diff = target - self + steps = gcd(diff.x, diff.y) + step_x = diff.x // steps + step_y = diff.y // steps + + return [Coordinate(self.x + step_x * i, self.y + step_y * i) for i in range(steps + 1)] + def __add__(self, other: Coordinate) -> Coordinate: if self.z is None: return Coordinate(self.x + other.x, self.y + other.y) From 311cb9edba85bbda348f7e1339abb492e23f9742 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 07:36:38 +0100 Subject: [PATCH 026/144] Coordinate: remember the 3rd dimension ... --- coordinate.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/coordinate.py b/coordinate.py index 835b6b3..afb9408 100644 --- a/coordinate.py +++ b/coordinate.py @@ -105,7 +105,7 @@ class Coordinate: def getAngleTo(self, target: Coordinate, normalized: bool = False) -> float: """normalized returns an angle going clockwise with 0 starting in the 'north'""" if self.z is not None: - raise NotImplementedError() + raise NotImplementedError() # which angle?!?! dx = target.x - self.x dy = target.y - self.y @@ -120,11 +120,18 @@ class Coordinate: def getLineTo(self, target: Coordinate) -> List[Coordinate]: diff = target - self - steps = gcd(diff.x, diff.y) - step_x = diff.x // steps - step_y = diff.y // steps - return [Coordinate(self.x + step_x * i, self.y + step_y * i) for i in range(steps + 1)] + if self.z is None: + steps = gcd(diff.x, diff.y) + step_x = diff.x // steps + step_y = diff.y // steps + return [Coordinate(self.x + step_x * i, self.y + step_y * i) for i in range(steps + 1)] + else: + steps = gcd(diff.x, diff.y, diff.z) + step_x = diff.x // steps + step_y = diff.y // steps + step_z = diff.z // steps + return [Coordinate(self.x + step_x * i, self.y + step_y * i, self.z + step_z * i) for i in range(steps + 1)] def __add__(self, other: Coordinate) -> Coordinate: if self.z is None: @@ -139,5 +146,14 @@ class Coordinate: return Coordinate(self.x - other.x, self.y - other.y, self.z - other.z) @staticmethod - def generate(from_x: int, to_x: int, from_y: int, to_y: int) -> List[Coordinate]: - return [Coordinate(x, y) for x in range(from_x, to_x + 1) for y in range(from_y, to_y + 1)] + def generate(from_x: int, to_x: int, from_y: int, to_y: int, + from_z: int = None, to_z: int = None) -> List[Coordinate]: + if from_z is None and to_z is None: + return [Coordinate(x, y) for x in range(from_x, to_x + 1) for y in range(from_y, to_y + 1)] + else: + return [ + Coordinate(x, y, z) + for x in range(from_x, to_x + 1) + for y in range(from_y, to_y + 1) + for z in range(from_z, to_z + 1) + ] From 766a16c314e38717e7375197458ac68efdae0623 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 07:38:46 +0100 Subject: [PATCH 027/144] remove unused import --- coordinate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/coordinate.py b/coordinate.py index afb9408..90a65be 100644 --- a/coordinate.py +++ b/coordinate.py @@ -4,8 +4,6 @@ from enum import Enum from math import gcd, sqrt, inf, atan2, degrees from typing import Union, List, Optional -from tools import compare - class DistanceAlgorithm(Enum): MANHATTAN = 0 From 266bc44752933cf137fc38cbfe8203a214f6e546 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 07:44:02 +0100 Subject: [PATCH 028/144] pythagoras is hard (not it isn't) --- coordinate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/coordinate.py b/coordinate.py index 90a65be..a7a9917 100644 --- a/coordinate.py +++ b/coordinate.py @@ -35,8 +35,7 @@ class Coordinate: if self.z is None: return sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2) else: - ab = sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2) - return sqrt(ab ** 2 + abs(self.z - target.z) ** 2) + return sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2 + abs(self.z - target.z) ** 2) elif mode == DistanceAlgorithm.MANHATTAN: if not includeDiagonals: if self.z is None: From 75c7df6f1954f26758ffc8830762f91e1f7a0973 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 07:53:17 +0100 Subject: [PATCH 029/144] useless list comprehension is useless --- grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid.py b/grid.py index fd3bbd3..baa20c0 100644 --- a/grid.py +++ b/grid.py @@ -96,7 +96,7 @@ class Grid: self.set(pos, self.get(pos) - value) def getActiveCells(self): - return [i for i in self.__grid.keys()] + return list(self.__grid.keys()) def getSum(self, includeNegative: bool = True): grid_sum = 0 From 37166532aede746b51b2dff050b54298ca90487f Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 08:21:26 +0100 Subject: [PATCH 030/144] grid.Grid: introduce rudimentary 3d support --- grid.py | 72 ++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/grid.py b/grid.py index baa20c0..7ab23c8 100644 --- a/grid.py +++ b/grid.py @@ -9,7 +9,8 @@ ON = True class GridTransformation(Enum): - FLIP_HORIZONTALLY = 1 + FLIP_X = 1 + FLIP_HORIZONTALLY = 1 # alias for FLIP_X; prep for 3d-transformations FLIP_VERTICALLY = 2 FLIP_DIAGONALLY = 3 FLIP_DIAGONALLY_REV = 4 @@ -26,19 +27,18 @@ class Grid: self.minY = 0 self.maxX = 0 self.maxY = 0 + self.minZ = 0 + self.maxZ = 0 + self.mode3D = False def __trackBoundaries(self, pos: Coordinate): - if pos.x < self.minX: - self.minX = pos.x - - if pos.y < self.minY: - self.minY = pos.y - - if pos.x > self.maxX: - self.maxX = pos.x - - if pos.y > self.maxY: - self.maxY = pos.y + self.minX = min(self.minX, pos.x) + self.minY = min(self.minY, pos.y) + self.maxX = max(self.maxX, pos.x) + self.maxY = max(self.maxY, pos.y) + if self.mode3D: + self.minZ = min(self.minZ, pos.z) + self.maxZ = max(self.maxZ, pos.z) def toggle(self, pos: Coordinate): if pos in self.__grid: @@ -48,6 +48,9 @@ class Grid: self.__grid[pos] = not self.__default def set(self, pos: Coordinate, value: Any = True): + if pos.z is not None: + self.mode3D = True + if (value == self.__default or value in OFF_STATES) and pos in self.__grid: del self.__grid[pos] elif value != self.__default and value not in OFF_STATES: @@ -66,28 +69,41 @@ class Grid: else: return self.__grid[pos] not in OFF_STATES - def getValue(self, pos: Coordinate) -> any: - if pos not in self.__grid: - return self.__default - else: - return self.__grid[pos] - def getOnCount(self): return len(self.__grid) def isSet(self, pos: Coordinate) -> bool: return pos in self.__grid + def getCorners(self): + if not self.mode3D: + return [ + Coordinate(self.minX, self.minY), + Coordinate(self.minX, self.maxY), + Coordinate(self.maxX, self.minY), + Coordinate(self.maxX, self.maxY), + ] + else: + return [ + Coordinate(self.minX, self.minY, self.minZ), + Coordinate(self.minX, self.minY, self.maxZ), + Coordinate(self.minX, self.maxY, self.minZ), + Coordinate(self.minX, self.maxY, self.maxZ), + Coordinate(self.maxX, self.minY, self.minZ), + Coordinate(self.maxX, self.minY, self.maxZ), + Coordinate(self.maxX, self.maxY, self.minZ), + Coordinate(self.maxX, self.maxY, self.maxZ), + ] + def isCorner(self, pos: Coordinate) -> bool: - return pos in [ - Coordinate(self.minX, self.minY), - Coordinate(self.minX, self.maxY), - Coordinate(self.maxX, self.minY), - Coordinate(self.maxX, self.maxY), - ] + return pos in self.getCorners() def isWithinBoundaries(self, pos: Coordinate) -> bool: - return self.minX <= pos.x <= self.maxX and self.minY <= pos.y <= self.maxY + if self.mode3D: + return self.minX <= pos.x <= self.maxX and self.minY <= pos.y <= self.maxY \ + and self.minZ <= pos.z <= self.maxZ + else: + return self.minX <= pos.x <= self.maxX and self.minY <= pos.y <= self.maxY def add(self, pos: Coordinate, value: Union[float, int] = 1): self.set(pos, self.get(pos) + value) @@ -112,7 +128,8 @@ class Grid: for neighbour in pos.getNeighbours( includeDiagonal=includeDiagonal, minX=self.minX, minY=self.minY, - maxX=self.maxX, maxY=self.maxY): + maxX=self.maxX, maxY=self.maxY, + minZ=self.minZ, maxZ=self.maxZ): if neighbour in self.__grid: if includeNegative or self.__grid[neighbour] > 0: neighbour_sum += self.__grid[neighbour] @@ -125,6 +142,9 @@ class Grid: self.set(c2, buf) def transform(self, mode: GridTransformation): + if self.mode3D: + raise NotImplementedError() # that will take some time and thought + if mode == GridTransformation.FLIP_HORIZONTALLY: for x in range(self.minX, (self.maxX - self.minX) // 2 + 1): for y in range(self.minY, self.maxY + 1): From f8db937643e41dea51254fbd39a08219072ec602 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 10:08:38 +0100 Subject: [PATCH 031/144] simple scheduler to be called from daemon processes or similar --- schedule.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 schedule.py diff --git a/schedule.py b/schedule.py new file mode 100644 index 0000000..d26c357 --- /dev/null +++ b/schedule.py @@ -0,0 +1,22 @@ +import datetime +from typing import Callable, List, Any + + +class Scheduler: + def __init__(self): + self.jobs = [] + + def schedule(self, timedelta: datetime.timedelta, func: Callable, *args: List[Any]): + self.jobs.append({ + 'func': func, + 'args': args, + 'timedelta': timedelta, + 'runat': (datetime.datetime.utcnow() + timedelta) + }) + + def run_pending(self): + now = datetime.datetime.utcnow() + for job in self.jobs: + if job['runat'] <= now: + job['runat'] += job['timedelta'] + job['func'](*job['args']) From 955f4ad88c70c0f7ec453fa70fab5362a4e2643c Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 10:14:00 +0100 Subject: [PATCH 032/144] simple scheduler to be called from daemon processes or similar --- schedule.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/schedule.py b/schedule.py index d26c357..a0bcc30 100644 --- a/schedule.py +++ b/schedule.py @@ -4,19 +4,23 @@ from typing import Callable, List, Any class Scheduler: def __init__(self): - self.jobs = [] + self.jobs = {} - def schedule(self, timedelta: datetime.timedelta, func: Callable, *args: List[Any]): - self.jobs.append({ - 'func': func, + def schedule(self, name: str, every: datetime.timedelta, func: Callable[..., None], *args: List[Any]): + self.jobs[name] = { + 'call': func, 'args': args, - 'timedelta': timedelta, - 'runat': (datetime.datetime.utcnow() + timedelta) - }) + 'timedelta': every, + 'runat': (datetime.datetime.utcnow() + every) + } + + def unschedule(self, name: str): + if name in self.jobs: + del self.jobs[name] def run_pending(self): now = datetime.datetime.utcnow() - for job in self.jobs: + for _, job in self.jobs: if job['runat'] <= now: job['runat'] += job['timedelta'] - job['func'](*job['args']) + job['call'](*job['args']) From cce6e058c27d7184ffd900c7cf89bcc286aa51c5 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 10:19:36 +0100 Subject: [PATCH 033/144] the famous daemon class from Joseph Ernest --- daemon.py | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 daemon.py diff --git a/daemon.py b/daemon.py new file mode 100644 index 0000000..780846d --- /dev/null +++ b/daemon.py @@ -0,0 +1,153 @@ +# Shamelessly stolen from https://gist.github.com/josephernest/77fdb0012b72ebdf4c9d19d6256a1119 +# +# From "A simple unix/linux daemon in Python" by Sander Marechal +# See http://stackoverflow.com/a/473702/1422096 and +# http://web.archive.org/web/20131017130434/http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/ +# +# Modified to add quit() that allows to run some code before closing the daemon +# See http://stackoverflow.com/a/40423758/1422096 +# +# Modified for Python 3 +# (see also: http://web.archive.org/web/20131017130434/http://www.jejik.com/files/examples/daemon3x.py) +# +# Joseph Ernest, 20200507_1220 + +import atexit +import os +import sys +import time +from signal import SIGTERM, signal + + +class Daemon: + """ + A generic daemon class. + + Usage: subclass the Daemon class and override the run() method + """ + + def __init__(self, pidfile='_.pid', stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.pidfile = pidfile + + def daemonize(self): + """ + do the UNIX double-fork magic, see Stevens' "Advanced + Programming in the UNIX Environment" for details (ISBN 0201563177) + http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + """ + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError as e: + sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + # decouple from parent environment + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + # exit from second parent + sys.exit(0) + except OSError as e: + sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + # redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = open(os.devnull, 'r') + so = open(os.devnull, 'a+') + se = open(os.devnull, 'a+') + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + atexit.register(self.onstop) + signal(SIGTERM, lambda signum, stack_frame: exit()) + + # write pidfile + pid = str(os.getpid()) + open(self.pidfile, 'w+').write("%s\n" % pid) + + def onstop(self): + self.quit() + os.remove(self.pidfile) + + def start(self): + """ + Start the daemon + """ + # Check for a pidfile to see if the daemon already runs + try: + pf = open(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if pid: + message = "pidfile %s already exist. Daemon already running?\n" + sys.stderr.write(message % self.pidfile) + sys.exit(1) + + # Start the daemon + self.daemonize() + self.run() + + def stop(self): + """ + Stop the daemon + """ + # Get the pid from the pidfile + try: + pf = open(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if not pid: + message = "pidfile %s does not exist. Daemon not running?\n" + sys.stderr.write(message % self.pidfile) + return # not an error in a restart + + # Try killing the daemon process + try: + while 1: + os.kill(pid, SIGTERM) + time.sleep(0.1) + except OSError as err: + err = str(err) + if err.find("No such process") > 0: + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + else: + print(str(err)) + sys.exit(1) + + def restart(self): + """ + Restart the daemon + """ + self.stop() + self.start() + + def run(self): + """ + You should override this method when you subclass Daemon. It will be called after the process has been + daemonized by start() or restart(). + """ + + def quit(self): + """ + You should override this method when you subclass Daemon. It will be called before the process is stopped. + """ From adff0f724fc3ca3069ba14cff99c4547dbaa29e0 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 10:34:41 +0100 Subject: [PATCH 034/144] simple helper to load (json) files into dicts --- datafiles.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 datafiles.py diff --git a/datafiles.py b/datafiles.py new file mode 100644 index 0000000..601c607 --- /dev/null +++ b/datafiles.py @@ -0,0 +1,38 @@ +import json +import os + + +class DataFile(dict): + def __init__(self, filename: str, create: bool): + super().__init__() + self.__filename = filename + + try: + os.stat(self.__filename) + except OSError as e: + if not create: + raise e + + self.load() + + def load(self): + pass + + def save(self): + pass + + +class JSONFile(DataFile): + def __init__(self, filename: str, create: bool): + super().__init__(filename, create) + + def load(self): + with open(self.__filename, "U") as f: + json_dict = json.loads(f.read()) + + for k in json_dict: + self[k] = json_dict[k] + + def save(self): + with open(self.__filename, "w") as f: + f.write(json.dumps(self.copy())) From f1c731bb7cb8bd59e133b36aa80c0b4f2906f7ab Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 11:14:23 +0100 Subject: [PATCH 035/144] shot at simplifying sockets .. somewhat --- simplesocket.py | 109 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 simplesocket.py diff --git a/simplesocket.py b/simplesocket.py new file mode 100644 index 0000000..99e5c10 --- /dev/null +++ b/simplesocket.py @@ -0,0 +1,109 @@ +import errno +import socket +import threading +import time +from typing import Callable, Union + + +class Socket: + def __init__(self, address_family: socket.AddressFamily, socket_kind: socket.SocketKind): + self.socket = socket.socket(family=address_family, type=socket_kind) + self.__recv_buffer = b"" + + def send(self, buffer: Union[str, bytes]): + if isinstance(buffer, str): + buffer = buffer.encode("UTF-8") + + send_bytes = 0 + while send_bytes < len(buffer): + send_bytes += self.socket.send(buffer[send_bytes:]) + + return send_bytes + + def recv(self, maxlen: int = 4096, blocking: bool = True) -> bytes: + maxlen -= len(self.__recv_buffer) + try: + if blocking: + ret = self.__recv_buffer + self.socket.recv(maxlen) + else: + ret = self.__recv_buffer + self.socket.recv(maxlen, socket.MSG_DONTWAIT) + + self.__recv_buffer = b"" + return ret + except socket.error as e: + err = e.args[0] + if err == errno.EAGAIN or err == errno.EWOULDBLOCK: + return self.__recv_buffer + else: + raise + + def sendline(self, line: str): + if not line.endswith("\n"): + line += "\n" + + self.send(line) + + def recvline(self, timeout: int = 10) -> str: + start = time.time() + while b"\n" not in self.__recv_buffer and b"\r" not in self.__recv_buffer: + self.__recv_buffer = self.recv(256, blocking=False) + if time.time() - start <= timeout: + time.sleep(0.01) # release *some* resources + else: + break + + newline = b"\n" + if newline not in self.__recv_buffer: + newline = b"\r" + if newline not in self.__recv_buffer: + ret = self.__recv_buffer.decode("UTF-8") + self.__recv_buffer = b"" + + ret = self.__recv_buffer[:self.__recv_buffer.index(newline)].decode("UTF-8") + self.__recv_buffer = self.__recv_buffer[self.__recv_buffer.index(newline) + 1:] + return ret + + def close(self): + self.socket.close() + + +class ClientSocket(Socket): + def __init__(self, addr: str, port: int, address_family: socket.AddressFamily = socket.AF_INET, + socket_kind: socket.SocketKind = socket.SOCK_STREAM): + super().__init__(address_family, socket_kind) + self.socket.connect((addr, port)) + self.laddr, self.lport = self.socket.getsockname() + self.raddr, self.rport = self.socket.getpeername() + + +class RemoteSocket(Socket): + def __init__(self, client_sock: socket.socket): + super().__init__(client_sock.family, client_sock.type) + self.socket = client_sock + self.laddr, self.lport = self.socket.getsockname() + self.raddr, self.rport = self.socket.getpeername() + + +class ServerSocket(Socket): + def __init__(self, addr: str, port: int, address_family: socket.AddressFamily = socket.AF_INET, + socket_kind: socket.SocketKind = socket.SOCK_STREAM): + super().__init__(address_family, socket_kind) + self.socket.bind((addr, port)) + self.socket.listen(5) + self.laddr, self.lport = self.socket.getsockname() + self.raddr, self.rport = None, None # Transport endpoint is not connected. Surprisingly. + + def _connection_acceptor(self, target: Callable[..., None]): + while 1: + (client_socket, client_address) = self.socket.accept() + connection_handler_thread = threading.Thread(target=target, args=(RemoteSocket(client_socket), )) + connection_handler_thread.start() + + def accept(self, target: Callable[..., None], blocking: bool = True): + if blocking: + self._connection_acceptor(target) + return None + else: + connection_accept_thread = threading.Thread(target=self._connection_acceptor, kwargs={'target': target}) + connection_accept_thread.start() + return connection_accept_thread From 779d77dad7876b240c505cab1b4c46edd142c3f7 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 13:20:05 +0100 Subject: [PATCH 036/144] a (very) simple irc client ... --- irc.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++++ simplesocket.py | 26 +++++++++++++---------- 2 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 irc.py diff --git a/irc.py b/irc.py new file mode 100644 index 0000000..75d7b1d --- /dev/null +++ b/irc.py @@ -0,0 +1,56 @@ +from simplesocket import ClientSocket +from typing import Callable + + +class Client: + def __init__(self, server: str, port: int, nick: str, username: str, realname: str = "Python Bot"): + self.nickname = nick + self.__server = ClientSocket(server, port) + self.__server.sendline("USER %s ignore ignore :%s" % (username, realname)) + self.__server.sendline("NICK %s" % self.nickname) + self.__function_register = { + 'NICK': self.on_nick + } + self.receive() + + def receive(self): + while line := self.__server.recvline(): + print(line) + if line.startswith("PING"): + self.__server.sendline("PONG " + line.split()[1]) + continue + + (msg_from, msg_type, msg_to, *msg) = line[1:].split() + if msg_type == "433": + self.nickname = msg[0] + "_" + self.__server.sendline("NICK %s" % self.nickname) + else: + if msg_type in self.__function_register: + self.__function_register[msg_type](msg_from, msg_to, " ".join(msg)[1:]) + + def register(self, msg_type: str, func: Callable[..., None]): + self.__function_register[msg_type] = func + + def on_nick(self, old_nick: str, new_nick: str, _): + old_nick = old_nick.split("!")[0] + if old_nick == self.nickname: + self.nickname = new_nick[1:] + + def nick(self, new_nick: str): + self.__server.sendline("NICK %s" % new_nick) + + def join(self, channel: str): + self.__server.sendline("JOIN %s" % channel) + self.receive() + + def leave(self, channel: str): + self.__server.sendline("LEAVE %s" % channel) + self.receive() + + def privmsg(self, target: str, message: str): + self.__server.sendline("PRIVMSG %s :%s" % (target, message)) + + def quit(self, message: str = "Elvis has left the building!"): + self.__server.sendline("QUIT :%s" % message) + self.receive() + self.__server.close() diff --git a/simplesocket.py b/simplesocket.py index 99e5c10..9457a8e 100644 --- a/simplesocket.py +++ b/simplesocket.py @@ -10,7 +10,7 @@ class Socket: self.socket = socket.socket(family=address_family, type=socket_kind) self.__recv_buffer = b"" - def send(self, buffer: Union[str, bytes]): + def send(self, buffer: Union[str, bytes]) -> int: if isinstance(buffer, str): buffer = buffer.encode("UTF-8") @@ -23,11 +23,8 @@ class Socket: def recv(self, maxlen: int = 4096, blocking: bool = True) -> bytes: maxlen -= len(self.__recv_buffer) try: - if blocking: - ret = self.__recv_buffer + self.socket.recv(maxlen) - else: - ret = self.__recv_buffer + self.socket.recv(maxlen, socket.MSG_DONTWAIT) - + self.socket.setblocking(blocking) + ret = self.__recv_buffer + self.socket.recv(maxlen) self.__recv_buffer = b"" return ret except socket.error as e: @@ -52,15 +49,22 @@ class Socket: else: break - newline = b"\n" + newline = b"\r\n" if newline not in self.__recv_buffer: - newline = b"\r" + newline = b"\n" if newline not in self.__recv_buffer: - ret = self.__recv_buffer.decode("UTF-8") - self.__recv_buffer = b"" + newline = b"\r" + if newline not in self.__recv_buffer: + ret = self.__recv_buffer.decode("UTF-8") + self.__recv_buffer = b"" + return ret ret = self.__recv_buffer[:self.__recv_buffer.index(newline)].decode("UTF-8") - self.__recv_buffer = self.__recv_buffer[self.__recv_buffer.index(newline) + 1:] + if len(self.__recv_buffer) - len(newline) > self.__recv_buffer.index(newline): + self.__recv_buffer = self.__recv_buffer[self.__recv_buffer.index(newline) + len(newline):] + else: + self.__recv_buffer = b"" + return ret def close(self): From 3933cdbadf4c46789ac2278187dc0a9053b34a41 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 15:27:31 +0100 Subject: [PATCH 037/144] properly keep track of own nickname --- irc.py | 181 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 172 insertions(+), 9 deletions(-) diff --git a/irc.py b/irc.py index 75d7b1d..1700b62 100644 --- a/irc.py +++ b/irc.py @@ -1,15 +1,166 @@ from simplesocket import ClientSocket from typing import Callable +from enum import Enum + + +class ServerReply(str, Enum): + RPL_WELCOME = "001" + RPL_YOURHOST = "002" + RPL_CREATED = "003" + RPL_MYINFO = "004" + RPL_BOUNCE = "005" + RPL_TRACELINK = "200" + RPL_TRACECONNECTING = "201" + RPL_TRACEHANDSHAKE = "202" + RPL_TRACEUNKNOWN = "203" + RPL_TRACEOPERATOR = "204" + RPL_TRACEUSER = "205" + RPL_TRACESERVER = "206" + RPL_TRACENEWTYPE = "208" + RPL_TRACECLASS = "209" + RPL_TRACERECONNECT = "210" + RPL_STATSLINKINFO = "211" + RPL_STATSCOMMANDS = "212" + RPL_STATSCLINE = "213" + RPL_STATSNLINE = "214" + RPL_STATSILINE = "215" + RPL_STATSKLINE = "216" + RPL_STATSYLINE = "218" + RPL_ENDOFSTATS = "219" + RPL_UMODEIS = "221" + RPL_SERVLIST = "234" + RPL_SERVLISTEND = "235" + RPL_STATSLLINE = "241" + RPL_STATSUPTIME = "242" + RPL_STATSOLINE = "243" + RPL_STATSHLINE = "244" + RPL_LUSERCLIENT = "251" + RPL_LUSEROP = "252" + RPL_LUSERUNKNOWN = "253" + RPL_LUSERCHANNELS = "254" + RPL_LUSERME = "255" + RPL_ADMINME = "256" + RPL_ADMINLOC1 = "257" + RPL_ADMINLOC2 = "258" + RPL_ADMINEMAIL = "259" + RPL_TRACELOG = "261" + RPL_TRACEEND = "262" + RPL_TRYAGAIN = "263" + RPL_NONE = "300" + RPL_AWAY = "301" + RPL_USERHOST = "302" + RPL_ISON = "303" + RPL_UNAWAY = "305" + RPL_NOWAWAY = "306" + RPL_WHOISUSER = "311" + RPL_WHOISSERVER = "312" + RPL_WHOISOPERATOR = "313" + RPL_WHOWASUSER = "314" + RPL_ENDOFWHO = "315" + RPL_WHOISIDLE = "317" + RPL_ENDOFWHOIS = "318" + RPL_WHOISCHANNELS = "319" + RPL_LISTSTART = "321" + RPL_LIST = "322" + RPL_LISTEND = "323" + RPL_CHANNELMODEIS = "324" + RPL_UNIQOPIS = "325" + RPL_NOTOPIC = "331" + RPL_TOPIC = "332" + RPL_INVITING = "341" + RPL_SUMMONING = "342" + RPL_INVITELIST = "346" + RPL_ENDOFINVITELIST = "347" + RPL_EXCEPTLIST = "348" + RPL_ENDOFEXCEPTLIST = "349" + RPL_VERSION = "351" + RPL_WHOREPLY = "352" + RPL_NAMEREPLY = "353" + RPL_LINKS = "364" + RPL_ENDOFLINKS = "365" + RPL_ENDOFNAMES = "366" + RPL_BANLIST = "367" + RPL_ENDOFBANLIST = "368" + RPL_ENDOFWHOWAS = "369" + RPL_INFO = "371" + RPL_MOTD = "372" + RPL_ENDOFINFO = "374" + RPL_MOTDSTART = "375" + RPL_ENDOFMOTD = "376" + RPL_YOUREOPER = "381" + RPL_REHASHING = "382" + RPL_YOURESERVICE = "383" + RPL_TIME = "391" + RPL_USERSTART = "392" + RPL_USERS = "393" + RPL_ENDOFUSERS = "394" + RPL_NOUSERS = "395" + ERR_NOSUCHNICK = "401" + ERR_NOSUCHSERVER = "402" + ERR_NOSUCHCHANNEL = "403" + ERR_CANNOTSENDTOCHAN = "404" + ERR_TOOMANYCHANNELS = "405" + ERR_WASNOSUCHNICK = "406" + ERR_TOOMANYTARGETS = "407" + ERR_NOSUCHSERVICE = "408" + ERR_NOORIGIN = "409" + ERR_NORECIPIENT = "411" + ERR_NOTEXTTOSEND = "412" + ERR_NOTOPLEVEL = "413" + ERR_WILDTOPLEVEL = "414" + ERR_BANMASK = "415" + ERR_UNKNOWNCOMMAND = "421" + ERR_NOMOTD = "422" + ERR_NOADMININFO = "423" + ERR_FILEERROR = "424" + ERR_NONICKNAMEGIVEN = "431" + ERR_ERRONEUSNICKNAME = "432" + ERR_NICKNAMEINUSE = "433" + ERR_NICKCOLLISION = "436" + ERR_UNAVAILRESOURCE = "437" + ERR_USERNOTINCHANNEL = "441" + ERR_NOTOONCHANNEL = "442" + ERR_USERONCHANNEL = "443" + ERR_NOLOGIN = "444" + ERR_SUMMONDISABLED = "445" + ERR_USERSDISABLED = "446" + ERR_NOTREGISTERED = "451" + ERR_NEEDMOREPARAMS = "461" + ERR_ALREADYREGISTERED = "462" + ERR_NOPERMFORHOST = "463" + ERR_PASSWDMISMATH = "464" + ERR_YOUREBANNEDCREEP = "465" + ERR_YOUWILLBEBANNED = "466" + ERR_KEYSET = "467" + ERR_CHANNELISFULL = "471" + ERR_UNKNOWNMODE = "472" + ERR_INVITEONLYCHAN = "473" + ERR_BANNEDFROMCHAN = "474" + ERR_BADCHANNELKEY = "475" + ERR_BADCHANMASK = "476" + ERR_BASCHANMODES = "477" + ERR_BANLISTFULL = "478" + ERR_NOPRIVILEGES = "481" + ERR_CHANOPRIVSNEEDED = "482" + ERR_CANTKILLSERVER = "483" + ERR_RESTRICTED = "484" + ERR_UNIQOPPRIVSNEEDED = "485" + ERR_NOOPERHOST = "491" + ERR_UMODEUNKNOWNFLAG = "501" + ERR_USERSDONTMATCH = "502" class Client: def __init__(self, server: str, port: int, nick: str, username: str, realname: str = "Python Bot"): - self.nickname = nick + self.nickname = None self.__server = ClientSocket(server, port) self.__server.sendline("USER %s ignore ignore :%s" % (username, realname)) - self.__server.sendline("NICK %s" % self.nickname) + self.__server.sendline("NICK %s" % nick) self.__function_register = { - 'NICK': self.on_nick + ServerReply.RPL_WELCOME: self.on_welcome, + ServerReply.ERR_NICKNAMEINUSE: self.on_nickname_in_use, + 'NICK': self.on_nick, + '__default__': self.unhandled_server_message, } self.receive() @@ -21,21 +172,33 @@ class Client: continue (msg_from, msg_type, msg_to, *msg) = line[1:].split() - if msg_type == "433": - self.nickname = msg[0] + "_" - self.__server.sendline("NICK %s" % self.nickname) + if msg[0].startswith(":"): + msg[0] = msg[0][1:] + message = " ".join(msg) + + if msg_type in self.__function_register: + self.__function_register[msg_type](msg_from, msg_to, message) else: - if msg_type in self.__function_register: - self.__function_register[msg_type](msg_from, msg_to, " ".join(msg)[1:]) + self.__function_register['__default__'](msg_from, msg_type, msg_to, message) def register(self, msg_type: str, func: Callable[..., None]): self.__function_register[msg_type] = func - def on_nick(self, old_nick: str, new_nick: str, _): + def on_welcome(self, msg_from: str, msg_to: str, message: str): + self.nickname = msg_to + + def on_nickname_in_use(self, msg_from: str, msg_to: str, message: str): + if self.nickname is None: + self.nick(message.split()[0] + "_") + + def on_nick(self, old_nick: str, new_nick: str, message: str): old_nick = old_nick.split("!")[0] if old_nick == self.nickname: self.nickname = new_nick[1:] + def unhandled_server_message(self, msg_from: str, msg_type: str, msg_to: str, message: str): + print(msg_from, msg_type, msg_to, message) + def nick(self, new_nick: str): self.__server.sendline("NICK %s" % new_nick) From 8c9206effe146658fbde9bcaab52ecafe6f0a584 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 16:06:25 +0100 Subject: [PATCH 038/144] filehandling woes --- datafiles.py | 18 +++++++++++------- irc.py | 3 +-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/datafiles.py b/datafiles.py index 601c607..35da69a 100644 --- a/datafiles.py +++ b/datafiles.py @@ -5,13 +5,15 @@ import os class DataFile(dict): def __init__(self, filename: str, create: bool): super().__init__() - self.__filename = filename + self.filename = filename try: - os.stat(self.__filename) + os.stat(self.filename) except OSError as e: if not create: raise e + else: + open(self.filename, "w").close() self.load() @@ -27,12 +29,14 @@ class JSONFile(DataFile): super().__init__(filename, create) def load(self): - with open(self.__filename, "U") as f: - json_dict = json.loads(f.read()) + with open(self.filename, "rt") as f: + c = f.read() - for k in json_dict: - self[k] = json_dict[k] + if len(c) > 0: + json_dict = json.loads(c) + for k in json_dict: + self[k] = json_dict[k] def save(self): - with open(self.__filename, "w") as f: + with open(self.filename, "wt") as f: f.write(json.dumps(self.copy())) diff --git a/irc.py b/irc.py index 1700b62..744c18c 100644 --- a/irc.py +++ b/irc.py @@ -166,13 +166,12 @@ class Client: def receive(self): while line := self.__server.recvline(): - print(line) if line.startswith("PING"): self.__server.sendline("PONG " + line.split()[1]) continue (msg_from, msg_type, msg_to, *msg) = line[1:].split() - if msg[0].startswith(":"): + if len(msg) > 0 and msg[0].startswith(":"): msg[0] = msg[0][1:] message = " ".join(msg) From d757a82d805484d47d8d7664826132395781c1eb Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 5 Dec 2021 16:15:40 +0100 Subject: [PATCH 039/144] using dicts is hard ... --- schedule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schedule.py b/schedule.py index a0bcc30..762653d 100644 --- a/schedule.py +++ b/schedule.py @@ -20,7 +20,7 @@ class Scheduler: def run_pending(self): now = datetime.datetime.utcnow() - for _, job in self.jobs: + for job in self.jobs.values(): if job['runat'] <= now: job['runat'] += job['timedelta'] job['call'](*job['args']) From 57ba56bf2bd2cd8e642533a8f9f06ff218a2030e Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 6 Dec 2021 05:56:26 +0100 Subject: [PATCH 040/144] convert timedelta to readable string --- tools.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tools.py b/tools.py index c47e9cd..c277cc6 100644 --- a/tools.py +++ b/tools.py @@ -1,3 +1,4 @@ +import datetime import inspect import os.path import sys @@ -23,3 +24,21 @@ def get_script_dir(follow_symlinks=True): def compare(a: Any, b: Any) -> int: """compare to values, return -1 if a is smaller than b, 1 if a is greater than b, 0 is both are equal""" return bool(a > b) - bool(a < b) + + +def human_readable_time_from_delta(delta: datetime.timedelta) -> str: + time_str = "" + if delta.days > 0: + time_str += "%d days, " % delta.days + + if delta.seconds > 3600: + time_str += "%02d:" % (delta.seconds // 3600) + else: + time_str += "00:" + + if delta.seconds % 3600 > 60: + time_str += "%02d:" % (delta.seconds % 3600 // 60) + else: + time_str += "00:" + + return time_str + "%02d" % (delta.seconds % 60) From f5d6f2deb0585ad93b2effed20cbed3efb18a10d Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 6 Dec 2021 08:35:05 +0100 Subject: [PATCH 041/144] catch empty lines and thelike --- irc.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/irc.py b/irc.py index 744c18c..7de2b91 100644 --- a/irc.py +++ b/irc.py @@ -170,7 +170,12 @@ class Client: self.__server.sendline("PONG " + line.split()[1]) continue - (msg_from, msg_type, msg_to, *msg) = line[1:].split() + try: + (msg_from, msg_type, msg_to, *msg) = line[1:].split() + except ValueError: + print("[E] Invalid message received:", line) + continue + if len(msg) > 0 and msg[0].startswith(":"): msg[0] = msg[0][1:] message = " ".join(msg) From 63dc8da100b1a5ec493360b082b2949ce8098ad2 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 6 Dec 2021 09:17:45 +0100 Subject: [PATCH 042/144] those are not only replys --- irc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/irc.py b/irc.py index 7de2b91..790f8dd 100644 --- a/irc.py +++ b/irc.py @@ -3,7 +3,7 @@ from typing import Callable from enum import Enum -class ServerReply(str, Enum): +class ServerMessage(str, Enum): RPL_WELCOME = "001" RPL_YOURHOST = "002" RPL_CREATED = "003" @@ -157,8 +157,8 @@ class Client: self.__server.sendline("USER %s ignore ignore :%s" % (username, realname)) self.__server.sendline("NICK %s" % nick) self.__function_register = { - ServerReply.RPL_WELCOME: self.on_welcome, - ServerReply.ERR_NICKNAMEINUSE: self.on_nickname_in_use, + ServerMessage.RPL_WELCOME: self.on_welcome, + ServerMessage.ERR_NICKNAMEINUSE: self.on_nickname_in_use, 'NICK': self.on_nick, '__default__': self.unhandled_server_message, } From 5c811280d746d5298a2ea8bcfe6e7ed875596ad8 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 7 Dec 2021 06:30:52 +0100 Subject: [PATCH 043/144] move everything into a submodule to avoid namespace collisions --- tools/__init__.py | 0 aoc.py => tools/aoc.py | 4 ++-- coordinate.py => tools/coordinate.py | 0 daemon.py => tools/daemon.py | 0 datafiles.py => tools/datafiles.py | 0 grid.py => tools/grid.py | 0 tools/int_seq.py | 5 +++++ irc.py => tools/irc.py | 0 schedule.py => tools/schedule.py | 0 setup.py => tools/setup.py | 0 simplesocket.py => tools/simplesocket.py | 0 tools.py => tools/tools.py | 0 12 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 tools/__init__.py rename aoc.py => tools/aoc.py (98%) rename coordinate.py => tools/coordinate.py (100%) rename daemon.py => tools/daemon.py (100%) rename datafiles.py => tools/datafiles.py (100%) rename grid.py => tools/grid.py (100%) create mode 100644 tools/int_seq.py rename irc.py => tools/irc.py (100%) rename schedule.py => tools/schedule.py (100%) rename setup.py => tools/setup.py (100%) rename simplesocket.py => tools/simplesocket.py (100%) rename tools.py => tools/tools.py (100%) diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aoc.py b/tools/aoc.py similarity index 98% rename from aoc.py rename to tools/aoc.py index b7236ca..7e21a9c 100644 --- a/aoc.py +++ b/tools/aoc.py @@ -1,8 +1,8 @@ import os -import tools from typing import List, Any, Type, Union +from .tools import get_script_dir -BASE_PATH = tools.get_script_dir() +BASE_PATH = get_script_dir() INPUTS_PATH = os.path.join(BASE_PATH, 'inputs') diff --git a/coordinate.py b/tools/coordinate.py similarity index 100% rename from coordinate.py rename to tools/coordinate.py diff --git a/daemon.py b/tools/daemon.py similarity index 100% rename from daemon.py rename to tools/daemon.py diff --git a/datafiles.py b/tools/datafiles.py similarity index 100% rename from datafiles.py rename to tools/datafiles.py diff --git a/grid.py b/tools/grid.py similarity index 100% rename from grid.py rename to tools/grid.py diff --git a/tools/int_seq.py b/tools/int_seq.py new file mode 100644 index 0000000..67dbd96 --- /dev/null +++ b/tools/int_seq.py @@ -0,0 +1,5 @@ +def triangular(n: int) -> int: + """ + 0, 1, 3, 6, 10, 15, ... + """ + return int(n * (n + 1) / 2) diff --git a/irc.py b/tools/irc.py similarity index 100% rename from irc.py rename to tools/irc.py diff --git a/schedule.py b/tools/schedule.py similarity index 100% rename from schedule.py rename to tools/schedule.py diff --git a/setup.py b/tools/setup.py similarity index 100% rename from setup.py rename to tools/setup.py diff --git a/simplesocket.py b/tools/simplesocket.py similarity index 100% rename from simplesocket.py rename to tools/simplesocket.py diff --git a/tools.py b/tools/tools.py similarity index 100% rename from tools.py rename to tools/tools.py From 4a2b99f8f25e8b2d7de3b38f61a294faa783fe08 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 7 Dec 2021 06:38:42 +0100 Subject: [PATCH 044/144] some more integer sequences to remember --- tools/int_seq.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tools/int_seq.py b/tools/int_seq.py index 67dbd96..c9e93af 100644 --- a/tools/int_seq.py +++ b/tools/int_seq.py @@ -1,5 +1,30 @@ +import math +from functools import cache + + +def factorial(n: int) -> int: + """ + n! = 1 * 2 * 3 * 4 * ... * n + 1, 1, 2, 6, 24, 120, 720, ... + """ + return math.factorial(n) + + +@cache +def fibonacci(n: int) -> int: + """ + F(n) = F(n-1) + F(n-2) with F(0) = 0 and F(1) = 1 + 0, 1, 1, 2, 3, 5, 8, 13, 21, ... + """ + if n < 2: + return n + + return fibonacci(n - 1) + fibonacci(n - 2) + + def triangular(n: int) -> int: """ + a(n) = binomial(n+1,2) = n*(n+1)/2 = 0 + 1 + 2 + ... + n 0, 1, 3, 6, 10, 15, ... """ return int(n * (n + 1) / 2) From c5d10980e96132c9968edb3e45376f4fdd64c91f Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 7 Dec 2021 06:49:23 +0100 Subject: [PATCH 045/144] relative import --- tools/irc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/irc.py b/tools/irc.py index 790f8dd..1636ba2 100644 --- a/tools/irc.py +++ b/tools/irc.py @@ -1,4 +1,4 @@ -from simplesocket import ClientSocket +from .simplesocket import ClientSocket from typing import Callable from enum import Enum From eb63ba9e996520382730d5813e927d27e73b2f79 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 7 Dec 2021 06:54:31 +0100 Subject: [PATCH 046/144] annotations, correct plural --- tools/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/tools.py b/tools/tools.py index c277cc6..69e9b0e 100644 --- a/tools/tools.py +++ b/tools/tools.py @@ -5,7 +5,7 @@ import sys from typing import Any -def get_script_dir(follow_symlinks=True): +def get_script_dir(follow_symlinks: bool = True) -> str: """return path of the executed script""" if getattr(sys, 'frozen', False): path = os.path.abspath(sys.executable) @@ -29,7 +29,7 @@ def compare(a: Any, b: Any) -> int: def human_readable_time_from_delta(delta: datetime.timedelta) -> str: time_str = "" if delta.days > 0: - time_str += "%d days, " % delta.days + time_str += "%d day%s, " % (delta.days, "s" if delta.days > 1 else "") if delta.seconds > 3600: time_str += "%02d:" % (delta.seconds // 3600) From 393e84692676cc7cd6e6ef8f33cf49e3c7b3e899 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 7 Dec 2021 08:50:02 +0100 Subject: [PATCH 047/144] import woes --- tools/grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/grid.py b/tools/grid.py index 7ab23c8..0bb695a 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -1,5 +1,5 @@ from __future__ import annotations -from coordinate import Coordinate +from .coordinate import Coordinate from enum import Enum from typing import Union, Any From 55a12f7dc8e98b0cd0b66097850e376de4143722 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Thu, 9 Dec 2021 06:20:57 +0100 Subject: [PATCH 048/144] don't assume what an "OFF_STATE" might be, use the supplied default instead --- tools/grid.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index 0bb695a..d90cae6 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -3,7 +3,6 @@ from .coordinate import Coordinate from enum import Enum from typing import Union, Any -OFF_STATES = [False, 0, None] OFF = False ON = True @@ -51,9 +50,9 @@ class Grid: if pos.z is not None: self.mode3D = True - if (value == self.__default or value in OFF_STATES) and pos in self.__grid: + if (value == self.__default) and pos in self.__grid: del self.__grid[pos] - elif value != self.__default and value not in OFF_STATES: + elif value != self.__default: self.__trackBoundaries(pos) self.__grid[pos] = value @@ -63,12 +62,6 @@ class Grid: else: return self.__default - def getState(self, pos: Coordinate) -> bool: - if pos not in self.__grid: - return False - else: - return self.__grid[pos] not in OFF_STATES - def getOnCount(self): return len(self.__grid) From abbf1c85e1479ea7edf2b0defdd9fd4c7a978ab8 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 11 Dec 2021 06:45:24 +0100 Subject: [PATCH 049/144] grid.Grid: finally add that stupid print() method I'm always writing for debugging grid.Grid: add(), sub() and set() return what they've actually wrote to the cell --- tools/grid.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index d90cae6..8111f27 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -1,7 +1,7 @@ from __future__ import annotations from .coordinate import Coordinate from enum import Enum -from typing import Union, Any +from typing import Any, List, Union OFF = False ON = True @@ -46,7 +46,7 @@ class Grid: self.__trackBoundaries(pos) self.__grid[pos] = not self.__default - def set(self, pos: Coordinate, value: Any = True): + def set(self, pos: Coordinate, value: Any = True) -> Any: if pos.z is not None: self.mode3D = True @@ -56,6 +56,8 @@ class Grid: self.__trackBoundaries(pos) self.__grid[pos] = value + return value + def get(self, pos: Coordinate) -> Any: if pos in self.__grid: return self.__grid[pos] @@ -98,11 +100,11 @@ class Grid: else: return self.minX <= pos.x <= self.maxX and self.minY <= pos.y <= self.maxY - def add(self, pos: Coordinate, value: Union[float, int] = 1): - self.set(pos, self.get(pos) + value) + def add(self, pos: Coordinate, value: Union[float, int] = 1) -> Union[float, int]: + return self.set(pos, self.get(pos) + value) - def sub(self, pos: Coordinate, value: Union[float, int] = 1): - self.set(pos, self.get(pos) - value) + def sub(self, pos: Coordinate, value: Union[float, int] = 1) -> Union[float, int]: + return self.set(pos, self.get(pos) - value) def getActiveCells(self): return list(self.__grid.keys()) @@ -115,6 +117,18 @@ class Grid: return grid_sum + def getNeighboursOf(self, pos: Coordinate, includeDefault: bool = False, includeDiagonal: bool = True) \ + -> List[Coordinate]: + neighbours = pos.getNeighbours( + includeDiagonal=includeDiagonal, + minX=self.minX, minY=self.minY, minZ=self.minZ, + maxX=self.maxX, maxY=self.maxY, maxZ=self.maxZ + ) + if includeDefault: + return neighbours + else: + return [x for x in neighbours if self.get(x) != self.__default] + def getNeighbourSum(self, pos: Coordinate, includeNegative: bool = True, includeDiagonal: bool = True) \ -> Union[float, int]: neighbour_sum = 0 @@ -169,3 +183,11 @@ class Grid: elif mode == GridTransformation.ROTATE_TWICE: self.transform(GridTransformation.ROTATE_RIGHT) self.transform(GridTransformation.ROTATE_RIGHT) + + def print(self, spacer: str = ""): + for y in range(self.minY, self.maxY + 1): + for x in range(self.minX, self.maxX + 1): + print(self.get(Coordinate(x, y)), end="") + print(spacer, end="") + + print() From 7b52ce4fba58033714070cca4387187e7a406564 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 11 Dec 2021 06:47:33 +0100 Subject: [PATCH 050/144] codeline cleanup --- tools/grid.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index 8111f27..050f651 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -133,10 +133,10 @@ class Grid: -> Union[float, int]: neighbour_sum = 0 for neighbour in pos.getNeighbours( - includeDiagonal=includeDiagonal, - minX=self.minX, minY=self.minY, - maxX=self.maxX, maxY=self.maxY, - minZ=self.minZ, maxZ=self.maxZ): + includeDiagonal=includeDiagonal, + minX=self.minX, minY=self.minY, minZ=self.minZ, + maxX=self.maxX, maxY=self.maxY, maxZ=self.maxZ + ): if neighbour in self.__grid: if includeNegative or self.__grid[neighbour] > 0: neighbour_sum += self.__grid[neighbour] From 4ab6519321676aa49803ba279f3b0f0dcf3e4472 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 11 Dec 2021 07:15:32 +0100 Subject: [PATCH 051/144] annotation cleanup add grid.mul() and grid.div() --- tools/grid.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index 050f651..5c2b98e 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -5,6 +5,7 @@ from typing import Any, List, Union OFF = False ON = True +Numeric = Union[int, float] class GridTransformation(Enum): @@ -58,19 +59,31 @@ class Grid: return value + def add(self, pos: Coordinate, value: Numeric = 1) -> Numeric: + return self.set(pos, self.get(pos) + value) + + def sub(self, pos: Coordinate, value: Numeric = 1) -> Numeric: + return self.set(pos, self.get(pos) - value) + + def mul(self, pos: Coordinate, value: Numeric = 1) -> Numeric: + return self.set(pos, self.get(pos) * value) + + def div(self, pos: Coordinate, value: Numeric = 1) -> Numeric: + return self.set(pos, self.get(pos) / value) + def get(self, pos: Coordinate) -> Any: if pos in self.__grid: return self.__grid[pos] else: return self.__default - def getOnCount(self): + def getOnCount(self) -> int: return len(self.__grid) def isSet(self, pos: Coordinate) -> bool: return pos in self.__grid - def getCorners(self): + def getCorners(self) -> List[Coordinate]: if not self.mode3D: return [ Coordinate(self.minX, self.minY), @@ -100,16 +113,10 @@ class Grid: else: return self.minX <= pos.x <= self.maxX and self.minY <= pos.y <= self.maxY - def add(self, pos: Coordinate, value: Union[float, int] = 1) -> Union[float, int]: - return self.set(pos, self.get(pos) + value) - - def sub(self, pos: Coordinate, value: Union[float, int] = 1) -> Union[float, int]: - return self.set(pos, self.get(pos) - value) - - def getActiveCells(self): + def getActiveCells(self) -> List[Coordinate]: return list(self.__grid.keys()) - def getSum(self, includeNegative: bool = True): + def getSum(self, includeNegative: bool = True) -> Numeric: grid_sum = 0 for value in self.__grid.values(): if includeNegative or value > 0: @@ -129,8 +136,7 @@ class Grid: else: return [x for x in neighbours if self.get(x) != self.__default] - def getNeighbourSum(self, pos: Coordinate, includeNegative: bool = True, includeDiagonal: bool = True) \ - -> Union[float, int]: + def getNeighbourSum(self, pos: Coordinate, includeNegative: bool = True, includeDiagonal: bool = True) -> Numeric: neighbour_sum = 0 for neighbour in pos.getNeighbours( includeDiagonal=includeDiagonal, From 4c56a767b2a86f8d8665992eca1a789e5dab7e40 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 11 Dec 2021 08:53:44 +0100 Subject: [PATCH 052/144] more robust interface, keep track of users and channels --- tools/irc.py | 194 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 162 insertions(+), 32 deletions(-) diff --git a/tools/irc.py b/tools/irc.py index 1636ba2..492a558 100644 --- a/tools/irc.py +++ b/tools/irc.py @@ -1,6 +1,6 @@ from .simplesocket import ClientSocket -from typing import Callable from enum import Enum +from typing import Callable, Dict, List, Union class ServerMessage(str, Enum): @@ -8,7 +8,9 @@ class ServerMessage(str, Enum): RPL_YOURHOST = "002" RPL_CREATED = "003" RPL_MYINFO = "004" - RPL_BOUNCE = "005" + RPL_ISUPPORT = "005" + RPL_BOUNCE = "010" + RPL_UNIQID = "042" RPL_TRACELINK = "200" RPL_TRACECONNECTING = "201" RPL_TRACEHANDSHAKE = "202" @@ -67,6 +69,7 @@ class ServerMessage(str, Enum): RPL_UNIQOPIS = "325" RPL_NOTOPIC = "331" RPL_TOPIC = "332" + RPL_TOPICBY = "333" RPL_INVITING = "341" RPL_SUMMONING = "342" RPL_INVITELIST = "346" @@ -148,26 +151,87 @@ class ServerMessage(str, Enum): ERR_NOOPERHOST = "491" ERR_UMODEUNKNOWNFLAG = "501" ERR_USERSDONTMATCH = "502" + MSG_NICK = "NICK" + MSG_TOPIC = "TOPIC" + MSG_MODE = "MODE" + MSG_PRIVMSG = "PRIVMSG" + MSG_JOIN = "JOIN" + MSG_PART = "PART" + MSG_QUIT = "QUIT" + RAW = "RAW" + + +class User: + user: str + nickname: str + username: str + hostname: str + + def __init__(self, user: str): + self.user = user + user, self.hostname = self.user.split("@") + self.nickname, self.username = user.split("!") + + def nick(self, new_nick: str): + self.user.replace("%s!" % self.nickname, "%s!" % new_nick) + self.nickname = new_nick + + +class Channel: + name: str + topic: str + userlist: Dict[str, User] + + def __init__(self, name: str): + self.name = name + self.topic = "" + self.userlist = {} + + def join(self, user: User): + if user.user not in self.userlist: + self.userlist[user.user] = user + + def quit(self, user: User): + if user.user in self.userlist: + del self.userlist[user.user] class Client: + __function_register: Dict[str, List[Callable]] + __server_socket: ClientSocket + __server_caps: Dict[str, Union[str, int]] + __userlist: Dict[str, User] + __channellist: Dict[str, Channel] + __server_name: str = None + __my_user: str = None + def __init__(self, server: str, port: int, nick: str, username: str, realname: str = "Python Bot"): - self.nickname = None - self.__server = ClientSocket(server, port) - self.__server.sendline("USER %s ignore ignore :%s" % (username, realname)) - self.__server.sendline("NICK %s" % nick) + self.__userlist = {} + self.__channellist = {} + self.__server_socket = ClientSocket(server, port) + self.__server_socket.sendline("USER %s ignore ignore :%s" % (username, realname)) + self.__server_socket.sendline("NICK %s" % nick) + self.__server_caps = { + 'MAXLEN': 255 + } self.__function_register = { - ServerMessage.RPL_WELCOME: self.on_welcome, - ServerMessage.ERR_NICKNAMEINUSE: self.on_nickname_in_use, - 'NICK': self.on_nick, - '__default__': self.unhandled_server_message, + ServerMessage.RPL_WELCOME: [self.on_rpl_welcome], + ServerMessage.RPL_TOPIC: [self.on_rpl_topic], + ServerMessage.RPL_ISUPPORT: [self.on_rpl_isupport], + ServerMessage.ERR_NICKNAMEINUSE: [self.on_err_nicknameinuse], + ServerMessage.MSG_JOIN: [self.on_join], + ServerMessage.MSG_PART: [self.on_part], + ServerMessage.MSG_QUIT: [self.on_quit], + ServerMessage.MSG_NICK: [self.on_nick], + ServerMessage.MSG_TOPIC: [self.on_topic], + ServerMessage.RAW: [self.on_raw], } self.receive() def receive(self): - while line := self.__server.recvline(): + while line := self.__server_socket.recvline(): if line.startswith("PING"): - self.__server.sendline("PONG " + line.split()[1]) + self.__server_socket.sendline("PONG " + line.split()[1]) continue try: @@ -181,43 +245,109 @@ class Client: message = " ".join(msg) if msg_type in self.__function_register: - self.__function_register[msg_type](msg_from, msg_to, message) - else: - self.__function_register['__default__'](msg_from, msg_type, msg_to, message) + for func in self.__function_register[msg_type]: + func(msg_from, msg_to, message) + + for func in self.__function_register['RAW']: + func(msg_from, msg_type, msg_to, message) def register(self, msg_type: str, func: Callable[..., None]): - self.__function_register[msg_type] = func + if msg_type in self.__function_register: + self.__function_register[msg_type].append(func) + else: + self.__function_register[msg_type] = [func] - def on_welcome(self, msg_from: str, msg_to: str, message: str): - self.nickname = msg_to + def on_rpl_welcome(self, msg_from: str, msg_to: str, message: str): + self.__server_name = msg_from + self.__my_user = message.split()[-1] + self.__userlist[self.__my_user] = User(self.__my_user) - def on_nickname_in_use(self, msg_from: str, msg_to: str, message: str): - if self.nickname is None: + def on_rpl_isupport(self, msg_from: str, msg_to: str, message: str): + for cap in message.split(): + if "=" not in cap: + self.__server_caps[cap] = True + else: + (a, b) = cap.split("=") + self.__server_caps[a] = b + + def on_rpl_topic(self, msg_from: str, msg_to: str, message: str): + channel, *topic = message.split() + if len(topic) > 0: + topic[0] = topic[0][1:] + + new_topic = " ".join(topic) + self.__channellist[channel].topic = new_topic + + def on_err_nicknameinuse(self, msg_from: str, msg_to: str, message: str): + if self.__my_user is None: self.nick(message.split()[0] + "_") - def on_nick(self, old_nick: str, new_nick: str, message: str): - old_nick = old_nick.split("!")[0] - if old_nick == self.nickname: - self.nickname = new_nick[1:] + def on_nick(self, msg_from: str, msg_to: str, message: str): + self.__userlist[msg_from].nick(msg_to) + self.__userlist[self.__userlist[msg_from].user] = self.__userlist[msg_from] + del self.__userlist[msg_from] - def unhandled_server_message(self, msg_from: str, msg_type: str, msg_to: str, message: str): + def on_join(self, msg_from: str, msg_to: str, message: str): + channel = msg_to[1:] + if msg_from == self.__my_user: + self.__channellist[channel] = Channel(channel) + # FIXME: get user list (NAMES just returns nicknames, not nick!user@host !!!) + + if msg_from not in self.__userlist: + self.__userlist[msg_from] = User(msg_from) + + self.__channellist[channel].join(self.__userlist[msg_from]) + + def on_topic(self, msg_from: str, msg_to: str, message: str): + self.__channellist[msg_to].topic = message + + def on_part(self, msg_from: str, msg_to: str, message: str): + self.__channellist[msg_to].quit(self.__userlist[msg_from]) + + def on_quit(self, msg_from: str, msg_to: str, message: str): + for c in self.__channellist: + self.__channellist[c].quit(self.__userlist[msg_from]) + + del self.__userlist[msg_from] + + def on_raw(self, msg_from: str, msg_type: str, msg_to: str, message: str): print(msg_from, msg_type, msg_to, message) def nick(self, new_nick: str): - self.__server.sendline("NICK %s" % new_nick) + self.__server_socket.sendline("NICK %s" % new_nick) def join(self, channel: str): - self.__server.sendline("JOIN %s" % channel) + self.__server_socket.sendline("JOIN %s" % channel) self.receive() - def leave(self, channel: str): - self.__server.sendline("LEAVE %s" % channel) + def part(self, channel: str): + self.__server_socket.sendline("PART %s" % channel) self.receive() def privmsg(self, target: str, message: str): - self.__server.sendline("PRIVMSG %s :%s" % (target, message)) + self.__server_socket.sendline("PRIVMSG %s :%s" % (target, message)) def quit(self, message: str = "Elvis has left the building!"): - self.__server.sendline("QUIT :%s" % message) + self.__server_socket.sendline("QUIT :%s" % message) self.receive() - self.__server.close() + self.__server_socket.close() + + def getUser(self, user: str = None) -> Union[User, None]: + if user is None: + return self.__userlist[self.__my_user] + elif user in self.__userlist: + return self.__userlist[user] + else: + return None + + def getUserList(self) -> List[User]: + return list(self.__userlist.values()) + + def getChannel(self, channel: str) -> Union[Channel, None]: + if channel in self.__channellist: + return self.__channellist[channel] + else: + return None + + def getChannelList(self) -> List[Channel]: + return list(self.__channellist.values()) From d806c838c4af806da7cdec0144e1cd6f6d4c90cd Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 11 Dec 2021 08:56:37 +0100 Subject: [PATCH 053/144] remove unnecessary int cast --- tools/int_seq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/int_seq.py b/tools/int_seq.py index c9e93af..bbaeb33 100644 --- a/tools/int_seq.py +++ b/tools/int_seq.py @@ -27,4 +27,4 @@ def triangular(n: int) -> int: a(n) = binomial(n+1,2) = n*(n+1)/2 = 0 + 1 + 2 + ... + n 0, 1, 3, 6, 10, 15, ... """ - return int(n * (n + 1) / 2) + return n * (n + 1) // 2 From ec059d535426921af08f33d57023ee5785b6d59e Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 12 Dec 2021 08:25:47 +0100 Subject: [PATCH 054/144] way better (and much quicker) solution to receive lines from sockets --- tools/irc.py | 12 +++++++----- tools/simplesocket.py | 33 ++++++++++++++------------------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/tools/irc.py b/tools/irc.py index 492a558..c9c1a3f 100644 --- a/tools/irc.py +++ b/tools/irc.py @@ -224,12 +224,13 @@ class Client: ServerMessage.MSG_QUIT: [self.on_quit], ServerMessage.MSG_NICK: [self.on_nick], ServerMessage.MSG_TOPIC: [self.on_topic], - ServerMessage.RAW: [self.on_raw], } self.receive() def receive(self): while line := self.__server_socket.recvline(): + line = line.strip() + if line.startswith("PING"): self.__server_socket.sendline("PONG " + line.split()[1]) continue @@ -244,14 +245,15 @@ class Client: msg[0] = msg[0][1:] message = " ".join(msg) + if ServerMessage.RAW in self.__function_register: + for func in self.__function_register[ServerMessage.RAW]: + func(msg_from, msg_type, msg_to, message) + if msg_type in self.__function_register: for func in self.__function_register[msg_type]: func(msg_from, msg_to, message) - for func in self.__function_register['RAW']: - func(msg_from, msg_type, msg_to, message) - - def register(self, msg_type: str, func: Callable[..., None]): + def subscribe(self, msg_type: str, func: Callable[..., None]): if msg_type in self.__function_register: self.__function_register[msg_type].append(func) else: diff --git a/tools/simplesocket.py b/tools/simplesocket.py index 9457a8e..7b07948 100644 --- a/tools/simplesocket.py +++ b/tools/simplesocket.py @@ -40,32 +40,27 @@ class Socket: self.send(line) - def recvline(self, timeout: int = 10) -> str: + def recvline(self, timeout: int = 0) -> Union[str, None]: + """ + Receive exactly one text line (delimiter: newline "\n" or "\r\n") from the socket. + + :param timeout: wait at most TIMEOUT seconds for a newline to appear in the buffer + :return: Either a str containing a line received or None if no newline was found + """ start = time.time() - while b"\n" not in self.__recv_buffer and b"\r" not in self.__recv_buffer: - self.__recv_buffer = self.recv(256, blocking=False) + while b"\n" not in self.__recv_buffer: + self.__recv_buffer += self.recv(1024, blocking=False) if time.time() - start <= timeout: time.sleep(0.01) # release *some* resources else: break - newline = b"\r\n" - if newline not in self.__recv_buffer: - newline = b"\n" - if newline not in self.__recv_buffer: - newline = b"\r" - if newline not in self.__recv_buffer: - ret = self.__recv_buffer.decode("UTF-8") - self.__recv_buffer = b"" - return ret - - ret = self.__recv_buffer[:self.__recv_buffer.index(newline)].decode("UTF-8") - if len(self.__recv_buffer) - len(newline) > self.__recv_buffer.index(newline): - self.__recv_buffer = self.__recv_buffer[self.__recv_buffer.index(newline) + len(newline):] + if b"\n" not in self.__recv_buffer: + return None else: - self.__recv_buffer = b"" - - return ret + line = self.__recv_buffer[:self.__recv_buffer.index(b"\n")] + self.__recv_buffer = self.__recv_buffer[self.__recv_buffer.index(b"\n") + 1:] + return line.decode("UTF-8") def close(self): self.socket.close() From d8bb8d8aba76da78e81895c16298486ac184601b Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 12 Dec 2021 15:15:28 +0100 Subject: [PATCH 055/144] setup.py needs to be top level --- tools/setup.py => setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tools/setup.py => setup.py (88%) diff --git a/tools/setup.py b/setup.py similarity index 88% rename from tools/setup.py rename to setup.py index 2aad0e9..d0dc819 100644 --- a/tools/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( packages=[''], url='', license='GPLv3', - author='pennywise', + author='Stefan Harmuth', author_email='pennywise@drock.de', description='Just some small tools to make life easier' ) From 904caf85ae3a595c9ec78b8562d1cfe21aa34b69 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 12 Dec 2021 15:28:33 +0100 Subject: [PATCH 056/144] fibonacci without functools (to work in pypy) --- tools/int_seq.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tools/int_seq.py b/tools/int_seq.py index bbaeb33..83c7425 100644 --- a/tools/int_seq.py +++ b/tools/int_seq.py @@ -1,5 +1,5 @@ import math -from functools import cache +from typing import Dict def factorial(n: int) -> int: @@ -10,8 +10,7 @@ def factorial(n: int) -> int: return math.factorial(n) -@cache -def fibonacci(n: int) -> int: +def fibonacci(n: int, cache: Dict[int, int] = None) -> int: """ F(n) = F(n-1) + F(n-2) with F(0) = 0 and F(1) = 1 0, 1, 1, 2, 3, 5, 8, 13, 21, ... @@ -19,7 +18,13 @@ def fibonacci(n: int) -> int: if n < 2: return n - return fibonacci(n - 1) + fibonacci(n - 2) + if cache is None: + cache = {} + + if n not in cache: + cache[n] = fibonacci(n - 1, cache) + fibonacci(n - 2, cache) + + return cache[n] def triangular(n: int) -> int: From 78e180871ef003dd19bb7249398920b42360bbd9 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 12 Dec 2021 17:58:03 +0100 Subject: [PATCH 057/144] accomodate for pypy --- setup.py | 2 +- tools/aoc.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index d0dc819..e1bee02 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup setup( name='py-tools', version='0.2', - packages=[''], + packages=['tools'], url='', license='GPLv3', author='Stefan Harmuth', diff --git a/tools/aoc.py b/tools/aoc.py index 7e21a9c..1407581 100644 --- a/tools/aoc.py +++ b/tools/aoc.py @@ -116,7 +116,7 @@ def printSolution(day: int, part: int, solution: Any, test: bool = None, test_ca if exec_time is None: time_output = "" else: - units = ['s', 'ms', 'μs', 'ns'] + units = ['s', 'ms', 'µs', 'ns'] unit = 0 while exec_time < 1: exec_time *= 1000 @@ -126,11 +126,19 @@ def printSolution(day: int, part: int, solution: Any, test: bool = None, test_ca if test is not None: print( - "%s (TEST day%d/part%d/case%d) -- got '%s' -- expected '%s'" - % ("OK" if test == solution else "FAIL", day, part, test_case, solution, test) + "%s (TEST day%d/part%d/case%d) -- got '%s' -- expected '%s'%s" + % ("OK" if test == solution else "FAIL", day, part, test_case, solution, test, time_output) ) else: - print("Solution to day %s, part %s: %s%s" % (day, part, solution, time_output)) + print( + "Solution to day %s, part %s: %s%s" + % ( + day, + part, + solution, + time_output + ) + ) def splitLine(line, split_char: str = ',', return_type: Union[Type, List[Type]] = None): From dfe5afb735e53abd6ec173f566029039a985dcd2 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 13 Dec 2021 05:08:35 +0100 Subject: [PATCH 058/144] write json files readable --- tools/datafiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/datafiles.py b/tools/datafiles.py index 35da69a..d87fc08 100644 --- a/tools/datafiles.py +++ b/tools/datafiles.py @@ -39,4 +39,4 @@ class JSONFile(DataFile): def save(self): with open(self.filename, "wt") as f: - f.write(json.dumps(self.copy())) + f.write(json.dumps(self.copy(), indent=4)) From d5a278ceef7f76176e66ce2ab2769f68049d6077 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 13 Dec 2021 06:18:25 +0100 Subject: [PATCH 059/144] a* (untested) --- tools/grid.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/tools/grid.py b/tools/grid.py index 5c2b98e..48439d0 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -1,6 +1,8 @@ from __future__ import annotations -from .coordinate import Coordinate +from .coordinate import Coordinate, DistanceAlgorithm +from dataclasses import dataclass from enum import Enum +from math import inf from typing import Any, List, Union OFF = False @@ -190,6 +192,61 @@ class Grid: self.transform(GridTransformation.ROTATE_RIGHT) self.transform(GridTransformation.ROTATE_RIGHT) + def getPath(self, pos_from: Coordinate, pos_to: Coordinate, includeDiagonal: bool, walls: List[Any] = None)\ + -> List[Coordinate]: + @dataclass(frozen=True) + class Node: + f_cost: int + h_cost: int + parent: Coordinate + + if walls in None: + walls = [self.__default] + + openNodes: Dict[Coordinate, Node] = {} + closedNodes: Dict[Coordinate, Node] = {} # Dict[Coordinate, Node] + + openNodes[pos_from] = Node( + pos_from.getDistanceTo(pos_to, DistanceAlgorithm.MANHATTAN, includeDiagonal), + pos_from.getDistanceTo(pos_to, DistanceAlgorithm.MANHATTAN, includeDiagonal), + None + ) + + while openNodes: + currentCoord = list(sorted(openNodes, key=lambda n: openNodes[n].f_cost))[0] + currentNode = openNodes[currentCoord] + + closedNodes[currentCoord] = currentNode + del openNodes[currentCoord] + if currentCoord == pos_to: + break + + for neighbour in self.getNeighboursOf(currentCoord, True, includeDiagonal): + if self.get(neighbour) in walls or neighbour in closedNodes: + continue + + neighbourDist = currentCoord.getDistanceTo(neighbour, DistanceAlgorithm.MANHATTAN, includeDiagonal) + targetDist = neighbour.getDistanceTo(pos_to, DistanceAlgorithm.MANHATTAN, includeDiagonal) + neighbourNode = Node( + targetDist + neighbourDist + currentNode.h_cost, + currentNode[2] + neighbourDist, + currentCoord + ) + + if neighbour not in openNodes or neighbourNode.f_cost < openNodes[neighbour].f_cost: + openNodes[neighbour] = neighbourNode + + if pos_to not in closedNodes: + return None + else: + currentNode = closedNodes[pos_to] + pathCoords = [pos_to] + while currentNode.parent: + pathCoords.append(currentNode.parent) + currentNode = closedNodes[currentNode.parent] + + return pathCoords + def print(self, spacer: str = ""): for y in range(self.minY, self.maxY + 1): for x in range(self.minX, self.maxX + 1): From 3c855e674986cf8bc8dfd7d2fd254d828abbffe6 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 13 Dec 2021 07:19:27 +0100 Subject: [PATCH 060/144] range methods better get() allow print() to be useful with false/true grids --- tools/grid.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index 48439d0..bb5e9a1 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -34,13 +34,22 @@ class Grid: self.mode3D = False def __trackBoundaries(self, pos: Coordinate): - self.minX = min(self.minX, pos.x) - self.minY = min(self.minY, pos.y) - self.maxX = max(self.maxX, pos.x) - self.maxY = max(self.maxY, pos.y) + self.minX = min(pos.x, self.minX) + self.minY = min(pos.y, self.minY) + self.maxX = max(pos.x, self.maxX) + self.maxY = max(pos.y, self.maxY) if self.mode3D: - self.minZ = min(self.minZ, pos.z) - self.maxZ = max(self.maxZ, pos.z) + self.minZ = min(pos.z, self.minZ) + self.maxZ = max(pos.z, self.maxZ) + + def rangeX(self): + return range(self.minX, self.maxX + 1) + + def rangeY(self): + return range(self.minY, self.maxY + 1) + + def rangeZ(self): + return range(self.minZ, self.maxZ + 1) def toggle(self, pos: Coordinate): if pos in self.__grid: @@ -74,10 +83,7 @@ class Grid: return self.set(pos, self.get(pos) / value) def get(self, pos: Coordinate) -> Any: - if pos in self.__grid: - return self.__grid[pos] - else: - return self.__default + return self.__grid.get(pos, self.__default) def getOnCount(self) -> int: return len(self.__grid) @@ -247,10 +253,13 @@ class Grid: return pathCoords - def print(self, spacer: str = ""): + def print(self, spacer: str = "", true_char: str = None): for y in range(self.minY, self.maxY + 1): for x in range(self.minX, self.maxX + 1): - print(self.get(Coordinate(x, y)), end="") + if true_char: + print(true_char if self.get(Coordinate(x, y)) else " ", end="") + else: + print(self.get(Coordinate(x, y)), end="") print(spacer, end="") print() From 0b8a477184a274d5814c37220efb432ee6145df9 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 13 Dec 2021 07:39:08 +0100 Subject: [PATCH 061/144] implement Chebyshev/Chessboard distance algorithm --- tools/coordinate.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tools/coordinate.py b/tools/coordinate.py index a7a9917..af5b94b 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -7,8 +7,10 @@ from typing import Union, List, Optional class DistanceAlgorithm(Enum): MANHATTAN = 0 - PYTHAGORAS = 1 - + EUCLIDEAN = 1 + PYTHAGOREAN = 1 + CHEBYSHEV = 2 + CHESSBOARD = 2 @dataclass(frozen=True, order=True) class Coordinate: @@ -16,7 +18,7 @@ class Coordinate: y: int z: Optional[int] = None - def getDistanceTo(self, target: Coordinate, mode: DistanceAlgorithm = DistanceAlgorithm.PYTHAGORAS, + def getDistanceTo(self, target: Coordinate, mode: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN, includeDiagonals: bool = False) -> Union[int, float]: """ Get distance to target Coordinate @@ -31,11 +33,16 @@ class Coordinate: assert isinstance(mode, DistanceAlgorithm) assert isinstance(includeDiagonals, bool) - if mode == DistanceAlgorithm.PYTHAGORAS: + if mode == DistanceAlgorithm.EUCLIDEAN: if self.z is None: return sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2) else: return sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2 + abs(self.z - target.z) ** 2) + elif mode == DistanceAlgorithm.CHEBYSHEV: + if self.z is None: + return max(abs(target.x - self.x), abs(target.y - self.y)) + else: + return max(abs(target.x - self.x), abs(target.y - self.y), abs(target.z - self.z)) elif mode == DistanceAlgorithm.MANHATTAN: if not includeDiagonals: if self.z is None: From e16c21b3bbeec51413dc2d3fd7c11643b89ce7d2 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 13 Dec 2021 07:44:21 +0100 Subject: [PATCH 062/144] Node() is not a list anymore --- tools/grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/grid.py b/tools/grid.py index bb5e9a1..2699aac 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -235,7 +235,7 @@ class Grid: targetDist = neighbour.getDistanceTo(pos_to, DistanceAlgorithm.MANHATTAN, includeDiagonal) neighbourNode = Node( targetDist + neighbourDist + currentNode.h_cost, - currentNode[2] + neighbourDist, + currentNode.h_cost + neighbourDist, currentCoord ) From a507b004f9c7e786d76b2f4bdc3c94d755afe4ca Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 13 Dec 2021 11:10:38 +0100 Subject: [PATCH 063/144] allow getActiveCells() to return only one row/column annotation/import fixed --- tools/grid.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index 2699aac..fa481af 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -2,8 +2,7 @@ from __future__ import annotations from .coordinate import Coordinate, DistanceAlgorithm from dataclasses import dataclass from enum import Enum -from math import inf -from typing import Any, List, Union +from typing import Any, Dict, List, Optional, Union OFF = False ON = True @@ -121,8 +120,13 @@ class Grid: else: return self.minX <= pos.x <= self.maxX and self.minY <= pos.y <= self.maxY - def getActiveCells(self) -> List[Coordinate]: - return list(self.__grid.keys()) + def getActiveCells(self, x: int = None, y: int = None) -> List[Coordinate]: + if x: + return [c for c in self.__grid.keys() if c.x == x] + elif y: + return [c for c in self.__grid.keys() if c.y == y] + else: + return list(self.__grid.keys()) def getSum(self, includeNegative: bool = True) -> Numeric: grid_sum = 0 @@ -199,18 +203,18 @@ class Grid: self.transform(GridTransformation.ROTATE_RIGHT) def getPath(self, pos_from: Coordinate, pos_to: Coordinate, includeDiagonal: bool, walls: List[Any] = None)\ - -> List[Coordinate]: + -> Union[None, List[Coordinate]]: @dataclass(frozen=True) class Node: f_cost: int h_cost: int - parent: Coordinate + parent: Optional[Coordinate] if walls in None: walls = [self.__default] openNodes: Dict[Coordinate, Node] = {} - closedNodes: Dict[Coordinate, Node] = {} # Dict[Coordinate, Node] + closedNodes: Dict[Coordinate, Node] = {} openNodes[pos_from] = Node( pos_from.getDistanceTo(pos_to, DistanceAlgorithm.MANHATTAN, includeDiagonal), From 1c83a41fd27863cc5835f61e9cfab47c6c81bf71 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 14 Dec 2021 22:14:39 +0100 Subject: [PATCH 064/144] correct annotation --- tools/aoc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoc.py b/tools/aoc.py index 1407581..8a472b1 100644 --- a/tools/aoc.py +++ b/tools/aoc.py @@ -112,7 +112,7 @@ class AOCDay: return return_array -def printSolution(day: int, part: int, solution: Any, test: bool = None, test_case: int = 0, exec_time: float = None): +def printSolution(day: int, part: int, solution: Any, test: Any = None, test_case: int = 0, exec_time: float = None): if exec_time is None: time_output = "" else: From 235a545c70913dc1f3c585a6482026f75bd2ae3a Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 15 Dec 2021 07:49:57 +0100 Subject: [PATCH 065/144] grid.getPath(): allow for grid values to be movement/distance weights also: lambdas are sloooooooow --- tools/grid.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index fa481af..d2d9190 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -2,6 +2,7 @@ from __future__ import annotations from .coordinate import Coordinate, DistanceAlgorithm from dataclasses import dataclass from enum import Enum +from math import inf from typing import Any, Dict, List, Optional, Union OFF = False @@ -202,29 +203,33 @@ class Grid: self.transform(GridTransformation.ROTATE_RIGHT) self.transform(GridTransformation.ROTATE_RIGHT) - def getPath(self, pos_from: Coordinate, pos_to: Coordinate, includeDiagonal: bool, walls: List[Any] = None)\ - -> Union[None, List[Coordinate]]: + def getPath(self, pos_from: Coordinate, pos_to: Coordinate, includeDiagonal: bool, walls: List[Any] = None, + weighted: bool = False) -> Union[None, List[Coordinate]]: @dataclass(frozen=True) class Node: - f_cost: int - h_cost: int + f_cost: Numeric + h_cost: Numeric parent: Optional[Coordinate] - if walls in None: + if walls is None: walls = [self.__default] openNodes: Dict[Coordinate, Node] = {} closedNodes: Dict[Coordinate, Node] = {} openNodes[pos_from] = Node( - pos_from.getDistanceTo(pos_to, DistanceAlgorithm.MANHATTAN, includeDiagonal), - pos_from.getDistanceTo(pos_to, DistanceAlgorithm.MANHATTAN, includeDiagonal), + pos_from.getDistanceTo(pos_to), + pos_from.getDistanceTo(pos_to), None ) while openNodes: - currentCoord = list(sorted(openNodes, key=lambda n: openNodes[n].f_cost))[0] - currentNode = openNodes[currentCoord] + currentNode = Node(inf, 0, None) + currentCoord = None + for c, n in openNodes.items(): + if n.f_cost < currentNode.f_cost: + currentNode = n + currentCoord = c closedNodes[currentCoord] = currentNode del openNodes[currentCoord] @@ -235,8 +240,12 @@ class Grid: if self.get(neighbour) in walls or neighbour in closedNodes: continue - neighbourDist = currentCoord.getDistanceTo(neighbour, DistanceAlgorithm.MANHATTAN, includeDiagonal) - targetDist = neighbour.getDistanceTo(pos_to, DistanceAlgorithm.MANHATTAN, includeDiagonal) + if weighted: + neighbourDist = self.get(neighbour) + else: + neighbourDist = currentCoord.getDistanceTo(neighbour, DistanceAlgorithm.MANHATTAN, includeDiagonal) + + targetDist = neighbour.getDistanceTo(pos_to) neighbourNode = Node( targetDist + neighbourDist + currentNode.h_cost, currentNode.h_cost + neighbourDist, From af2aea1a34965b9ddc3d14d4b0d4fcceafd579c1 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 15 Dec 2021 09:42:10 +0100 Subject: [PATCH 066/144] still trying to make grid.getPath() faster --- tools/grid.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index d2d9190..9a46e58 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -2,6 +2,7 @@ from __future__ import annotations from .coordinate import Coordinate, DistanceAlgorithm from dataclasses import dataclass from enum import Enum +from heapq import heappop, heappush from math import inf from typing import Any, Dict, List, Optional, Union @@ -205,10 +206,11 @@ class Grid: def getPath(self, pos_from: Coordinate, pos_to: Coordinate, includeDiagonal: bool, walls: List[Any] = None, weighted: bool = False) -> Union[None, List[Coordinate]]: - @dataclass(frozen=True) + @dataclass(frozen=True, order=True) class Node: f_cost: Numeric h_cost: Numeric + coord: Coordinate parent: Optional[Coordinate] if walls is None: @@ -220,16 +222,17 @@ class Grid: openNodes[pos_from] = Node( pos_from.getDistanceTo(pos_to), pos_from.getDistanceTo(pos_to), + pos_from, None ) while openNodes: - currentNode = Node(inf, 0, None) - currentCoord = None - for c, n in openNodes.items(): - if n.f_cost < currentNode.f_cost: - currentNode = n - currentCoord = c + currentNode = min(openNodes.values()) + currentCoord = currentNode.coord + #for c, n in openNodes.items(): + # if n.f_cost < currentNode.f_cost: + # currentNode = n + # currentCoord = c closedNodes[currentCoord] = currentNode del openNodes[currentCoord] @@ -249,6 +252,7 @@ class Grid: neighbourNode = Node( targetDist + neighbourDist + currentNode.h_cost, currentNode.h_cost + neighbourDist, + neighbour, currentCoord ) From 2c859033fdb290f6c14a79e69b3f00aba07b2b18 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 15 Dec 2021 11:09:58 +0100 Subject: [PATCH 067/144] grid.getPath(): Use heap to ease finding smallest f_cost node --- tools/grid.py | 53 ++++++++++++++++++--------------------------------- 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index 9a46e58..80b0f05 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -1,10 +1,8 @@ from __future__ import annotations from .coordinate import Coordinate, DistanceAlgorithm -from dataclasses import dataclass from enum import Enum from heapq import heappop, heappush -from math import inf -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Union OFF = False ON = True @@ -206,33 +204,21 @@ class Grid: def getPath(self, pos_from: Coordinate, pos_to: Coordinate, includeDiagonal: bool, walls: List[Any] = None, weighted: bool = False) -> Union[None, List[Coordinate]]: - @dataclass(frozen=True, order=True) - class Node: - f_cost: Numeric - h_cost: Numeric - coord: Coordinate - parent: Optional[Coordinate] - + f_costs = [] if walls is None: walls = [self.__default] - openNodes: Dict[Coordinate, Node] = {} - closedNodes: Dict[Coordinate, Node] = {} + openNodes: Dict[Coordinate, tuple] = {} + closedNodes: Dict[Coordinate, tuple] = {} - openNodes[pos_from] = Node( - pos_from.getDistanceTo(pos_to), - pos_from.getDistanceTo(pos_to), - pos_from, - None - ) + openNodes[pos_from] = (0, pos_from.getDistanceTo(pos_to), None) + heappush(f_costs, (0, pos_from)) while openNodes: - currentNode = min(openNodes.values()) - currentCoord = currentNode.coord - #for c, n in openNodes.items(): - # if n.f_cost < currentNode.f_cost: - # currentNode = n - # currentCoord = c + _, currentCoord = heappop(f_costs) + if currentCoord not in openNodes: + continue + currentNode = openNodes[currentCoord] closedNodes[currentCoord] = currentNode del openNodes[currentCoord] @@ -245,28 +231,27 @@ class Grid: if weighted: neighbourDist = self.get(neighbour) + elif not includeDiagonal: + neighbourDist = 1 else: neighbourDist = currentCoord.getDistanceTo(neighbour, DistanceAlgorithm.MANHATTAN, includeDiagonal) targetDist = neighbour.getDistanceTo(pos_to) - neighbourNode = Node( - targetDist + neighbourDist + currentNode.h_cost, - currentNode.h_cost + neighbourDist, - neighbour, - currentCoord - ) + f_cost = targetDist + neighbourDist + currentNode[1] + neighbourNode = (f_cost, currentNode[1] + neighbourDist, currentCoord) - if neighbour not in openNodes or neighbourNode.f_cost < openNodes[neighbour].f_cost: + if neighbour not in openNodes or f_cost < openNodes[neighbour][0]: openNodes[neighbour] = neighbourNode + heappush(f_costs, (f_cost, neighbour)) if pos_to not in closedNodes: return None else: currentNode = closedNodes[pos_to] pathCoords = [pos_to] - while currentNode.parent: - pathCoords.append(currentNode.parent) - currentNode = closedNodes[currentNode.parent] + while currentNode[2]: + pathCoords.append(currentNode[2]) + currentNode = closedNodes[currentNode[2]] return pathCoords From 11604338e8714e6acafe28d9c061f827f60a6cf7 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 15 Dec 2021 11:30:30 +0100 Subject: [PATCH 068/144] make things faster/cleaner --- tools/coordinate.py | 19 +++++++------------ tools/grid.py | 16 +++++----------- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/tools/coordinate.py b/tools/coordinate.py index af5b94b..83f1a66 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -12,38 +12,35 @@ class DistanceAlgorithm(Enum): CHEBYSHEV = 2 CHESSBOARD = 2 + @dataclass(frozen=True, order=True) class Coordinate: x: int y: int z: Optional[int] = None - def getDistanceTo(self, target: Coordinate, mode: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN, + def getDistanceTo(self, target: Coordinate, algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN, includeDiagonals: bool = False) -> Union[int, float]: """ Get distance to target Coordinate :param target: - :param mode: Calculation Mode (0 = Manhattan, 1 = Pythagoras) + :param algorithm: Calculation Mode (0 = Manhattan, 1 = Pythagoras) :param includeDiagonals: in Manhattan Mode specify if diagonal movements are allowed (counts as 1.4 in 2D, 1.7 in 3D) :return: Distance to Target """ - assert isinstance(target, Coordinate) - assert isinstance(mode, DistanceAlgorithm) - assert isinstance(includeDiagonals, bool) - - if mode == DistanceAlgorithm.EUCLIDEAN: + if algorithm == DistanceAlgorithm.EUCLIDEAN: if self.z is None: return sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2) else: return sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2 + abs(self.z - target.z) ** 2) - elif mode == DistanceAlgorithm.CHEBYSHEV: + elif algorithm == DistanceAlgorithm.CHEBYSHEV: if self.z is None: return max(abs(target.x - self.x), abs(target.y - self.y)) else: return max(abs(target.x - self.x), abs(target.y - self.y), abs(target.z - self.z)) - elif mode == DistanceAlgorithm.MANHATTAN: + elif algorithm == DistanceAlgorithm.MANHATTAN: if not includeDiagonals: if self.z is None: return abs(self.x - target.x) + abs(self.y - target.y) @@ -93,9 +90,7 @@ class Coordinate: nb_list = [(x, y, z) for x in [-1, 0, 1] for y in [-1, 0, 1] for z in [-1, 0, 1]] nb_list.remove((0, 0, 0)) else: - nb_list = [ - (-1, 0, 0), (0, -1, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (0, 0, -1) - ] + nb_list = [(-1, 0, 0), (0, -1, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (0, 0, -1)] for dx, dy, dz in nb_list: tx = self.x + dx diff --git a/tools/grid.py b/tools/grid.py index 80b0f05..ab2c2f4 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -146,18 +146,13 @@ class Grid: if includeDefault: return neighbours else: - return [x for x in neighbours if self.get(x) != self.__default] + return [x for x in neighbours if x in self.__grid] def getNeighbourSum(self, pos: Coordinate, includeNegative: bool = True, includeDiagonal: bool = True) -> Numeric: neighbour_sum = 0 - for neighbour in pos.getNeighbours( - includeDiagonal=includeDiagonal, - minX=self.minX, minY=self.minY, minZ=self.minZ, - maxX=self.maxX, maxY=self.maxY, maxZ=self.maxZ - ): - if neighbour in self.__grid: - if includeNegative or self.__grid[neighbour] > 0: - neighbour_sum += self.__grid[neighbour] + for neighbour in self.getNeighboursOf(pos, includeDefault=includeDiagonal): + if includeNegative or self.get(neighbour) > 0: + neighbour_sum += self.get(neighbour) return neighbour_sum @@ -238,10 +233,9 @@ class Grid: targetDist = neighbour.getDistanceTo(pos_to) f_cost = targetDist + neighbourDist + currentNode[1] - neighbourNode = (f_cost, currentNode[1] + neighbourDist, currentCoord) if neighbour not in openNodes or f_cost < openNodes[neighbour][0]: - openNodes[neighbour] = neighbourNode + openNodes[neighbour] = (f_cost, currentNode[1] + neighbourDist, currentCoord) heappush(f_costs, (f_cost, neighbour)) if pos_to not in closedNodes: From 072ba02831ba3fe08748724382474745d76cf30a Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 15 Dec 2021 11:49:14 +0100 Subject: [PATCH 069/144] make things faster/cleaner --- tools/grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/grid.py b/tools/grid.py index ab2c2f4..2d4e23f 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -209,7 +209,7 @@ class Grid: openNodes[pos_from] = (0, pos_from.getDistanceTo(pos_to), None) heappush(f_costs, (0, pos_from)) - while openNodes: + while f_costs: _, currentCoord = heappop(f_costs) if currentCoord not in openNodes: continue From a5df60e839a749be008bc19ace40230af4de18c8 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Thu, 16 Dec 2021 08:38:15 +0100 Subject: [PATCH 070/144] some irc servers refuse to adhere to standards ... --- tools/irc.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/irc.py b/tools/irc.py index c9c1a3f..a0e30a6 100644 --- a/tools/irc.py +++ b/tools/irc.py @@ -169,8 +169,14 @@ class User: def __init__(self, user: str): self.user = user - user, self.hostname = self.user.split("@") - self.nickname, self.username = user.split("!") + if "@" not in self.user: + self.nickname = self.hostname = self.user + else: + user, self.hostname = self.user.split("@") + if "!" in user: + self.nickname, self.username = user.split("!") + else: + self.nickname = self.username = user def nick(self, new_nick: str): self.user.replace("%s!" % self.nickname, "%s!" % new_nick) From 898d4a8d8529326514d843c3687c70b98197d4b1 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Thu, 16 Dec 2021 08:54:44 +0100 Subject: [PATCH 071/144] auto-detect new users and my own user (libera RPL_WELCOME fuckup workaround) --- tools/irc.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/irc.py b/tools/irc.py index a0e30a6..865fae9 100644 --- a/tools/irc.py +++ b/tools/irc.py @@ -255,6 +255,12 @@ class Client: for func in self.__function_register[ServerMessage.RAW]: func(msg_from, msg_type, msg_to, message) + if "!" in msg_from and msg_from not in self.__userlist: + self.__userlist[msg_from] = User(msg_from) + if self.__userlist[msg_from].nickname == self.__userlist[self.__my_user].nickname: + del self.__userlist[self.__my_user] + self.__my_user = msg_from + if msg_type in self.__function_register: for func in self.__function_register[msg_type]: func(msg_from, msg_to, message) From 74df9e928742803b2a1db89a77f1fa0f5dd528a1 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 18 Dec 2021 20:48:01 +0100 Subject: [PATCH 072/144] general irc bot interface --- tools/irc.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tools/irc.py b/tools/irc.py index 865fae9..44150af 100644 --- a/tools/irc.py +++ b/tools/irc.py @@ -1,4 +1,8 @@ +from time import sleep + +from .schedule import Scheduler from .simplesocket import ClientSocket +from datetime import timedelta from enum import Enum from typing import Callable, Dict, List, Union @@ -365,3 +369,43 @@ class Client: def getChannelList(self) -> List[Channel]: return list(self.__channellist.values()) + + +class IrcBot(Client): + def __init__(self, server: str, port: int, nick: str, username: str, realname: str = "Python Bot"): + super().__init__(server, port, nick, username, realname) + self._scheduler = Scheduler() + self._channel_commands = {} + self._privmsg_commands = {} + self.subscribe(ServerMessage.MSG_PRIVMSG, self.on_privmsg) + + def on(self, *args, **kwargs): + self.subscribe(*args, **kwargs) + + def schedule(self, name: str, every: timedelta, func: Callable): + self._scheduler.schedule(name, every, func) + + def register_channel_command(self, command: str, channel: str, func: Callable): + if channel not in self._channel_commands: + self._channel_commands[channel] = {} + + self._channel_commands[channel][command] = func + + def register_privmsg_command(self, command: str, func: Callable): + self._privmsg_commands[command] = func + + def on_privmsg(self, msg_from, msg_to, message): + if not message: + return + command = message.split()[0] + if msg_to in self._channel_commands and command in self._channel_commands[msg_to]: + self._channel_commands[msg_to][command](msg_from, " ".join(message.split()[1:])) + + if msg_to == self.getUser().nickname and command in self._privmsg_commands: + self._privmsg_commands[command](msg_from, " ".join(message.split()[1:])) + + def run(self): + while True: + self._scheduler.run_pending() + self.receive() + sleep(0.01) From d8d71120980d4b0daf8e7fecc9ca04622270bc9c Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 19 Dec 2021 16:26:50 +0100 Subject: [PATCH 073/144] Grid.transform() in 3D! --- tools/grid.py | 94 ++++++++++++++++++++++++++++---------------------- tools/types.py | 6 ++++ 2 files changed, 58 insertions(+), 42 deletions(-) create mode 100644 tools/types.py diff --git a/tools/grid.py b/tools/grid.py index 2d4e23f..22f2210 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -1,23 +1,33 @@ from __future__ import annotations from .coordinate import Coordinate, DistanceAlgorithm +from .types import Numeric from enum import Enum from heapq import heappop, heappush from typing import Any, Dict, List, Union OFF = False ON = True -Numeric = Union[int, float] class GridTransformation(Enum): - FLIP_X = 1 - FLIP_HORIZONTALLY = 1 # alias for FLIP_X; prep for 3d-transformations - FLIP_VERTICALLY = 2 - FLIP_DIAGONALLY = 3 - FLIP_DIAGONALLY_REV = 4 - ROTATE_RIGHT = 5 - ROTATE_LEFT = 6 - ROTATE_TWICE = 7 + # Rotations always take the axis to rotate around as if it were the z-axis and then rotate clockwise + # Counter-Rotations likewise, just anti-clockwise + # 3D-only OPs have a number > 10 + ROTATE_Z = 3 + ROTATE_X = 11 + ROTATE_Y = 12 + COUNTER_ROTATE_X = 14 + COUNTER_ROTATE_Y = 15 + COUNTER_ROTATE_Z = 7 + FLIP_X = 4 + FLIP_Y = 5 + FLIP_Z = 13 + + # Handy aliases + FLIP_HORIZONTALLY = 5 + FLIP_VERTICALLY = 4 + ROTATE_RIGHT = 3 + ROTATE_LEFT = 7 class Grid: @@ -162,40 +172,40 @@ class Grid: self.set(c2, buf) def transform(self, mode: GridTransformation): - if self.mode3D: - raise NotImplementedError() # that will take some time and thought + if mode.value > 10 and not self.mode3D: + raise ValueError("Operation not possible in 2D space", mode) - if mode == GridTransformation.FLIP_HORIZONTALLY: - for x in range(self.minX, (self.maxX - self.minX) // 2 + 1): - for y in range(self.minY, self.maxY + 1): - self.flip(Coordinate(x, y), Coordinate(self.maxX - x, y)) - elif mode == GridTransformation.FLIP_VERTICALLY: - for y in range(self.minY, (self.maxY - self.minY) // 2 + 1): - for x in range(self.minX, self.maxX + 1): - self.flip(Coordinate(x, y), Coordinate(x, self.maxY - y)) - elif mode == GridTransformation.FLIP_DIAGONALLY: - self.transform(GridTransformation.ROTATE_LEFT) - self.transform(GridTransformation.FLIP_HORIZONTALLY) - elif mode == GridTransformation.FLIP_DIAGONALLY_REV: - self.transform(GridTransformation.ROTATE_RIGHT) - self.transform(GridTransformation.FLIP_HORIZONTALLY) - elif mode == GridTransformation.ROTATE_LEFT: - newGrid = Grid() - for x in range(self.maxX, self.minX - 1, -1): - for y in range(self.minY, self.maxY + 1): - newGrid.set(Coordinate(y, self.maxX - x), self.get(Coordinate(x, y))) - - self.__dict__.update(newGrid.__dict__) - elif mode == GridTransformation.ROTATE_RIGHT: - newGrid = Grid() - for x in range(self.minX, self.maxX + 1): - for y in range(self.maxY, self.minY - 1, -1): - newGrid.set(Coordinate(self.maxY - y, x), self.get(Coordinate(x, y))) - - self.__dict__.update(newGrid.__dict__) - elif mode == GridTransformation.ROTATE_TWICE: - self.transform(GridTransformation.ROTATE_RIGHT) - self.transform(GridTransformation.ROTATE_RIGHT) + coords = self.__grid + self.__grid, self.minX, self.maxX, self.minY, self.maxY, self.minZ, self.maxZ = {}, 0, 0, 0, 0, 0, 0 + if mode == GridTransformation.ROTATE_X: + for c, v in coords.items(): + self.set(Coordinate(c.x, -c.z, c.y), v) + elif mode == GridTransformation.ROTATE_Y: + for c, v in coords.items(): + self.set(Coordinate(-c.z, c.y, c.x), v) + elif mode == GridTransformation.ROTATE_Z: + for c, v in coords.items(): + self.set(Coordinate(c.y, -c.x, c.z), v) + elif mode == GridTransformation.COUNTER_ROTATE_X: + for c, v in coords.items(): + self.set(Coordinate(c.x, c.z, -c.y), v) + elif mode == GridTransformation.COUNTER_ROTATE_Y: + for c, v in coords.items(): + self.set(Coordinate(c.z, c.y, -c.x), v) + elif mode == GridTransformation.COUNTER_ROTATE_Z: + for c, v in coords.items(): + self.set(Coordinate(-c.y, c.x, c.z), v) + elif mode == GridTransformation.FLIP_X: + for c, v in coords.items(): + self.set(Coordinate(-c.x, c.y, c.z), v) + elif mode == GridTransformation.FLIP_Y: + for c, v in coords.items(): + self.set(Coordinate(c.x, -c.y, c.z), v) + elif mode == GridTransformation.FLIP_Z: + for c, v in coords.items(): + self.set(Coordinate(c.x, c.y, -c.z), v) + else: + raise NotImplementedError(mode) def getPath(self, pos_from: Coordinate, pos_to: Coordinate, includeDiagonal: bool, walls: List[Any] = None, weighted: bool = False) -> Union[None, List[Coordinate]]: diff --git a/tools/types.py b/tools/types.py new file mode 100644 index 0000000..b19ac97 --- /dev/null +++ b/tools/types.py @@ -0,0 +1,6 @@ +from typing import Union + +Numeric = Union[int, float] +IntOrNone = Union[int, None] +FloatOrNone = Union[float, None] +NumericOrNone = Union[Numeric, None] From 3b0a6480a3b18bb584a086e58c93b78c7e81d283 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 20 Dec 2021 08:30:19 +0100 Subject: [PATCH 074/144] Grid.range[XYZ]: ability to pad the range Grid.toggleGrid(): toggle everything! Grid.getSum(): give correct sum even if the default is not 0 --- tools/grid.py | 48 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index 22f2210..21b28a7 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -51,14 +51,16 @@ class Grid: self.minZ = min(pos.z, self.minZ) self.maxZ = max(pos.z, self.maxZ) - def rangeX(self): - return range(self.minX, self.maxX + 1) + def rangeX(self, pad: int = 0): + return range(self.minX - pad, self.maxX + pad + 1) - def rangeY(self): - return range(self.minY, self.maxY + 1) + def rangeY(self, pad: int = 0): + return range(self.minY - pad, self.maxY + pad + 1) - def rangeZ(self): - return range(self.minZ, self.maxZ + 1) + def rangeZ(self, pad: int = 0): + if not self.mode3D: + raise ValueError("rangeZ not available in 2D space") + return range(self.minZ - pad, self.maxZ + pad + 1) def toggle(self, pos: Coordinate): if pos in self.__grid: @@ -67,6 +69,15 @@ class Grid: self.__trackBoundaries(pos) self.__grid[pos] = not self.__default + def toggleGrid(self): + for x in self.rangeX(): + for y in self.rangeY(): + if not self.mode3D: + self.toggle(Coordinate(x, y)) + else: + for z in self.rangeZ(): + self.toggle(Coordinate(x, y, z)) + def set(self, pos: Coordinate, value: Any = True) -> Any: if pos.z is not None: self.mode3D = True @@ -139,12 +150,21 @@ class Grid: return list(self.__grid.keys()) def getSum(self, includeNegative: bool = True) -> Numeric: - grid_sum = 0 - for value in self.__grid.values(): - if includeNegative or value > 0: - grid_sum += value - - return grid_sum + if not self.mode3D: + return sum( + self.get(Coordinate(x, y)) + for x in self.rangeX() + for y in self.rangeY() + if includeNegative or self.get(Coordinate(x, y)) >= 0 + ) + else: + return sum( + self.get(Coordinate(x, y, z)) + for x in self.rangeX() + for y in self.rangeY() + for z in self.rangeZ() + if includeNegative or self.get(Coordinate(x, y)) >= 0 + ) def getNeighboursOf(self, pos: Coordinate, includeDefault: bool = False, includeDiagonal: bool = True) \ -> List[Coordinate]: @@ -259,11 +279,11 @@ class Grid: return pathCoords - def print(self, spacer: str = "", true_char: str = None): + def print(self, spacer: str = "", true_char: str = None, false_char: str = " "): for y in range(self.minY, self.maxY + 1): for x in range(self.minX, self.maxX + 1): if true_char: - print(true_char if self.get(Coordinate(x, y)) else " ", end="") + print(true_char if self.get(Coordinate(x, y)) else false_char, end="") else: print(self.get(Coordinate(x, y)), end="") print(spacer, end="") From fa68aa61094a4cbf4d16dd7194ce482e06679275 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 21 Dec 2021 10:08:18 +0100 Subject: [PATCH 075/144] caching for when functools.cache() is not available (looking at you, pypy!) --- tools/int_seq.py | 13 ++++--------- tools/tools.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/tools/int_seq.py b/tools/int_seq.py index 83c7425..ee701a7 100644 --- a/tools/int_seq.py +++ b/tools/int_seq.py @@ -1,5 +1,5 @@ import math -from typing import Dict +from .tools import cache def factorial(n: int) -> int: @@ -10,7 +10,8 @@ def factorial(n: int) -> int: return math.factorial(n) -def fibonacci(n: int, cache: Dict[int, int] = None) -> int: +@cache +def fibonacci(n: int) -> int: """ F(n) = F(n-1) + F(n-2) with F(0) = 0 and F(1) = 1 0, 1, 1, 2, 3, 5, 8, 13, 21, ... @@ -18,13 +19,7 @@ def fibonacci(n: int, cache: Dict[int, int] = None) -> int: if n < 2: return n - if cache is None: - cache = {} - - if n not in cache: - cache[n] = fibonacci(n - 1, cache) + fibonacci(n - 2, cache) - - return cache[n] + return fibonacci(n - 1) + fibonacci(n - 2) def triangular(n: int) -> int: diff --git a/tools/tools.py b/tools/tools.py index 69e9b0e..0266884 100644 --- a/tools/tools.py +++ b/tools/tools.py @@ -2,6 +2,7 @@ import datetime import inspect import os.path import sys +from functools import wraps from typing import Any @@ -42,3 +43,18 @@ def human_readable_time_from_delta(delta: datetime.timedelta) -> str: time_str += "00:" return time_str + "%02d" % (delta.seconds % 60) + + +def cache(func): + saved = {} + + @wraps(func) + def newfunc(*args): + if args in saved: + return saved[args] + + result = func(*args) + saved[args] = result + return result + + return newfunc From 7656e909843a7ef439a6b0131f3b4e09366df542 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 22 Dec 2021 08:44:15 +0100 Subject: [PATCH 076/144] Coordinate order - not sure which comparison is the "correct" one --- tools/coordinate.py | 29 +++++++++++++++++++++++++++++ tools/trees.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tools/trees.py diff --git a/tools/coordinate.py b/tools/coordinate.py index 83f1a66..861b3a8 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -144,6 +144,35 @@ class Coordinate: else: return Coordinate(self.x - other.x, self.y - other.y, self.z - other.z) + """ + def __eq__(self, other): + return self.x == other.x and self.y == other.y and self.z == other.z + + def __gt__(self, other): + if self.z is None: + return self.x > other.x and self.y > other.y + else: + return self.x > other.x and self.y > other.y and self.z > other.z + + def __ge__(self, other): + if self.z is None: + return self.x >= other.x and self.y >= other.y + else: + return self.x >= other.x and self.y >= other.y and self.z >= other.z + + def __lt__(self, other): + if self.z is None: + return self.x < other.x and self.y < other.y + else: + return self.x < other.x and self.y < other.y and self.z < other.z + + def __le__(self, other): + if self.z is None: + return self.x <= other.x and self.y <= other.y + else: + return self.x <= other.x and self.y <= other.y and self.z <= other.z + """ + @staticmethod def generate(from_x: int, to_x: int, from_y: int, to_y: int, from_z: int = None, to_z: int = None) -> List[Coordinate]: diff --git a/tools/trees.py b/tools/trees.py new file mode 100644 index 0000000..513b1bc --- /dev/null +++ b/tools/trees.py @@ -0,0 +1,39 @@ +from typing import Any, Union + + +class BinaryTreeNode: + data: Any + left: Union['BinaryTreeNode', None] + right: Union['BinaryTreeNode', None] + + def __init__(self, data: Any): + self.data = data + self.left = None + self.right = None + + def traverse_inorder(self): + if self.left: + self.left.traverse_inorder() + + yield self.data + + if self.right: + self.right.traverse_inorder() + + def traverse_preorder(self): + yield self.data + + if self.left: + self.left.traverse_preorder() + + if self.right: + self.right.traverse_preorder() + + def traverse_postorder(self): + if self.left: + self.left.traverse_preorder() + + if self.right: + self.right.traverse_postorder() + + yield self.data From 709b0f471b9a7e0d5e8d2015c18898450f6b0ed8 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 22 Dec 2021 09:25:50 +0100 Subject: [PATCH 077/144] Coordinate order - not sure which comparison is the "correct" one - can't make up my mind --- tools/coordinate.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tools/coordinate.py b/tools/coordinate.py index 861b3a8..6057cec 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -13,7 +13,7 @@ class DistanceAlgorithm(Enum): CHESSBOARD = 2 -@dataclass(frozen=True, order=True) +@dataclass(frozen=True) class Coordinate: x: int y: int @@ -144,7 +144,6 @@ class Coordinate: else: return Coordinate(self.x - other.x, self.y - other.y, self.z - other.z) - """ def __eq__(self, other): return self.x == other.x and self.y == other.y and self.z == other.z @@ -171,7 +170,6 @@ class Coordinate: return self.x <= other.x and self.y <= other.y else: return self.x <= other.x and self.y <= other.y and self.z <= other.z - """ @staticmethod def generate(from_x: int, to_x: int, from_y: int, to_y: int, From 5bf2ec1c47316193f138d21bd7b009719735328a Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 25 Dec 2021 06:48:17 +0100 Subject: [PATCH 078/144] Grid.range[XYZ]: make ranges reverable --- tools/grid.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index 21b28a7..da18d43 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -51,16 +51,25 @@ class Grid: self.minZ = min(pos.z, self.minZ) self.maxZ = max(pos.z, self.maxZ) - def rangeX(self, pad: int = 0): - return range(self.minX - pad, self.maxX + pad + 1) + def rangeX(self, pad: int = 0, reverse=False): + if reverse: + return range(self.maxX + pad, self.minX - pad - 1, -1) + else: + return range(self.minX - pad, self.maxX + pad + 1) - def rangeY(self, pad: int = 0): - return range(self.minY - pad, self.maxY + pad + 1) + def rangeY(self, pad: int = 0, reverse=False): + if reverse: + return range(self.maxY + pad, self.minY - pad - 1, -1) + else: + return range(self.minY - pad, self.maxY + pad + 1) - def rangeZ(self, pad: int = 0): + def rangeZ(self, pad: int = 0, reverse=False): if not self.mode3D: raise ValueError("rangeZ not available in 2D space") - return range(self.minZ - pad, self.maxZ + pad + 1) + if reverse: + return range(self.maxZ + pad, self.minZ - pad - 1, -1) + else: + return range(self.minZ - pad, self.maxZ + pad + 1) def toggle(self, pos: Coordinate): if pos in self.__grid: From 117eeec768b3d9d5cd99d52dd6be6bd82b81e27c Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 27 Dec 2021 16:29:46 +0100 Subject: [PATCH 079/144] updated AOCDay Interface --- tools/aoc.py | 74 +++++++++++++++++++++++++++++++++++----------- tools/stopwatch.py | 35 ++++++++++++++++++++++ 2 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 tools/stopwatch.py diff --git a/tools/aoc.py b/tools/aoc.py index 8a472b1..0993368 100644 --- a/tools/aoc.py +++ b/tools/aoc.py @@ -1,5 +1,6 @@ import os -from typing import List, Any, Type, Union +from tools.stopwatch import StopWatch +from typing import Any, Callable, Dict, List, Tuple, Type, Union from .tools import get_script_dir BASE_PATH = get_script_dir() @@ -8,14 +9,13 @@ INPUTS_PATH = os.path.join(BASE_PATH, 'inputs') class AOCDay: day: int - input: List # our input is always a list of str/lines - test_solutions_p1: List - test_solutions_p2: List + input: List[str] # our input is always a list of str/lines + inputs: List[List[Tuple[Any, str]]] + part_func: List[Callable] def __init__(self, day: int): self.day = day - with open(os.path.join(INPUTS_PATH, "input%02d" % day)) as f: - self.input = f.read().splitlines() + self.part_func = [self.part1, self.part2] def part1(self) -> Any: pass @@ -23,9 +23,55 @@ class AOCDay: def part2(self) -> Any: pass + def runPart(self, part: int, verbose: bool = False, measure_runtime: bool = False, timeit_number: int = 50): + case_count = 0 + for solution, input_file in self.inputs[part]: + exec_time = None + answer = None + self._loadInput(input_file) + + if not measure_runtime or case_count < len(self.input[part]) - 1: + answer = self.part_func[part]() + else: + stopwatch = StopWatch() + for _ in range(timeit_number): + answer = self.part_func[part]() + stopwatch.stop() + exec_time = str(stopwatch) + + if solution is None: + printSolution(self.day, part + 1, answer, solution, case_count, exec_time) + # FIXME: self._submit(part + 1, answer) + else: + if verbose or answer != solution: + printSolution(self.day, part + 1, answer, solution, case_count, exec_time) + + if answer != solution: + return False + + case_count += 1 + if case_count == len(self.inputs[part]) and not verbose: + printSolution(self.day, part + 1, answer, exec_time=exec_time) + + def run(self, parts: int = 3, verbose: bool = False, measure_runtime: bool = False, timeit_number: int = 50): + if parts & 1: + self.runPart(0, verbose, measure_runtime, timeit_number) + if parts & 2: + self.runPart(1, verbose, measure_runtime, timeit_number) + + def _loadInput(self, filename): + with open(os.path.join(INPUTS_PATH, filename)) as f: + self.input = f.read().splitlines() + + def _downloadInput(self, filename): + pass + + def _submit(self, answer): + pass + def test_part1(self, silent: bool = False) -> bool: live_input = self.input.copy() - for case, solution in enumerate(self.test_solutions_p1): + for case, solution in enumerate(self.inputs_p1): with open(os.path.join(INPUTS_PATH, "test_input%02d_1_%d" % (self.day, case))) as f: self.input = f.read().splitlines() @@ -43,7 +89,7 @@ class AOCDay: def test_part2(self, silent: bool = False) -> bool: live_input = self.input.copy() - for case, solution in enumerate(self.test_solutions_p2): + for case, solution in enumerate(self.inputs_p2): with open(os.path.join(INPUTS_PATH, "test_input%02d_2_%d" % (self.day, case))) as f: self.input = f.read().splitlines() @@ -112,21 +158,15 @@ class AOCDay: return return_array -def printSolution(day: int, part: int, solution: Any, test: Any = None, test_case: int = 0, exec_time: float = None): +def printSolution(day: int, part: int, solution: Any, test: Any = None, test_case: int = 0, exec_time: str = None): if exec_time is None: time_output = "" else: - units = ['s', 'ms', 'µs', 'ns'] - unit = 0 - while exec_time < 1: - exec_time *= 1000 - unit += 1 - - time_output = " (Average run time: %1.2f%s)" % (exec_time, units[unit]) + time_output = " (Average run time: %s)" % exec_time if test is not None: print( - "%s (TEST day%d/part%d/case%d) -- got '%s' -- expected '%s'%s" + "%s (TEST day%d/part%d/case%d): got '%s'; expected '%s'%s" % ("OK" if test == solution else "FAIL", day, part, test_case, solution, test, time_output) ) else: diff --git a/tools/stopwatch.py b/tools/stopwatch.py new file mode 100644 index 0000000..bab00dd --- /dev/null +++ b/tools/stopwatch.py @@ -0,0 +1,35 @@ +from time import time +from .types import FloatOrNone + + +class StopWatch: + started: FloatOrNone = None + stopped: FloatOrNone = None + + def __init__(self, auto_start=True): + if auto_start: + self.start() + + def start(self): + self.started = time() + self.stopped = None + + def stop(self) -> float: + self.stopped = time() + return self.elapsed() + + def elapsed(self) -> float: + if self.stopped is None: + return time() - self.started + else: + return self.stopped - self.started + + def __str__(self): + units = ['s', 'ms', 'µs', 'ns'] + unit = 0 + elapsed = self.elapsed() + while elapsed < 1: + elapsed *= 1000 + unit += 1 + + return "%1.2f%s" % (elapsed, units[unit]) From 5a60b71139b84a80539e26b6575c2361c3d4a707 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 27 Dec 2021 17:33:53 +0100 Subject: [PATCH 080/144] updated AOCDay Interface; output exec_time seperately --- tools/aoc.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/tools/aoc.py b/tools/aoc.py index 0993368..5151f65 100644 --- a/tools/aoc.py +++ b/tools/aoc.py @@ -8,13 +8,15 @@ INPUTS_PATH = os.path.join(BASE_PATH, 'inputs') class AOCDay: + year: int day: int input: List[str] # our input is always a list of str/lines inputs: List[List[Tuple[Any, str]]] part_func: List[Callable] - def __init__(self, day: int): + def __init__(self, year: int, day: int): self.day = day + self.year = year self.part_func = [self.part1, self.part2] def part1(self) -> Any: @@ -30,7 +32,7 @@ class AOCDay: answer = None self._loadInput(input_file) - if not measure_runtime or case_count < len(self.input[part]) - 1: + if not measure_runtime or case_count < len(self.inputs[part]) - 1: answer = self.part_func[part]() else: stopwatch = StopWatch() @@ -63,10 +65,10 @@ class AOCDay: with open(os.path.join(INPUTS_PATH, filename)) as f: self.input = f.read().splitlines() - def _downloadInput(self, filename): + def _downloadInput(self, filename: str): pass - def _submit(self, answer): + def _submit(self, part: int, answer: Any): pass def test_part1(self, silent: bool = False) -> bool: @@ -159,27 +161,24 @@ class AOCDay: def printSolution(day: int, part: int, solution: Any, test: Any = None, test_case: int = 0, exec_time: str = None): - if exec_time is None: - time_output = "" - else: - time_output = " (Average run time: %s)" % exec_time - if test is not None: print( - "%s (TEST day%d/part%d/case%d): got '%s'; expected '%s'%s" - % ("OK" if test == solution else "FAIL", day, part, test_case, solution, test, time_output) + "%s (TEST day%d/part%d/case%d): got '%s'; expected '%s'" + % ("OK" if test == solution else "FAIL", day, part, test_case, solution, test) ) else: print( - "Solution to day %s, part %s: %s%s" + "Solution to day %s, part %s: %s" % ( day, part, solution, - time_output ) ) + if exec_time: + print("Day %s, Part %s - Average run time: %s" % (day, part, exec_time)) + def splitLine(line, split_char: str = ',', return_type: Union[Type, List[Type]] = None): if split_char: From 5df8926378b3553e28fbd060c842439901473a01 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 28 Dec 2021 11:49:14 +0100 Subject: [PATCH 081/144] pythons round() behaves unexpected (for me at least) --- tools/math.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tools/math.py diff --git a/tools/math.py b/tools/math.py new file mode 100644 index 0000000..92ad48c --- /dev/null +++ b/tools/math.py @@ -0,0 +1,7 @@ +from decimal import Decimal, ROUND_HALF_UP +from .types import Numeric + + +def round_half_up(number: Numeric) -> int: + """ pythons round() rounds .5 to the *even* number; 0.5 == 0 """ + return int(Decimal(number).to_integral(ROUND_HALF_UP)) From 149da572c2464ee011adc417d4bfdbbd808af1ca Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 28 Dec 2021 12:50:37 +0100 Subject: [PATCH 082/144] auto input-download and answer-submit --- tools/aoc.py | 122 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 91 insertions(+), 31 deletions(-) diff --git a/tools/aoc.py b/tools/aoc.py index 5151f65..e465131 100644 --- a/tools/aoc.py +++ b/tools/aoc.py @@ -1,4 +1,11 @@ import os +import re +import time +import webbrowser + +import requests +from bs4 import BeautifulSoup +from tools.datafiles import JSONFile from tools.stopwatch import StopWatch from typing import Any, Callable, Dict, List, Tuple, Type, Union from .tools import get_script_dir @@ -43,7 +50,8 @@ class AOCDay: if solution is None: printSolution(self.day, part + 1, answer, solution, case_count, exec_time) - # FIXME: self._submit(part + 1, answer) + if answer not in {u"", b"", None, b"None", u"None"}: + self._submit(part + 1, answer) else: if verbose or answer != solution: printSolution(self.day, part + 1, answer, solution, case_count, exec_time) @@ -62,50 +70,102 @@ class AOCDay: self.runPart(1, verbose, measure_runtime, timeit_number) def _loadInput(self, filename): + file_path = os.path.join(INPUTS_PATH, filename) + if not os.path.exists(file_path): + self._downloadInput(file_path) + with open(os.path.join(INPUTS_PATH, filename)) as f: self.input = f.read().splitlines() def _downloadInput(self, filename: str): - pass + # FIXME: implement wait time for current day before 06:00:00 ? + session_id = open(".session", "r").readlines()[0].strip() + response = requests.get( + "https://adventofcode.com/%d/day/%d/input" % (self.year, self.day), + cookies={'session': session_id} + ) + print(response) + print(response.content) + if response.status_code != 200: + print("FAIL") + with open(filename, "wb") as f: + f.write(response.content) + f.flush() + def _submit(self, part: int, answer: Any): - pass + answer_cache = JSONFile("answer_cache.json", create=True) + print(answer_cache) + str_day = str(self.day) + str_part = str(part) + if str_day not in answer_cache: + answer_cache[str_day] = {} - def test_part1(self, silent: bool = False) -> bool: - live_input = self.input.copy() - for case, solution in enumerate(self.inputs_p1): - with open(os.path.join(INPUTS_PATH, "test_input%02d_1_%d" % (self.day, case))) as f: - self.input = f.read().splitlines() + if str_part not in answer_cache[str_day]: + answer_cache[str_day][str_part] = { + 'wrong': [], + 'correct': None + } - check = self.part1() - if not silent: - printSolution(self.day, 1, check, solution, case) + if answer in answer_cache[str_day][str_part]['wrong']: + print("Already tried %s. It was WRONG." % answer) + return - if check != solution: - if silent: - printSolution(self.day, 1, check, solution, case) - return False + if answer_cache[str_day][str_part]['correct'] is not None: + if answer == answer_cache[str_day][str_part]['correct']: + print("Already submitted %s. It was CORRECT." % answer) + return + else: + print("Already submitted an answer, but another one") + print("CORRECT was: %s" % answer_cache[str_day][str_part]['correct']) + print("Your answer: %s" % answer) + return - self.input = live_input - return True + return + print("Submitting %s as answer for %d part %d" % (answer, self.day, part)) + session_id = open(".session", "r").readlines()[0].strip() + response = requests.post( + "https://adventofcode.com/%d/day/%d/answer" % (self.year, self.day), + cookies={'session': session_id}, + data={'level': part, 'answer': answer} + ) - def test_part2(self, silent: bool = False) -> bool: - live_input = self.input.copy() - for case, solution in enumerate(self.inputs_p2): - with open(os.path.join(INPUTS_PATH, "test_input%02d_2_%d" % (self.day, case))) as f: - self.input = f.read().splitlines() + if not response.ok: + print("Failed to submit answer: (%s) %s" % (response.status_code, response.text)) - check = self.part2() - if not silent: - printSolution(self.day, 2, check, solution, case) + soup = BeautifulSoup(response.text, "html.parser") + message = soup.article.text + if "That's the right answer" in message: + answer_cache[str_day][str_part]['correct'] = answer + print("That's correct!") + webbrowser.open("https://adventofcode.com/%d/day/%d#part2" % (self.year, self.day)) + elif "That's not the right answer" in message: + answer_cache[str_day][str_part]['wrong'].append(answer) + print("That's WRONG!") + elif "You gave an answer too recently" in message: + # WAIT and retry + wait_pattern = r"You have (?:(\d+)m )?(\d+)s left to wait" + try: + [(minutes, seconds)] = re.findall(wait_pattern, message) + except ValueError: + print("wait_pattern unable to find wait_time in:") + print(message) + return - if check != solution: - if silent: - printSolution(self.day, 2, check, solution, case) - return False + seconds = int(seconds) + if minutes: + seconds *= int(minutes) * 60 - self.input = live_input - return True + print("TOO SOON. Waiting %d seconds until auto-retry." % seconds) + time.sleep(seconds) + self._submit(part, answer) + return + else: + print("I don't know what this means:") + print(message) + return + + answer_cache.save() def getInput(self) -> Union[str, List]: if len(self.input) == 1: From 082c61beca0a69ba5837a84f3b80c770d276ee65 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 28 Dec 2021 12:51:26 +0100 Subject: [PATCH 083/144] auto input-download and answer-submit --- tools/aoc.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/aoc.py b/tools/aoc.py index e465131..85e2dc7 100644 --- a/tools/aoc.py +++ b/tools/aoc.py @@ -84,10 +84,10 @@ class AOCDay: "https://adventofcode.com/%d/day/%d/input" % (self.year, self.day), cookies={'session': session_id} ) - print(response) - print(response.content) - if response.status_code != 200: - print("FAIL") + if not response.ok: + print("FAILED to download input: (%s) %s" % (response.status_code, response.text)) + return + with open(filename, "wb") as f: f.write(response.content) f.flush() From 6b5d3de95b9fcd9768b670aee57f89edfabe02ee Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 28 Dec 2021 21:11:07 +0100 Subject: [PATCH 084/144] allow stopwatch to return averages fix AOCDay._submit() --- tools/aoc.py | 4 +--- tools/stopwatch.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tools/aoc.py b/tools/aoc.py index 85e2dc7..22fa9ba 100644 --- a/tools/aoc.py +++ b/tools/aoc.py @@ -46,7 +46,7 @@ class AOCDay: for _ in range(timeit_number): answer = self.part_func[part]() stopwatch.stop() - exec_time = str(stopwatch) + exec_time = stopwatch.avg_string(timeit_number) if solution is None: printSolution(self.day, part + 1, answer, solution, case_count, exec_time) @@ -92,7 +92,6 @@ class AOCDay: f.write(response.content) f.flush() - def _submit(self, part: int, answer: Any): answer_cache = JSONFile("answer_cache.json", create=True) print(answer_cache) @@ -121,7 +120,6 @@ class AOCDay: print("Your answer: %s" % answer) return - return print("Submitting %s as answer for %d part %d" % (answer, self.day, part)) session_id = open(".session", "r").readlines()[0].strip() response = requests.post( diff --git a/tools/stopwatch.py b/tools/stopwatch.py index bab00dd..c5940c1 100644 --- a/tools/stopwatch.py +++ b/tools/stopwatch.py @@ -24,12 +24,18 @@ class StopWatch: else: return self.stopped - self.started - def __str__(self): + def avg_elapsed(self, divider: int) -> float: + return self.elapsed() / divider + + def avg_string(self, divider: int) -> str: units = ['s', 'ms', 'µs', 'ns'] unit = 0 - elapsed = self.elapsed() + elapsed = self.avg_elapsed(divider) while elapsed < 1: elapsed *= 1000 unit += 1 return "%1.2f%s" % (elapsed, units[unit]) + + def __str__(self): + return self.avg_string(1) From c9f91bbd98cc2bb32ebb8fce6fbc67bcb31d6579 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 29 Dec 2021 08:22:58 +0100 Subject: [PATCH 085/144] aoc.py: input cleanup requirements! --- requirements.txt | 4 ++++ tools/aoc.py | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a558087 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +requests~=2.26.0 +bs4~=0.0.1 +beautifulsoup4~=4.10.0 +setuptools~=56.0.0 diff --git a/tools/aoc.py b/tools/aoc.py index 22fa9ba..2dca347 100644 --- a/tools/aoc.py +++ b/tools/aoc.py @@ -1,13 +1,12 @@ import os import re +import requests import time import webbrowser - -import requests from bs4 import BeautifulSoup from tools.datafiles import JSONFile from tools.stopwatch import StopWatch -from typing import Any, Callable, Dict, List, Tuple, Type, Union +from typing import Any, Callable, List, Tuple, Type, Union from .tools import get_script_dir BASE_PATH = get_script_dir() From ed90adc75cf2dcf58b522ce1ccb74c4bfab32889 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 29 Dec 2021 08:46:40 +0100 Subject: [PATCH 086/144] stopwatch: deal with elapsed == 0 (for whatever reason) --- tools/stopwatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/stopwatch.py b/tools/stopwatch.py index c5940c1..aff17c3 100644 --- a/tools/stopwatch.py +++ b/tools/stopwatch.py @@ -31,7 +31,7 @@ class StopWatch: units = ['s', 'ms', 'µs', 'ns'] unit = 0 elapsed = self.avg_elapsed(divider) - while elapsed < 1: + while 0 < elapsed < 1: elapsed *= 1000 unit += 1 From c4cfd0b2980545434010af403126fe1a7ba74e36 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 29 Dec 2021 09:49:58 +0100 Subject: [PATCH 087/144] remove redundant printing --- tools/aoc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/aoc.py b/tools/aoc.py index 2dca347..daecc9f 100644 --- a/tools/aoc.py +++ b/tools/aoc.py @@ -93,7 +93,6 @@ class AOCDay: def _submit(self, part: int, answer: Any): answer_cache = JSONFile("answer_cache.json", create=True) - print(answer_cache) str_day = str(self.day) str_part = str(part) if str_day not in answer_cache: From 5e3bf28e7eafbf8d2d75f3f2a5099e7489b94304 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 29 Dec 2021 12:26:17 +0100 Subject: [PATCH 088/144] deal with shapes (like squares and cubes) --- tools/coordinate.py | 83 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/tools/coordinate.py b/tools/coordinate.py index 6057cec..17a457d 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -174,7 +174,7 @@ class Coordinate: @staticmethod def generate(from_x: int, to_x: int, from_y: int, to_y: int, from_z: int = None, to_z: int = None) -> List[Coordinate]: - if from_z is None and to_z is None: + if from_z is None or to_z is None: return [Coordinate(x, y) for x in range(from_x, to_x + 1) for y in range(from_y, to_y + 1)] else: return [ @@ -183,3 +183,84 @@ class Coordinate: for y in range(from_y, to_y + 1) for z in range(from_z, to_z + 1) ] + + +class Shape: + top_left: Coordinate + bottom_right: Coordinate + mode_3d: bool + + def __init__(self, top_left: Coordinate, bottom_right: Coordinate): + """ + in 2D mode: top_left is the upper left corner and bottom_right the lower right + (top_left.x <= bottom_right.x and top_left.y <= bottom_right.y) + in 3D mode: same logic applied, just for 3D Coordinates + top_left is the upper left rear corner and bottom_right the lower right front + (top_left.x <= bottom_right.x and top_left.y <= bottom_right.y and top_left.z <= bottom_right.z) + """ + self.top_left = top_left + self.bottom_right = bottom_right + self.mode_3d = top_left.z is not None and bottom_right.z is not None + + def size(self): + if not self.mode_3d: + return (self.bottom_right.x - self.top_left.x + 1) * (self.bottom_right.y - self.top_left.y + 1) + else: + return ( + (self.bottom_right.x - self.top_left.x + 1) + * (self.bottom_right.y - self.top_left.y + 1) + * (self.bottom_right.z - self.top_left.z + 1) + ) + + def intersection(self, other: Shape) -> Union[Shape, None]: + """ + returns a Shape of the intersecting part, or None if the Shapes don't intersect + """ + if self.mode_3d != other.mode_3d: + raise ValueError("Cannot calculate intersection between 2d and 3d shape") + + if not self.mode_3d: + intersect_top_left = Coordinate( + self.top_left.x if self.top_left.x > other.top_left.x else other.top_left.x, + self.top_left.y if self.top_left.y > other.top_left.y else other.top_left.y, + ) + intersect_bottom_right = Coordinate( + self.bottom_right.x if self.bottom_right.x < other.bottom_right.x else other.bottom_right.x, + self.bottom_right.y if self.bottom_right.y < other.bottom_right.y else other.bottom_right.y, + ) + else: + intersect_top_left = Coordinate( + self.top_left.x if self.top_left.x > other.top_left.x else other.top_left.x, + self.top_left.y if self.top_left.y > other.top_left.y else other.top_left.y, + self.top_left.z if self.top_left.z > other.top_left.z else other.top_left.z, + ) + intersect_bottom_right = Coordinate( + self.bottom_right.x if self.bottom_right.x < other.bottom_right.x else other.bottom_right.x, + self.bottom_right.y if self.bottom_right.y < other.bottom_right.y else other.bottom_right.y, + self.bottom_right.z if self.bottom_right.z < other.bottom_right.z else other.bottom_right.z, + ) + + if intersect_top_left <= intersect_bottom_right: + return self.__class__(intersect_top_left, intersect_bottom_right) + + def __and__(self, other): + return self.intersection(other) + + def __rand__(self, other): + return self.intersection(other) + + def __str__(self): + return "%s(%s, %s)" % (self.__class__.__name__, self.top_left, self.bottom_right) + + +class Square(Shape): + def __init__(self, top_left, bottom_right): + super().__init__(top_left, bottom_right) + self.mode_3d = False + + +class Cube(Shape): + def __init__(self, top_left, bottom_right): + if top_left.z is None or bottom_right.z is None: + raise ValueError("Both Coordinates need to be 3D") + super().__init__(top_left, bottom_right) From afefa8dcf0ec3a7d97bd3a364675011215cdaf28 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 29 Dec 2021 12:27:50 +0100 Subject: [PATCH 089/144] use git if available --- tools/aoc.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/aoc.py b/tools/aoc.py index daecc9f..d8d3ced 100644 --- a/tools/aoc.py +++ b/tools/aoc.py @@ -1,5 +1,7 @@ import os import re +import subprocess + import requests import time import webbrowser @@ -91,6 +93,9 @@ class AOCDay: f.write(response.content) f.flush() + if os.path.exists(".git"): + subprocess.call(["git", "add", filename]) + def _submit(self, part: int, answer: Any): answer_cache = JSONFile("answer_cache.json", create=True) str_day = str(self.day) From e239196065db2dba0b33217d20a704c9ffeff021 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Fri, 14 Jan 2022 10:19:30 +0100 Subject: [PATCH 090/144] starting linked lists --- tools/lists.py | 167 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 tools/lists.py diff --git a/tools/lists.py b/tools/lists.py new file mode 100644 index 0000000..5905196 --- /dev/null +++ b/tools/lists.py @@ -0,0 +1,167 @@ +from dataclasses import dataclass +from typing import Any, Union + + +@dataclass +class Node: + value: Any + next: 'Node' = None + prev: 'Node' = None + + +class Stack: + _tail: Node = None + size: int = 0 + + def push(self, obj: Any): + self._tail = Node(obj, self._tail) + self.size += 1 + + def pop(self) -> Any: + if self._tail is None: + raise ValueError('No more objects on stack') + + res = self._tail.value + self._tail = self._tail.next + self.size -= 1 + return res + + def peek(self) -> Any: + if self._tail is None: + raise ValueError('No more objects on stack') + + return self._tail.value + + def __contains__(self, obj: Any) -> bool: + x = self._tail + while x.value != obj and x.next is not None: + x = x.next + + return x.value == obj + + +class Queue: + pass + + +class LinkedList: + _head: Union[Node, None] = None + _tail: Union[Node, None] = None + size: int = 0 + + def _append(self, obj: Any): + node = Node(obj) + if self._head is None: + self._head = node + + if self._tail is None: + self._tail = node + else: + self._tail.next = node + node.prev = self._tail + self._tail = node + + self.size += 1 + + def _insert(self, index: int, obj: Any): + i_node = Node(obj) + node = self._get_node(index) + + i_node.prev, i_node.next = node.prev, node + node.prev = i_node + if node.prev is not None: + node.prev.next = i_node + + if index == 0: + self._head = i_node + + if index == self.size - 1: + self._tail = i_node + + self.size += 1 + + def _get_node(self, index: int) -> Node: + if abs(index) >= self.size: + raise IndexError("index out of bounds") + + if index < 0: + index = self.size + index + + if index <= self.size // 2: + x = 0 + node = self._head + while x < index: + x += 1 + node = node.next + else: + x = self.size - 1 + node = self._tail + while x > index: + x -= 1 + node = node.prev + + return node + + def _get(self, index: int) -> Any: + return self._get_node(index).value + + def _pop(self, index: int = None) -> Any: + if self.size == 0: + raise IndexError("pop from empty list") + + if index is None: # pop from the tail + node = self._tail + if self.size > 1: + self._tail = self._tail.prev + self._tail.next = None + else: + self._head = None + self._tail = None + + ret = node.value + elif index == 0: # pop from the head + node = self._head + if self.size > 1: + self._head = self._head.next + self._head.prev = None + else: + self._head = None + self._tail = None + + ret = node.value + else: + node = self._get_node(index) + if node.prev is not None: + node.prev.next = node.next + if node.next is not None: + node.next.prev = node.prev + node.prev = None + node.next = None + ret = node.value + + self.size -= 1 + return ret + + def append(self, obj: Any): + self._append(obj) + + def insert(self, index: int, obj: Any): + self._insert(index, obj) + + def get(self, index: int) -> Any: + return self._get(index) + + def pop(self, index: int = None) -> Any: + return self._pop(index) + + def debug(self): + print("head:", self._head) + print("tail:", self._tail) + if self.size > 0: + x = 0 + o = self._head + print(x, o.value) + while o.next is not None: + x += 1 + o = o.next + print(x, o.value) From dbd308dbf3ed7eb7c6a55f20a952465d8849f872 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Fri, 14 Jan 2022 11:23:00 +0100 Subject: [PATCH 091/144] that should cover linked lists (and their derivates Stacks and Queues) --- tools/lists.py | 154 ++++++++++++++++++++++++------------------------- 1 file changed, 76 insertions(+), 78 deletions(-) diff --git a/tools/lists.py b/tools/lists.py index 5905196..cb90fde 100644 --- a/tools/lists.py +++ b/tools/lists.py @@ -9,41 +9,6 @@ class Node: prev: 'Node' = None -class Stack: - _tail: Node = None - size: int = 0 - - def push(self, obj: Any): - self._tail = Node(obj, self._tail) - self.size += 1 - - def pop(self) -> Any: - if self._tail is None: - raise ValueError('No more objects on stack') - - res = self._tail.value - self._tail = self._tail.next - self.size -= 1 - return res - - def peek(self) -> Any: - if self._tail is None: - raise ValueError('No more objects on stack') - - return self._tail.value - - def __contains__(self, obj: Any) -> bool: - x = self._tail - while x.value != obj and x.next is not None: - x = x.next - - return x.value == obj - - -class Queue: - pass - - class LinkedList: _head: Union[Node, None] = None _tail: Union[Node, None] = None @@ -69,19 +34,16 @@ class LinkedList: i_node.prev, i_node.next = node.prev, node node.prev = i_node - if node.prev is not None: - node.prev.next = i_node + if i_node.prev is not None: + i_node.prev.next = i_node if index == 0: self._head = i_node - if index == self.size - 1: - self._tail = i_node - self.size += 1 def _get_node(self, index: int) -> Node: - if abs(index) >= self.size: + if index >= self.size or index < -self.size: raise IndexError("index out of bounds") if index < 0: @@ -110,36 +72,22 @@ class LinkedList: raise IndexError("pop from empty list") if index is None: # pop from the tail - node = self._tail - if self.size > 1: - self._tail = self._tail.prev - self._tail.next = None - else: - self._head = None - self._tail = None + index = -1 - ret = node.value - elif index == 0: # pop from the head - node = self._head - if self.size > 1: - self._head = self._head.next - self._head.prev = None - else: - self._head = None - self._tail = None - - ret = node.value + node = self._get_node(index) + if node.prev is not None: + node.prev.next = node.next else: - node = self._get_node(index) - if node.prev is not None: - node.prev.next = node.next - if node.next is not None: - node.next.prev = node.prev - node.prev = None - node.next = None - ret = node.value + self._head = node.next + if node.next is not None: + node.next.prev = node.prev + else: + self._tail = node.prev + ret = node.value + del node self.size -= 1 + return ret def append(self, obj: Any): @@ -154,14 +102,64 @@ class LinkedList: def pop(self, index: int = None) -> Any: return self._pop(index) - def debug(self): - print("head:", self._head) - print("tail:", self._tail) - if self.size > 0: - x = 0 - o = self._head - print(x, o.value) - while o.next is not None: - x += 1 - o = o.next - print(x, o.value) + def __contains__(self, obj: Any) -> bool: + x = self._head + while x.value != obj and x.next is not None: + x = x.next + + return x.value == obj + + def __add__(self, other: 'LinkedList') -> 'LinkedList': + self._tail.next = other._head + other._head.prev = self._tail + self._tail = other._tail + self.size += other.size + return self + + def __getitem__(self, index: int): + return self._get(index) + + def __setitem__(self, index: int, obj: Any): + self._get_node(index).value = obj + + def __len__(self): + return self.size + + def __repr__(self): + x = self._head + if x is None: + v = "" + else: + v = str(x.value) + while x.next is not None: + x = x.next + v += ", " + str(x.value) + return "%s(%s)" % (self.__class__.__name__, v) + + def __str__(self): + return self.__repr__() + + +class Stack(LinkedList): + def push(self, obj: Any): + self._append(obj) + + def pop(self) -> Any: + return self._pop() + + def peek(self) -> Any: + return self._tail.value + + +class Queue(LinkedList): + def enqueue(self, obj: Any): + self._append(obj) + + def dequeue(self) -> Any: + return self._pop(0) + + def peek(self) -> Any: + return self._head.value + + push = enqueue + pop = dequeue From 3590c2550c87eeca309473e243868a22696d7fea Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Fri, 14 Jan 2022 11:58:26 +0100 Subject: [PATCH 092/144] apparently this is faster than min()/max() ... ?! --- tools/grid.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index da18d43..fe48479 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -43,13 +43,13 @@ class Grid: self.mode3D = False def __trackBoundaries(self, pos: Coordinate): - self.minX = min(pos.x, self.minX) - self.minY = min(pos.y, self.minY) - self.maxX = max(pos.x, self.maxX) - self.maxY = max(pos.y, self.maxY) + self.minX = pos.x if pos.x < self.minX else self.minX + self.minY = pos.y if pos.y < self.minY else self.minY + self.maxX = pos.x if pos.x > self.maxX else self.maxX + self.maxY = pos.y if pos.y > self.maxY else self.maxY if self.mode3D: - self.minZ = min(pos.z, self.minZ) - self.maxZ = max(pos.z, self.maxZ) + self.minZ = pos.z if pos.z < self.minZ else self.minZ + self.maxZ = pos.z if pos.z > self.maxZ else self.maxZ def rangeX(self, pad: int = 0, reverse=False): if reverse: From 1e1f5ef126382c014ed8b4e955c969504392dce9 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Fri, 14 Jan 2022 12:29:54 +0100 Subject: [PATCH 093/144] list comp is faster than building lists yourself --- tools/coordinate.py | 23 ++++++++++------------- tools/grid.py | 2 +- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/tools/coordinate.py b/tools/coordinate.py index 17a457d..7912fb5 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -80,11 +80,11 @@ class Coordinate: else: nb_list = [(-1, 0), (1, 0), (0, -1), (0, 1)] - for dx, dy in nb_list: - tx = self.x + dx - ty = self.y + dy - if minX <= tx <= maxX and minY <= ty <= maxY: - neighbourList.append(Coordinate(tx, ty)) + return [ + Coordinate(self.x + dx, self.y + dy) + for dx, dy in nb_list + if minX <= self.x + dx <= maxX and minY <= self.y + dy <= maxY + ] else: if includeDiagonal: nb_list = [(x, y, z) for x in [-1, 0, 1] for y in [-1, 0, 1] for z in [-1, 0, 1]] @@ -92,14 +92,11 @@ class Coordinate: else: nb_list = [(-1, 0, 0), (0, -1, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (0, 0, -1)] - for dx, dy, dz in nb_list: - tx = self.x + dx - ty = self.y + dy - tz = self.z + dz - if minX <= tx <= maxX and minY <= ty <= maxY and minZ <= tz <= maxZ: - neighbourList.append(Coordinate(tx, ty, tz)) - - return neighbourList + return [ + Coordinate(self.x + dx, self.y + dy, self.z + dz) + for dx, dy, dz in nb_list + if minX <= self.x + dx <= maxX and minY <= self.y + dy <= maxY and minZ <= self.z + dz <= maxZ + ] def getAngleTo(self, target: Coordinate, normalized: bool = False) -> float: """normalized returns an angle going clockwise with 0 starting in the 'north'""" diff --git a/tools/grid.py b/tools/grid.py index fe48479..04f7cb2 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -259,7 +259,7 @@ class Grid: if currentCoord == pos_to: break - for neighbour in self.getNeighboursOf(currentCoord, True, includeDiagonal): + for neighbour in self.getNeighboursOf(currentCoord, includeDefault=True, includeDiagonal=includeDiagonal): if self.get(neighbour) in walls or neighbour in closedNodes: continue From a1eb51eb807c7697f06f6ca61dc3b1212d321118 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Fri, 14 Jan 2022 12:39:14 +0100 Subject: [PATCH 094/144] nicer reprs --- tools/coordinate.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tools/coordinate.py b/tools/coordinate.py index 7912fb5..02f59bc 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -25,7 +25,7 @@ class Coordinate: Get distance to target Coordinate :param target: - :param algorithm: Calculation Mode (0 = Manhattan, 1 = Pythagoras) + :param algorithm: Calculation Algorithm (s. DistanceAlgorithm) :param includeDiagonals: in Manhattan Mode specify if diagonal movements are allowed (counts as 1.4 in 2D, 1.7 in 3D) :return: Distance to Target @@ -168,6 +168,18 @@ class Coordinate: else: return self.x <= other.x and self.y <= other.y and self.z <= other.z + def __str__(self): + if self.z is None: + return "(%d,%d)" % (self.x, self.y) + else: + return "(%d,%d,%d)" % (self.x, self.y, self.z) + + def __repr__(self): + if self.z is None: + return "%s(x=%d, y=%d)" % (self.__class__.__name__, self.x, self.y) + else: + return "%s(x=%d, y=%d, z=%d)" % (self.__class__.__name__, self.x, self.y, self.z) + @staticmethod def generate(from_x: int, to_x: int, from_y: int, to_y: int, from_z: int = None, to_z: int = None) -> List[Coordinate]: @@ -247,6 +259,9 @@ class Shape: return self.intersection(other) def __str__(self): + return "%s(%s -> %s)" % (self.__class__.__name__, self.top_left, self.bottom_right) + + def __repr__(self): return "%s(%s, %s)" % (self.__class__.__name__, self.top_left, self.bottom_right) From 9d373596a1994b41dc7c8e0d2a73b47a8fdb1ee7 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 16 Jan 2022 17:39:37 +0100 Subject: [PATCH 095/144] binary trees; slow af, but functional :D --- btree_test.py | 55 +++++++++ tools/lists.py | 29 ++++- tools/trees.py | 300 ++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 353 insertions(+), 31 deletions(-) create mode 100644 btree_test.py diff --git a/btree_test.py b/btree_test.py new file mode 100644 index 0000000..ed5c261 --- /dev/null +++ b/btree_test.py @@ -0,0 +1,55 @@ +from heapq import heappop, heappush +from tools.trees import MinHeap, BinarySearchTree +from tools.stopwatch import StopWatch + + +b = BinarySearchTree() +for x in range(16): + b.add(x) +b.print() +print("---") +b.remove(7) +b.remove(6) +b.remove(4) +b.remove(5) +b.print() + +exit() + +# timing below :'-( + +s = StopWatch() +h = [] +for x in range(10_000): + heappush(h, x) +print("Heappush:", s.elapsed()) +while h: + heappop(h) +print("Heappop:", s.elapsed()) + +s = StopWatch() +h = MinHeap() +for x in range(10_000): + h.add(x) +print("MinHeap.add():", s.elapsed()) +while not h.empty(): + h.pop() +print("MinHeap.pop():", s.elapsed()) + +s = StopWatch() +b = set() +for x in range(1_000_000): + b.add(x) +print("set.add():", s.elapsed()) +for x in range(1_000_000): + _ = x in b +print("x in set:", s.elapsed()) + +s = StopWatch() +b = BinarySearchTree() +for x in range(1_000_000): + b.add(x) +print("AVL.add():", s.elapsed()) +for x in range(1_000_000): + _ = x in b +print("x in AVL:", s.elapsed()) diff --git a/tools/lists.py b/tools/lists.py index cb90fde..047a803 100644 --- a/tools/lists.py +++ b/tools/lists.py @@ -14,6 +14,25 @@ class LinkedList: _tail: Union[Node, None] = None size: int = 0 + def _get_head(self): + return self._head + + def _get_tail(self): + return self._tail + + def _set_head(self, node: Node): + node.next = self._head + self._head.prev = node + self._head = node + + def _set_tail(self, node: Node): + node.prev = self._tail + self._tail.next = node + self._tail = node + + head = property(_get_head, _set_head) + tail = property(_get_tail, _set_tail) + def _append(self, obj: Any): node = Node(obj) if self._head is None: @@ -110,9 +129,9 @@ class LinkedList: return x.value == obj def __add__(self, other: 'LinkedList') -> 'LinkedList': - self._tail.next = other._head - other._head.prev = self._tail - self._tail = other._tail + self._tail.next = other.head + other.head.prev = self._tail + self._tail = other.tail self.size += other.size return self @@ -161,5 +180,5 @@ class Queue(LinkedList): def peek(self) -> Any: return self._head.value - push = enqueue - pop = dequeue + push = put = enqueue + pop = get = dequeue diff --git a/tools/trees.py b/tools/trees.py index 513b1bc..af6f565 100644 --- a/tools/trees.py +++ b/tools/trees.py @@ -1,39 +1,287 @@ +from dataclasses import dataclass +from enum import Enum +from tools.lists import Queue from typing import Any, Union -class BinaryTreeNode: - data: Any - left: Union['BinaryTreeNode', None] - right: Union['BinaryTreeNode', None] +class Rotate(Enum): + LEFT = 0 + RIGHT = 1 - def __init__(self, data: Any): - self.data = data - self.left = None - self.right = None - def traverse_inorder(self): - if self.left: - self.left.traverse_inorder() +@dataclass +class TreeNode: + value: Any + parent: Union['TreeNode', None] = None + left: Union['TreeNode', None] = None + right: Union['TreeNode', None] = None + balance_factor: int = 0 + height: int = 0 - yield self.data + def __str__(self): + return "TreeNode:(%s; bf: %d, d: %d, p: %s, l: %s, r: %s)" \ + % (self.value, self.balance_factor, self.height, + self.parent.value if self.parent else "None", + self.left.value if self.left else "None", + self.right.value if self.right else "None") - if self.right: - self.right.traverse_inorder() + def __repr__(self): + return str(self) - def traverse_preorder(self): - yield self.data - if self.left: - self.left.traverse_preorder() +def update_node(node: TreeNode): + left_depth = node.left.height if node.left is not None else -1 + right_depth = node.right.height if node.right is not None else -1 + node.height = 1 + max(left_depth, right_depth) + node.balance_factor = right_depth - left_depth - if self.right: - self.right.traverse_preorder() - def traverse_postorder(self): - if self.left: - self.left.traverse_preorder() +class BinarySearchTree: + root: Union[TreeNode, None] = None + node_count: int = 0 - if self.right: - self.right.traverse_postorder() + def _balance(self, node: TreeNode) -> TreeNode: + if node.balance_factor == -2: + if node.left.balance_factor <= 0: + return self.rotate(Rotate.RIGHT, node) + else: + return self.rotate(Rotate.RIGHT, self.rotate(Rotate.LEFT, node.left)) + elif node.balance_factor == 2: + if node.right.balance_factor >= 0: + return self.rotate(Rotate.LEFT, node) + else: + return self.rotate(Rotate.LEFT, self.rotate(Rotate.RIGHT, node.right)) + else: + return node - yield self.data + def _insert(self, node: TreeNode, parent: TreeNode, obj: Any) -> TreeNode: + if node is None: + return TreeNode(obj, parent) + + if obj < node.value: + node.left = self._insert(node.left, node, obj) + else: + node.right = self._insert(node.right, node, obj) + + update_node(node) + return self._balance(node) + + def add(self, obj: Any): + if obj is None or obj in self: + raise ValueError("obj is None or already present in tree") + + self.root = self._insert(self.root, self.root, obj) + self.node_count += 1 + + def remove(self, obj: Any, root_node: TreeNode = None): + if self.root is None: + raise IndexError("remove from empty tree") + if root_node is None: + root_node = self.root + + node = root_node + while node is not None: + if obj < node.value: + node = node.left + continue + elif obj > node.value: + node = node.right + continue + else: + if node.left is None and node.right is None: # leaf node + if node.parent is not None: + if node.parent.left == node: + node.parent.left = None + else: + node.parent.right = None + elif node.left is not None and node.right is not None: # both subtrees present + d_node = node.left + while d_node.right is not None: + d_node = d_node.right + node.value = d_node.value + self.remove(node.value, d_node) + elif node.left is None: # only a subtree on the right + if node.parent is not None: + if node.parent.left == node: + node.parent.left = node.right + else: + node.parent.right = node.right + node.right.parent = node.parent + else: # only a subtree on the left + if node.parent is not None: + if node.parent.left == node: + node.parent.left = node.left + else: + node.parent.right = node.left + node.left.parent = node.parent + + update_node(root_node) + self._balance(root_node) + return + + raise ValueError("obj not in tree:", obj) + + def rotate(self, direction: Rotate, node: TreeNode = None) -> TreeNode: + if node is None: + node = self.root + + parent = node.parent + if direction == Rotate.LEFT: + pivot = node.right + node.right = pivot.left + if pivot.left is not None: + pivot.left.parent = node + pivot.left = node + else: + pivot = node.left + node.left = pivot.right + if pivot.right is not None: + pivot.right.parent = node + pivot.right = node + + node.parent = pivot + pivot.parent = parent + + if parent is not None: + if parent.left == node: + parent.left = pivot + else: + parent.right = pivot + + if node == self.root: + self.root = pivot + + update_node(node) + update_node(pivot) + + return pivot + + def print(self, node: TreeNode = None, level: int = 0): + if node is None: + if level == 0 and self.root is not None: + node = self.root + else: + return + + self.print(node.right, level + 1) + print(" " * 4 * level + '->', node) + self.print(node.left, level + 1) + + def __contains__(self, obj: Any) -> bool: + if self.root is None: + return False + + c_node = self.root + while c_node is not None: + if obj == c_node.value: + return True + elif obj < c_node.value: + c_node = c_node.left + else: + c_node = c_node.right + + return False + + def __len__(self) -> int: + return self.node_count + + +class Heap(BinarySearchTree): + def _find_left(self) -> TreeNode: + heap = Queue() + heap.put(self.root) + while c_node := heap.get(): + if c_node.left is None or c_node.right is None: + return c_node + else: + heap.put(c_node.left) + heap.put(c_node.right) + + def _find_right(self) -> TreeNode: + heap = [self.root] + while c_node := heap.pop(): + if c_node.left is None and c_node.right is None: + return c_node + else: + if c_node.left is not None: + heap.append(c_node.left) + if c_node.right is not None: + heap.append(c_node.right) + + def _sort_up(self, node: TreeNode): + pass + + def _heapify(self): + pass + + def empty(self) -> bool: + return self.root is None + + def add(self, obj: Any): + node = TreeNode(obj) + if self.root is None: + self.root = node + else: + t_node = self._find_left() + if t_node.left is None: + t_node.left = node + else: + t_node.right = node + node.parent = t_node + self._sort_up(node) + + def pop(self) -> Any: + if self.root is None: + raise IndexError("pop from empty heap") + + ret = self.root.value + if self.root.left is None and self.root.right is None: + self.root = None + else: + d_node = self._find_right() + self.root.value = d_node.value + if d_node.parent.left == d_node: + d_node.parent.left = None + else: + d_node.parent.right = None + self._heapify() + + return ret + + +class MinHeap(Heap): + def _sort_up(self, node: TreeNode): + while node.parent is not None and node.value < node.parent.value: + node.value, node.parent.value = node.parent.value, node.value + node = node.parent + + def _heapify(self): + node = self.root + while node is not None: + if node.left and node.left.value < node.value and (not node.right or node.right.value > node.left.value): + node.left.value, node.value = node.value, node.left.value + node = node.left + elif node.right and node.right.value < node.value and (not node.left or node.left.value > node.right.value): + node.right.value, node.value = node.value, node.right.value + node = node.right + else: + break + + +class MaxHeap(Heap): + def _sort_up(self, node: TreeNode): + while node.parent is not None and node.value > node.parent.value: + node.value, node.parent.value = node.parent.value, node.value + node = node.parent + + def _heapify(self): + node = self.root + while node is not None: + if node.left and node.left.value > node.value and (not node.right or node.right.value < node.left.value): + node.left.value, node.value = node.value, node.left.value + node = node.left + elif node.right and node.right.value > node.value and (not node.left or node.left.value < node.right.value): + node.right.value, node.value = node.value, node.right.value + node = node.right + else: + break From cdac29dee7bbb6e553f466d5090d77e7ced89105 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 16 Jan 2022 21:03:24 +0100 Subject: [PATCH 096/144] found some execution time; also ditch way to complicated heap implementation and just use the balancing binary tree --- tools/trees.py | 125 ++++++++++++++----------------------------------- 1 file changed, 36 insertions(+), 89 deletions(-) diff --git a/tools/trees.py b/tools/trees.py index af6f565..8e21c59 100644 --- a/tools/trees.py +++ b/tools/trees.py @@ -32,7 +32,7 @@ class TreeNode: def update_node(node: TreeNode): left_depth = node.left.height if node.left is not None else -1 right_depth = node.right.height if node.right is not None else -1 - node.height = 1 + max(left_depth, right_depth) + node.height = 1 + (left_depth if left_depth > right_depth else right_depth) node.balance_factor = right_depth - left_depth @@ -60,15 +60,17 @@ class BinarySearchTree: if obj < node.value: node.left = self._insert(node.left, node, obj) - else: + elif obj > node.value: node.right = self._insert(node.right, node, obj) + else: + raise ValueError("obj already present in tree: %s" % obj) update_node(node) return self._balance(node) def add(self, obj: Any): - if obj is None or obj in self: - raise ValueError("obj is None or already present in tree") + if obj is None: + return self.root = self._insert(self.root, self.root, obj) self.node_count += 1 @@ -94,6 +96,8 @@ class BinarySearchTree: node.parent.left = None else: node.parent.right = None + else: + self.root = None elif node.left is not None and node.right is not None: # both subtrees present d_node = node.left while d_node.right is not None: @@ -106,6 +110,8 @@ class BinarySearchTree: node.parent.left = node.right else: node.parent.right = node.right + else: + self.root = node.right node.right.parent = node.parent else: # only a subtree on the left if node.parent is not None: @@ -113,10 +119,13 @@ class BinarySearchTree: node.parent.left = node.left else: node.parent.right = node.left + else: + self.root = node.left node.left.parent = node.parent update_node(root_node) self._balance(root_node) + self.node_count -= 1 return raise ValueError("obj not in tree:", obj) @@ -187,101 +196,39 @@ class BinarySearchTree: class Heap(BinarySearchTree): - def _find_left(self) -> TreeNode: - heap = Queue() - heap.put(self.root) - while c_node := heap.get(): - if c_node.left is None or c_node.right is None: - return c_node - else: - heap.put(c_node.left) - heap.put(c_node.right) - - def _find_right(self) -> TreeNode: - heap = [self.root] - while c_node := heap.pop(): - if c_node.left is None and c_node.right is None: - return c_node - else: - if c_node.left is not None: - heap.append(c_node.left) - if c_node.right is not None: - heap.append(c_node.right) - - def _sort_up(self, node: TreeNode): - pass - - def _heapify(self): - pass - - def empty(self) -> bool: + def empty(self): return self.root is None - def add(self, obj: Any): - node = TreeNode(obj) - if self.root is None: - self.root = node - else: - t_node = self._find_left() - if t_node.left is None: - t_node.left = node - else: - t_node.right = node - node.parent = t_node - self._sort_up(node) - - def pop(self) -> Any: + def popMin(self): if self.root is None: raise IndexError("pop from empty heap") - ret = self.root.value - if self.root.left is None and self.root.right is None: - self.root = None - else: - d_node = self._find_right() - self.root.value = d_node.value - if d_node.parent.left == d_node: - d_node.parent.left = None - else: - d_node.parent.right = None - self._heapify() + c_node = self.root + while c_node.left is not None: + c_node = c_node.left + ret = c_node.value + self.remove(ret, c_node.parent) + return ret + + def popMax(self): + if self.root is None: + raise IndexError("pop from empty heap") + + c_node = self.root + while c_node.right is not None: + c_node = c_node.right + + ret = c_node.value + self.remove(ret, c_node.parent) return ret class MinHeap(Heap): - def _sort_up(self, node: TreeNode): - while node.parent is not None and node.value < node.parent.value: - node.value, node.parent.value = node.parent.value, node.value - node = node.parent - - def _heapify(self): - node = self.root - while node is not None: - if node.left and node.left.value < node.value and (not node.right or node.right.value > node.left.value): - node.left.value, node.value = node.value, node.left.value - node = node.left - elif node.right and node.right.value < node.value and (not node.left or node.left.value > node.right.value): - node.right.value, node.value = node.value, node.right.value - node = node.right - else: - break + def pop(self): + return self.popMin() class MaxHeap(Heap): - def _sort_up(self, node: TreeNode): - while node.parent is not None and node.value > node.parent.value: - node.value, node.parent.value = node.parent.value, node.value - node = node.parent - - def _heapify(self): - node = self.root - while node is not None: - if node.left and node.left.value > node.value and (not node.right or node.right.value < node.left.value): - node.left.value, node.value = node.value, node.left.value - node = node.left - elif node.right and node.right.value > node.value and (not node.left or node.left.value < node.right.value): - node.right.value, node.value = node.value, node.right.value - node = node.right - else: - break + def pop(self): + return self.popMax() From f700f0cb32e4d9d865033c98cad40f62fd656979 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 17 Jan 2022 09:40:02 +0100 Subject: [PATCH 097/144] some execution time testings --- btree_test.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/btree_test.py b/btree_test.py index ed5c261..648c074 100644 --- a/btree_test.py +++ b/btree_test.py @@ -3,24 +3,9 @@ from tools.trees import MinHeap, BinarySearchTree from tools.stopwatch import StopWatch -b = BinarySearchTree() -for x in range(16): - b.add(x) -b.print() -print("---") -b.remove(7) -b.remove(6) -b.remove(4) -b.remove(5) -b.print() - -exit() - -# timing below :'-( - s = StopWatch() h = [] -for x in range(10_000): +for x in range(1_000_000): heappush(h, x) print("Heappush:", s.elapsed()) while h: @@ -29,7 +14,7 @@ print("Heappop:", s.elapsed()) s = StopWatch() h = MinHeap() -for x in range(10_000): +for x in range(1_000_000): h.add(x) print("MinHeap.add():", s.elapsed()) while not h.empty(): From 622613a0ad501b4316b7b03ae5f4e6847e282033 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 24 Jan 2022 13:37:38 +0100 Subject: [PATCH 098/144] introduce unbalanced binary trees (incredibly fun when inserting already sorted values!) --- btree_test.py | 20 +++- tools/stopwatch.py | 2 + tools/trees.py | 258 +++++++++++++++++++++++++++------------------ 3 files changed, 178 insertions(+), 102 deletions(-) diff --git a/btree_test.py b/btree_test.py index 648c074..97d5b77 100644 --- a/btree_test.py +++ b/btree_test.py @@ -5,18 +5,20 @@ from tools.stopwatch import StopWatch s = StopWatch() h = [] -for x in range(1_000_000): +for x in range(100_000): heappush(h, x) print("Heappush:", s.elapsed()) +s.reset() while h: heappop(h) print("Heappop:", s.elapsed()) s = StopWatch() h = MinHeap() -for x in range(1_000_000): +for x in range(100_000): h.add(x) print("MinHeap.add():", s.elapsed()) +s.reset() while not h.empty(): h.pop() print("MinHeap.pop():", s.elapsed()) @@ -26,6 +28,7 @@ b = set() for x in range(1_000_000): b.add(x) print("set.add():", s.elapsed()) +s.reset() for x in range(1_000_000): _ = x in b print("x in set:", s.elapsed()) @@ -35,6 +38,19 @@ b = BinarySearchTree() for x in range(1_000_000): b.add(x) print("AVL.add():", s.elapsed()) +s.reset() for x in range(1_000_000): _ = x in b print("x in AVL:", s.elapsed()) + +print("DFS/BFS Test") +b = BinarySearchTree() +for x in range(20): + b.add(x) +b.print() +print("DFS:") +for x in b.iter_depth_first(): + print(x) +print("BFS:") +for x in b.iter_breadth_first(): + print(x) diff --git a/tools/stopwatch.py b/tools/stopwatch.py index aff17c3..1c520a0 100644 --- a/tools/stopwatch.py +++ b/tools/stopwatch.py @@ -18,6 +18,8 @@ class StopWatch: self.stopped = time() return self.elapsed() + reset = start + def elapsed(self) -> float: if self.stopped is None: return time() - self.started diff --git a/tools/trees.py b/tools/trees.py index 8e21c59..1bca07d 100644 --- a/tools/trees.py +++ b/tools/trees.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from enum import Enum -from tools.lists import Queue +from tools.lists import Queue, Stack from typing import Any, Union @@ -36,10 +36,157 @@ def update_node(node: TreeNode): node.balance_factor = right_depth - left_depth -class BinarySearchTree: +class BinaryTree: root: Union[TreeNode, None] = None node_count: int = 0 + def _insert(self, node: TreeNode, parent: TreeNode, obj: Any) -> TreeNode: + new_node = TreeNode(obj, parent) + if node is None: + return new_node + + found = False + while not found: + if obj < node.value: + if node.left is not None: + node = node.left + else: + node.left = new_node + found = True + elif obj > node.value: + if node.right is not None: + node = node.right + else: + node.right = new_node + found = True + else: + raise ValueError("obj already present in tree: %s" % obj) + + new_node.parent = node + return new_node + + def _remove(self, node: TreeNode): + if node.left is None and node.right is None: # leaf node + if node.parent is not None: + if node.parent.left == node: + node.parent.left = None + else: + node.parent.right = None + else: + self.root = None + elif node.left is not None and node.right is not None: # both subtrees present + d_node = node.left + while d_node.right is not None: + d_node = d_node.right + node.value = d_node.value + self.remove(node.value, d_node) + elif node.left is None: # only a subtree on the right + if node.parent is not None: + if node.parent.left == node: + node.parent.left = node.right + else: + node.parent.right = node.right + else: + self.root = node.right + node.right.parent = node.parent + else: # only a subtree on the left + if node.parent is not None: + if node.parent.left == node: + node.parent.left = node.left + else: + node.parent.right = node.left + else: + self.root = node.left + node.left.parent = node.parent + + self.node_count -= 1 + + def _get_node_by_value(self, obj: Any, root_node: TreeNode = None) -> TreeNode: + if self.root is None: + raise IndexError("get node from empty tree") + + if root_node is None: + root_node = self.root + + node = root_node + while node is not None: + if obj < node.value: + node = node.left + continue + elif obj > node.value: + node = node.right + continue + else: + return node + + raise ValueError("obj not in tree:", obj) + + def add(self, obj: Any): + if obj is None: + return + + new_node = self._insert(self.root, self.root, obj) + if self.root is None: + self.root = new_node + self.node_count += 1 + + def remove(self, obj: Any, root_node: TreeNode = None): + node = self._get_node_by_value(obj, root_node) + self._remove(node) + + def iter_depth_first(self): + stack = Stack() + stack.push(self.root) + while len(stack): + node = stack.pop() + if node.right is not None: + stack.push(node.right) + if node.left is not None: + stack.push(node.left) + yield node.value + + def iter_breadth_first(self): + queue = Queue() + queue.push(self.root) + while len(queue): + node = queue.pop() + if node.left is not None: + queue.push(node.left) + if node.right is not None: + queue.push(node.right) + yield node.value + + def print(self, node: TreeNode = None, level: int = 0): + if node is None: + if level == 0 and self.root is not None: + node = self.root + else: + return + + self.print(node.right, level + 1) + print(" " * 4 * level + '->', node) + self.print(node.left, level + 1) + + def __contains__(self, obj: Any) -> bool: + if self.root is None: + return False + + c_node = self.root + while c_node is not None: + if obj == c_node.value: + return True + elif obj < c_node.value: + c_node = c_node.left + else: + c_node = c_node.right + + return False + + def __len__(self) -> int: + return self.node_count + + +class BinarySearchTree(BinaryTree): def _balance(self, node: TreeNode) -> TreeNode: if node.balance_factor == -2: if node.left.balance_factor <= 0: @@ -55,80 +202,20 @@ class BinarySearchTree: return node def _insert(self, node: TreeNode, parent: TreeNode, obj: Any) -> TreeNode: - if node is None: - return TreeNode(obj, parent) - - if obj < node.value: - node.left = self._insert(node.left, node, obj) - elif obj > node.value: - node.right = self._insert(node.right, node, obj) - else: - raise ValueError("obj already present in tree: %s" % obj) - - update_node(node) - return self._balance(node) - - def add(self, obj: Any): - if obj is None: - return - - self.root = self._insert(self.root, self.root, obj) - self.node_count += 1 + node = super()._insert(node, parent, obj) + if self.root is not None: + while node is not None: + update_node(node) + node = self._balance(node).parent + return node def remove(self, obj: Any, root_node: TreeNode = None): - if self.root is None: - raise IndexError("remove from empty tree") if root_node is None: root_node = self.root - node = root_node - while node is not None: - if obj < node.value: - node = node.left - continue - elif obj > node.value: - node = node.right - continue - else: - if node.left is None and node.right is None: # leaf node - if node.parent is not None: - if node.parent.left == node: - node.parent.left = None - else: - node.parent.right = None - else: - self.root = None - elif node.left is not None and node.right is not None: # both subtrees present - d_node = node.left - while d_node.right is not None: - d_node = d_node.right - node.value = d_node.value - self.remove(node.value, d_node) - elif node.left is None: # only a subtree on the right - if node.parent is not None: - if node.parent.left == node: - node.parent.left = node.right - else: - node.parent.right = node.right - else: - self.root = node.right - node.right.parent = node.parent - else: # only a subtree on the left - if node.parent is not None: - if node.parent.left == node: - node.parent.left = node.left - else: - node.parent.right = node.left - else: - self.root = node.left - node.left.parent = node.parent - - update_node(root_node) - self._balance(root_node) - self.node_count -= 1 - return - - raise ValueError("obj not in tree:", obj) + super().remove(obj, root_node) + update_node(root_node) + self._balance(root_node) def rotate(self, direction: Rotate, node: TreeNode = None) -> TreeNode: if node is None: @@ -165,35 +252,6 @@ class BinarySearchTree: return pivot - def print(self, node: TreeNode = None, level: int = 0): - if node is None: - if level == 0 and self.root is not None: - node = self.root - else: - return - - self.print(node.right, level + 1) - print(" " * 4 * level + '->', node) - self.print(node.left, level + 1) - - def __contains__(self, obj: Any) -> bool: - if self.root is None: - return False - - c_node = self.root - while c_node is not None: - if obj == c_node.value: - return True - elif obj < c_node.value: - c_node = c_node.left - else: - c_node = c_node.right - - return False - - def __len__(self) -> int: - return self.node_count - class Heap(BinarySearchTree): def empty(self): From 04c6357f9c8e7604f63402552b961b45d6db1a6f Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 19 Apr 2022 09:15:49 +0200 Subject: [PATCH 099/144] factorization is always useful to have --- tools/math.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/math.py b/tools/math.py index 92ad48c..5bf6a2c 100644 --- a/tools/math.py +++ b/tools/math.py @@ -1,3 +1,4 @@ +import math from decimal import Decimal, ROUND_HALF_UP from .types import Numeric @@ -5,3 +6,13 @@ from .types import Numeric def round_half_up(number: Numeric) -> int: """ pythons round() rounds .5 to the *even* number; 0.5 == 0 """ return int(Decimal(number).to_integral(ROUND_HALF_UP)) + + +def get_factors(num: int) -> set: + f = {num} + for x in range(1, int(math.sqrt(num)) + 1): + if num % x == 0: + f.add(x) + f.add(num // x) + + return f From 8f8201d8c82e5177c58da0e144ebcd8ea037f80a Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 4 May 2022 10:39:32 +0200 Subject: [PATCH 100/144] Support pickled datafiles. Not readable by humans, but way more versatile --- tools/datafiles.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tools/datafiles.py b/tools/datafiles.py index d87fc08..d589441 100644 --- a/tools/datafiles.py +++ b/tools/datafiles.py @@ -1,5 +1,6 @@ import json import os +import pickle class DataFile(dict): @@ -40,3 +41,21 @@ class JSONFile(DataFile): def save(self): with open(self.filename, "wt") as f: f.write(json.dumps(self.copy(), indent=4)) + + +class PickleFile(DataFile): + def __init__(self, filename: str, create: bool) -> None: + super().__init__(filename, create) + + def load(self) -> None: + with open(self.filename, "rb") as f: + c = f.read() + + if len(c) > 0: + pickle_dict = pickle.loads(c) + for k in pickle_dict: + self[k] = pickle_dict[k] + + def save(self) -> None: + with open(self.filename, "wb") as f: + pickle.dump(self.copy(), f) From c8bf84c578c1df13d6d94a4ba19312861aecb796 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 4 May 2022 10:45:55 +0200 Subject: [PATCH 101/144] Perhaps someday in the future you're able to remember that dicts have an update() method ... --- tools/datafiles.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tools/datafiles.py b/tools/datafiles.py index d589441..c8f2795 100644 --- a/tools/datafiles.py +++ b/tools/datafiles.py @@ -34,9 +34,7 @@ class JSONFile(DataFile): c = f.read() if len(c) > 0: - json_dict = json.loads(c) - for k in json_dict: - self[k] = json_dict[k] + self.update(json.loads(c)) def save(self): with open(self.filename, "wt") as f: @@ -52,9 +50,7 @@ class PickleFile(DataFile): c = f.read() if len(c) > 0: - pickle_dict = pickle.loads(c) - for k in pickle_dict: - self[k] = pickle_dict[k] + self.update(pickle.loads(c)) def save(self) -> None: with open(self.filename, "wb") as f: From 2ac37ad56734181c3f943032f140580e8b06fa6a Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 7 Aug 2022 18:45:33 +0200 Subject: [PATCH 102/144] pentagonal number sequence --- tools/int_seq.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/int_seq.py b/tools/int_seq.py index ee701a7..92b5250 100644 --- a/tools/int_seq.py +++ b/tools/int_seq.py @@ -28,3 +28,11 @@ def triangular(n: int) -> int: 0, 1, 3, 6, 10, 15, ... """ return n * (n + 1) // 2 + + +def pentagonal(n: int) -> int: + """ + A pentagonal number is a figurate number that extends the concept of triangular and square numbers to the pentagon + 0, 1, 5, 12, 22, 35, ... + """ + return ((3 * n * n) - n) / 2 From c752c286d20eac711eb4b21a3293a95fd0514c1d Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 7 Aug 2022 18:50:08 +0200 Subject: [PATCH 103/144] pentagonal number sequence (really int) --- tools/int_seq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/int_seq.py b/tools/int_seq.py index 92b5250..3e00b20 100644 --- a/tools/int_seq.py +++ b/tools/int_seq.py @@ -35,4 +35,4 @@ def pentagonal(n: int) -> int: A pentagonal number is a figurate number that extends the concept of triangular and square numbers to the pentagon 0, 1, 5, 12, 22, 35, ... """ - return ((3 * n * n) - n) / 2 + return ((3 * n * n) - n) // 2 From 210d407bf90e4d4115f8391f3ac6b029affc13aa Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 7 Aug 2022 19:34:37 +0200 Subject: [PATCH 104/144] remove class variables (should be instance variables) --- tools/coordinate.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tools/coordinate.py b/tools/coordinate.py index 02f59bc..3609eff 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -195,10 +195,6 @@ class Coordinate: class Shape: - top_left: Coordinate - bottom_right: Coordinate - mode_3d: bool - def __init__(self, top_left: Coordinate, bottom_right: Coordinate): """ in 2D mode: top_left is the upper left corner and bottom_right the lower right @@ -211,7 +207,7 @@ class Shape: self.bottom_right = bottom_right self.mode_3d = top_left.z is not None and bottom_right.z is not None - def size(self): + def __len__(self): if not self.mode_3d: return (self.bottom_right.x - self.top_left.x + 1) * (self.bottom_right.y - self.top_left.y + 1) else: From f5d59cd74f9b173e450be9c7f43a396a33ff54ba Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 7 Aug 2022 19:36:48 +0200 Subject: [PATCH 105/144] cleanup --- tools/coordinate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/coordinate.py b/tools/coordinate.py index 3609eff..de3edc3 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -62,7 +62,7 @@ class Coordinate: def getNeighbours(self, includeDiagonal: bool = True, minX: int = -inf, minY: int = -inf, maxX: int = inf, maxY: int = inf, minZ: int = -inf, maxZ: int = inf) -> list[Coordinate]: """ - Get a list of neighbouring coordinates + Get a list of neighbouring coordinates. :param includeDiagonal: include diagonal neighbours :param minX: ignore all neighbours that would have an X value below this @@ -73,7 +73,6 @@ class Coordinate: :param maxZ: ignore all neighbours that would have an Z value above this :return: list of Coordinate """ - neighbourList = [] if self.z is None: if includeDiagonal: nb_list = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] From 3bbb37526d521bc319fba61edbf5afa5c94ac726 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 10 Aug 2022 10:09:15 +0200 Subject: [PATCH 106/144] some minor refactorings suggested by sonarcube --- tools/aoc.py | 32 ++++++++++++++++---------------- tools/daemon.py | 3 ++- tools/datafiles.py | 4 ++-- tools/irc.py | 35 +++++++++++++++++------------------ tools/lists.py | 3 --- tools/trees.py | 16 ++++++++++------ tools/types.py | 1 + 7 files changed, 48 insertions(+), 46 deletions(-) diff --git a/tools/aoc.py b/tools/aoc.py index d8d3ced..08f8449 100644 --- a/tools/aoc.py +++ b/tools/aoc.py @@ -28,17 +28,17 @@ class AOCDay: self.part_func = [self.part1, self.part2] def part1(self) -> Any: - pass + raise NotImplementedError() def part2(self) -> Any: - pass + raise NotImplementedError() - def runPart(self, part: int, verbose: bool = False, measure_runtime: bool = False, timeit_number: int = 50): + def run_part(self, part: int, verbose: bool = False, measure_runtime: bool = False, timeit_number: int = 50): case_count = 0 for solution, input_file in self.inputs[part]: exec_time = None answer = None - self._loadInput(input_file) + self._load_input(input_file) if not measure_runtime or case_count < len(self.inputs[part]) - 1: answer = self.part_func[part]() @@ -50,35 +50,35 @@ class AOCDay: exec_time = stopwatch.avg_string(timeit_number) if solution is None: - printSolution(self.day, part + 1, answer, solution, case_count, exec_time) + print_solution(self.day, part + 1, answer, solution, case_count, exec_time) if answer not in {u"", b"", None, b"None", u"None"}: self._submit(part + 1, answer) else: if verbose or answer != solution: - printSolution(self.day, part + 1, answer, solution, case_count, exec_time) + print_solution(self.day, part + 1, answer, solution, case_count, exec_time) if answer != solution: return False case_count += 1 if case_count == len(self.inputs[part]) and not verbose: - printSolution(self.day, part + 1, answer, exec_time=exec_time) + print_solution(self.day, part + 1, answer, exec_time=exec_time) def run(self, parts: int = 3, verbose: bool = False, measure_runtime: bool = False, timeit_number: int = 50): if parts & 1: - self.runPart(0, verbose, measure_runtime, timeit_number) + self.run_part(0, verbose, measure_runtime, timeit_number) if parts & 2: - self.runPart(1, verbose, measure_runtime, timeit_number) + self.run_part(1, verbose, measure_runtime, timeit_number) - def _loadInput(self, filename): + def _load_input(self, filename): file_path = os.path.join(INPUTS_PATH, filename) if not os.path.exists(file_path): - self._downloadInput(file_path) + self._download_input(file_path) with open(os.path.join(INPUTS_PATH, filename)) as f: self.input = f.read().splitlines() - def _downloadInput(self, filename: str): + def _download_input(self, filename: str): # FIXME: implement wait time for current day before 06:00:00 ? session_id = open(".session", "r").readlines()[0].strip() response = requests.get( @@ -212,16 +212,16 @@ class AOCDay: if input has multiple lines, returns a 2d array (a[line][values]) """ if len(self.input) == 1: - return splitLine(line=self.input[0], split_char=split_char, return_type=return_type) + return split_line(line=self.input[0], split_char=split_char, return_type=return_type) else: return_array = [] for line in self.input: - return_array.append(splitLine(line=line, split_char=split_char, return_type=return_type)) + return_array.append(split_line(line=line, split_char=split_char, return_type=return_type)) return return_array -def printSolution(day: int, part: int, solution: Any, test: Any = None, test_case: int = 0, exec_time: str = None): +def print_solution(day: int, part: int, solution: Any, test: Any = None, test_case: int = 0, exec_time: str = None): if test is not None: print( "%s (TEST day%d/part%d/case%d): got '%s'; expected '%s'" @@ -241,7 +241,7 @@ def printSolution(day: int, part: int, solution: Any, test: Any = None, test_cas print("Day %s, Part %s - Average run time: %s" % (day, part, exec_time)) -def splitLine(line, split_char: str = ',', return_type: Union[Type, List[Type]] = None): +def split_line(line, split_char: str = ',', return_type: Union[Type, List[Type]] = None): if split_char: line = line.split(split_char) diff --git a/tools/daemon.py b/tools/daemon.py index 780846d..c75cb1b 100644 --- a/tools/daemon.py +++ b/tools/daemon.py @@ -18,6 +18,7 @@ import sys import time from signal import SIGTERM, signal +DEV_NULL = "/dev/null" class Daemon: """ @@ -26,7 +27,7 @@ class Daemon: Usage: subclass the Daemon class and override the run() method """ - def __init__(self, pidfile='_.pid', stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): + def __init__(self, pidfile='_.pid', stdin=DEV_NULL, stdout=DEV_NULL, stderr=DEV_NULL): self.stdin = stdin self.stdout = stdout self.stderr = stderr diff --git a/tools/datafiles.py b/tools/datafiles.py index c8f2795..ba37546 100644 --- a/tools/datafiles.py +++ b/tools/datafiles.py @@ -19,10 +19,10 @@ class DataFile(dict): self.load() def load(self): - pass + raise NotImplementedError() def save(self): - pass + raise NotImplementedError() class JSONFile(DataFile): diff --git a/tools/irc.py b/tools/irc.py index 44150af..1e60459 100644 --- a/tools/irc.py +++ b/tools/irc.py @@ -2,6 +2,7 @@ from time import sleep from .schedule import Scheduler from .simplesocket import ClientSocket +from .types import StrOrNone from datetime import timedelta from enum import Enum from typing import Callable, Dict, List, Union @@ -166,24 +167,24 @@ class ServerMessage(str, Enum): class User: - user: str + identifier: str nickname: str username: str hostname: str - def __init__(self, user: str): - self.user = user - if "@" not in self.user: - self.nickname = self.hostname = self.user + def __init__(self, identifier: str): + self.identifier = identifier + if "@" not in self.identifier: + self.nickname = self.hostname = self.identifier else: - user, self.hostname = self.user.split("@") - if "!" in user: - self.nickname, self.username = user.split("!") + identifier, self.hostname = self.identifier.split("@") + if "!" in identifier: + self.nickname, self.username = identifier.split("!") else: - self.nickname = self.username = user + self.nickname = self.username = identifier def nick(self, new_nick: str): - self.user.replace("%s!" % self.nickname, "%s!" % new_nick) + self.identifier.replace("%s!" % self.nickname, "%s!" % new_nick) self.nickname = new_nick @@ -198,12 +199,12 @@ class Channel: self.userlist = {} def join(self, user: User): - if user.user not in self.userlist: - self.userlist[user.user] = user + if user.identifier not in self.userlist: + self.userlist[user.identifier] = user def quit(self, user: User): - if user.user in self.userlist: - del self.userlist[user.user] + if user.identifier in self.userlist: + del self.userlist[user.identifier] class Client: @@ -212,8 +213,7 @@ class Client: __server_caps: Dict[str, Union[str, int]] __userlist: Dict[str, User] __channellist: Dict[str, Channel] - __server_name: str = None - __my_user: str = None + __my_user: StrOrNone def __init__(self, server: str, port: int, nick: str, username: str, realname: str = "Python Bot"): self.__userlist = {} @@ -276,7 +276,6 @@ class Client: self.__function_register[msg_type] = [func] def on_rpl_welcome(self, msg_from: str, msg_to: str, message: str): - self.__server_name = msg_from self.__my_user = message.split()[-1] self.__userlist[self.__my_user] = User(self.__my_user) @@ -302,7 +301,7 @@ class Client: def on_nick(self, msg_from: str, msg_to: str, message: str): self.__userlist[msg_from].nick(msg_to) - self.__userlist[self.__userlist[msg_from].user] = self.__userlist[msg_from] + self.__userlist[self.__userlist[msg_from].identifier] = self.__userlist[msg_from] del self.__userlist[msg_from] def on_join(self, msg_from: str, msg_to: str, message: str): diff --git a/tools/lists.py b/tools/lists.py index 047a803..2f44b66 100644 --- a/tools/lists.py +++ b/tools/lists.py @@ -163,9 +163,6 @@ class Stack(LinkedList): def push(self, obj: Any): self._append(obj) - def pop(self) -> Any: - return self._pop() - def peek(self) -> Any: return self._tail.value diff --git a/tools/trees.py b/tools/trees.py index 1bca07d..a067b74 100644 --- a/tools/trees.py +++ b/tools/trees.py @@ -1,7 +1,7 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum from tools.lists import Queue, Stack -from typing import Any, Union +from typing import Any, List, Union class Rotate(Enum): @@ -29,6 +29,12 @@ class TreeNode: return str(self) +class TrieNode: + value: str + parent: Union['TrieNode', None] = None + children: List['TrieNode'] = field(default_factory=list) + + def update_node(node: TreeNode): left_depth = node.left.height if node.left is not None else -1 right_depth = node.right.height if node.right is not None else -1 @@ -112,10 +118,8 @@ class BinaryTree: while node is not None: if obj < node.value: node = node.left - continue elif obj > node.value: node = node.right - continue else: return node @@ -266,7 +270,7 @@ class Heap(BinarySearchTree): c_node = c_node.left ret = c_node.value - self.remove(ret, c_node.parent) + self._remove(c_node) return ret def popMax(self): @@ -278,7 +282,7 @@ class Heap(BinarySearchTree): c_node = c_node.right ret = c_node.value - self.remove(ret, c_node.parent) + self._remove(c_node) return ret diff --git a/tools/types.py b/tools/types.py index b19ac97..cc123d5 100644 --- a/tools/types.py +++ b/tools/types.py @@ -1,6 +1,7 @@ from typing import Union Numeric = Union[int, float] +StrOrNone = Union[str, None] IntOrNone = Union[int, None] FloatOrNone = Union[float, None] NumericOrNone = Union[Numeric, None] From f83709758cc4613714b49ab53a31bd2c0b5418de Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 10 Aug 2022 23:34:58 +0200 Subject: [PATCH 107/144] set shaped areas in a grid --- tools/grid.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tools/grid.py b/tools/grid.py index 04f7cb2..91be2bd 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -1,5 +1,5 @@ from __future__ import annotations -from .coordinate import Coordinate, DistanceAlgorithm +from .coordinate import Coordinate, DistanceAlgorithm, Shape from .types import Numeric from enum import Enum from heapq import heappop, heappush @@ -111,6 +111,17 @@ class Grid: def div(self, pos: Coordinate, value: Numeric = 1) -> Numeric: return self.set(pos, self.get(pos) / value) + def add_shape(self, shape: Shape, value: Numeric = 1) -> None: + for x in range(shape.top_left.x, shape.bottom_right.x + 1): + for y in range(shape.top_left.y, shape.bottom_right.y + 1): + if not shape.mode_3d: + pos = Coordinate(x, y) + self.set(pos, self.get(pos) + value) + else: + for z in range(shape.top_left.z, shape.bottom_right.z + 1): + pos = Coordinate(x, y, z) + self.set(pos, self.get(pos) + value) + def get(self, pos: Coordinate) -> Any: return self.__grid.get(pos, self.__default) @@ -158,6 +169,9 @@ class Grid: else: return list(self.__grid.keys()) + def values(self): + return self.__grid.values() + def getSum(self, includeNegative: bool = True) -> Numeric: if not self.mode3D: return sum( From 1f633f312519f1ec8eef113bad43722d2245da93 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 14 Aug 2022 11:51:02 +0200 Subject: [PATCH 108/144] min and max in the same function --- tools/tools.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tools/tools.py b/tools/tools.py index 0266884..e235655 100644 --- a/tools/tools.py +++ b/tools/tools.py @@ -27,6 +27,24 @@ def compare(a: Any, b: Any) -> int: return bool(a > b) - bool(a < b) +def minmax(*arr: tuple) -> (Any, Any): + """return the min and max value of an array (or arbitrary amount of arguments)""" + if len(arr) == 1: + if isinstance(arr[0], list): + arr = arr[0] + else: + return arr[0], arr[0] + + arr = set(arr) + smallest = min(arr) + biggest = max(arr) + if smallest == biggest: + arr.remove(smallest) + biggest = max(arr) + + return smallest, biggest + + def human_readable_time_from_delta(delta: datetime.timedelta) -> str: time_str = "" if delta.days > 0: From 0ddf7596e0a791b7816695507ace01c2cbe353d7 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 14 Aug 2022 11:53:45 +0200 Subject: [PATCH 109/144] better annotation --- tools/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/tools.py b/tools/tools.py index e235655..824c1a1 100644 --- a/tools/tools.py +++ b/tools/tools.py @@ -3,7 +3,7 @@ import inspect import os.path import sys from functools import wraps -from typing import Any +from typing import Any, Union def get_script_dir(follow_symlinks: bool = True) -> str: @@ -27,7 +27,7 @@ def compare(a: Any, b: Any) -> int: return bool(a > b) - bool(a < b) -def minmax(*arr: tuple) -> (Any, Any): +def minmax(*arr: Any) -> (Any, Any): """return the min and max value of an array (or arbitrary amount of arguments)""" if len(arr) == 1: if isinstance(arr[0], list): From c5428a4ea5e4d3d801049601d605fe0af9e67a2c Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 14 Aug 2022 12:32:37 +0200 Subject: [PATCH 110/144] keep a more accurate track of boundaries --- tools/grid.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index 91be2bd..7a5863b 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -34,22 +34,29 @@ class Grid: def __init__(self, default=False): self.__default = default self.__grid = {} - self.minX = 0 - self.minY = 0 - self.maxX = 0 - self.maxY = 0 - self.minZ = 0 - self.maxZ = 0 + self.minX = None + self.minY = None + self.maxX = None + self.maxY = None + self.minZ = None + self.maxZ = None self.mode3D = False def __trackBoundaries(self, pos: Coordinate): - self.minX = pos.x if pos.x < self.minX else self.minX - self.minY = pos.y if pos.y < self.minY else self.minY - self.maxX = pos.x if pos.x > self.maxX else self.maxX - self.maxY = pos.y if pos.y > self.maxY else self.maxY + if self.minX is None: + self.minX, self.maxX, self.minY, self.maxY = pos.x, pos.x, pos.y, pos.y + else: + self.minX = pos.x if pos.x < self.minX else self.minX + self.minY = pos.y if pos.y < self.minY else self.minY + self.maxX = pos.x if pos.x > self.maxX else self.maxX + self.maxY = pos.y if pos.y > self.maxY else self.maxY + if self.mode3D: - self.minZ = pos.z if pos.z < self.minZ else self.minZ - self.maxZ = pos.z if pos.z > self.maxZ else self.maxZ + if self.minZ is None: + self.minZ = self.maxZ = pos.z + else: + self.minZ = pos.z if pos.z < self.minZ else self.minZ + self.maxZ = pos.z if pos.z > self.maxZ else self.maxZ def rangeX(self, pad: int = 0, reverse=False): if reverse: From 987a5bab288801611d477e28e9cb18a57d67164f Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 14 Aug 2022 13:25:33 +0200 Subject: [PATCH 111/144] better dealing with boundaries getting circles (mind the FIXME) --- tools/coordinate.py | 32 ++++++++++++++++++++++++++++++++ tools/grid.py | 7 +++++++ 2 files changed, 39 insertions(+) diff --git a/tools/coordinate.py b/tools/coordinate.py index de3edc3..7fce03c 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -59,6 +59,38 @@ class Coordinate: o_dist = max(dist) - min(dist) return 1.7 * d_steps + o_dist + 1.4 * min(dist) + def inBoundaries(self, minX: int, minY: int, maxX: int, maxY: int, minZ: int = -inf, maxZ: int = inf) -> bool: + if self.z is None: + return minX <= self.x <= maxX and minY <= self.y <= maxY + else: + return minX <= self.x <= maxX and minY <= self.y <= maxY and minZ <= self.z <= maxZ + + def getCircle(self, radius: int = 1, includeDiagonal: bool = True, minX: int = -inf, minY: int = -inf, + maxX: int = inf, maxY: int = inf, minZ: int = -inf, maxZ: int = inf) -> list[Coordinate]: + # FIXME: includeDiagonal == True returns way too few coordinates + ret = [] + if self.z is None: # mode 2D + for x in range(self.x - radius * 2, self.x + radius * 2 + 1): + for y in range(self.y - radius * 2, self.y + radius * 2 + 1): + target = Coordinate(x, y) + if not target.inBoundaries(minX, minY, maxX, maxY): + continue + dist = self.getDistanceTo(target, DistanceAlgorithm.MANHATTAN, includeDiagonals=includeDiagonal) + if dist == radius: + ret.append(target) + else: + for x in range(self.x - radius * 2, self.x + radius * 2 + 1): + for y in range(self.y - radius * 2, self.y + radius * 2 + 1): + for z in range(self.z - radius * 2, self.z + radius * 2 + 1): + target = Coordinate(x, y) + if not target.inBoundaries(minX, minY, maxX, maxY, minZ, maxZ): + continue + dist = self.getDistanceTo(target, DistanceAlgorithm.MANHATTAN, includeDiagonals=includeDiagonal) + if dist == radius: + ret.append(target) + + return ret + def getNeighbours(self, includeDiagonal: bool = True, minX: int = -inf, minY: int = -inf, maxX: int = inf, maxY: int = inf, minZ: int = -inf, maxZ: int = inf) -> list[Coordinate]: """ diff --git a/tools/grid.py b/tools/grid.py index 7a5863b..825d7b7 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -3,6 +3,7 @@ from .coordinate import Coordinate, DistanceAlgorithm, Shape from .types import Numeric from enum import Enum from heapq import heappop, heappush +from math import inf from typing import Any, Dict, List, Union OFF = False @@ -58,6 +59,12 @@ class Grid: self.minZ = pos.z if pos.z < self.minZ else self.minZ self.maxZ = pos.z if pos.z > self.maxZ else self.maxZ + def getBoundaries(self) -> (int, int, int, int, int, int): + if self.mode3D: + return self.minX, self.minY, self.maxX, self.maxY, self.minZ, self.maxZ + else: + return self.minX, self.minY, self.maxX, self.maxY, -inf, inf + def rangeX(self, pad: int = 0, reverse=False): if reverse: return range(self.maxX + pad, self.minX - pad - 1, -1) From 3c5e27cf36d745ad23be62838fefe9df05907a1a Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 14 Aug 2022 13:36:55 +0200 Subject: [PATCH 112/144] waaaay better circles --- tools/coordinate.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tools/coordinate.py b/tools/coordinate.py index 7fce03c..3a8a5cb 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -1,9 +1,11 @@ from __future__ import annotations from dataclasses import dataclass from enum import Enum -from math import gcd, sqrt, inf, atan2, degrees +from math import gcd, sqrt, inf, atan2, degrees, floor from typing import Union, List, Optional +from tools.math import round_half_up + class DistanceAlgorithm(Enum): MANHATTAN = 0 @@ -65,8 +67,9 @@ class Coordinate: else: return minX <= self.x <= maxX and minY <= self.y <= maxY and minZ <= self.z <= maxZ - def getCircle(self, radius: int = 1, includeDiagonal: bool = True, minX: int = -inf, minY: int = -inf, - maxX: int = inf, maxY: int = inf, minZ: int = -inf, maxZ: int = inf) -> list[Coordinate]: + def getCircle(self, radius: int = 1, algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN, + minX: int = -inf, minY: int = -inf, maxX: int = inf, maxY: int = inf, + minZ: int = -inf, maxZ: int = inf) -> list[Coordinate]: # FIXME: includeDiagonal == True returns way too few coordinates ret = [] if self.z is None: # mode 2D @@ -75,9 +78,10 @@ class Coordinate: target = Coordinate(x, y) if not target.inBoundaries(minX, minY, maxX, maxY): continue - dist = self.getDistanceTo(target, DistanceAlgorithm.MANHATTAN, includeDiagonals=includeDiagonal) + dist = round_half_up(self.getDistanceTo(target, algorithm=algorithm, includeDiagonals=False)) if dist == radius: ret.append(target) + else: for x in range(self.x - radius * 2, self.x + radius * 2 + 1): for y in range(self.y - radius * 2, self.y + radius * 2 + 1): From 8e0b28159f87e6ec1ad8012bb4bbf4fd7fb94e3d Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 14 Aug 2022 13:38:41 +0200 Subject: [PATCH 113/144] waaaay better circles - also for 3d coords --- tools/coordinate.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tools/coordinate.py b/tools/coordinate.py index 3a8a5cb..619eb6a 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -1,11 +1,10 @@ from __future__ import annotations from dataclasses import dataclass from enum import Enum -from math import gcd, sqrt, inf, atan2, degrees, floor +from math import gcd, sqrt, inf, atan2, degrees +from .math import round_half_up from typing import Union, List, Optional -from tools.math import round_half_up - class DistanceAlgorithm(Enum): MANHATTAN = 0 @@ -70,7 +69,6 @@ class Coordinate: def getCircle(self, radius: int = 1, algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN, minX: int = -inf, minY: int = -inf, maxX: int = inf, maxY: int = inf, minZ: int = -inf, maxZ: int = inf) -> list[Coordinate]: - # FIXME: includeDiagonal == True returns way too few coordinates ret = [] if self.z is None: # mode 2D for x in range(self.x - radius * 2, self.x + radius * 2 + 1): @@ -89,7 +87,7 @@ class Coordinate: target = Coordinate(x, y) if not target.inBoundaries(minX, minY, maxX, maxY, minZ, maxZ): continue - dist = self.getDistanceTo(target, DistanceAlgorithm.MANHATTAN, includeDiagonals=includeDiagonal) + dist = round_half_up(self.getDistanceTo(target, algorithm=algorithm, includeDiagonals=False)) if dist == radius: ret.append(target) From 199d6c190882e2e279539a0649d7be104cb2e15a Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 19 Nov 2022 12:57:40 +0100 Subject: [PATCH 114/144] more precise stopwatch --- tools/stopwatch.py | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/tools/stopwatch.py b/tools/stopwatch.py index 1c520a0..ce2fcea 100644 --- a/tools/stopwatch.py +++ b/tools/stopwatch.py @@ -1,43 +1,51 @@ -from time import time -from .types import FloatOrNone +from time import perf_counter_ns +from .types import IntOrNone + + +def ns_to_string(ns: int) -> str: + units = ['ns', 'µs', 'ms', 's'] + unit = 0 + while ns > 1_000: + ns /= 1_000 + unit += 1 + if unit == len(units) - 1: + break + + return f"{ns:1.2f}{units[unit]}" class StopWatch: - started: FloatOrNone = None - stopped: FloatOrNone = None + started: IntOrNone = None + stopped: IntOrNone = None def __init__(self, auto_start=True): if auto_start: self.start() def start(self): - self.started = time() + self.started = perf_counter_ns() self.stopped = None - def stop(self) -> float: - self.stopped = time() + def stop(self) -> int: + self.stopped = perf_counter_ns() return self.elapsed() reset = start - def elapsed(self) -> float: + def elapsed(self) -> int: if self.stopped is None: - return time() - self.started + return perf_counter_ns() - self.started else: return self.stopped - self.started - def avg_elapsed(self, divider: int) -> float: - return self.elapsed() / divider + def avg_elapsed(self, divider: int) -> int: + return self.elapsed() // divider # in ns precision loosing some rounding error probably will not hurt + + def elapsed_string(self): + return ns_to_string(self.elapsed()) def avg_string(self, divider: int) -> str: - units = ['s', 'ms', 'µs', 'ns'] - unit = 0 - elapsed = self.avg_elapsed(divider) - while 0 < elapsed < 1: - elapsed *= 1000 - unit += 1 - - return "%1.2f%s" % (elapsed, units[unit]) + return ns_to_string(self.avg_elapsed(divider)) def __str__(self): return self.avg_string(1) From 4f45d1f32d372bf2af9ab9d11285ce14a20bfb33 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 28 Nov 2022 08:18:43 +0100 Subject: [PATCH 115/144] start on hexgrids --- tools/grid.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tools/grid.py b/tools/grid.py index 825d7b7..3e5fbec 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -326,3 +326,33 @@ class Grid: print(spacer, end="") print() + + +class HexGrid(Grid): + """ + https://www.redblobgames.com/grids/hexagons/#coordinates-cube + Treat as 3d Grid + +y -x +z + y x z + yxz + z x y + -z +x -y + """ + def __init__(self, default=False): + super().__init__(default=default) + + def getNeighboursOf(self, pos: Coordinate, includeDefault: bool = False, includeDiagonal: bool = None) \ + -> List[Coordinate]: + """ + includeDiagonal is just here because of signature reasons, makes no difference in a hex grid + """ + vectors = [ + Coordinate(-1, 1, 0), # nw + Coordinate(-1, 0, 1), # ne + Coordinate(0, -1, 1), # e + Coordinate(1, -1, 0), # se + Coordinate(1, 0, -1), # sw + Coordinate(0, 1, -1), # w + ] + + return [pos + v for v in vectors if includeDefault or self.get(pos + v) != self.__default] From d92686dd28fff52d24af1d903f392452da7a0029 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 29 Nov 2022 19:36:55 +0100 Subject: [PATCH 116/144] better stopwatch --- tools/stopwatch.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/tools/stopwatch.py b/tools/stopwatch.py index ce2fcea..eb059bc 100644 --- a/tools/stopwatch.py +++ b/tools/stopwatch.py @@ -2,16 +2,23 @@ from time import perf_counter_ns from .types import IntOrNone -def ns_to_string(ns: int) -> str: +def get_time_string_from_ns(ns: int) -> str: + # mis, ns = ns // 1000, ns % 1000 + # ms, mis = mis // 1000, mis % 1000 + # s, ms = ms // 1000, ms % 1000 + # m, s = s // 60, s % 60 + # h, m = m // 60, m % 60 + # d, h = h // 24, h % 24 + units = ['ns', 'µs', 'ms', 's'] unit = 0 while ns > 1_000: - ns /= 1_000 - unit += 1 - if unit == len(units) - 1: + if unit > 3: break + ns /= 1000 + unit += 1 - return f"{ns:1.2f}{units[unit]}" + return "%1.2f%s" % (ns, units[unit]) class StopWatch: @@ -26,26 +33,26 @@ class StopWatch: self.started = perf_counter_ns() self.stopped = None - def stop(self) -> int: + def stop(self) -> float: self.stopped = perf_counter_ns() return self.elapsed() reset = start - def elapsed(self) -> int: + def elapsed(self) -> float: if self.stopped is None: return perf_counter_ns() - self.started else: return self.stopped - self.started - def avg_elapsed(self, divider: int) -> int: - return self.elapsed() // divider # in ns precision loosing some rounding error probably will not hurt + def elapsed_string(self) -> str: + return get_time_string_from_ns(self.elapsed()) - def elapsed_string(self): - return ns_to_string(self.elapsed()) + def avg_elapsed(self, divider: int) -> float: + return self.elapsed() / divider def avg_string(self, divider: int) -> str: - return ns_to_string(self.avg_elapsed(divider)) + return get_time_string_from_ns(int(self.avg_elapsed(divider))) def __str__(self): return self.avg_string(1) From c16bc0d1cf01f90189761c60bd56b26038075777 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 29 Nov 2022 20:37:46 +0100 Subject: [PATCH 117/144] *real* start to hex coordinates --- tools/coordinate.py | 81 ++++++++++++++++++++++++++++++++++++++++----- tools/grid.py | 30 ----------------- 2 files changed, 72 insertions(+), 39 deletions(-) diff --git a/tools/coordinate.py b/tools/coordinate.py index 619eb6a..ebc6fc4 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -13,7 +13,6 @@ class DistanceAlgorithm(Enum): CHEBYSHEV = 2 CHESSBOARD = 2 - @dataclass(frozen=True) class Coordinate: x: int @@ -154,25 +153,25 @@ class Coordinate: steps = gcd(diff.x, diff.y) step_x = diff.x // steps step_y = diff.y // steps - return [Coordinate(self.x + step_x * i, self.y + step_y * i) for i in range(steps + 1)] + return [self.__class__(self.x + step_x * i, self.y + step_y * i) for i in range(steps + 1)] else: steps = gcd(diff.x, diff.y, diff.z) step_x = diff.x // steps step_y = diff.y // steps step_z = diff.z // steps - return [Coordinate(self.x + step_x * i, self.y + step_y * i, self.z + step_z * i) for i in range(steps + 1)] + return [self.__class__(self.x + step_x * i, self.y + step_y * i, self.z + step_z * i) for i in range(steps + 1)] def __add__(self, other: Coordinate) -> Coordinate: if self.z is None: - return Coordinate(self.x + other.x, self.y + other.y) + return self.__class__(self.x + other.x, self.y + other.y) else: - return Coordinate(self.x + other.x, self.y + other.y, self.z + other.z) + return self.__class__(self.x + other.x, self.y + other.y, self.z + other.z) def __sub__(self, other: Coordinate) -> Coordinate: if self.z is None: - return Coordinate(self.x - other.x, self.y - other.y) + return self.__class__(self.x - other.x, self.y - other.y) else: - return Coordinate(self.x - other.x, self.y - other.y, self.z - other.z) + return self.__class__(self.x - other.x, self.y - other.y, self.z - other.z) def __eq__(self, other): return self.x == other.x and self.y == other.y and self.z == other.z @@ -217,16 +216,80 @@ class Coordinate: def generate(from_x: int, to_x: int, from_y: int, to_y: int, from_z: int = None, to_z: int = None) -> List[Coordinate]: if from_z is None or to_z is None: - return [Coordinate(x, y) for x in range(from_x, to_x + 1) for y in range(from_y, to_y + 1)] + return [self.__class__(x, y) for x in range(from_x, to_x + 1) for y in range(from_y, to_y + 1)] else: return [ - Coordinate(x, y, z) + self.__class__(x, y, z) for x in range(from_x, to_x + 1) for y in range(from_y, to_y + 1) for z in range(from_z, to_z + 1) ] +class HexCoordinate(Coordinate): + """ + https://www.redblobgames.com/grids/hexagons/#coordinates-cube + Treat as 3d Coordinate + +y -x +z + y x z + yxz + z x y + -z +x -y + """ + neighbour_vectors = { + 'ne': Coordinate(-1, 0, 1), + 'nw': Coordinate(-1, 1, 0), + 'e': Coordinate(0, -1, 1), + 'w': Coordinate(0, 1, -1), + 'sw': Coordinate(1, 0, -1), + 'se': Coordinate(1, -1, 0), + } + + def __init__(self, x: int, y: int, z: int): + assert (x + y + z) == 0 + super().__init__(x, y, z) + + def get_length(self) -> int: + return (abs(self.x) + abs(self.y) + abs(self.z)) // 2 + + def getDistanceTo(self, target: Coordinate, algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN, + includeDiagonals: bool = True) -> Union[int, float]: + # includeDiagonals makes no sense in a hex grid, it's just here for signature reasons + if algorithm == DistanceAlgorithm.MANHATTAN: + return (self - target).get_length() + + def getNeighbours(self, includeDiagonal: bool = True, minX: int = -inf, minY: int = -inf, + maxX: int = inf, maxY: int = inf, minZ: int = -inf, maxZ: int = inf) -> list[Coordinate]: + # includeDiagonals makes no sense in a hex grid, it's just here for signature reasons + return [ + self + x for x in self.neighbour_vectors.values() + if minX <= (self + x).x <= maxX and minY <= (self + x).y <= maxY and minZ <= (self + x).z <= maxZ + ] + +HexCoordinateR = HexCoordinate +class HexCoordinateF(HexCoordinate): + """ + https://www.redblobgames.com/grids/hexagons/#coordinates-cube + Treat as 3d Coordinate + +y -x + y x + -z z yxz z +z + x y + +x -y + """ + neighbour_vectors = { + 'ne': Coordinate(-1, 0, 1), + 'nw': Coordinate(0, 1, -1), + 'n': Coordinate(-1, 1, 0), + 's': Coordinate(1, -1, 0), + 'sw': Coordinate(1, 0, -1), + 'se': Coordinate(0, -1, 1), + } + + def __init__(self, x: int, y: int, z: int): + super().__init__(x, y, z) + + class Shape: def __init__(self, top_left: Coordinate, bottom_right: Coordinate): """ diff --git a/tools/grid.py b/tools/grid.py index 3e5fbec..825d7b7 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -326,33 +326,3 @@ class Grid: print(spacer, end="") print() - - -class HexGrid(Grid): - """ - https://www.redblobgames.com/grids/hexagons/#coordinates-cube - Treat as 3d Grid - +y -x +z - y x z - yxz - z x y - -z +x -y - """ - def __init__(self, default=False): - super().__init__(default=default) - - def getNeighboursOf(self, pos: Coordinate, includeDefault: bool = False, includeDiagonal: bool = None) \ - -> List[Coordinate]: - """ - includeDiagonal is just here because of signature reasons, makes no difference in a hex grid - """ - vectors = [ - Coordinate(-1, 1, 0), # nw - Coordinate(-1, 0, 1), # ne - Coordinate(0, -1, 1), # e - Coordinate(1, -1, 0), # se - Coordinate(1, 0, -1), # sw - Coordinate(0, 1, -1), # w - ] - - return [pos + v for v in vectors if includeDefault or self.get(pos + v) != self.__default] From b6360d18ed54d2edecc5349fab080b42496a19b0 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 30 Nov 2022 11:20:48 +0100 Subject: [PATCH 118/144] grid.Grid(): getActiveRegion(); returns connected !default regions --- tools/grid.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tools/grid.py b/tools/grid.py index 825d7b7..049e549 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -183,6 +183,19 @@ class Grid: else: return list(self.__grid.keys()) + def getActiveRegion(self, start: Coordinate, includeDiagonal: bool = False, ignore: List[Coordinate] = None) \ + -> List[Coordinate]: + if not self.get(start): + return [] + if ignore is None: + ignore = [] + ignore.append(start) + for c in self.getNeighboursOf(start, includeDiagonal=includeDiagonal): + if c not in ignore: + ignore = self.getActiveRegion(c, includeDiagonal, ignore) + + return ignore + def values(self): return self.__grid.values() From b497ee4bc5f9708f1a7b37cc86bf5150d028fb3a Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 3 Dec 2022 06:23:12 +0100 Subject: [PATCH 119/144] actually never implemented a simple list intersection? --- tools/tools.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tools/tools.py b/tools/tools.py index 824c1a1..f70eeaa 100644 --- a/tools/tools.py +++ b/tools/tools.py @@ -3,7 +3,7 @@ import inspect import os.path import sys from functools import wraps -from typing import Any, Union +from typing import Any, Union, List def get_script_dir(follow_symlinks: bool = True) -> str: @@ -76,3 +76,11 @@ def cache(func): return result return newfunc + + +def list_intersection(*args) -> list: + ret = set(args[0]) + for l in args[1:]: + ret = ret.intersection(l) + + return list(ret) \ No newline at end of file From 87ead886105b7066ec32836a5ddc84fdb49ac367 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 3 Dec 2022 08:05:45 +0100 Subject: [PATCH 120/144] set()'s .intersection for list and str --- requirements.txt | 7 ++++--- tools/tools.py | 17 +++++++++++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index a558087..02300b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -requests~=2.26.0 bs4~=0.0.1 -beautifulsoup4~=4.10.0 -setuptools~=56.0.0 +beautifulsoup4==4.11.1 +fishhook~=0.2.5 +requests==2.28.1 +setuptools==65.6.3 diff --git a/tools/tools.py b/tools/tools.py index f70eeaa..94463e7 100644 --- a/tools/tools.py +++ b/tools/tools.py @@ -2,8 +2,9 @@ import datetime import inspect import os.path import sys +from fishhook import hook from functools import wraps -from typing import Any, Union, List +from typing import Any def get_script_dir(follow_symlinks: bool = True) -> str: @@ -78,9 +79,13 @@ def cache(func): return newfunc -def list_intersection(*args) -> list: - ret = set(args[0]) - for l in args[1:]: - ret = ret.intersection(l) +@hook(list) +def intersection(self, *args) -> list: + ret = set(self).intersection(*args) + return list(ret) - return list(ret) \ No newline at end of file + +@hook(str) +def intersection(self, *args) -> str: + ret = set(self).intersection(*args) + return "".join(list(ret)) \ No newline at end of file From 33c645ac042569964ce5e21415b960bb8b04f991 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 3 Dec 2022 08:09:51 +0100 Subject: [PATCH 121/144] set()'s __and__ for list and str --- tools/tools.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/tools.py b/tools/tools.py index 94463e7..21e5c0f 100644 --- a/tools/tools.py +++ b/tools/tools.py @@ -85,7 +85,17 @@ def intersection(self, *args) -> list: return list(ret) +@hook(list) +def __and__(self, *args) -> list: + return self.intersection(*args) + + @hook(str) def intersection(self, *args) -> str: ret = set(self).intersection(*args) - return "".join(list(ret)) \ No newline at end of file + return "".join(list(ret)) + + +@hook(str) +def __and__(self, *args) -> str: + return self.intersection(*args) \ No newline at end of file From a2aa81388c8e22192ba047ddb2637e0f8c632234 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 3 Dec 2022 10:06:31 +0100 Subject: [PATCH 122/144] better human readable time from ns --- tools/stopwatch.py | 26 ++++---------------------- tools/tools.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/tools/stopwatch.py b/tools/stopwatch.py index eb059bc..2d0ed6e 100644 --- a/tools/stopwatch.py +++ b/tools/stopwatch.py @@ -1,26 +1,8 @@ from time import perf_counter_ns +from .tools import human_readable_time_from_ns from .types import IntOrNone -def get_time_string_from_ns(ns: int) -> str: - # mis, ns = ns // 1000, ns % 1000 - # ms, mis = mis // 1000, mis % 1000 - # s, ms = ms // 1000, ms % 1000 - # m, s = s // 60, s % 60 - # h, m = m // 60, m % 60 - # d, h = h // 24, h % 24 - - units = ['ns', 'µs', 'ms', 's'] - unit = 0 - while ns > 1_000: - if unit > 3: - break - ns /= 1000 - unit += 1 - - return "%1.2f%s" % (ns, units[unit]) - - class StopWatch: started: IntOrNone = None stopped: IntOrNone = None @@ -39,20 +21,20 @@ class StopWatch: reset = start - def elapsed(self) -> float: + def elapsed(self) -> int: if self.stopped is None: return perf_counter_ns() - self.started else: return self.stopped - self.started def elapsed_string(self) -> str: - return get_time_string_from_ns(self.elapsed()) + return human_readable_time_from_ns(self.elapsed()) def avg_elapsed(self, divider: int) -> float: return self.elapsed() / divider def avg_string(self, divider: int) -> str: - return get_time_string_from_ns(int(self.avg_elapsed(divider))) + return human_readable_time_from_ns(int(self.avg_elapsed(divider))) def __str__(self): return self.avg_string(1) diff --git a/tools/tools.py b/tools/tools.py index 21e5c0f..eec7dcf 100644 --- a/tools/tools.py +++ b/tools/tools.py @@ -64,6 +64,25 @@ def human_readable_time_from_delta(delta: datetime.timedelta) -> str: return time_str + "%02d" % (delta.seconds % 60) +def human_readable_time_from_ns(ns: int) -> str: + units = [ + (1000, 'ns'), + (1000, 'µs'), + (1000, 'ms'), + (60, 's'), + (60, 'm'), + (60, 'h'), + (24, 'd'), + ] + + time_parts = [] + for div, unit in units: + ns, p = ns // div, ns % div + time_parts.insert(0, "%d%s" % (p, unit)) + if ns == 0: + return ", ".join(time_parts) + + def cache(func): saved = {} From 353ff51411fbfa9a044c69839d5f84fb1bd820b0 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 3 Dec 2022 11:10:03 +0100 Subject: [PATCH 123/144] make AOCDay.getInput() return type casted input --- tools/aoc.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tools/aoc.py b/tools/aoc.py index 08f8449..c54aeac 100644 --- a/tools/aoc.py +++ b/tools/aoc.py @@ -168,17 +168,17 @@ class AOCDay: answer_cache.save() - def getInput(self) -> Union[str, List]: + def getInput(self, return_type: Type = None) -> Any: if len(self.input) == 1: - return self.input[0] + if return_type: + return return_type(self.input[0]) + else: + return self.input[0] else: - return self.input.copy() - - def getInputListAsType(self, return_type: Type) -> List: - """ - get input as list casted to return_type, each line representing one list entry - """ - return [return_type(i) for i in self.input] + if return_type: + return [return_type(i) for i in self.input] + else: + return self.input.copy() def getMultiLineInputAsArray(self, return_type: Type = None, join_char: str = None) -> List: """ From ab05a1a7706d358d38a71493af0781ae18278a31 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 4 Dec 2022 10:57:28 +0100 Subject: [PATCH 124/144] fix transformations --- tools/aoc.py | 5 ++++- tools/grid.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/tools/aoc.py b/tools/aoc.py index c54aeac..4a35224 100644 --- a/tools/aoc.py +++ b/tools/aoc.py @@ -26,6 +26,8 @@ class AOCDay: self.day = day self.year = year self.part_func = [self.part1, self.part2] + self._current_test_file = None + self._current_test_solution = None def part1(self) -> Any: raise NotImplementedError() @@ -36,6 +38,7 @@ class AOCDay: def run_part(self, part: int, verbose: bool = False, measure_runtime: bool = False, timeit_number: int = 50): case_count = 0 for solution, input_file in self.inputs[part]: + self._current_test_solution, self._current_test_file = solution, input_file exec_time = None answer = None self._load_input(input_file) @@ -51,7 +54,7 @@ class AOCDay: if solution is None: print_solution(self.day, part + 1, answer, solution, case_count, exec_time) - if answer not in {u"", b"", None, b"None", u"None"}: + if answer not in {u"", b"", None, b"None", u"None", 0, '0'}: self._submit(part + 1, answer) else: if verbose or answer != solution: diff --git a/tools/grid.py b/tools/grid.py index 049e549..6c5b53a 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -246,25 +246,31 @@ class Grid: raise ValueError("Operation not possible in 2D space", mode) coords = self.__grid - self.__grid, self.minX, self.maxX, self.minY, self.maxY, self.minZ, self.maxZ = {}, 0, 0, 0, 0, 0, 0 + self.__grid = {} if mode == GridTransformation.ROTATE_X: + self.minY, self.maxY, self.minZ, self.maxZ = self.minZ, self.maxZ, -self.maxY, -self.minY for c, v in coords.items(): self.set(Coordinate(c.x, -c.z, c.y), v) elif mode == GridTransformation.ROTATE_Y: + self.minX, self.maxX, self.minZ, self.maxZ = -self.maxZ, -self.minZ, self.minX, self.maxX for c, v in coords.items(): self.set(Coordinate(-c.z, c.y, c.x), v) elif mode == GridTransformation.ROTATE_Z: + self.minX, self.maxX, self.minY, self.maxY = -self.maxY, -self.minY, self.minX, self.minY for c, v in coords.items(): - self.set(Coordinate(c.y, -c.x, c.z), v) + self.set(Coordinate(-c.y, c.x, c.z), v) elif mode == GridTransformation.COUNTER_ROTATE_X: + self.minY, self.maxY, self.minZ, self.maxZ = -self.maxZ, -self.minZ, self.minY, self.maxY for c, v in coords.items(): self.set(Coordinate(c.x, c.z, -c.y), v) elif mode == GridTransformation.COUNTER_ROTATE_Y: + self.minX, self.maxX, self.minZ, self.maxZ = self.minZ, self.maxZ, -self.minX, -self.maxX for c, v in coords.items(): self.set(Coordinate(c.z, c.y, -c.x), v) elif mode == GridTransformation.COUNTER_ROTATE_Z: + self.minX, self.maxX, self.minY, self.maxY = self.minY, self.maxY, -self.minX, -self.maxX for c, v in coords.items(): - self.set(Coordinate(-c.y, c.x, c.z), v) + self.set(Coordinate(c.y, -c.x, c.z), v) elif mode == GridTransformation.FLIP_X: for c, v in coords.items(): self.set(Coordinate(-c.x, c.y, c.z), v) @@ -329,6 +335,27 @@ class Grid: return pathCoords + def sub_grid(self, from_x: int, from_y: int, to_x: int, to_y: int) -> 'Grid': + count_x, count_y = 0, 0 + new_grid = Grid() + for x in range(from_x, to_x + 1): + for y in range(from_y, to_y + 1): + new_grid.set(Coordinate(count_x, count_y), self.get(Coordinate(x, y))) + count_y += 1 + count_y = 0 + count_x += 1 + + return new_grid + + def update(self, x: int, y: int, grid: Grid) -> None: + put_x, put_y = x, y + for get_x in grid.rangeX(): + for get_y in grid.rangeY(): + self.set(Coordinate(put_x, put_y), grid.get(Coordinate(get_x, get_y))) + put_y += 1 + put_y = y + put_x += 1 + def print(self, spacer: str = "", true_char: str = None, false_char: str = " "): for y in range(self.minY, self.maxY + 1): for x in range(self.minX, self.maxX + 1): @@ -339,3 +366,21 @@ class Grid: print(spacer, end="") print() + + def __str__(self, true_char: str = '#', false_char: str = "."): + return "/".join( + "".join( + true_char if self.get(Coordinate(x, y)) else false_char + for x in range(self.minX, self.maxX + 1) + ) + for y in range(self.minY, self.maxY + 1) + ) + + @classmethod + def from_str(cls, grid_string: str, true_char: str = '#') -> 'Grid': + ret = cls() + for y, line in enumerate(grid_string.split("/")): + for x, c in enumerate(line): + ret.set(Coordinate(x, y), c == true_char) + + return ret From afcafbba0ae6dfd4d48b523c6c97ee6473b4f59b Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 4 Dec 2022 10:58:17 +0100 Subject: [PATCH 125/144] set default in from_string --- tools/grid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index 6c5b53a..50190da 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -377,8 +377,8 @@ class Grid: ) @classmethod - def from_str(cls, grid_string: str, true_char: str = '#') -> 'Grid': - ret = cls() + def from_str(cls, grid_string: str, true_char: str = '#', default: Any = False) -> 'Grid': + ret = cls(default=default) for y, line in enumerate(grid_string.split("/")): for x, c in enumerate(line): ret.set(Coordinate(x, y), c == true_char) From 011abd7fb50cdfd9c2ea34dbc811d3390e80b5c1 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 4 Dec 2022 11:11:27 +0100 Subject: [PATCH 126/144] set default in from_string and allow "true" to become any value --- tools/grid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index 50190da..c9e3125 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -377,10 +377,10 @@ class Grid: ) @classmethod - def from_str(cls, grid_string: str, true_char: str = '#', default: Any = False) -> 'Grid': + def from_str(cls, grid_string: str, true_char: str = '#', true_value: Any = True, default: Any = False) -> 'Grid': ret = cls(default=default) for y, line in enumerate(grid_string.split("/")): for x, c in enumerate(line): - ret.set(Coordinate(x, y), c == true_char) + ret.set(Coordinate(x, y), true_value if c == true_char else default) return ret From 35972bffe2c504c63af7a0302d47513ac05ba921 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 10 Dec 2022 10:29:39 +0100 Subject: [PATCH 127/144] aoc_ocr --- tools/aoc_ocr.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ tools/grid.py | 4 ++++ 2 files changed, 65 insertions(+) create mode 100644 tools/aoc_ocr.py diff --git a/tools/aoc_ocr.py b/tools/aoc_ocr.py new file mode 100644 index 0000000..815e918 --- /dev/null +++ b/tools/aoc_ocr.py @@ -0,0 +1,61 @@ +# Copyright (c) 2020-present Benjamin Soyka +# Original and Licence at https://github.com/bsoyka/advent-of-code-ocr + +from collections.abc import Sequence + +ALPHABET_6 = { + ".##.\n#..#\n#..#\n####\n#..#\n#..#": "A", + "###.\n#..#\n###.\n#..#\n#..#\n###.": "B", + ".##.\n#..#\n#...\n#...\n#..#\n.##.": "C", + "####\n#...\n###.\n#...\n#...\n####": "E", + "####\n#...\n###.\n#...\n#...\n#...": "F", + ".##.\n#..#\n#...\n#.##\n#..#\n.###": "G", + "#..#\n#..#\n####\n#..#\n#..#\n#..#": "H", + ".###\n..#.\n..#.\n..#.\n..#.\n.###": "I", + "..##\n...#\n...#\n...#\n#..#\n.##.": "J", + "#..#\n#.#.\n##..\n#.#.\n#.#.\n#..#": "K", + "#...\n#...\n#...\n#...\n#...\n####": "L", + ".##.\n#..#\n#..#\n#..#\n#..#\n.##.": "O", + "###.\n#..#\n#..#\n###.\n#...\n#...": "P", + "###.\n#..#\n#..#\n###.\n#.#.\n#..#": "R", + ".###\n#...\n#...\n.##.\n...#\n###.": "S", + "#..#\n#..#\n#..#\n#..#\n#..#\n.##.": "U", + "#...\n#...\n.#.#\n..#.\n..#.\n..#.": "Y", + "####\n...#\n..#.\n.#..\n#...\n####": "Z", +} + + +def convert_6(input_text: str, *, fill_pixel: str = "#", empty_pixel: str = ".") -> str: + """Convert height 6 text to characters""" + input_text = input_text.replace(fill_pixel, "#").replace(empty_pixel, ".") + prepared_array = [list(line) for line in input_text.split("\n")] + return _convert_6(prepared_array) + + +def convert_array_6(array: Sequence[Sequence[str | int]], *, fill_pixel: str | int = "#", empty_pixel: str | int = ".") -> str: + """Convert a height 6 NumPy array or nested list to characters""" + prepared_array = [ + [ + "#" if pixel == fill_pixel else "." if pixel == empty_pixel else "" + for pixel in line + ] + for line in array + ] + return _convert_6(prepared_array) + + +def _convert_6(array: list[list[str]]) -> str: + """Convert a prepared height 6 array to characters""" + rows, cols = len(array), len(array[0]) + if any(len(row) != cols for row in array): + raise ValueError("all rows should have the same number of columns") + if rows != 6: + raise ValueError("incorrect number of rows (expected 6)") + + indices = [slice(start, start + 4) for start in range(0, cols, 5)] + result = [ + ALPHABET_6["\n".join("".join(row[index]) for row in array)] + for index in indices + ] + + return "".join(result) diff --git a/tools/grid.py b/tools/grid.py index c9e3125..8e53427 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -1,4 +1,5 @@ from __future__ import annotations +from .aoc_ocr import convert_array_6 from .coordinate import Coordinate, DistanceAlgorithm, Shape from .types import Numeric from enum import Enum @@ -367,6 +368,9 @@ class Grid: print() + def get_aoc_ocr_string(self, x_shift: int = 0, y_shift: int = 0): + return convert_array_6([['#' if self.get(Coordinate(x + x_shift, y + y_shift)) else '.' for x in self.rangeX()] for y in self.rangeY()]) + def __str__(self, true_char: str = '#', false_char: str = "."): return "/".join( "".join( From 5df82d2359e2aa9aa59f1fc5bba573fd045e6936 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 10 Dec 2022 14:50:22 +0100 Subject: [PATCH 128/144] reversable coordinates; grid.print() can now mark important spots --- tools/coordinate.py | 18 ++++++++++++++---- tools/grid.py | 6 ++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/tools/coordinate.py b/tools/coordinate.py index ebc6fc4..8dea4ef 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -13,6 +13,7 @@ class DistanceAlgorithm(Enum): CHEBYSHEV = 2 CHESSBOARD = 2 + @dataclass(frozen=True) class Coordinate: x: int @@ -161,6 +162,12 @@ class Coordinate: step_z = diff.z // steps return [self.__class__(self.x + step_x * i, self.y + step_y * i, self.z + step_z * i) for i in range(steps + 1)] + def reverse(self) -> Coordinate: + if self.z is None: + return self.__class__(-self.x, -self.y) + else: + return self.__class__(-self.x, -self.y, -self.z) + def __add__(self, other: Coordinate) -> Coordinate: if self.z is None: return self.__class__(self.x + other.x, self.y + other.y) @@ -212,14 +219,14 @@ class Coordinate: else: return "%s(x=%d, y=%d, z=%d)" % (self.__class__.__name__, self.x, self.y, self.z) - @staticmethod - def generate(from_x: int, to_x: int, from_y: int, to_y: int, + @classmethod + def generate(cls, from_x: int, to_x: int, from_y: int, to_y: int, from_z: int = None, to_z: int = None) -> List[Coordinate]: if from_z is None or to_z is None: - return [self.__class__(x, y) for x in range(from_x, to_x + 1) for y in range(from_y, to_y + 1)] + return [cls(x, y) for x in range(from_x, to_x + 1) for y in range(from_y, to_y + 1)] else: return [ - self.__class__(x, y, z) + cls(x, y, z) for x in range(from_x, to_x + 1) for y in range(from_y, to_y + 1) for z in range(from_z, to_z + 1) @@ -266,7 +273,10 @@ class HexCoordinate(Coordinate): if minX <= (self + x).x <= maxX and minY <= (self + x).y <= maxY and minZ <= (self + x).z <= maxZ ] + HexCoordinateR = HexCoordinate + + class HexCoordinateF(HexCoordinate): """ https://www.redblobgames.com/grids/hexagons/#coordinates-cube diff --git a/tools/grid.py b/tools/grid.py index 8e53427..0404366 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -357,10 +357,12 @@ class Grid: put_y = y put_x += 1 - def print(self, spacer: str = "", true_char: str = None, false_char: str = " "): + def print(self, spacer: str = "", true_char: str = None, false_char: str = " ", mark: list = None): for y in range(self.minY, self.maxY + 1): for x in range(self.minX, self.maxX + 1): - if true_char: + if mark and Coordinate(x, y) in mark: + print("X", end="") + elif true_char: print(true_char if self.get(Coordinate(x, y)) else false_char, end="") else: print(self.get(Coordinate(x, y)), end="") From 00de38a27791515e10b94088bcc922aacd3e499f Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 12 Dec 2022 08:11:29 +0100 Subject: [PATCH 129/144] Coordinate.getNeighbours() and Grid.getNeighboursOf() can be generators; no need to create extra lists every time --- tools/coordinate.py | 16 ++++++---------- tools/grid.py | 7 +++---- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/tools/coordinate.py b/tools/coordinate.py index 8dea4ef..a7c63e7 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -113,11 +113,9 @@ class Coordinate: else: nb_list = [(-1, 0), (1, 0), (0, -1), (0, 1)] - return [ - Coordinate(self.x + dx, self.y + dy) - for dx, dy in nb_list - if minX <= self.x + dx <= maxX and minY <= self.y + dy <= maxY - ] + for dx, dy in nb_list: + if minX <= self.x + dx <= maxX and minY <= self.y + dy <= maxY: + yield self.__class__(self.x + dx, self.y + dy) else: if includeDiagonal: nb_list = [(x, y, z) for x in [-1, 0, 1] for y in [-1, 0, 1] for z in [-1, 0, 1]] @@ -125,11 +123,9 @@ class Coordinate: else: nb_list = [(-1, 0, 0), (0, -1, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (0, 0, -1)] - return [ - Coordinate(self.x + dx, self.y + dy, self.z + dz) - for dx, dy, dz in nb_list - if minX <= self.x + dx <= maxX and minY <= self.y + dy <= maxY and minZ <= self.z + dz <= maxZ - ] + for dx, dy, dz in nb_list: + if minX <= self.x + dx <= maxX and minY <= self.y + dy <= maxY and minZ <= self.z + dz <= maxZ: + yield self.__class__(self.x + dx, self.y + dy, self.z + dz) def getAngleTo(self, target: Coordinate, normalized: bool = False) -> float: """normalized returns an angle going clockwise with 0 starting in the 'north'""" diff --git a/tools/grid.py b/tools/grid.py index 0404366..91def3e 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -224,10 +224,9 @@ class Grid: minX=self.minX, minY=self.minY, minZ=self.minZ, maxX=self.maxX, maxY=self.maxY, maxZ=self.maxZ ) - if includeDefault: - return neighbours - else: - return [x for x in neighbours if x in self.__grid] + for x in neighbours: + if includeDefault or x in self.__grid: + yield x def getNeighbourSum(self, pos: Coordinate, includeNegative: bool = True, includeDiagonal: bool = True) -> Numeric: neighbour_sum = 0 From 642a32b8840a3e78cf4a007bc562f744609149c8 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 14 Dec 2022 06:51:55 +0100 Subject: [PATCH 130/144] Grid.count(); Grid.print() not handles Enums --- tools/grid.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/grid.py b/tools/grid.py index 91def3e..9312d72 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -143,6 +143,9 @@ class Grid: def getOnCount(self) -> int: return len(self.__grid) + def count(self, value: Any) -> int: + return len([x for x in self.values() if x == value]) + def isSet(self, pos: Coordinate) -> bool: return pos in self.__grid @@ -364,7 +367,11 @@ class Grid: elif true_char: print(true_char if self.get(Coordinate(x, y)) else false_char, end="") else: - print(self.get(Coordinate(x, y)), end="") + value = self.get(Coordinate(x, y)) + if isinstance(value, Enum): + print(value.value, end="") + else: + print(value, end="") print(spacer, end="") print() From 2692f4b560f7035e73c5f1cd75e3c617e2f08756 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Wed, 14 Dec 2022 19:37:03 +0100 Subject: [PATCH 131/144] Grid.getPath_BFS() --- tools/grid.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tools/grid.py b/tools/grid.py index 9312d72..40d2c03 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -1,4 +1,5 @@ from __future__ import annotations +from collections import deque from .aoc_ocr import convert_array_6 from .coordinate import Coordinate, DistanceAlgorithm, Shape from .types import Numeric @@ -144,7 +145,7 @@ class Grid: return len(self.__grid) def count(self, value: Any) -> int: - return len([x for x in self.values() if x == value]) + return list(self.__grid.values()).count(value) def isSet(self, pos: Coordinate) -> bool: return pos in self.__grid @@ -286,6 +287,39 @@ class Grid: else: raise NotImplementedError(mode) + def getPath_BFS(self, pos_from: Coordinate, pos_to: Coordinate, includeDiagonal: bool, walls: List[Any] = None, + stop_at_first: Any = None) -> Union[None, List[Coordinate]]: + queue = deque() + came_from = {pos_from: None} + queue.append(pos_from) + if walls is None: + walls = [self.__default] + + while queue: + current = queue.popleft() + found_end = False + for c in self.getNeighboursOf(current, includeDiagonal=includeDiagonal, includeDefault=self.__default not in walls): + if c in came_from and self.get(c) in walls: + continue + came_from[c] = current + if c == pos_to or (stop_at_first is not None and self.get(c) == stop_at_first): + pos_to = c + found_end = True + break + queue.append(c) + if found_end: + break + + if pos_to not in came_from: + return None + + ret = [] + while pos_to in came_from: + ret.insert(0, pos_to) + pos_to = came_from[pos_to] + + return ret + def getPath(self, pos_from: Coordinate, pos_to: Coordinate, includeDiagonal: bool, walls: List[Any] = None, weighted: bool = False) -> Union[None, List[Coordinate]]: f_costs = [] From f5544299da20e326a43a39182ee246495e76925d Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 19 Dec 2022 04:48:08 +0100 Subject: [PATCH 132/144] Cache(): a dict that keeps track of "x in Cache()" calls to enable statistics --- tools/tools.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tools/tools.py b/tools/tools.py index eec7dcf..ec386c9 100644 --- a/tools/tools.py +++ b/tools/tools.py @@ -7,6 +7,25 @@ from functools import wraps from typing import Any +class Cache(dict): + def __init__(self): + super().__init__() + self.hits = 0 + self.misses = 0 + + def str_hits(self) -> str: + return "%d (%1.2f)" % (self.hits, self.hits / (self.misses + self.hits) * 100) + + def __contains__(self, item: Any) -> bool: + r = super().__contains__(item) + if r: + self.hits += 1 + else: + self.misses += 1 + + return r + + def get_script_dir(follow_symlinks: bool = True) -> str: """return path of the executed script""" if getattr(sys, 'frozen', False): From 798e8c3faac0dc4db17ce3bd40408319e23f1b35 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 19 Dec 2022 04:49:46 +0100 Subject: [PATCH 133/144] reformat --- tools/grid.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index 40d2c03..d50d94b 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -176,7 +176,7 @@ class Grid: def isWithinBoundaries(self, pos: Coordinate) -> bool: if self.mode3D: return self.minX <= pos.x <= self.maxX and self.minY <= pos.y <= self.maxY \ - and self.minZ <= pos.z <= self.maxZ + and self.minZ <= pos.z <= self.maxZ else: return self.minX <= pos.x <= self.maxX and self.minY <= pos.y <= self.maxY @@ -189,7 +189,7 @@ class Grid: return list(self.__grid.keys()) def getActiveRegion(self, start: Coordinate, includeDiagonal: bool = False, ignore: List[Coordinate] = None) \ - -> List[Coordinate]: + -> List[Coordinate]: if not self.get(start): return [] if ignore is None: @@ -298,7 +298,8 @@ class Grid: while queue: current = queue.popleft() found_end = False - for c in self.getNeighboursOf(current, includeDiagonal=includeDiagonal, includeDefault=self.__default not in walls): + for c in self.getNeighboursOf(current, includeDiagonal=includeDiagonal, + includeDefault=self.__default not in walls): if c in came_from and self.get(c) in walls: continue came_from[c] = current @@ -411,7 +412,9 @@ class Grid: print() def get_aoc_ocr_string(self, x_shift: int = 0, y_shift: int = 0): - return convert_array_6([['#' if self.get(Coordinate(x + x_shift, y + y_shift)) else '.' for x in self.rangeX()] for y in self.rangeY()]) + return convert_array_6( + [['#' if self.get(Coordinate(x + x_shift, y + y_shift)) else '.' for x in self.rangeX()] for y in + self.rangeY()]) def __str__(self, true_char: str = '#', false_char: str = "."): return "/".join( From afac0e484d1cf9b4491232a1f3d1141f71ef3ee1 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Tue, 20 Dec 2022 12:09:57 +0100 Subject: [PATCH 134/144] int_seq.fibonacci: less recursive, more memory efficient --- tools/int_seq.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/int_seq.py b/tools/int_seq.py index 3e00b20..aedcd07 100644 --- a/tools/int_seq.py +++ b/tools/int_seq.py @@ -10,7 +10,6 @@ def factorial(n: int) -> int: return math.factorial(n) -@cache def fibonacci(n: int) -> int: """ F(n) = F(n-1) + F(n-2) with F(0) = 0 and F(1) = 1 @@ -19,7 +18,11 @@ def fibonacci(n: int) -> int: if n < 2: return n - return fibonacci(n - 1) + fibonacci(n - 2) + l, r = 1, 1 + for _ in range(n - 2): + l, r = l + r, l + + return l def triangular(n: int) -> int: From 47446f3f35c40b34f5f39a6495f71593afc5ee3e Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Thu, 22 Dec 2022 09:35:09 +0100 Subject: [PATCH 135/144] Grid.shift() - shift whole coordinate system --- tools/grid.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tools/grid.py b/tools/grid.py index d50d94b..4f9f708 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -287,6 +287,15 @@ class Grid: else: raise NotImplementedError(mode) + def shift(self, shift_x: int = None, shift_y: int = None): + self.minX, self.minY = self.minX + shift_x, self.minY + shift_y + self.maxX, self.maxY = self.maxX + shift_x, self.maxY + shift_y + coords = self.__grid + self.__grid = {} + for c, v in coords.items(): + nc = Coordinate(c.x + shift_x, c.y + shift_y) + self.set(nc, v) + def getPath_BFS(self, pos_from: Coordinate, pos_to: Coordinate, includeDiagonal: bool, walls: List[Any] = None, stop_at_first: Any = None) -> Union[None, List[Coordinate]]: queue = deque() From 5bae00234c1a64066c53d7f78616a0ed4ea36be5 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Fri, 23 Dec 2022 08:00:21 +0100 Subject: [PATCH 136/144] Grid.recalcBoundaries() (for when grids get smaller, which is not tracked by default) Grid.from_str() and Grid.print() now accept translate dicts for more complex grid values --- tools/grid.py | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index 4f9f708..22e8a3a 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -61,6 +61,11 @@ class Grid: self.minZ = pos.z if pos.z < self.minZ else self.minZ self.maxZ = pos.z if pos.z > self.maxZ else self.maxZ + def recalcBoundaries(self) -> None: + self.minX, self.maxX, self.minY, self.maxY, self.minZ, self.maxZ = None, None, None, None, None, None + for c in self.__grid: + self.__trackBoundaries(c) + def getBoundaries(self) -> (int, int, int, int, int, int): if self.mode3D: return self.minX, self.minY, self.maxX, self.maxY, self.minZ, self.maxZ @@ -403,19 +408,26 @@ class Grid: put_y = y put_x += 1 - def print(self, spacer: str = "", true_char: str = None, false_char: str = " ", mark: list = None): + def print(self, spacer: str = "", true_char: str = '#', false_char: str = " ", translate: dict = None, mark: list = None): + if translate is None: + translate = {} + + if true_char is not None and True not in translate: + translate[True] = true_char + if false_char is not None and False not in translate: + translate[False] = false_char + for y in range(self.minY, self.maxY + 1): for x in range(self.minX, self.maxX + 1): - if mark and Coordinate(x, y) in mark: + pos = Coordinate(x, y) + if mark and pos in mark: print("X", end="") - elif true_char: - print(true_char if self.get(Coordinate(x, y)) else false_char, end="") else: - value = self.get(Coordinate(x, y)) + value = self.get(pos) if isinstance(value, Enum): - print(value.value, end="") - else: - print(value, end="") + value = value.value + + print(value if value not in translate else translate[value], end="") print(spacer, end="") print() @@ -435,10 +447,18 @@ class Grid: ) @classmethod - def from_str(cls, grid_string: str, true_char: str = '#', true_value: Any = True, default: Any = False) -> 'Grid': + def from_str(cls, grid_string: str, default: Any = False, true_char: str = '#', true_value: Any = True, translate: dict = None) -> 'Grid': + if translate is None: + translate = {} + if true_char is not None and True not in translate.values(): + translate[true_char] = true_value if true_value is not None else True + ret = cls(default=default) for y, line in enumerate(grid_string.split("/")): for x, c in enumerate(line): - ret.set(Coordinate(x, y), true_value if c == true_char else default) + if c in translate: + ret.set(Coordinate(x, y), translate[c]) + else: + ret.set(Coordinate(x, y), c) return ret From 9963a21821f1f570eb4eb99ba5635af02a6932a6 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 24 Dec 2022 06:12:44 +0100 Subject: [PATCH 137/144] Grid: finalizing shift(), adding shift_zero() --- tools/grid.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index 22e8a3a..46a4d1e 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -292,15 +292,24 @@ class Grid: else: raise NotImplementedError(mode) - def shift(self, shift_x: int = None, shift_y: int = None): - self.minX, self.minY = self.minX + shift_x, self.minY + shift_y - self.maxX, self.maxY = self.maxX + shift_x, self.maxY + shift_y + def shift(self, shift_x: int = 0, shift_y: int = 0, shift_z: int = 0): + self.minX, self.minY, self.minZ = self.minX + shift_x, self.minY + shift_y, self.minZ + shift_z + self.maxX, self.maxY, self.minZ = self.maxX + shift_x, self.maxY + shift_y, self.minZ + shift_z coords = self.__grid self.__grid = {} for c, v in coords.items(): - nc = Coordinate(c.x + shift_x, c.y + shift_y) + if self.mode3D: + nc = Coordinate(c.x + shift_x, c.y + shift_y, c.z + shift_z) + else: + nc = Coordinate(c.x + shift_x, c.y + shift_y) self.set(nc, v) + def shift_zero(self, recalc: bool = True): + # self.shift() to (0, 0, 0) being top, left, front + if recalc: + self.recalcBoundaries() + self.shift(0 - self.minX, 0 - self.minY, 0 - self.minZ) + def getPath_BFS(self, pos_from: Coordinate, pos_to: Coordinate, includeDiagonal: bool, walls: List[Any] = None, stop_at_first: Any = None) -> Union[None, List[Coordinate]]: queue = deque() From ca4c67f80550ec23464142414cafa7109feaeb85 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 25 Dec 2022 05:19:57 +0100 Subject: [PATCH 138/144] some bug fixing around 3d grids --- tools/coordinate.py | 2 ++ tools/grid.py | 33 ++++++++++++++++++++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/tools/coordinate.py b/tools/coordinate.py index a7c63e7..a27d7bb 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -177,6 +177,8 @@ class Coordinate: return self.__class__(self.x - other.x, self.y - other.y, self.z - other.z) def __eq__(self, other): + if not isinstance(other, Coordinate): + return False return self.x == other.x and self.y == other.y and self.z == other.z def __gt__(self, other): diff --git a/tools/grid.py b/tools/grid.py index 46a4d1e..3994b02 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -185,11 +185,13 @@ class Grid: else: return self.minX <= pos.x <= self.maxX and self.minY <= pos.y <= self.maxY - def getActiveCells(self, x: int = None, y: int = None) -> List[Coordinate]: + def getActiveCells(self, x: int = None, y: int = None, z: int = None) -> List[Coordinate]: if x: return [c for c in self.__grid.keys() if c.x == x] elif y: return [c for c in self.__grid.keys() if c.y == y] + elif z: + return [c for c in self.__grid.keys() if c.z == z] else: return list(self.__grid.keys()) @@ -293,8 +295,10 @@ class Grid: raise NotImplementedError(mode) def shift(self, shift_x: int = 0, shift_y: int = 0, shift_z: int = 0): - self.minX, self.minY, self.minZ = self.minX + shift_x, self.minY + shift_y, self.minZ + shift_z - self.maxX, self.maxY, self.minZ = self.maxX + shift_x, self.maxY + shift_y, self.minZ + shift_z + self.minX, self.minY = self.minX + shift_x, self.minY + shift_y + self.maxX, self.maxY = self.maxX + shift_x, self.maxY + shift_y + if self.mode3D: + self.minZ, self.maxZ = self.minZ + shift_z, self.maxZ + shift_z coords = self.__grid self.__grid = {} for c, v in coords.items(): @@ -308,7 +312,10 @@ class Grid: # self.shift() to (0, 0, 0) being top, left, front if recalc: self.recalcBoundaries() - self.shift(0 - self.minX, 0 - self.minY, 0 - self.minZ) + if self.mode3D: + self.shift(0 - self.minX, 0 - self.minY, 0 - self.minZ) + else: + self.shift(0 - self.minX, 0 - self.minY) def getPath_BFS(self, pos_from: Coordinate, pos_to: Coordinate, includeDiagonal: bool, walls: List[Any] = None, stop_at_first: Any = None) -> Union[None, List[Coordinate]]: @@ -433,6 +440,9 @@ class Grid: print("X", end="") else: value = self.get(pos) + if isinstance(value, list): + value = len(value) + if isinstance(value, Enum): value = value.value @@ -456,18 +466,23 @@ class Grid: ) @classmethod - def from_str(cls, grid_string: str, default: Any = False, true_char: str = '#', true_value: Any = True, translate: dict = None) -> 'Grid': + def from_str(cls, grid_string: str, default: Any = False, true_char: str = '#', true_value: Any = True, translate: dict = None, mode3d: bool = False) -> 'Grid': if translate is None: translate = {} - if true_char is not None and True not in translate.values(): + if true_char is not None and True not in translate.values() and true_char not in translate: translate[true_char] = true_value if true_value is not None else True ret = cls(default=default) for y, line in enumerate(grid_string.split("/")): for x, c in enumerate(line): - if c in translate: - ret.set(Coordinate(x, y), translate[c]) + if mode3d: + coord = Coordinate(x, y, 0) else: - ret.set(Coordinate(x, y), c) + coord = Coordinate(x, y) + + if c in translate: + ret.set(coord, translate[c]) + else: + ret.set(coord, c) return ret From 69a63c94f64b1891ce212e2d17cc79f3d61af1f0 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 25 Dec 2022 10:29:18 +0100 Subject: [PATCH 139/144] correctly calculate time left on wrong answer --- tools/aoc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoc.py b/tools/aoc.py index 4a35224..31b07be 100644 --- a/tools/aoc.py +++ b/tools/aoc.py @@ -158,7 +158,7 @@ class AOCDay: seconds = int(seconds) if minutes: - seconds *= int(minutes) * 60 + seconds += int(minutes) * 60 print("TOO SOON. Waiting %d seconds until auto-retry." % seconds) time.sleep(seconds) From 9f11e12bc438427eaf367482b7dce92a19d53223 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 25 Dec 2022 18:36:37 +0100 Subject: [PATCH 140/144] Grid: a whole bunch of bugfixing around transformations and mode3d Grid: added __eq__() --- tools/grid.py | 94 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 67 insertions(+), 27 deletions(-) diff --git a/tools/grid.py b/tools/grid.py index 3994b02..7027a7b 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -186,12 +186,13 @@ class Grid: return self.minX <= pos.x <= self.maxX and self.minY <= pos.y <= self.maxY def getActiveCells(self, x: int = None, y: int = None, z: int = None) -> List[Coordinate]: - if x: - return [c for c in self.__grid.keys() if c.x == x] - elif y: - return [c for c in self.__grid.keys() if c.y == y] - elif z: - return [c for c in self.__grid.keys() if c.z == z] + if x is not None or y is not None or z is not None: + return [ + c for c in self.__grid.keys() + if (c.x == x if x is not None else True) + and (c.y == y if y is not None else True) + and (c.z == z if z is not None else True) + ] else: return list(self.__grid.keys()) @@ -259,41 +260,55 @@ class Grid: coords = self.__grid self.__grid = {} if mode == GridTransformation.ROTATE_X: - self.minY, self.maxY, self.minZ, self.maxZ = self.minZ, self.maxZ, -self.maxY, -self.minY - for c, v in coords.items(): - self.set(Coordinate(c.x, -c.z, c.y), v) - elif mode == GridTransformation.ROTATE_Y: - self.minX, self.maxX, self.minZ, self.maxZ = -self.maxZ, -self.minZ, self.minX, self.maxX - for c, v in coords.items(): - self.set(Coordinate(-c.z, c.y, c.x), v) - elif mode == GridTransformation.ROTATE_Z: - self.minX, self.maxX, self.minY, self.maxY = -self.maxY, -self.minY, self.minX, self.minY - for c, v in coords.items(): - self.set(Coordinate(-c.y, c.x, c.z), v) - elif mode == GridTransformation.COUNTER_ROTATE_X: - self.minY, self.maxY, self.minZ, self.maxZ = -self.maxZ, -self.minZ, self.minY, self.maxY + shift_z = self.maxY for c, v in coords.items(): self.set(Coordinate(c.x, c.z, -c.y), v) + self.shift(shift_z=shift_z) + elif mode == GridTransformation.ROTATE_Y: + shift_x = self.maxX + for c, v in coords.items(): + self.set(Coordinate(-c.z, c.y, c.x), v) + self.shift(shift_x=shift_x) + elif mode == GridTransformation.ROTATE_Z: + shift_x = self.maxX + for c, v in coords.items(): + self.set(Coordinate(-c.y, c.x, c.z), v) + self.shift(shift_x=shift_x) + elif mode == GridTransformation.COUNTER_ROTATE_X: + shift_y = self.maxY + for c, v in coords.items(): + self.set(Coordinate(c.x, -c.z, c.y), v) + self.shift(shift_y=shift_y) elif mode == GridTransformation.COUNTER_ROTATE_Y: - self.minX, self.maxX, self.minZ, self.maxZ = self.minZ, self.maxZ, -self.minX, -self.maxX + shift_z = self.maxZ for c, v in coords.items(): self.set(Coordinate(c.z, c.y, -c.x), v) + self.shift(shift_z=shift_z) elif mode == GridTransformation.COUNTER_ROTATE_Z: - self.minX, self.maxX, self.minY, self.maxY = self.minY, self.maxY, -self.minX, -self.maxX + shift_y = self.maxY for c, v in coords.items(): self.set(Coordinate(c.y, -c.x, c.z), v) + self.shift(shift_y=shift_y) elif mode == GridTransformation.FLIP_X: + shift_x = self.maxX for c, v in coords.items(): self.set(Coordinate(-c.x, c.y, c.z), v) + self.shift(shift_x=shift_x) elif mode == GridTransformation.FLIP_Y: + shift_y = self.maxY for c, v in coords.items(): self.set(Coordinate(c.x, -c.y, c.z), v) + self.shift(shift_y=shift_y) elif mode == GridTransformation.FLIP_Z: + shift_z = self.maxZ for c, v in coords.items(): self.set(Coordinate(c.x, c.y, -c.z), v) + self.shift(shift_z=shift_z) else: raise NotImplementedError(mode) + self.recalcBoundaries() + def shift(self, shift_x: int = 0, shift_y: int = 0, shift_z: int = 0): self.minX, self.minY = self.minX + shift_x, self.minY + shift_y self.maxX, self.maxY = self.maxX + shift_x, self.maxY + shift_y @@ -403,12 +418,21 @@ class Grid: return pathCoords - def sub_grid(self, from_x: int, from_y: int, to_x: int, to_y: int) -> 'Grid': - count_x, count_y = 0, 0 - new_grid = Grid() + def sub_grid(self, from_x: int, from_y: int, to_x: int, to_y: int, from_z: int = None, to_z: int = None) -> 'Grid': + if self.mode3D and (from_z is None or to_z is None): + raise ValueError("sub_grid() on mode3d Grids requires from_z and to_z to be set") + count_x, count_y, count_z = 0, 0, 0 + new_grid = Grid(self.__default) for x in range(from_x, to_x + 1): for y in range(from_y, to_y + 1): - new_grid.set(Coordinate(count_x, count_y), self.get(Coordinate(x, y))) + if not self.mode3D: + new_grid.set(Coordinate(count_x, count_y), self.get(Coordinate(x, y))) + else: + for z in range(from_z, to_z + 1): + new_grid.set(Coordinate(count_x, count_y, count_z), self.get(Coordinate(x, y, z))) + count_z += 1 + + count_z = 0 count_y += 1 count_y = 0 count_x += 1 @@ -424,7 +448,7 @@ class Grid: put_y = y put_x += 1 - def print(self, spacer: str = "", true_char: str = '#', false_char: str = " ", translate: dict = None, mark: list = None): + def print(self, spacer: str = "", true_char: str = '#', false_char: str = " ", translate: dict = None, mark: list = None, z_level: int = None): if translate is None: translate = {} @@ -435,7 +459,8 @@ class Grid: for y in range(self.minY, self.maxY + 1): for x in range(self.minX, self.maxX + 1): - pos = Coordinate(x, y) + pos = Coordinate(x, y, z_level) + if mark and pos in mark: print("X", end="") else: @@ -486,3 +511,18 @@ class Grid: ret.set(coord, c) return ret + + def __eq__(self, other: Grid) -> bool: + if not isinstance(other, Grid): + return False + + other_active = set(other.getActiveCells()) + for c, v in self.__grid.items(): + if other.get(c) != v: + return False + other_active.remove(c) + + if other_active: + return False + + return True From b0a8388a8b7d0b3cd9702644756c44aeb3c62e11 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 25 Feb 2023 14:29:54 +0100 Subject: [PATCH 141/144] tools.Dict(): ansible template style dictionary with defaults --- tools/tools.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tools/tools.py b/tools/tools.py index ec386c9..87ae83a 100644 --- a/tools/tools.py +++ b/tools/tools.py @@ -26,6 +26,53 @@ class Cache(dict): return r +class Dict(dict): + _initialized: bool = False + + def __init__(self, *args, **kwargs): + if "default" in kwargs: + self.__default = kwargs['default'] + del kwargs['default'] + else: + self.__default = None + + super(Dict, self).__init__(*args, **kwargs) + self.convert() + self._initialized = True + + def convert(self): + for k in self: + if isinstance(self[k], dict) and not isinstance(self[k], Dict): + self[k] = Dict(self[k]) + elif isinstance(self[k], list): + for i in range(len(self[k])): + if isinstance(self[k][i], dict) and not isinstance(self[k][i], Dict): + self[k][i] = Dict(self[k][i]) + + def update(self, other: dict, **kwargs): + super(Dict, self).update(other, **kwargs) + self.convert() + + def __getattr__(self, item): + try: + return self[item] + except KeyError: + if self.__default is None: + raise AttributeError(item) + else: + return self.__default + + def __setattr__(self, key, value): + if not self._initialized: + super(Dict, self).__setattr__(key, value) + else: + self[key] = value + + def __setitem__(self, key, value): + super(Dict, self).__setitem__(key, value) + self.convert() + + def get_script_dir(follow_symlinks: bool = True) -> str: """return path of the executed script""" if getattr(sys, 'frozen', False): From ec64dce3478ac84dc11fe9c9d3901f8fbffcd779 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sat, 8 Jul 2023 14:12:25 +0200 Subject: [PATCH 142/144] int.sum_digits() --- requirements.txt | 1 + tools/tools.py | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 02300b0..2bd1a6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ bs4~=0.0.1 beautifulsoup4==4.11.1 fishhook~=0.2.5 +pygame~=2.4.0 requests==2.28.1 setuptools==65.6.3 diff --git a/tools/tools.py b/tools/tools.py index 87ae83a..1108544 100644 --- a/tools/tools.py +++ b/tools/tools.py @@ -183,4 +183,15 @@ def intersection(self, *args) -> str: @hook(str) def __and__(self, *args) -> str: - return self.intersection(*args) \ No newline at end of file + return self.intersection(*args) + + +@hook(int) +def sum_digits(self) -> int: + s = 0 + num = self + while num > 0: + s += num % 10 + num //= 10 + + return s \ No newline at end of file From cc9a6bcbc57aed0bbe39965bba0d309a5c6dffbe Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 17 Sep 2023 04:07:47 +0200 Subject: [PATCH 143/144] Coordinate.__mul__, Coordinate.__truediv__, Coordinate.__floordiv__ --- tools/coordinate.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tools/coordinate.py b/tools/coordinate.py index a27d7bb..0871fe4 100644 --- a/tools/coordinate.py +++ b/tools/coordinate.py @@ -176,6 +176,21 @@ class Coordinate: else: return self.__class__(self.x - other.x, self.y - other.y, self.z - other.z) + def __mul__(self, other: int) -> Coordinate: + if self.z is None: + return self.__class__(self.x * other, self.y * other) + else: + return self.__class__(self.x * other, self.y * other, self.z * other) + + def __floordiv__(self, other) -> Coordinate: + if self.z is None: + return self.__class__(self.x // other, self.y // other) + else: + return self.__class__(self.x // other, self.y // other, self.z // other) + + def __truediv__(self, other): + return self // other + def __eq__(self, other): if not isinstance(other, Coordinate): return False From 75013fbcdda36e012b6f214ae2c6e553e628ef72 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 17 Sep 2023 04:08:00 +0200 Subject: [PATCH 144/144] Grid.move() --- tools/grid.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tools/grid.py b/tools/grid.py index 7027a7b..7cc94a4 100644 --- a/tools/grid.py +++ b/tools/grid.py @@ -120,6 +120,12 @@ class Grid: return value + def move(self, pos: Coordinate, vec: Coordinate,): + target = pos + vec + self.set(target, self.get(pos)) + if pos in self.__grid: + del self.__grid[pos] + def add(self, pos: Coordinate, value: Numeric = 1) -> Numeric: return self.set(pos, self.get(pos) + value) @@ -448,7 +454,7 @@ class Grid: put_y = y put_x += 1 - def print(self, spacer: str = "", true_char: str = '#', false_char: str = " ", translate: dict = None, mark: list = None, z_level: int = None): + def print(self, spacer: str = "", true_char: str = '#', false_char: str = " ", translate: dict = None, mark: list = None, z_level: int = None, bool_mode: bool = False): if translate is None: translate = {} @@ -463,6 +469,8 @@ class Grid: if mark and pos in mark: print("X", end="") + elif bool_mode: + print(true_char if self.get(pos) else false_char, end="") else: value = self.get(pos) if isinstance(value, list):