Compare commits

..

No commits in common. "main" and "0.3" have entirely different histories.
main ... 0.3

15 changed files with 546 additions and 1506 deletions

View File

@ -5,11 +5,8 @@ build-backend = 'setuptools.build_meta'
[tool.setuptools-git-versioning] [tool.setuptools-git-versioning]
enabled = true enabled = true
[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
[project] [project]
dynamic = ['version', 'dependencies'] dynamic = ['version']
name = "shs-tools" name = "shs-tools"
authors = [ authors = [
{ name="Stefan Harmuth", email="pennywise@drock.de" }, { name="Stefan Harmuth", email="pennywise@drock.de" },

View File

@ -1,4 +1,6 @@
bs4~=0.0.1 bs4~=0.0.1
beautifulsoup4~=4.12 beautifulsoup4==4.11.1
requests~=2.32 fishhook~=0.2.5
tqdm~=4.66 pygame~=2.4.0
requests==2.28.1
setuptools==65.6.3

View File

@ -1,17 +1,14 @@
from __future__ import annotations from __future__ import annotations
import os import os
import re import re
import requests
import subprocess import subprocess
import sys import requests
import time import time
import uuid
import webbrowser import webbrowser
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from .datafiles import JSONFile from .datafiles import JSONFile
from .stopwatch import StopWatch from .stopwatch import StopWatch
from typing import Any, Callable, Type from typing import Any, Callable, List, Tuple, Type
from tqdm.auto import tqdm
from .tools import get_script_dir from .tools import get_script_dir
BASE_PATH = get_script_dir() BASE_PATH = get_script_dir()
@ -21,9 +18,9 @@ INPUTS_PATH = os.path.join(BASE_PATH, "inputs")
class AOCDay: class AOCDay:
year: int year: int
day: int day: int
input: list[str] # our input is always a list of str/lines input: List[str] # our input is always a list of str/lines
inputs: list[list[tuple[Any, str]]] inputs: List[List[Tuple[Any, str]]]
part_func: list[Callable] part_func: List[Callable]
def __init__(self, year: int, day: int): def __init__(self, year: int, day: int):
self.day = day self.day = day
@ -31,11 +28,6 @@ class AOCDay:
self.part_func = [self.part1, self.part2] self.part_func = [self.part1, self.part2]
self._current_test_file = None self._current_test_file = None
self._current_test_solution = None self._current_test_solution = None
self.DP = {}
self.SEEN = set()
self.__main_progress_bar_id = None
self.__main_progress_bar_pos = 0
self.progress_bars = {}
def part1(self) -> Any: def part1(self) -> Any:
raise NotImplementedError() raise NotImplementedError()
@ -43,16 +35,6 @@ class AOCDay:
def part2(self) -> Any: def part2(self) -> Any:
raise NotImplementedError() raise NotImplementedError()
def is_test(self) -> bool:
return "test" in self._current_test_file
def _call_part_func(self, func: Callable) -> Any:
ans = func()
for p, pbar in self.progress_bars.items():
pbar.close()
self.progress_bars = {}
return ans
def run_part( def run_part(
self, self,
part: int, part: int,
@ -68,27 +50,25 @@ class AOCDay:
self._load_input(input_file) self._load_input(input_file)
if not measure_runtime or case_count < len(self.inputs[part]) - 1: if not measure_runtime or case_count < len(self.inputs[part]) - 1:
self.DP = {} answer = self.part_func[part]()
self.seen_reset()
answer = self._call_part_func(self.part_func[part])
else: else:
stopwatch = StopWatch(auto_start=False) stopwatch = StopWatch()
self.__main_progress_bar_pos = 1 for _ in range(timeit_number):
for _ in tqdm(range(timeit_number), desc=f"Part {part+1}", leave=False): answer = self.part_func[part]()
self.DP = {} stopwatch.stop()
self.seen_reset()
stopwatch.start()
answer = self._call_part_func(self.part_func[part])
stopwatch.stop()
exec_time = stopwatch.avg_string(timeit_number) exec_time = stopwatch.avg_string(timeit_number)
if solution is None: if solution is None:
print_solution(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 {"", b"", None, b"None", "None", 0, "0"}: if answer not in {"", b"", None, b"None", "None", 0, "0"}:
self._submit(part + 1, answer) self._submit(part + 1, answer)
else: else:
if verbose or answer != solution: if verbose or answer != solution:
print_solution(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: if answer != solution:
return False return False
@ -110,9 +90,6 @@ class AOCDay:
self.run_part(1, verbose, measure_runtime, timeit_number) self.run_part(1, verbose, measure_runtime, timeit_number)
def _load_input(self, filename): def _load_input(self, filename):
if not os.path.exists(INPUTS_PATH):
os.mkdir(INPUTS_PATH)
file_path = os.path.join(INPUTS_PATH, filename) file_path = os.path.join(INPUTS_PATH, filename)
if not os.path.exists(file_path): if not os.path.exists(file_path):
self._download_input(file_path) self._download_input(file_path)
@ -128,7 +105,10 @@ class AOCDay:
cookies={"session": session_id}, cookies={"session": session_id},
) )
if not response.ok: if not response.ok:
print("FAILED to download input: (%s) %s" % (response.status_code, response.text)) print(
"FAILED to download input: (%s) %s"
% (response.status_code, response.text)
)
return return
with open(filename, "wb") as f: with open(filename, "wb") as f:
@ -171,19 +151,22 @@ class AOCDay:
) )
if not response.ok: if not response.ok:
print("Failed to submit answer: (%s) %s" % (response.status_code, response.text)) print(
"Failed to submit answer: (%s) %s"
% (response.status_code, response.text)
)
soup = BeautifulSoup(response.text, "html.parser") soup = BeautifulSoup(response.text, "html.parser")
message = soup.article.text message = soup.article.text
if "That's the right answer" in message: if "That's the right answer" in message:
answer_cache[str_day][str_part]["correct"] = answer answer_cache[str_day][str_part]["correct"] = answer
has_rank = re.findall(r"You achieved.*rank (\d+)", message) print("That's correct!")
print("That's correct!%s" % f" (Rank {has_rank[0]})" if has_rank else "") webbrowser.open(
webbrowser.open("https://adventofcode.com/%d/day/%d#part2" % (self.year, self.day)) "https://adventofcode.com/%d/day/%d#part2" % (self.year, self.day)
)
elif "That's not the right answer" in message: elif "That's not the right answer" in message:
hilo = re.findall("your answer is too (high|low)", message)
answer_cache[str_day][str_part]["wrong"].append(answer) answer_cache[str_day][str_part]["wrong"].append(answer)
print("That's WRONG%s!" % (f" (too {hilo[0]})" if hilo else "")) print("That's WRONG!")
elif "You gave an answer too recently" in message: elif "You gave an answer too recently" in message:
# WAIT and retry # WAIT and retry
wait_pattern = r"You have (?:(\d+)m )?(\d+)s left to wait" wait_pattern = r"You have (?:(\d+)m )?(\d+)s left to wait"
@ -221,13 +204,9 @@ class AOCDay:
else: else:
return self.input.copy() return self.input.copy()
def getIntsFromInput(self) -> list: def getMultiLineInputAsArray(
if len(self.input) == 1: self, return_type: Type = None, join_char: str = None
return list(map(int, re.findall(r"-?\d+", self.input[0]))) ) -> List:
else:
return [list(map(int, re.findall(r"-?\d+", l))) for l in self.input]
def getMultiLineInputAsArray(self, return_type: Type = None, join_char: str = None) -> list[list[Any]]:
""" """
get input for day x as 2d array, split by empty lines get input for day x as 2d array, split by empty lines
""" """
@ -253,50 +232,28 @@ class AOCDay:
return return_array return return_array
def getInputAsArraySplit( def getInputAsArraySplit(
self, split_char: str = ",", return_type: Type | list[Type] = None self, split_char: str = ",", return_type: Type | List[Type] = None
) -> list[Any] | list[list[Any]]: ) -> List:
""" """
get input for day x with the lines split by split_char 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 only one line, returns a 1d array with the values
if input has multiple lines, returns a 2d array (a[line][values]) if input has multiple lines, returns a 2d array (a[line][values])
""" """
if len(self.input) == 1: if len(self.input) == 1:
return split_line(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: else:
return_array = [] return_array = []
for line in self.input: for line in self.input:
return_array.append(split_line(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 return return_array
def progress(self, total: int, add: int = 1, bar_id: str = None) -> None:
if bar_id is None:
if self.__main_progress_bar_id is None:
self.__main_progress_bar_id = uuid.uuid4()
bar_id = self.__main_progress_bar_id
if bar_id not in self.progress_bars:
pbar = tqdm(
total=total,
position=len(self.progress_bars) + self.__main_progress_bar_pos,
leave=False,
file=sys.stdout,
)
self.progress_bars[bar_id] = pbar
pbar = self.progress_bars[bar_id]
pbar.update(add)
def seen_reset(self):
self.SEEN = set()
def seen(self, value: Any, auto_add: bool = True) -> bool:
if value in self.SEEN:
return True
if auto_add:
self.SEEN.add(value)
return False
def print_solution( def print_solution(
day: int, day: int,
@ -332,13 +289,15 @@ def print_solution(
print("Day %s, Part %s - Average run time: %s" % (day, part, exec_time)) print("Day %s, Part %s - Average run time: %s" % (day, part, exec_time))
def split_line(line, split_char: str = ",", return_type: Type | list[Type] = None): def split_line(line, split_char: str = ",", return_type: Type | List[Type] = None):
if split_char: if split_char:
line = line.split(split_char) line = line.split(split_char)
if return_type is None: if return_type is None:
return line return line
elif isinstance(return_type, list): elif isinstance(return_type, list):
return [return_type[x](i) if len(return_type) > x else i for x, i in enumerate(line)] return [
return_type[x](i) if len(return_type) > x else i for x, i in enumerate(line)
]
else: else:
return [return_type(i) for i in line] return [return_type(i) for i in line]

View File

@ -1,79 +0,0 @@
from math import log10
ABSOLUTE_ZERO_C = -273.15
def celsius_to_fahrenheit(celsius: float) -> float:
return celsius * 9 / 5 + 32
def fahrenheit_to_celsius(fahrenheit: float) -> float:
return (fahrenheit - 32) / 9 * 5
def celsius_to_kelvin(celsius: float) -> float:
return celsius - ABSOLUTE_ZERO_C
def kelvin_to_celsius(kelvin: float) -> float:
return kelvin + ABSOLUTE_ZERO_C
def fahrenheit_to_kelvin(fahrenheit: float) -> float:
return celsius_to_kelvin(fahrenheit_to_celsius(fahrenheit))
def kelvin_to_fahrenheit(kelvin: float) -> float:
return celsius_to_fahrenheit(kelvin_to_celsius(kelvin))
def saturation_vapor_pressure(temperature: float) -> float:
"""
Saturation vapor pressure for a given temperature (in °C) in hPa
"""
a = 7.5 if temperature >= 0 else 7.6
b = 237.3 if temperature >= 0 else 240.7
return 6.1078 * 10 ** ((a * temperature) / (b + temperature))
def vapor_pressure(relative_humidity: float, temperature: float) -> float:
"""
Vapor pressure for a given relative humidity (in %) and temperature (in °C) in hPa
"""
return relative_humidity / 100 * saturation_vapor_pressure(temperature)
def relative_humidity(temperature: float, due_point: float) -> float:
"""
Relative humidity for a given temperature (in °C) in and due point (in °C)
"""
return 100 * saturation_vapor_pressure(due_point) / saturation_vapor_pressure(temperature)
def absolute_humidity_from_humidity(relative_humidity: float, temperature: float) -> float:
"""
Absolute humidity for a given relative humidity (in %) and temperature (in °C) in g/
"""
mw = 18.016
rs = 8314.3
return 10**5 * mw / rs * vapor_pressure(relative_humidity, temperature) / celsius_to_kelvin(temperature)
def absolute_humidity_from_due_point(due_point: float, temperature: float) -> float:
"""
Absolute humidity for a given due point (in °C) and temperature (in °C) in g/
"""
mw = 18.016
rs = 8314.3
return 10**5 * mw / rs * saturation_vapor_pressure(due_point) / celsius_to_kelvin(temperature)
def due_point(relative_humidity: float, temperature: float) -> float:
"""
Due Point for a given relative humidity (in %) and temperature (in °C) in °C
"""
a = 7.5 if temperature >= 0 else 7.6
b = 237.3 if temperature >= 0 else 240.7
v = log10(vapor_pressure(relative_humidity, temperature) / 6.1078)
return b * v / (a - v)

View File

@ -1,24 +1,8 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import Enum
from math import gcd, sqrt, inf, atan2, degrees, isclose from math import gcd, sqrt, inf, atan2, degrees
from .math import round_half_up from .math import round_half_up
from typing import Union, List, Iterable from typing import Union, List
from .tools import minmax
NEIGHBOURS = [(0, -1), (1, 0), (0, 1), (-1, 0)]
NEIGHBOURS_3D = [(0, -1, 0), (0, 0, -1), (1, 0, 0), (0, 0, -1), (0, 1, 0), (-1, 0, 0)]
DIAGONAL_NEIGHBOURS = [(-1, -1), (1, -1), (-1, 1), (1, 1)]
DIAGONAL_NEIGHBOURS_3D = [
(-1, -1, -1),
(1, -1, -1),
(-1, -1, 1),
(1, -1, 1),
(-1, 1, -1),
(-1, 1, 1),
(1, 1, -1),
(1, 1, 1),
]
class DistanceAlgorithm(Enum): class DistanceAlgorithm(Enum):
@ -30,30 +14,30 @@ class DistanceAlgorithm(Enum):
class Coordinate(tuple): class Coordinate(tuple):
def __new__(cls, x: int | float, y: int | float, z: int | float | None = None): def __new__(cls, x: int, y: int, z: int = None) -> Coordinate:
return tuple.__new__(cls, (x, y, z)) return tuple.__new__(Coordinate, (x, y, z))
@property @property
def x(self) -> int | float: def x(self):
return self[0] return self[0]
@property @property
def y(self) -> int | float: def y(self):
return self[1] return self[1]
@property @property
def z(self) -> int | float: def z(self):
return self[2] return self[2]
def is3D(self) -> bool: def is3D(self) -> bool:
return self[2] is not None return self.z is not None
def getDistanceTo( def getDistanceTo(
self, self,
target: Coordinate | tuple, target: Coordinate,
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN, algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
includeDiagonals: bool = False, includeDiagonals: bool = False,
) -> int | float: ) -> Union[int, float]:
""" """
Get distance to target Coordinate Get distance to target Coordinate
@ -64,34 +48,40 @@ class Coordinate(tuple):
:return: Distance to Target :return: Distance to Target
""" """
if algorithm == DistanceAlgorithm.EUCLIDEAN: if algorithm == DistanceAlgorithm.EUCLIDEAN:
if self[2] is None: if self.z is None:
return sqrt(abs(self[0] - target[0]) ** 2 + abs(self[1] - target[1]) ** 2) return sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2)
else: else:
return sqrt( return sqrt(
abs(self[0] - target[0]) ** 2 + abs(self[1] - target[1]) ** 2 + abs(self[2] - target[2]) ** 2 abs(self.x - target.x) ** 2
+ abs(self.y - target.y) ** 2
+ abs(self.z - target.z) ** 2
) )
elif algorithm == DistanceAlgorithm.CHEBYSHEV: elif algorithm == DistanceAlgorithm.CHEBYSHEV:
if self[2] is None: if self.z is None:
return max(abs(target[0] - self[0]), abs(target[1] - self[1])) return max(abs(target.x - self.x), abs(target.y - self.y))
else: else:
return max( return max(
abs(target[0] - self[0]), abs(target.x - self.x),
abs(target[1] - self[1]), abs(target.y - self.y),
abs(target[2] - self[2]), abs(target.z - self.z),
) )
elif algorithm == DistanceAlgorithm.MANHATTAN: elif algorithm == DistanceAlgorithm.MANHATTAN:
if not includeDiagonals: if not includeDiagonals:
if self[2] is None: if self.z is None:
return abs(self[0] - target[0]) + abs(self[1] - target[1]) return abs(self.x - target.x) + abs(self.y - target.y)
else: else:
return abs(self[0] - target[0]) + abs(self[1] - target[1]) + abs(self[2] - target[2]) return (
abs(self.x - target.x)
+ abs(self.y - target.y)
+ abs(self.z - target.z)
)
else: else:
dist = [abs(self[0] - target[0]), abs(self[1] - target[1])] dist = [abs(self.x - target.x), abs(self.y - target.y)]
if self[2] is None: if self.z is None:
o_dist = max(dist) - min(dist) o_dist = max(dist) - min(dist)
return o_dist + 1.4 * min(dist) return o_dist + 1.4 * min(dist)
else: else:
dist.append(abs(self[2] - target[2])) dist.append(abs(self.z - target.z))
d_steps = min(dist) d_steps = min(dist)
dist.remove(min(dist)) dist.remove(min(dist))
dist = [x - d_steps for x in dist] dist = [x - d_steps for x in dist]
@ -100,48 +90,60 @@ class Coordinate(tuple):
def inBoundaries( def inBoundaries(
self, self,
minX: int | float, minX: int,
minY: int | float, minY: int,
maxX: int | float, maxX: int,
maxY: int | float, maxY: int,
minZ: int | float = -inf, minZ: int = -inf,
maxZ: int | float = inf, maxZ: int = inf,
) -> bool: ) -> bool:
if self[2] is None: if self.z is None:
return minX <= self[0] <= maxX and minY <= self[1] <= maxY return minX <= self.x <= maxX and minY <= self.y <= maxY
else: else:
return minX <= self[0] <= maxX and minY <= self[1] <= maxY and minZ <= self[2] <= maxZ return (
minX <= self.x <= maxX
and minY <= self.y <= maxY
and minZ <= self.z <= maxZ
)
def getCircle( def getCircle(
self, self,
radius: int | float = 1, radius: int = 1,
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN, algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
minX: int | float = -inf, minX: int = -inf,
minY: int | float = -inf, minY: int = -inf,
maxX: int | float = inf, maxX: int = inf,
maxY: int | float = inf, maxY: int = inf,
minZ: int | float = -inf, minZ: int = -inf,
maxZ: int | float = inf, maxZ: int = inf,
) -> list[Coordinate]: ) -> list[Coordinate]:
ret = [] ret = []
if self[2] is None: # mode 2D if self.z is None: # mode 2D
for x in range(self[0] - radius * 2, self[0] + radius * 2 + 1): for x in range(self.x - radius * 2, self.x + radius * 2 + 1):
for y in range(self[1] - radius * 2, self[1] + radius * 2 + 1): for y in range(self.y - radius * 2, self.y + radius * 2 + 1):
target = Coordinate(x, y) target = Coordinate(x, y)
if not target.inBoundaries(minX, minY, maxX, maxY): if not target.inBoundaries(minX, minY, maxX, maxY):
continue continue
dist = round_half_up(self.getDistanceTo(target, algorithm=algorithm, includeDiagonals=False)) dist = round_half_up(
self.getDistanceTo(
target, algorithm=algorithm, includeDiagonals=False
)
)
if dist == radius: if dist == radius:
ret.append(target) ret.append(target)
else: else:
for x in range(self[0] - radius * 2, self[0] + radius * 2 + 1): for x in range(self.x - radius * 2, self.x + radius * 2 + 1):
for y in range(self[1] - radius * 2, self[1] + radius * 2 + 1): for y in range(self.y - radius * 2, self.y + radius * 2 + 1):
for z in range(self[2] - radius * 2, self[2] + radius * 2 + 1): for z in range(self.z - radius * 2, self.z + radius * 2 + 1):
target = Coordinate(x, y) target = Coordinate(x, y)
if not target.inBoundaries(minX, minY, maxX, maxY, minZ, maxZ): if not target.inBoundaries(minX, minY, maxX, maxY, minZ, maxZ):
continue continue
dist = round_half_up(self.getDistanceTo(target, algorithm=algorithm, includeDiagonals=False)) dist = round_half_up(
self.getDistanceTo(
target, algorithm=algorithm, includeDiagonals=False
)
)
if dist == radius: if dist == radius:
ret.append(target) ret.append(target)
@ -150,13 +152,12 @@ class Coordinate(tuple):
def getNeighbours( def getNeighbours(
self, self,
includeDiagonal: bool = True, includeDiagonal: bool = True,
minX: int | float = -inf, minX: int = -inf,
minY: int | float = -inf, minY: int = -inf,
maxX: int | float = inf, maxX: int = inf,
maxY: int | float = inf, maxY: int = inf,
minZ: int | float = -inf, minZ: int = -inf,
maxZ: int | float = inf, maxZ: int = inf,
dist: int | float = 1,
) -> list[Coordinate]: ) -> list[Coordinate]:
""" """
Get a list of neighbouring coordinates. Get a list of neighbouring coordinates.
@ -168,33 +169,60 @@ class Coordinate(tuple):
:param maxX: ignore all neighbours that would have an X value above 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 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 :param maxZ: ignore all neighbours that would have an Z value above this
:param dist: distance to neighbour coordinates
:return: list of Coordinate :return: list of Coordinate
""" """
if self[2] is None: if self.z is None:
nb_list = [x * dist for x in NEIGHBOURS]
if includeDiagonal: if includeDiagonal:
nb_list += [x * dist for x in DIAGONAL_NEIGHBOURS] 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: for dx, dy in nb_list:
if minX <= self[0] + dx <= maxX and minY <= self[1] + dy <= maxY: if minX <= self.x + dx <= maxX and minY <= self.y + dy <= maxY:
yield self.__class__(self[0] + dx, self[1] + dy) yield self.__class__(self.x + dx, self.y + dy)
else: else:
nb_list = [x * dist for x in NEIGHBOURS_3D]
if includeDiagonal: if includeDiagonal:
nb_list += [x * dist for x in DIAGONAL_NEIGHBOURS_3D] 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, dz in nb_list: for dx, dy, dz in nb_list:
if minX <= self[0] + dx <= maxX and minY <= self[1] + dy <= maxY and minZ <= self[2] + dz <= maxZ: if (
yield self.__class__(self[0] + dx, self[1] + dy, self[2] + dz) 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 | tuple, normalized: bool = False) -> float: def getAngleTo(self, target: Coordinate, normalized: bool = False) -> float:
"""normalized returns an angle going clockwise with 0 starting in the 'north'""" """normalized returns an angle going clockwise with 0 starting in the 'north'"""
if self[2] is not None: if self.z is not None:
raise NotImplementedError() # which angle?!?! raise NotImplementedError() # which angle?!?!
dx = target[0] - self[0] dx = target.x - self.x
dy = target[1] - self[1] dy = target.y - self.y
if not normalized: if not normalized:
return degrees(atan2(dy, dx)) return degrees(atan2(dy, dx))
else: else:
@ -204,117 +232,212 @@ class Coordinate(tuple):
else: else:
return 180.0 + abs(angle) return 180.0 + abs(angle)
def getLineTo(self, target: Coordinate | tuple) -> List[Coordinate]: def getLineTo(self, target: Coordinate) -> List[Coordinate]:
"""this will probably not yield what you expect, when using float coordinates"""
if target == self:
return [self]
diff = target - self diff = target - self
if self[2] is None: if self.z is None:
steps = gcd(diff[0], diff[1]) steps = gcd(diff.x, diff.y)
step_x = diff[0] // steps step_x = diff.x // steps
step_y = diff[1] // steps step_y = diff.y // steps
return [self.__class__(self[0] + step_x * i, self[1] + step_y * i) for i in range(steps + 1)]
else:
steps = gcd(diff[0], diff[1], diff[2])
step_x = diff[0] // steps
step_y = diff[1] // steps
step_z = diff[2] // steps
return [ return [
self.__class__(self[0] + step_x * i, self[1] + step_y * i, self[2] + step_z * i) 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 [
self.__class__(
self.x + step_x * i, self.y + step_y * i, self.z + step_z * i
)
for i in range(steps + 1) for i in range(steps + 1)
] ]
def reverse(self) -> Coordinate: def reverse(self) -> Coordinate:
if self[2] is None: if self.z is None:
return self.__class__(-self[0], -self[1]) return self.__class__(-self.x, -self.y)
else: else:
return self.__class__(-self[0], -self[1], -self[2]) return self.__class__(-self.x, -self.y, -self.z)
def __hash__(self) -> int: def __add__(self, other: Coordinate) -> Coordinate:
return hash((self[0], self[1], self[2])) if self.z is None:
return self.__class__(self.x + other.x, self.y + other.y)
def __eq__(self, other: Coordinate | tuple) -> bool:
if self[2] is None:
return self[0] == other[0] and self[1] == other[1]
else: else:
return self[0] == other[0] and self[1] == other[1] and self[2] == other[2] return self.__class__(self.x + other.x, self.y + other.y, self.z + other.z)
def __add__(self, other: Coordinate | tuple) -> Coordinate: def __sub__(self, other: Coordinate) -> Coordinate:
if self[2] is None: if self.z is None:
return self.__class__(self[0] + other[0], self[1] + other[1]) return self.__class__(self.x - other.x, self.y - other.y)
else: else:
return self.__class__(self[0] + other[0], self[1] + other[1], self[2] + other[2]) return self.__class__(self.x - other.x, self.y - other.y, self.z - other.z)
def __sub__(self, other: Coordinate | tuple) -> Coordinate: def __mul__(self, other: int) -> Coordinate:
if self[2] is None: if self.z is None:
return self.__class__(self[0] - other[0], self[1] - other[1]) return self.__class__(self.x * other, self.y * other)
else: else:
return self.__class__(self[0] - other[0], self[1] - other[1], self[2] - other[2]) return self.__class__(self.x * other, self.y * other, self.z * other)
def __mul__(self, other: int | float) -> Coordinate: def __floordiv__(self, other) -> Coordinate:
if self[2] is None: if self.z is None:
return self.__class__(self[0] * other, self[1] * other) return self.__class__(self.x // other, self.y // other)
else: else:
return self.__class__(self[0] * other, self[1] * other, self[2] * other) return self.__class__(self.x // other, self.y // other, self.z // other)
def __mod__(self, other: int | float) -> Coordinate: def __truediv__(self, other):
if self[2] is None: return self // other
return self.__class__(self[0] % other, self[1] % other)
else:
return self.__class__(self[0] % other, self[1] % other, self[2] % other)
def __floordiv__(self, other: int | float) -> Coordinate: def __gt__(self, other):
if self[2] is None: if self.z is None:
return self.__class__(self[0] // other, self[1] // other) return self.x > other.x and self.y > other.y
else: else:
return self.__class__(self[0] // other, self[1] // other, self[2] // other) return self.x > other.x and self.y > other.y and self.z > other.z
def __truediv__(self, other: int | float) -> Coordinate: def __ge__(self, other):
if self[2] is None: if self.z is None:
return self.__class__(self[0] / other, self[1] / other) return self.x >= other.x and self.y >= other.y
else: else:
return self.__class__(self[0] / other, self[1] / other, self[2] / other) 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
def __str__(self): def __str__(self):
if self[2] is None: if self.z is None:
return "({},{})".format(self[0], self[1]) return "(%d,%d)" % (self.x, self.y)
else: else:
return "({},{},{})".format(self[0], self[1], self[2]) return "(%d,%d,%d)" % (self.x, self.y, self.z)
def __repr__(self): def __repr__(self):
if self[2] is None: if self.z is None:
return "{}(x={}, y={})".format(self.__class__.__name__, self[0], self[1]) return "%s(x=%d, y=%d)" % (self.__class__.__name__, self.x, self.y)
else: else:
return "{}(x={}, y={}, z={})".format( return "%s(x=%d, y=%d, z=%d)" % (
self.__class__.__name__, self.__class__.__name__,
self[0], self.x,
self[1], self.y,
self[2], self.z,
) )
@classmethod @classmethod
def generate( def generate(
cls, cls,
from_x: int | float, from_x: int,
to_x: int | float, to_x: int,
from_y: int | float, from_y: int,
to_y: int | float, to_y: int,
from_z: int | float = None, from_z: int = None,
to_z: int | float = None, to_z: int = None,
step: int | float = 1,
) -> List[Coordinate]: ) -> List[Coordinate]:
if from_z is None or to_z is None: if from_z is None or to_z is None:
return [cls(x, y) for x in range(from_x, to_x + step, step) for y in range(from_y, to_y + step, step)] return [
cls(x, y)
for x in range(from_x, to_x + 1)
for y in range(from_y, to_y + 1)
]
else: else:
return [ return [
cls(x, y, z) cls(x, y, z)
for x in range(from_x, to_x + step, step) for x in range(from_x, to_x + 1)
for y in range(from_y, to_y + step, step) for y in range(from_y, to_y + 1)
for z in range(from_z, to_z + step, step) 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(HexCoordinate, self).__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(HexCoordinateF, self).__init__(x, y, z)
class Shape: class Shape:
def __init__(self, top_left: Coordinate, bottom_right: Coordinate): def __init__(self, top_left: Coordinate, bottom_right: Coordinate):
""" """
@ -330,7 +453,9 @@ class Shape:
def __len__(self): def __len__(self):
if not self.mode_3d: if not self.mode_3d:
return (self.bottom_right.x - self.top_left.x + 1) * (self.bottom_right.y - self.top_left.y + 1) return (self.bottom_right.x - self.top_left.x + 1) * (
self.bottom_right.y - self.top_left.y + 1
)
else: else:
return ( return (
(self.bottom_right.x - self.top_left.x + 1) (self.bottom_right.x - self.top_left.x + 1)
@ -347,23 +472,43 @@ class Shape:
if not self.mode_3d: if not self.mode_3d:
intersect_top_left = Coordinate( 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.x
self.top_left.y if self.top_left.y > other.top_left.y else other.top_left.y, 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( 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.x
self.bottom_right.y if self.bottom_right.y < other.bottom_right.y else other.bottom_right.y, 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: else:
intersect_top_left = Coordinate( 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.x
self.top_left.y if self.top_left.y > other.top_left.y else other.top_left.y, if self.top_left.x > other.top_left.x
self.top_left.z if self.top_left.z > other.top_left.z else other.top_left.z, 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( 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.x
self.bottom_right.y if self.bottom_right.y < other.bottom_right.y else other.bottom_right.y, if self.bottom_right.x < other.bottom_right.x
self.bottom_right.z if self.bottom_right.z < other.bottom_right.z else other.bottom_right.z, 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: if intersect_top_left <= intersect_bottom_right:
@ -375,16 +520,6 @@ class Shape:
def __rand__(self, other): def __rand__(self, other):
return self.intersection(other) return self.intersection(other)
def __contains__(self, item: Coordinate) -> bool:
if not self.mode_3d:
return self.top_left.x <= item.x <= self.bottom_right.x and self.top_left.y <= item.y <= self.bottom_right.y
else:
return (
self.top_left.x <= item.x <= self.bottom_right.x
and self.top_left.y <= item.y <= self.bottom_right.y
and self.top_left.z <= item.z <= self.bottom_right.z
)
def __str__(self): def __str__(self):
return "%s(%s -> %s)" % ( return "%s(%s -> %s)" % (
self.__class__.__name__, self.__class__.__name__,
@ -400,9 +535,9 @@ class Shape:
) )
class Rectangle(Shape): class Square(Shape):
def __init__(self, top_left, bottom_right): def __init__(self, top_left, bottom_right):
super(Rectangle, self).__init__(top_left, bottom_right) super(Square, self).__init__(top_left, bottom_right)
self.mode_3d = False self.mode_3d = False
@ -411,131 +546,3 @@ class Cube(Shape):
if top_left.z is None or bottom_right.z is None: if top_left.z is None or bottom_right.z is None:
raise ValueError("Both Coordinates need to be 3D") raise ValueError("Both Coordinates need to be 3D")
super(Cube, self).__init__(top_left, bottom_right) super(Cube, self).__init__(top_left, bottom_right)
# FIXME: Line could probably also just be a subclass of Shape
class Line:
def __init__(self, start: Coordinate, end: Coordinate):
if start[2] is not None or end[2] is not None:
raise NotImplementedError("3D Lines are hard(er)")
self.start, self.end = minmax(start, end)
def is_horizontal(self) -> bool:
return self.start[1] == self.end[1]
def is_vertical(self) -> bool:
return self.start[0] == self.end[0]
def connects_to(self, other: Line) -> bool:
return self.start == other.start or self.start == other.end or self.end == other.start or self.end == other.end
def intersects(self, other: Line, strict: bool = True) -> bool:
try:
self.get_intersection(other, strict=strict)
return True
except ValueError:
return False
def get_intersection(self, other: Line, strict: bool = True) -> Coordinate:
xdiff = (self.start[0] - self.end[0], other.start[0] - other.end[0])
ydiff = (self.start[1] - self.end[1], other.start[1] - other.end[1])
def det(a, b):
return a[0] * b[1] - a[1] * b[0]
div = det(xdiff, ydiff)
if div == 0:
raise ValueError("lines do not intersect")
d = (det(self.start, self.end), det(other.start, other.end))
x = det(d, xdiff) / div
y = det(d, ydiff) / div
ret = Coordinate(x, y)
if not strict:
return ret
else:
if ret in self and ret in other:
return ret
else:
raise ValueError("intersection out of bounds")
def __hash__(self):
return hash((self.start, self.end))
def __eq__(self, other: Line) -> bool:
return hash(self) == hash(other)
def __lt__(self, other: Line) -> bool:
return self.start < other.start
def __contains__(self, point: Coordinate | tuple) -> bool:
return isclose(
self.start.getDistanceTo(self.end),
self.start.getDistanceTo(point) + self.end.getDistanceTo(point),
)
def __len__(self) -> int:
return int(self.start.getDistanceTo(self.end))
def __str__(self):
return f"Line({self.start} -> {self.end})"
def __repr__(self):
return str(self)
class Polygon:
def __init__(self, points: list[Coordinate]) -> None:
"""points have to be in (counter)clockwise order, not repeating the first coordinate"""
if len(set(points)) != len(points):
raise ValueError("Polygon contains repeated points")
self.points = points
self.lines = set()
for i in range(len(points) - 1):
self.lines.add(Line(points[i], points[i + 1]))
self.lines.add(Line(points[-1], points[0]))
def get_circumference(self) -> float:
return sum(len(x) for x in self.lines)
def get_area(self) -> float:
S = 0
for i in range(len(self.points)):
S += (
self.points[i].x * self.points[(i + 1) % len(self.points)].y
- self.points[(i + 1) % len(self.points)].x * self.points[i].y
)
return abs(S) / 2
def decompose(self) -> Iterable[Rectangle]:
points_left = list(self.points)
def flip(point: Coordinate):
if point in points_left:
points_left.remove(point)
else:
points_left.append(point)
while points_left:
pk, pl, pm = None, None, None
for c in sorted(points_left, key=lambda p: (p[1], p[0])):
if pk is None:
pk = c
continue
if pl is None:
pl = c
continue
if pk.x <= c.x < pl.x and pk.y < c.y:
pm = c
break
flip(pk)
flip(pl)
flip(Coordinate(pk.x, pm.y))
flip(Coordinate(pl.x, pm.y))
yield Rectangle(pk, Coordinate(pl.x, pm.y))

View File

@ -1,13 +1,11 @@
from __future__ import annotations from __future__ import annotations
import re from collections import deque
from .aoc_ocr import convert_array_6 from .aoc_ocr import convert_array_6
from .coordinate import Coordinate, DistanceAlgorithm, Shape from .coordinate import Coordinate, DistanceAlgorithm, Shape
from collections import deque
from collections.abc import Callable
from enum import Enum from enum import Enum
from heapq import heappop, heappush from heapq import heappop, heappush
from math import inf from math import inf
from typing import Any, Dict, List, Iterable, Mapping from typing import Any, Dict, List
OFF = False OFF = False
ON = True ON = True
@ -48,19 +46,19 @@ class Grid:
def __trackBoundaries(self, pos: Coordinate): def __trackBoundaries(self, pos: Coordinate):
if self.minX is None: if self.minX is None:
self.minX, self.maxX, self.minY, self.maxY = pos[0], pos[0], pos[1], pos[1] self.minX, self.maxX, self.minY, self.maxY = pos.x, pos.x, pos.y, pos.y
else: else:
self.minX = pos[0] if pos[0] < self.minX else self.minX self.minX = pos.x if pos.x < self.minX else self.minX
self.minY = pos[1] if pos[1] < self.minY else self.minY self.minY = pos.y if pos.y < self.minY else self.minY
self.maxX = pos[0] if pos[0] > self.maxX else self.maxX self.maxX = pos.x if pos.x > self.maxX else self.maxX
self.maxY = pos[1] if pos[1] > self.maxY else self.maxY self.maxY = pos.y if pos.y > self.maxY else self.maxY
if self.mode3D: if self.mode3D:
if self.minZ is None: if self.minZ is None:
self.minZ = self.maxZ = pos[2] self.minZ = self.maxZ = pos.z
else: else:
self.minZ = pos[2] if pos[2] < self.minZ else self.minZ self.minZ = pos.z if pos.z < self.minZ else self.minZ
self.maxZ = pos[2] if pos[2] > self.maxZ else self.maxZ self.maxZ = pos.z if pos.z > self.maxZ else self.maxZ
def recalcBoundaries(self) -> None: def recalcBoundaries(self) -> None:
self.minX, self.maxX, self.minY, self.maxY, self.minZ, self.maxZ = ( self.minX, self.maxX, self.minY, self.maxY, self.minZ, self.maxZ = (
@ -100,12 +98,6 @@ class Grid:
else: else:
return range(self.minZ - pad, self.maxZ + pad + 1) return range(self.minZ - pad, self.maxZ + pad + 1)
def get_column(self, column: int) -> list[Any]:
return [self.get(Coordinate(column, y)) for y in self.rangeY()]
def get_row(self, row: int) -> list[Any]:
return [self.get(Coordinate(x, row)) for x in self.rangeX()]
def toggle(self, pos: Coordinate): def toggle(self, pos: Coordinate):
if pos in self.__grid: if pos in self.__grid:
del self.__grid[pos] del self.__grid[pos]
@ -123,7 +115,7 @@ class Grid:
self.toggle(Coordinate(x, y, z)) self.toggle(Coordinate(x, y, z))
def set(self, pos: Coordinate, value: Any = True) -> Any: def set(self, pos: Coordinate, value: Any = True) -> Any:
if pos[2] is not None: if pos.z is not None:
self.mode3D = True self.mode3D = True
if (value == self.__default) and pos in self.__grid: if (value == self.__default) and pos in self.__grid:
@ -157,13 +149,13 @@ class Grid:
return self.set(pos, self.get(pos) / value) return self.set(pos, self.get(pos) / value)
def add_shape(self, shape: Shape, value: int | float = 1) -> None: def add_shape(self, shape: Shape, value: int | float = 1) -> None:
for x in range(shape.top_left[0], shape.bottom_right[0] + 1): for x in range(shape.top_left.x, shape.bottom_right.x + 1):
for y in range(shape.top_left[1], shape.bottom_right[1] + 1): for y in range(shape.top_left.y, shape.bottom_right.y + 1):
if not shape.mode_3d: if not shape.mode_3d:
pos = Coordinate(x, y) pos = Coordinate(x, y)
self.set(pos, self.get(pos) + value) self.set(pos, self.get(pos) + value)
else: else:
for z in range(shape.top_left[2], shape.bottom_right[2] + 1): for z in range(shape.top_left.z, shape.bottom_right.z + 1):
pos = Coordinate(x, y, z) pos = Coordinate(x, y, z)
self.set(pos, self.get(pos) + value) self.set(pos, self.get(pos) + value)
@ -173,11 +165,6 @@ class Grid:
def getOnCount(self) -> int: def getOnCount(self) -> int:
return len(self.__grid) return len(self.__grid)
def find(self, value: Any) -> Iterable[Coordinate]:
for k, v in self.__grid.items():
if v == value:
yield k
def count(self, value: Any) -> int: def count(self, value: Any) -> int:
return list(self.__grid.values()).count(value) return list(self.__grid.values()).count(value)
@ -207,41 +194,29 @@ class Grid:
def isCorner(self, pos: Coordinate) -> bool: def isCorner(self, pos: Coordinate) -> bool:
return pos in self.getCorners() return pos in self.getCorners()
def isWithinBoundaries(self, pos: Coordinate, pad: int = 0) -> bool: def isWithinBoundaries(self, pos: Coordinate) -> bool:
if self.mode3D: if self.mode3D:
return ( return (
self.minX + pad <= pos[0] <= self.maxX - pad self.minX <= pos.x <= self.maxX
and self.minY + pad <= pos[1] <= self.maxY - pad and self.minY <= pos.y <= self.maxY
and self.minZ + pad <= pos[2] <= self.maxZ - pad and self.minZ <= pos.z <= self.maxZ
) )
else: else:
return self.minX + pad <= pos[0] <= self.maxX - pad and self.minY + pad <= pos[1] <= self.maxY - pad 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) -> Iterable[Coordinate]: def getActiveCells(
self, x: int = None, y: int = None, z: int = None
) -> List[Coordinate]:
if x is not None or y is not None or z is not None: if x is not None or y is not None or z is not None:
return ( return [
c c
for c in self.__grid.keys() for c in self.__grid.keys()
if (c[0] == x if x is not None else True) if (c.x == x if x is not None else True)
and (c[1] == y if y is not None else True) and (c.y == y if y is not None else True)
and (c[2] == z if z is not None else True) and (c.z == z if z is not None else True)
) ]
else: else:
return self.__grid.keys() return list(self.__grid.keys())
def getRegion(self, start: Coordinate, includeDiagonal: bool = False) -> Iterable[Coordinate]:
start_value = self.get(start)
queue = deque()
queue.append(start)
visited = set()
while queue:
next_coord = queue.popleft()
if next_coord in visited or not self.isWithinBoundaries(next_coord) or self.get(next_coord) != start_value:
continue
visited.add(next_coord)
yield next_coord
for n in self.getNeighboursOf(next_coord, includeDefault=True, includeDiagonal=includeDiagonal):
queue.append(n)
def getActiveRegion( def getActiveRegion(
self, self,
@ -285,7 +260,7 @@ class Grid:
pos: Coordinate, pos: Coordinate,
includeDefault: bool = False, includeDefault: bool = False,
includeDiagonal: bool = True, includeDiagonal: bool = True,
) -> Iterable[Coordinate]: ) -> List[Coordinate]:
neighbours = pos.getNeighbours( neighbours = pos.getNeighbours(
includeDiagonal=includeDiagonal, includeDiagonal=includeDiagonal,
minX=self.minX, minX=self.minX,
@ -326,47 +301,47 @@ class Grid:
if mode == GridTransformation.ROTATE_X: if mode == GridTransformation.ROTATE_X:
shift_z = self.maxY shift_z = self.maxY
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(c[0], c[2], -c[1]), v) self.set(Coordinate(c.x, c.z, -c.y), v)
self.shift(shift_z=shift_z) self.shift(shift_z=shift_z)
elif mode == GridTransformation.ROTATE_Y: elif mode == GridTransformation.ROTATE_Y:
shift_x = self.maxX shift_x = self.maxX
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(-c[2], c[1], c[0]), v) self.set(Coordinate(-c.z, c.y, c.x), v)
self.shift(shift_x=shift_x) self.shift(shift_x=shift_x)
elif mode == GridTransformation.ROTATE_Z: elif mode == GridTransformation.ROTATE_Z:
shift_x = self.maxX shift_x = self.maxX
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(-c[1], c[0], c[2]), v) self.set(Coordinate(-c.y, c.x, c.z), v)
self.shift(shift_x=shift_x) self.shift(shift_x=shift_x)
elif mode == GridTransformation.COUNTER_ROTATE_X: elif mode == GridTransformation.COUNTER_ROTATE_X:
shift_y = self.maxY shift_y = self.maxY
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(c[0], -c[2], c[1]), v) self.set(Coordinate(c.x, -c.z, c.y), v)
self.shift(shift_y=shift_y) self.shift(shift_y=shift_y)
elif mode == GridTransformation.COUNTER_ROTATE_Y: elif mode == GridTransformation.COUNTER_ROTATE_Y:
shift_z = self.maxZ shift_z = self.maxZ
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(c[2], c[1], -c[0]), v) self.set(Coordinate(c.z, c.y, -c.x), v)
self.shift(shift_z=shift_z) self.shift(shift_z=shift_z)
elif mode == GridTransformation.COUNTER_ROTATE_Z: elif mode == GridTransformation.COUNTER_ROTATE_Z:
shift_y = self.maxY shift_y = self.maxY
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(c[1], -c[0], c[2]), v) self.set(Coordinate(c.y, -c.x, c.z), v)
self.shift(shift_y=shift_y) self.shift(shift_y=shift_y)
elif mode == GridTransformation.FLIP_X: elif mode == GridTransformation.FLIP_X:
shift_x = self.maxX shift_x = self.maxX
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(-c[0], c[1], c[2]), v) self.set(Coordinate(-c.x, c.y, c.z), v)
self.shift(shift_x=shift_x) self.shift(shift_x=shift_x)
elif mode == GridTransformation.FLIP_Y: elif mode == GridTransformation.FLIP_Y:
shift_y = self.maxY shift_y = self.maxY
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(c[0], -c[1], c[2]), v) self.set(Coordinate(c.x, -c.y, c.z), v)
self.shift(shift_y=shift_y) self.shift(shift_y=shift_y)
elif mode == GridTransformation.FLIP_Z: elif mode == GridTransformation.FLIP_Z:
shift_z = self.maxZ shift_z = self.maxZ
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(c[0], c[1], -c[2]), v) self.set(Coordinate(c.x, c.y, -c.z), v)
self.shift(shift_z=shift_z) self.shift(shift_z=shift_z)
else: else:
raise NotImplementedError(mode) raise NotImplementedError(mode)
@ -382,9 +357,9 @@ class Grid:
self.__grid = {} self.__grid = {}
for c, v in coords.items(): for c, v in coords.items():
if self.mode3D: if self.mode3D:
nc = Coordinate(c[0] + shift_x, c[1] + shift_y, c[2] + shift_z) nc = Coordinate(c.x + shift_x, c.y + shift_y, c.z + shift_z)
else: else:
nc = Coordinate(c[0] + shift_x, c[1] + shift_y) nc = Coordinate(c.x + shift_x, c.y + shift_y)
self.set(nc, v) self.set(nc, v)
def shift_zero(self, recalc: bool = True): def shift_zero(self, recalc: bool = True):
@ -421,7 +396,9 @@ class Grid:
if c in came_from and self.get(c) in walls: if c in came_from and self.get(c) in walls:
continue continue
came_from[c] = current came_from[c] = current
if c == pos_to or (stop_at_first is not None and self.get(c) == stop_at_first): if c == pos_to or (
stop_at_first is not None and self.get(c) == stop_at_first
):
pos_to = c pos_to = c
found_end = True found_end = True
break break
@ -468,7 +445,9 @@ class Grid:
if currentCoord == pos_to: if currentCoord == pos_to:
break break
for neighbour in self.getNeighboursOf(currentCoord, includeDefault=True, includeDiagonal=includeDiagonal): for neighbour in self.getNeighboursOf(
currentCoord, includeDefault=True, includeDiagonal=includeDiagonal
):
if self.get(neighbour) in walls or neighbour in closedNodes: if self.get(neighbour) in walls or neighbour in closedNodes:
continue continue
@ -477,7 +456,9 @@ class Grid:
elif not includeDiagonal: elif not includeDiagonal:
neighbourDist = 1 neighbourDist = 1
else: else:
neighbourDist = currentCoord.getDistanceTo(neighbour, DistanceAlgorithm.MANHATTAN, includeDiagonal) neighbourDist = currentCoord.getDistanceTo(
neighbour, DistanceAlgorithm.MANHATTAN, includeDiagonal
)
targetDist = neighbour.getDistanceTo(pos_to) targetDist = neighbour.getDistanceTo(pos_to)
f_cost = targetDist + neighbourDist + currentNode[1] f_cost = targetDist + neighbourDist + currentNode[1]
@ -511,13 +492,17 @@ class Grid:
to_z: int = None, to_z: int = None,
) -> "Grid": ) -> "Grid":
if self.mode3D and (from_z is None or to_z is None): 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") 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 count_x, count_y, count_z = 0, 0, 0
new_grid = Grid(self.__default) new_grid = Grid(self.__default)
for x in range(from_x, to_x + 1): for x in range(from_x, to_x + 1):
for y in range(from_y, to_y + 1): for y in range(from_y, to_y + 1):
if not self.mode3D: if not self.mode3D:
new_grid.set(Coordinate(count_x, count_y), self.get(Coordinate(x, y))) new_grid.set(
Coordinate(count_x, count_y), self.get(Coordinate(x, y))
)
else: else:
for z in range(from_z, to_z + 1): for z in range(from_z, to_z + 1):
new_grid.set( new_grid.set(
@ -584,14 +569,20 @@ class Grid:
def get_aoc_ocr_string(self, x_shift: int = 0, y_shift: int = 0): def get_aoc_ocr_string(self, x_shift: int = 0, y_shift: int = 0):
return convert_array_6( return convert_array_6(
[ [
["#" if self.get(Coordinate(x + x_shift, y + y_shift)) else "." for x in self.rangeX()] [
"#" if self.get(Coordinate(x + x_shift, y + y_shift)) else "."
for x in self.rangeX()
]
for y in self.rangeY() for y in self.rangeY()
] ]
) )
def __str__(self, true_char: str = "#", false_char: str = "."): def __str__(self, true_char: str = "#", false_char: str = "."):
return "/".join( return "/".join(
"".join(true_char if self.get(Coordinate(x, y)) else false_char for x in range(self.minX, self.maxX + 1)) "".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) for y in range(self.minY, self.maxY + 1)
) )
@ -607,7 +598,11 @@ class Grid:
) -> "Grid": ) -> "Grid":
if translate is None: if translate is None:
translate = {} translate = {}
if true_char is not None and True not in translate.values() and true_char not in translate: 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 translate[true_char] = true_value if true_value is not None else True
ret = cls(default=default) ret = cls(default=default)
@ -625,56 +620,17 @@ class Grid:
return ret return ret
@classmethod def __eq__(self, other: Grid) -> bool:
def from_data( if not isinstance(other, Grid):
cls, return False
data: Iterable[Iterable],
default: Any = False,
translate: Mapping[str, Any] = None,
gen_3d: bool = False,
) -> Grid:
"""
Every entry in data will be treated as row, every entry in data[entry] will be a separate column.
gen_3d = True will just add z=0 to every Coordinate
translate is used on every data[entry] and if present as key, its value will be used instead
a value in translate can be a function with the following signature: def translate(value: Any) -> Any
a key in translate is either a string of len 1 or it will be treated as regexp
if multiple regexp match, the first encountered wins
if there is a key that matches the entry it wins over any mathing regexp
"""
grid = cls(default=default)
regex_in_translate = False other_active = set(other.getActiveCells())
if translate is not None: for c, v in self.__grid.items():
for k in translate: if other.get(c) != v:
if len(k) > 1: return False
regex_in_translate = True other_active.remove(c)
for y, row in enumerate(data): if other_active:
for x, col in enumerate(row): return False
if translate is not None and col in translate:
if isinstance(translate[col], Callable):
col = translate[col](col)
else:
col = translate[col]
elif regex_in_translate:
for k, v in translate.items():
if len(k) == 1:
continue
if re.search(k, col): return True
if isinstance(v, Callable):
col = translate[k](col)
else:
col = v
break
if gen_3d:
grid.set(Coordinate(x, y, 0), col)
else:
grid.set(Coordinate(x, y), col)
return grid
def __hash__(self):
return hash(frozenset(self.__grid.items()))

View File

@ -1,20 +1,12 @@
def factorial(n: int, start: int = 1) -> int: import math
def factorial(n: int) -> int:
""" """
n! = 1 * 2 * 3 * 4 * ... * n n! = 1 * 2 * 3 * 4 * ... * n
1, 1, 2, 6, 24, 120, 720, ... 1, 1, 2, 6, 24, 120, 720, ...
If you're looking for efficiency with start == 1, just use math.factorial(n)
which takes just 25% of the compute time on average, but this is the fastest
pure python implementation I could come up with and it allows for partial
multiplications, like 5 * 6 * 7 * 8 * .... * 17
""" """
if start == n: return math.factorial(n)
return n
if n - start == 1:
return n * start
middle = start + (n - start) // 2
return factorial(middle, start) * factorial(n, middle + 1)
def fibonacci(n: int) -> int: def fibonacci(n: int) -> int:
@ -46,10 +38,3 @@ def pentagonal(n: int) -> int:
0, 1, 5, 12, 22, 35, ... 0, 1, 5, 12, 22, 35, ...
""" """
return ((3 * n * n) - n) // 2 return ((3 * n * n) - n) // 2
def hexagonal(n: int) -> int:
if n == 1:
return 1
return n * 2 + (n - 1) * 2 + (n - 2) * 2 + hexagonal(n - 1)

View File

@ -1,11 +1,9 @@
from __future__ import annotations from __future__ import annotations
from time import sleep
from .schedule import Scheduler from .schedule import Scheduler
from .simplesocket import ClientSocket from .simplesocket import ClientSocket
from base64 import b64encode
from collections import deque
from datetime import timedelta from datetime import timedelta
from enum import Enum from enum import Enum
from time import sleep
from typing import Callable from typing import Callable
@ -157,15 +155,6 @@ class ServerMessage(str, Enum):
ERR_NOOPERHOST = "491" ERR_NOOPERHOST = "491"
ERR_UMODEUNKNOWNFLAG = "501" ERR_UMODEUNKNOWNFLAG = "501"
ERR_USERSDONTMATCH = "502" ERR_USERSDONTMATCH = "502"
RPL_LOGGEDIN = "900"
RPL_LOGGEDOUT = "901"
ERR_NICKLOCKED = "902"
RPL_SASLSUCCESS = "903"
ERR_SASLFAIL = "904"
ERR_SASLTOOLONG = "905"
ERR_SASLABORTED = "906"
ERR_SASLALREADY = "907"
RPL_SASLMECHS = "908"
MSG_NICK = "NICK" MSG_NICK = "NICK"
MSG_TOPIC = "TOPIC" MSG_TOPIC = "TOPIC"
MSG_MODE = "MODE" MSG_MODE = "MODE"
@ -232,30 +221,20 @@ class Client:
nick: str, nick: str,
username: str, username: str,
realname: str = "Python Bot", realname: str = "Python Bot",
sasl_password: str = None,
): ):
self.__connected = False
self.__send_queue = deque()
self.__userlist = {} self.__userlist = {}
self.__channellist = {} self.__channellist = {}
self.__server_socket = ClientSocket(server, port) self.__server_socket = ClientSocket(server, port)
self.__server_socket.sendline(
if sasl_password is not None: "USER %s ignore ignore :%s" % (username, realname)
self.__server_socket.sendline("CAP REQ :sasl") )
self.__server_socket.sendline("USER %s ignore ignore :%s" % (username, realname))
self.__server_socket.sendline("NICK %s" % nick) self.__server_socket.sendline("NICK %s" % nick)
if sasl_password is not None:
self.__server_socket.sendline("AUTHENTICATE PLAIN")
auth_string = b64encode(bytes("%s\0%s\0%s" % (nick, nick, sasl_password), "utf-8")).decode("ascii")
self.__server_socket.sendline("AUTHENTICATE %s" % auth_string)
self.__server_caps = {"MAXLEN": 255} self.__server_caps = {"MAXLEN": 255}
self.__function_register = { self.__function_register = {
ServerMessage.RPL_WELCOME: [self.on_rpl_welcome], ServerMessage.RPL_WELCOME: [self.on_rpl_welcome],
ServerMessage.RPL_TOPIC: [self.on_rpl_topic], ServerMessage.RPL_TOPIC: [self.on_rpl_topic],
ServerMessage.RPL_ISUPPORT: [self.on_rpl_isupport], ServerMessage.RPL_ISUPPORT: [self.on_rpl_isupport],
ServerMessage.ERR_NICKNAMEINUSE: [self.on_err_nicknameinuse], ServerMessage.ERR_NICKNAMEINUSE: [self.on_err_nicknameinuse],
ServerMessage.RPL_LOGGEDIN: [self.on_auth],
ServerMessage.MSG_JOIN: [self.on_join], ServerMessage.MSG_JOIN: [self.on_join],
ServerMessage.MSG_PART: [self.on_part], ServerMessage.MSG_PART: [self.on_part],
ServerMessage.MSG_QUIT: [self.on_quit], ServerMessage.MSG_QUIT: [self.on_quit],
@ -264,17 +243,6 @@ class Client:
} }
self.receive() self.receive()
def send_raw(self, msg: str):
self.__send_queue.append(msg)
def send_queue(self):
if not self.__connected or not self.__send_queue:
return
msg = self.__send_queue.popleft()
print(f"-> {msg}")
self.__server_socket.sendline(msg)
def receive(self): def receive(self):
while line := self.__server_socket.recvline(): while line := self.__server_socket.recvline():
line = line.strip() line = line.strip()
@ -299,7 +267,10 @@ class Client:
if "!" in msg_from and msg_from not in self.__userlist: if "!" in msg_from and msg_from not in self.__userlist:
self.__userlist[msg_from] = User(msg_from) self.__userlist[msg_from] = User(msg_from)
if self.__userlist[msg_from].nickname == self.__userlist[self.__my_user].nickname: if (
self.__userlist[msg_from].nickname
== self.__userlist[self.__my_user].nickname
):
del self.__userlist[self.__my_user] del self.__userlist[self.__my_user]
self.__my_user = msg_from self.__my_user = msg_from
@ -313,14 +284,9 @@ class Client:
else: else:
self.__function_register[msg_type] = [func] self.__function_register[msg_type] = [func]
def on_auth(self, msg_from: str, msg_to: str, message: str):
print("authed")
self.__server_socket.sendline("CAP END")
def on_rpl_welcome(self, msg_from: str, msg_to: str, message: str): def on_rpl_welcome(self, msg_from: str, msg_to: str, message: str):
self.__my_user = message.split()[-1] self.__my_user = message.split()[-1]
self.__userlist[self.__my_user] = User(self.__my_user) self.__userlist[self.__my_user] = User(self.__my_user)
self.__connected = True
def on_rpl_isupport(self, msg_from: str, msg_to: str, message: str): def on_rpl_isupport(self, msg_from: str, msg_to: str, message: str):
for cap in message.split(): for cap in message.split():
@ -344,7 +310,9 @@ class Client:
def on_nick(self, msg_from: str, msg_to: str, message: str): def on_nick(self, msg_from: str, msg_to: str, message: str):
self.__userlist[msg_from].nick(msg_to) self.__userlist[msg_from].nick(msg_to)
self.__userlist[self.__userlist[msg_from].identifier] = self.__userlist[msg_from] self.__userlist[self.__userlist[msg_from].identifier] = self.__userlist[
msg_from
]
del self.__userlist[msg_from] del self.__userlist[msg_from]
def on_join(self, msg_from: str, msg_to: str, message: str): def on_join(self, msg_from: str, msg_to: str, message: str):
@ -374,21 +342,21 @@ class Client:
print(msg_from, msg_type, msg_to, message) print(msg_from, msg_type, msg_to, message)
def nick(self, new_nick: str): def nick(self, new_nick: str):
self.send_raw("NICK %s" % new_nick) self.__server_socket.sendline("NICK %s" % new_nick)
def join(self, channel: str): def join(self, channel: str):
self.send_raw("JOIN %s" % channel) self.__server_socket.sendline("JOIN %s" % channel)
self.receive() self.receive()
def part(self, channel: str): def part(self, channel: str):
self.send_raw("PART %s" % channel) self.__server_socket.sendline("PART %s" % channel)
self.receive() self.receive()
def privmsg(self, target: str, message: str): def privmsg(self, target: str, message: str):
self.send_raw("PRIVMSG %s :%s" % (target, message)) self.__server_socket.sendline("PRIVMSG %s :%s" % (target, message))
def quit(self, message: str = "Elvis has left the building!"): def quit(self, message: str = "Elvis has left the building!"):
self.send_raw("QUIT :%s" % message) self.__server_socket.sendline("QUIT :%s" % message)
self.receive() self.receive()
self.__server_socket.close() self.__server_socket.close()
@ -421,9 +389,8 @@ class IrcBot(Client):
nick: str, nick: str,
username: str, username: str,
realname: str = "Python Bot", realname: str = "Python Bot",
sasl_password: str = None,
): ):
super().__init__(server, port, nick, username, realname, sasl_password) super().__init__(server, port, nick, username, realname)
self._scheduler = Scheduler() self._scheduler = Scheduler()
self._channel_commands = {} self._channel_commands = {}
self._privmsg_commands = {} self._privmsg_commands = {}
@ -448,8 +415,13 @@ class IrcBot(Client):
if not message: if not message:
return return
command = message.split()[0] command = message.split()[0]
if msg_to in self._channel_commands and command in self._channel_commands[msg_to]: if (
self._channel_commands[msg_to][command](msg_from, " ".join(message.split()[1:])) 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: if msg_to == self.getUser().nickname and command in self._privmsg_commands:
self._privmsg_commands[command](msg_from, " ".join(message.split()[1:])) self._privmsg_commands[command](msg_from, " ".join(message.split()[1:]))
@ -457,6 +429,5 @@ class IrcBot(Client):
def run(self): def run(self):
while True: while True:
self._scheduler.run_pending() self._scheduler.run_pending()
self.send_queue()
self.receive() self.receive()
sleep(0.01) sleep(0.01)

View File

@ -1,41 +0,0 @@
from math import factorial, comb
from typing import Sized, Iterator
def len_combinations(iterable: Sized, r: int) -> int:
"""How many options will itertools.combinations(iterable, r) yield?"""
n = len(iterable)
if r > n:
return 0
else:
return factorial(n) // factorial(r) // factorial(n - r)
def len_permutations(iterable: Sized, r: int) -> int:
"""How many options will itertools.permutations(iterable, r) yield?"""
n = len(iterable)
if r > n:
return 0
else:
return factorial(n) // factorial(n - r)
def combinations_of_sum(total_sum: int, length: int = None, min_value: int = 0) -> Iterator[tuple[int]]:
if length is None:
length = total_sum
if length == 1:
yield (total_sum,)
else:
for value in range(min_value, total_sum + 1):
for permutation in combinations_of_sum(total_sum - value, length - 1, min_value):
yield (value,) + permutation
def len_combinations_of_sum(total_sum: int, length: int = None, min_value: int = 0) -> int:
"""
How many options will combinations_of_sum(total_sum, length) yield?
No idea how to factor in min_value, yet, so if using min_value, the answer will always be too high
"""
return comb(total_sum + length - 1, total_sum)

View File

@ -6,8 +6,8 @@ from typing import Any
@dataclass @dataclass
class Node: class Node:
value: Any value: Any
next: Node = None next: "Node" = None
prev: Node = None prev: "Node" = None
class LinkedList: class LinkedList:

View File

@ -1,15 +1,6 @@
from __future__ import annotations from __future__ import annotations
import math import math
from decimal import Decimal, ROUND_HALF_UP from decimal import Decimal, ROUND_HALF_UP
from typing import Iterable
MAXINT32 = 2**32
MAXINT64 = 2**64
MAXINT32_SIGNED = 2**31
MAXINT64_SIGNED = 2**63
MAXINT32_UNSIGNED = MAXINT32
MAXINT64_UNSIGNED = MAXINT64
def round_half_up(number: int | float) -> int: def round_half_up(number: int | float) -> int:
@ -25,20 +16,3 @@ def get_factors(num: int) -> set:
f.add(num // x) f.add(num // x)
return f return f
def mul(ints: Iterable[int]) -> int:
"""similar to sum(), just for multiplication"""
ret = 1
for x in ints:
ret *= x
return ret
def magnitude(value: int | float) -> int:
return math.floor(math.log10(value))
def concat(value1: int, value2: int) -> int:
return value1 * (10 ** (1 + magnitude(value2))) + value2

View File

@ -8,7 +8,6 @@ class StopWatch:
stopped: int | None = None stopped: int | None = None
def __init__(self, auto_start=True): def __init__(self, auto_start=True):
self.total_elapsed = 0
if auto_start: if auto_start:
self.start() self.start()
@ -16,13 +15,11 @@ class StopWatch:
self.started = perf_counter_ns() self.started = perf_counter_ns()
self.stopped = None self.stopped = None
def stop(self): def stop(self) -> float:
self.stopped = perf_counter_ns() self.stopped = perf_counter_ns()
self.total_elapsed += self.elapsed() return self.elapsed()
def reset(self): reset = start
self.total_elapsed = 0
self.start()
def elapsed(self) -> int: def elapsed(self) -> int:
if self.stopped is None: if self.stopped is None:
@ -31,10 +28,10 @@ class StopWatch:
return self.stopped - self.started return self.stopped - self.started
def elapsed_string(self) -> str: def elapsed_string(self) -> str:
return human_readable_time_from_ns(self.total_elapsed) return human_readable_time_from_ns(self.elapsed())
def avg_elapsed(self, divider: int) -> float: def avg_elapsed(self, divider: int) -> float:
return self.total_elapsed / divider return self.elapsed() / divider
def avg_string(self, divider: int) -> str: def avg_string(self, divider: int) -> str:
return human_readable_time_from_ns(int(self.avg_elapsed(divider))) return human_readable_time_from_ns(int(self.avg_elapsed(divider)))

View File

@ -2,6 +2,8 @@ import datetime
import inspect import inspect
import os.path import os.path
import sys import sys
from fishhook import hook
from functools import wraps
from typing import Any from typing import Any
@ -44,7 +46,9 @@ class Dict(dict):
self[k] = Dict(self[k]) self[k] = Dict(self[k])
elif isinstance(self[k], list): elif isinstance(self[k], list):
for i in range(len(self[k])): for i in range(len(self[k])):
if isinstance(self[k][i], dict) and not isinstance(self[k][i], Dict): if isinstance(self[k][i], dict) and not isinstance(
self[k][i], Dict
):
self[k][i] = Dict(self[k][i]) self[k][i] = Dict(self[k][i])
def update(self, other: dict, **kwargs): def update(self, other: dict, **kwargs):
@ -100,7 +104,14 @@ def minmax(*arr: Any) -> (Any, Any):
else: else:
return arr[0], arr[0] return arr[0], arr[0]
return min(arr), max(arr) 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: def human_readable_time_from_delta(delta: datetime.timedelta) -> str:
@ -109,16 +120,16 @@ def human_readable_time_from_delta(delta: datetime.timedelta) -> str:
time_str += "%d day%s, " % (delta.days, "s" if delta.days > 1 else "") time_str += "%d day%s, " % (delta.days, "s" if delta.days > 1 else "")
if delta.seconds > 3600: if delta.seconds > 3600:
time_str += "%02d hours, " % (delta.seconds // 3600) time_str += "%02d:" % (delta.seconds // 3600)
else: else:
time_str += "" time_str += "00:"
if delta.seconds % 3600 > 60: if delta.seconds % 3600 > 60:
time_str += "%02d minutes, " % (delta.seconds % 3600 // 60) time_str += "%02d:" % (delta.seconds % 3600 // 60)
else: else:
time_str += "" time_str += "00:"
return time_str + "%02d seconds" % (delta.seconds % 60) return time_str + "%02d" % (delta.seconds % 60)
def human_readable_time_from_ns(ns: int) -> str: def human_readable_time_from_ns(ns: int) -> str:
@ -138,3 +149,51 @@ def human_readable_time_from_ns(ns: int) -> str:
time_parts.insert(0, "%d%s" % (p, unit)) time_parts.insert(0, "%d%s" % (p, unit))
if ns == 0: if ns == 0:
return ", ".join(time_parts) return ", ".join(time_parts)
def cache(func):
saved = {}
@wraps(func)
def new_func(*args):
if args in saved:
return saved[args]
result = func(*args)
saved[args] = result
return result
return new_func
@hook(list)
def intersection(self, *args) -> list:
ret = set(self).intersection(*args)
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))
@hook(str)
def __and__(self, *args) -> str:
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

View File

@ -1,90 +0,0 @@
from __future__ import annotations
from collections.abc import Iterable
from math import ceil
class Integer(int):
def digits(self) -> Iterable[int]:
for x in str(self):
yield int(x)
def digits_sum(self) -> int:
return sum(int(x) for x in str(self))
class String(str):
def swap(self, x: int, y: int) -> String:
x, y = min(x, y), max(x, y)
return String(self[:x] + self[y] + self[x + 1 : y] + self[x] + self[y + 1 :])
def rotate(self, n: int) -> String:
if n == 0:
return self
while n < 0:
n += len(self)
while n > len(self):
n -= len(self)
return String(self[-n:] + self[: len(self) - n])
def reverse(self, start: int = 0, end: int = None) -> String:
if end is None:
end = len(self) - 1
return String(self[:start] + "".join(reversed(self[start : end + 1])) + self[end + 1 :])
def __getitem__(self, item) -> String:
return String(super().__getitem__(item))
def __floordiv__(self, other: int) -> list[String]:
d = ceil(len(self) / other)
return [self[i * d:i * d + d] for i in range(other)]
def __truediv__(self, other: int) -> list[String]:
if len(self) % other != 0:
raise ValueError("String length is Not a multiple of {}".format(other))
d = len(self) // other
return [self[i * d:i * d + d] for i in range(other)]
class List(list):
def moving_window(self, size: int = 2) -> Iterable[List]:
if len(self) % size != 0:
raise ValueError("number of list items must be divisible by size")
for i in range(len(self) // size):
this_window = List()
for j in range(size):
this_window.append(self[i * size + j])
yield this_window
class Range:
def __init__(self, start: int, end: int) -> None:
self.start = start
self.end = end
def overlaps(self, other: Range) -> bool:
return other.start <= self.start <= other.end or other.start <= self.end <= other.end or (other.start <= self.start and other.end >= self.end) or (self.start <= other.start and self.end >= other.end)
def merge(self, other: Range) -> Range:
if not self.overlaps(other):
raise ValueError("Ranges do not overlap")
return Range(min(self.start, other.start), max(self.end, other.end))
def __contains__(self, item: int) -> bool:
return self.start <= item <= self.end
def __len__(self) -> int:
return self.end - self.start + 1
def __iter__(self) -> Iterable[int]:
for x in range(self.start, self.end + 1):
yield x
def __reversed__(self) -> Iterable[int]:
for x in range(self.end, self.start - 1, -1):
yield x

View File

@ -1,657 +0,0 @@
from __future__ import annotations
from .coordinate import Line, Coordinate, Rectangle
from enum import Enum
import tkinter as tk
class Color(str, Enum):
ALICEBLUE = "#F0F8FF"
ANTIQUEWHITE = "#FAEBD7"
ANTIQUEWHITE1 = "#FFEFDB"
ANTIQUEWHITE2 = "#EEDFCC"
ANTIQUEWHITE3 = "#CDC0B0"
ANTIQUEWHITE4 = "#8B8378"
AQUA = "#00FFFF"
AQUAMARINE1 = "#7FFFD4"
AQUAMARINE2 = "#76EEC6"
AQUAMARINE3 = "#66CDAA"
AQUAMARINE4 = "#458B74"
AZURE = "#007FFF"
AZURE1 = "#F0FFFF"
AZURE2 = "#E0EEEE"
AZURE3 = "#C1CDCD"
AZURE4 = "#838B8B"
BANANA = "#E3CF57"
BEIGE = "#F5F5DC"
BISQUE1 = "#FFE4C4"
BISQUE2 = "#EED5B7"
BISQUE3 = "#CDB79E"
BISQUE4 = "#8B7D6B"
BLACK = "#000000"
BLANCHEDALMOND = "#FFEBCD"
BLUE = "#0000FF"
BLUE2 = "#0000EE"
BLUE3 = "#0000CD"
BLUE4 = "#00008B"
BLUEVIOLET = "#8A2BE2"
BRICK = "#9C661F"
BROWN = "#A52A2A"
BROWN1 = "#FF4040"
BROWN2 = "#EE3B3B"
BROWN3 = "#CD3333"
BROWN4 = "#8B2323"
BURLYWOOD = "#DEB887"
BURLYWOOD1 = "#FFD39B"
BURLYWOOD2 = "#EEC591"
BURLYWOOD3 = "#CDAA7D"
BURLYWOOD4 = "#8B7355"
BURNTSIENNA = "#8A360F"
BURNTUMBER = "#8A3324"
CADETBLUE = "#5F9EA0"
CADETBLUE1 = "#98F5FF"
CADETBLUE2 = "#8EE5EE"
CADETBLUE3 = "#7AC5CD"
CADETBLUE4 = "#53868B"
CADMIUMORANGE = "#FF6103"
CADMIUMYELLOW = "#FF9912"
CARROT = "#ED9121"
CHARTREUSE = "#DFFF00"
CHARTREUSE1 = "#7FFF00"
CHARTREUSE2 = "#76EE00"
CHARTREUSE3 = "#66CD00"
CHARTREUSE4 = "#458B00"
CHOCOLATE = "#D2691E"
CHOCOLATE1 = "#FF7F24"
CHOCOLATE2 = "#EE7621"
CHOCOLATE3 = "#CD661D"
CHOCOLATE4 = "#8B4513"
COBALT = "#3D59AB"
COBALTGREEN = "#3D9140"
COLDGREY = "#808A87"
CORAL = "#FF7F50"
CORAL1 = "#FF7256"
CORAL2 = "#EE6A50"
CORAL3 = "#CD5B45"
CORAL4 = "#8B3E2F"
CORNFLOWERBLUE = "#6495ED"
CORNSILK = "#FFF8DC"
CORNSILK1 = "#FFF8DC"
CORNSILK2 = "#EEE8CD"
CORNSILK3 = "#CDC8B1"
CORNSILK4 = "#8B8878"
CRIMSON = "#DC143C"
CYAN = "#00FFFF"
CYAN2 = "#00EEEE"
CYAN3 = "#00CDCD"
CYAN4 = "#008B8B"
DARKGOLDENROD = "#B8860B"
DARKGOLDENROD1 = "#FFB90F"
DARKGOLDENROD2 = "#EEAD0E"
DARKGOLDENROD3 = "#CD950C"
DARKGOLDENROD4 = "#8B6508"
DARKGRAY = "#A9A9A9"
DARKGREEN = "#006400"
DARKKHAKI = "#BDB76B"
DARKOLIVEGREEN = "#556B2F"
DARKOLIVEGREEN1 = "#CAFF70"
DARKOLIVEGREEN2 = "#BCEE68"
DARKOLIVEGREEN3 = "#A2CD5A"
DARKOLIVEGREEN4 = "#6E8B3D"
DARKORANGE = "#FF8C00"
DARKORANGE1 = "#FF7F00"
DARKORANGE2 = "#EE7600"
DARKORANGE3 = "#CD6600"
DARKORANGE4 = "#8B4500"
DARKORCHID = "#9932CC"
DARKORCHID1 = "#BF3EFF"
DARKORCHID2 = "#B23AEE"
DARKORCHID3 = "#9A32CD"
DARKORCHID4 = "#68228B"
DARKSALMON = "#E9967A"
DARKSEAGREEN = "#8FBC8F"
DARKSEAGREEN1 = "#C1FFC1"
DARKSEAGREEN2 = "#B4EEB4"
DARKSEAGREEN3 = "#9BCD9B"
DARKSEAGREEN4 = "#698B69"
DARKSLATEBLUE = "#483D8B"
DARKSLATEGRAY = "#2F4F4F"
DARKSLATEGRAY1 = "#97FFFF"
DARKSLATEGRAY2 = "#8DEEEE"
DARKSLATEGRAY3 = "#79CDCD"
DARKSLATEGRAY4 = "#528B8B"
DARKTURQUOISE = "#00CED1"
DARKVIOLET = "#9400D3"
DEEPPINK1 = "#FF1493"
DEEPPINK2 = "#EE1289"
DEEPPINK3 = "#CD1076"
DEEPPINK4 = "#8B0A50"
DEEPSKYBLUE = "#00BFFF"
DEEPSKYBLUE1 = "#00BFFF"
DEEPSKYBLUE2 = "#00B2EE"
DEEPSKYBLUE3 = "#009ACD"
DEEPSKYBLUE4 = "#00688B"
DIMGRAY = "#696969"
DODGERBLUE1 = "#1E90FF"
DODGERBLUE2 = "#1C86EE"
DODGERBLUE3 = "#1874CD"
DODGERBLUE4 = "#104E8B"
EGGSHELL = "#FCE6C9"
EMERALDGREEN = "#00C957"
FIREBRICK = "#B22222"
FIREBRICK1 = "#FF3030"
FIREBRICK2 = "#EE2C2C"
FIREBRICK3 = "#CD2626"
FIREBRICK4 = "#8B1A1A"
FLESH = "#FF7D40"
FLORALWHITE = "#FFFAF0"
FORESTGREEN = "#228B22"
GAINSBORO = "#DCDCDC"
GHOSTWHITE = "#F8F8FF"
GOLD1 = "#FFD700"
GOLD2 = "#EEC900"
GOLD3 = "#CDAD00"
GOLD4 = "#8B7500"
GOLDENROD = "#DAA520"
GOLDENROD1 = "#FFC125"
GOLDENROD2 = "#EEB422"
GOLDENROD3 = "#CD9B1D"
GOLDENROD4 = "#8B6914"
GRAY = "#808080"
GRAY1 = "#030303"
GRAY10 = "#1A1A1A"
GRAY11 = "#1C1C1C"
GRAY12 = "#1F1F1F"
GRAY13 = "#212121"
GRAY14 = "#242424"
GRAY15 = "#262626"
GRAY16 = "#292929"
GRAY17 = "#2B2B2B"
GRAY18 = "#2E2E2E"
GRAY19 = "#303030"
GRAY2 = "#050505"
GRAY20 = "#333333"
GRAY21 = "#363636"
GRAY22 = "#383838"
GRAY23 = "#3B3B3B"
GRAY24 = "#3D3D3D"
GRAY25 = "#404040"
GRAY26 = "#424242"
GRAY27 = "#454545"
GRAY28 = "#474747"
GRAY29 = "#4A4A4A"
GRAY3 = "#080808"
GRAY30 = "#4D4D4D"
GRAY31 = "#4F4F4F"
GRAY32 = "#525252"
GRAY33 = "#545454"
GRAY34 = "#575757"
GRAY35 = "#595959"
GRAY36 = "#5C5C5C"
GRAY37 = "#5E5E5E"
GRAY38 = "#616161"
GRAY39 = "#636363"
GRAY4 = "#0A0A0A"
GRAY40 = "#666666"
GRAY42 = "#6B6B6B"
GRAY43 = "#6E6E6E"
GRAY44 = "#707070"
GRAY45 = "#737373"
GRAY46 = "#757575"
GRAY47 = "#787878"
GRAY48 = "#7A7A7A"
GRAY49 = "#7D7D7D"
GRAY5 = "#0D0D0D"
GRAY50 = "#7F7F7F"
GRAY51 = "#828282"
GRAY52 = "#858585"
GRAY53 = "#878787"
GRAY54 = "#8A8A8A"
GRAY55 = "#8C8C8C"
GRAY56 = "#8F8F8F"
GRAY57 = "#919191"
GRAY58 = "#949494"
GRAY59 = "#969696"
GRAY6 = "#0F0F0F"
GRAY60 = "#999999"
GRAY61 = "#9C9C9C"
GRAY62 = "#9E9E9E"
GRAY63 = "#A1A1A1"
GRAY64 = "#A3A3A3"
GRAY65 = "#A6A6A6"
GRAY66 = "#A8A8A8"
GRAY67 = "#ABABAB"
GRAY68 = "#ADADAD"
GRAY69 = "#B0B0B0"
GRAY7 = "#121212"
GRAY70 = "#B3B3B3"
GRAY71 = "#B5B5B5"
GRAY72 = "#B8B8B8"
GRAY73 = "#BABABA"
GRAY74 = "#BDBDBD"
GRAY75 = "#BFBFBF"
GRAY76 = "#C2C2C2"
GRAY77 = "#C4C4C4"
GRAY78 = "#C7C7C7"
GRAY79 = "#C9C9C9"
GRAY8 = "#141414"
GRAY80 = "#CCCCCC"
GRAY81 = "#CFCFCF"
GRAY82 = "#D1D1D1"
GRAY83 = "#D4D4D4"
GRAY84 = "#D6D6D6"
GRAY85 = "#D9D9D9"
GRAY86 = "#DBDBDB"
GRAY87 = "#DEDEDE"
GRAY88 = "#E0E0E0"
GRAY89 = "#E3E3E3"
GRAY9 = "#171717"
GRAY90 = "#E5E5E5"
GRAY91 = "#E8E8E8"
GRAY92 = "#EBEBEB"
GRAY93 = "#EDEDED"
GRAY94 = "#F0F0F0"
GRAY95 = "#F2F2F2"
GRAY97 = "#F7F7F7"
GRAY98 = "#FAFAFA"
GRAY99 = "#FCFCFC"
GREEN = "#008000"
GREEN1 = "#00FF00"
GREEN2 = "#00EE00"
GREEN3 = "#00CD00"
GREEN4 = "#008B00"
GREENYELLOW = "#ADFF2F"
HONEYDEW1 = "#F0FFF0"
HONEYDEW2 = "#E0EEE0"
HONEYDEW3 = "#C1CDC1"
HONEYDEW4 = "#838B83"
HOTPINK = "#FF69B4"
HOTPINK1 = "#FF6EB4"
HOTPINK2 = "#EE6AA7"
HOTPINK3 = "#CD6090"
HOTPINK4 = "#8B3A62"
INDIANRED = "#CD5C5C"
INDIANRED1 = "#FF6A6A"
INDIANRED2 = "#EE6363"
INDIANRED3 = "#CD5555"
INDIANRED4 = "#8B3A3A"
INDIGO = "#4B0082"
IVORY1 = "#FFFFF0"
IVORY2 = "#EEEEE0"
IVORY3 = "#CDCDC1"
IVORY4 = "#8B8B83"
IVORYBLACK = "#292421"
KHAKI = "#F0E68C"
KHAKI1 = "#FFF68F"
KHAKI2 = "#EEE685"
KHAKI3 = "#CDC673"
KHAKI4 = "#8B864E"
LAVENDER = "#E6E6FA"
LAVENDERBLUSH1 = "#FFF0F5"
LAVENDERBLUSH2 = "#EEE0E5"
LAVENDERBLUSH3 = "#CDC1C5"
LAVENDERBLUSH4 = "#8B8386"
LAWNGREEN = "#7CFC00"
LEMONCHIFFON1 = "#FFFACD"
LEMONCHIFFON2 = "#EEE9BF"
LEMONCHIFFON3 = "#CDC9A5"
LEMONCHIFFON4 = "#8B8970"
LIGHTBLUE = "#ADD8E6"
LIGHTBLUE1 = "#BFEFFF"
LIGHTBLUE2 = "#B2DFEE"
LIGHTBLUE3 = "#9AC0CD"
LIGHTBLUE4 = "#68838B"
LIGHTCORAL = "#F08080"
LIGHTCYAN1 = "#E0FFFF"
LIGHTCYAN2 = "#D1EEEE"
LIGHTCYAN3 = "#B4CDCD"
LIGHTCYAN4 = "#7A8B8B"
LIGHTGOLDENROD1 = "#FFEC8B"
LIGHTGOLDENROD2 = "#EEDC82"
LIGHTGOLDENROD3 = "#CDBE70"
LIGHTGOLDENROD4 = "#8B814C"
LIGHTGOLDENRODYELLOW = "#FAFAD2"
LIGHTGREY = "#D3D3D3"
LIGHTPINK = "#FFB6C1"
LIGHTPINK1 = "#FFAEB9"
LIGHTPINK2 = "#EEA2AD"
LIGHTPINK3 = "#CD8C95"
LIGHTPINK4 = "#8B5F65"
LIGHTSALMON1 = "#FFA07A"
LIGHTSALMON2 = "#EE9572"
LIGHTSALMON3 = "#CD8162"
LIGHTSALMON4 = "#8B5742"
LIGHTSEAGREEN = "#20B2AA"
LIGHTSKYBLUE = "#87CEFA"
LIGHTSKYBLUE1 = "#B0E2FF"
LIGHTSKYBLUE2 = "#A4D3EE"
LIGHTSKYBLUE3 = "#8DB6CD"
LIGHTSKYBLUE4 = "#607B8B"
LIGHTSLATEBLUE = "#8470FF"
LIGHTSLATEGRAY = "#778899"
LIGHTSTEELBLUE = "#B0C4DE"
LIGHTSTEELBLUE1 = "#CAE1FF"
LIGHTSTEELBLUE2 = "#BCD2EE"
LIGHTSTEELBLUE3 = "#A2B5CD"
LIGHTSTEELBLUE4 = "#6E7B8B"
LIGHTYELLOW1 = "#FFFFE0"
LIGHTYELLOW2 = "#EEEED1"
LIGHTYELLOW3 = "#CDCDB4"
LIGHTYELLOW4 = "#8B8B7A"
LIMEGREEN = "#32CD32"
LINEN = "#FAF0E6"
MAGENTA = "#FF00FF"
MAGENTA2 = "#EE00EE"
MAGENTA3 = "#CD00CD"
MAGENTA4 = "#8B008B"
MANGANESEBLUE = "#03A89E"
MAROON = "#800000"
MAROON1 = "#FF34B3"
MAROON2 = "#EE30A7"
MAROON3 = "#CD2990"
MAROON4 = "#8B1C62"
MEDIUMORCHID = "#BA55D3"
MEDIUMORCHID1 = "#E066FF"
MEDIUMORCHID2 = "#D15FEE"
MEDIUMORCHID3 = "#B452CD"
MEDIUMORCHID4 = "#7A378B"
MEDIUMPURPLE = "#9370DB"
MEDIUMPURPLE1 = "#AB82FF"
MEDIUMPURPLE2 = "#9F79EE"
MEDIUMPURPLE3 = "#8968CD"
MEDIUMPURPLE4 = "#5D478B"
MEDIUMSEAGREEN = "#3CB371"
MEDIUMSLATEBLUE = "#7B68EE"
MEDIUMSPRINGGREEN = "#00FA9A"
MEDIUMTURQUOISE = "#48D1CC"
MEDIUMVIOLETRED = "#C71585"
MELON = "#E3A869"
MIDNIGHTBLUE = "#191970"
MINT = "#BDFCC9"
MINTCREAM = "#F5FFFA"
MISTYROSE1 = "#FFE4E1"
MISTYROSE2 = "#EED5D2"
MISTYROSE3 = "#CDB7B5"
MISTYROSE4 = "#8B7D7B"
MOCCASIN = "#FFE4B5"
NAVAJOWHITE1 = "#FFDEAD"
NAVAJOWHITE2 = "#EECFA1"
NAVAJOWHITE3 = "#CDB38B"
NAVAJOWHITE4 = "#8B795E"
NAVY = "#000080"
OLDLACE = "#FDF5E6"
OLIVE = "#808000"
OLIVEDRAB = "#6B8E23"
OLIVEDRAB1 = "#C0FF3E"
OLIVEDRAB2 = "#B3EE3A"
OLIVEDRAB3 = "#9ACD32"
OLIVEDRAB4 = "#698B22"
ORANGE = "#FF8000"
ORANGE1 = "#FFA500"
ORANGE2 = "#EE9A00"
ORANGE3 = "#CD8500"
ORANGE4 = "#8B5A00"
ORANGERED1 = "#FF4500"
ORANGERED2 = "#EE4000"
ORANGERED3 = "#CD3700"
ORANGERED4 = "#8B2500"
ORCHID = "#DA70D6"
ORCHID1 = "#FF83FA"
ORCHID2 = "#EE7AE9"
ORCHID3 = "#CD69C9"
ORCHID4 = "#8B4789"
PALEGOLDENROD = "#EEE8AA"
PALEGREEN = "#98FB98"
PALEGREEN1 = "#9AFF9A"
PALEGREEN2 = "#90EE90"
PALEGREEN3 = "#7CCD7C"
PALEGREEN4 = "#548B54"
PALETURQUOISE1 = "#BBFFFF"
PALETURQUOISE2 = "#AEEEEE"
PALETURQUOISE3 = "#96CDCD"
PALETURQUOISE4 = "#668B8B"
PALEVIOLETRED = "#DB7093"
PALEVIOLETRED1 = "#FF82AB"
PALEVIOLETRED2 = "#EE799F"
PALEVIOLETRED3 = "#CD6889"
PALEVIOLETRED4 = "#8B475D"
PAPAYAWHIP = "#FFEFD5"
PEACHPUFF1 = "#FFDAB9"
PEACHPUFF2 = "#EECBAD"
PEACHPUFF3 = "#CDAF95"
PEACHPUFF4 = "#8B7765"
PEACOCK = "#33A1C9"
PINK = "#FFC0CB"
PINK1 = "#FFB5C5"
PINK2 = "#EEA9B8"
PINK3 = "#CD919E"
PINK4 = "#8B636C"
PLUM = "#DDA0DD"
PLUM1 = "#FFBBFF"
PLUM2 = "#EEAEEE"
PLUM3 = "#CD96CD"
PLUM4 = "#8B668B"
POWDERBLUE = "#B0E0E6"
PURPLE = "#800080"
PURPLE1 = "#9B30FF"
PURPLE2 = "#912CEE"
PURPLE3 = "#7D26CD"
PURPLE4 = "#551A8B"
RASPBERRY = "#872657"
RAWSIENNA = "#C76114"
RED1 = "#FF0000"
RED2 = "#EE0000"
RED3 = "#CD0000"
RED4 = "#8B0000"
ROSYBROWN = "#BC8F8F"
ROSYBROWN1 = "#FFC1C1"
ROSYBROWN2 = "#EEB4B4"
ROSYBROWN3 = "#CD9B9B"
ROSYBROWN4 = "#8B6969"
ROYALBLUE = "#4169E1"
ROYALBLUE1 = "#4876FF"
ROYALBLUE2 = "#436EEE"
ROYALBLUE3 = "#3A5FCD"
ROYALBLUE4 = "#27408B"
SALMON = "#FA8072"
SALMON1 = "#FF8C69"
SALMON2 = "#EE8262"
SALMON3 = "#CD7054"
SALMON4 = "#8B4C39"
SANDYBROWN = "#F4A460"
SAPGREEN = "#308014"
SEAGREEN1 = "#54FF9F"
SEAGREEN2 = "#4EEE94"
SEAGREEN3 = "#43CD80"
SEAGREEN4 = "#2E8B57"
SEASHELL1 = "#FFF5EE"
SEASHELL2 = "#EEE5DE"
SEASHELL3 = "#CDC5BF"
SEASHELL4 = "#8B8682"
SEPIA = "#5E2612"
SGIBEET = "#8E388E"
SGIBRIGHTGRAY = "#C5C1AA"
SGICHARTREUSE = "#71C671"
SGIDARKGRAY = "#555555"
SGIGRAY12 = "#1E1E1E"
SGIGRAY16 = "#282828"
SGIGRAY32 = "#515151"
SGIGRAY36 = "#5B5B5B"
SGIGRAY52 = "#848484"
SGIGRAY56 = "#8E8E8E"
SGIGRAY72 = "#B7B7B7"
SGIGRAY76 = "#C1C1C1"
SGIGRAY92 = "#EAEAEA"
SGIGRAY96 = "#F4F4F4"
SGILIGHTBLUE = "#7D9EC0"
SGILIGHTGRAY = "#AAAAAA"
SGIOLIVEDRAB = "#8E8E38"
SGISALMON = "#C67171"
SGISLATEBLUE = "#7171C6"
SGITEAL = "#388E8E"
SIENNA = "#A0522D"
SIENNA1 = "#FF8247"
SIENNA2 = "#EE7942"
SIENNA3 = "#CD6839"
SIENNA4 = "#8B4726"
SILVER = "#C0C0C0"
SKYBLUE = "#87CEEB"
SKYBLUE1 = "#87CEFF"
SKYBLUE2 = "#7EC0EE"
SKYBLUE3 = "#6CA6CD"
SKYBLUE4 = "#4A708B"
SLATEBLUE = "#6A5ACD"
SLATEBLUE1 = "#836FFF"
SLATEBLUE2 = "#7A67EE"
SLATEBLUE3 = "#6959CD"
SLATEBLUE4 = "#473C8B"
SLATEGRAY = "#708090"
SLATEGRAY1 = "#C6E2FF"
SLATEGRAY2 = "#B9D3EE"
SLATEGRAY3 = "#9FB6CD"
SLATEGRAY4 = "#6C7B8B"
SNOW1 = "#FFFAFA"
SNOW2 = "#EEE9E9"
SNOW3 = "#CDC9C9"
SNOW4 = "#8B8989"
SPRINGGREEN = "#00FF7F"
SPRINGGREEN1 = "#00EE76"
SPRINGGREEN2 = "#00CD66"
SPRINGGREEN3 = "#008B45"
STEELBLUE = "#4682B4"
STEELBLUE1 = "#63B8FF"
STEELBLUE2 = "#5CACEE"
STEELBLUE3 = "#4F94CD"
STEELBLUE4 = "#36648B"
TAN = "#D2B48C"
TAN1 = "#FFA54F"
TAN2 = "#EE9A49"
TAN3 = "#CD853F"
TAN4 = "#8B5A2B"
TEAL = "#008080"
THISTLE = "#D8BFD8"
THISTLE1 = "#FFE1FF"
THISTLE2 = "#EED2EE"
THISTLE3 = "#CDB5CD"
THISTLE4 = "#8B7B8B"
TOMATO1 = "#FF6347"
TOMATO2 = "#EE5C42"
TOMATO3 = "#CD4F39"
TOMATO4 = "#8B3626"
TURQUOISE = "#40E0D0"
TURQUOISE1 = "#00F5FF"
TURQUOISE2 = "#00E5EE"
TURQUOISE3 = "#00C5CD"
TURQUOISE4 = "#00868B"
TURQUOISEBLUE = "#00C78C"
VIOLET = "#EE82EE"
VIOLETRED = "#D02090"
VIOLETRED1 = "#FF3E96"
VIOLETRED2 = "#EE3A8C"
VIOLETRED3 = "#CD3278"
VIOLETRED4 = "#8B2252"
WARMGREY = "#808069"
WHEAT = "#F5DEB3"
WHEAT1 = "#FFE7BA"
WHEAT2 = "#EED8AE"
WHEAT3 = "#CDBA96"
WHEAT4 = "#8B7E66"
WHITE = "#FFFFFF"
WHITESMOKE = "#F5F5F5"
YELLOW1 = "#FFFF00"
YELLOW2 = "#EEEE00"
YELLOW3 = "#CDCD00"
YELLOW4 = "#8B8B00"
class Window:
def __init__(self, width: int = 1280, height: int = 1024, bg_color: str = Color.BLACK, fg_color: str = Color.WHITE):
self.width, self.height = width, height
self.bg_color, self.fg_color = bg_color, fg_color
self.__tk_root = tk.Tk()
self.__canvas = tk.Canvas(master=self.__tk_root, width=width, height=height, bg=bg_color)
self.__canvas.pack(fill=tk.BOTH, expand=tk.YES)
self.__canvas.bind("<MouseWheel>", self._zoom)
self.__canvas.bind("<ButtonPress-1>", self._scroll_start)
self.__canvas.bind("<B1-Motion>", self._scroll_move)
self.__boundary_box = [None, None, None, None]
def _update_boundaries(self, coord: Coordinate | tuple[int, int]) -> None:
if self.__boundary_box[0] is None or coord[0] < self.__boundary_box[0]:
self.__boundary_box[0] = coord[0]
if self.__boundary_box[2] is None or coord[0] > self.__boundary_box[2]:
self.__boundary_box[2] = coord[0]
if self.__boundary_box[1] is None or coord[1] < self.__boundary_box[1]:
self.__boundary_box[1] = coord[1]
if self.__boundary_box[3] is None or coord[1] > self.__boundary_box[3]:
self.__boundary_box[3] = coord[1]
def _zoom(self, event: tk.Event) -> None:
amount = 0.95 if event.delta < 0 else 1.05
self.__canvas.scale(tk.ALL, 0, 0, amount, amount)
def _scroll_start(self, event: tk.Event) -> None:
self.__canvas.scan_mark(event.x, event.y)
def _scroll_move(self, event: tk.Event) -> None:
self.__canvas.scan_dragto(event.x, event.y, gain=1)
def clear(self, keep_boundaries: bool = False) -> None:
self.__canvas.delete(tk.ALL)
if not keep_boundaries:
self.__boundary_box = [None, None, None, None]
def draw_point(self, coord: Coordinate, size: int = 5, color: str = None):
if color is None:
color = self.fg_color
offset = size / 2
self.__canvas.create_oval(coord.x - offset, coord.y - offset, coord.x + offset, coord.y + offset, fill=color)
self._update_boundaries(coord)
def draw_line(self, line: Line, thickness: int = 1, color: str = None) -> None:
if color is None:
color = self.fg_color
self.__canvas.create_line(line.start.x, line.start.y, line.end.x, line.end.y, fill=color, width=thickness)
self._update_boundaries(line.start)
self._update_boundaries(line.end)
def draw_rectangle(self, rectangle: Rectangle, thickness: int = 1, color: str = None, fill: bool = False) -> None:
if color is None:
color = self.fg_color
if fill:
fill = color
else:
fill = self.bg_color
self.__canvas.create_rectangle(
rectangle.top_left.x,
rectangle.top_left.y,
rectangle.bottom_right.x,
rectangle.bottom_right.y,
fill=fill,
outline=color,
width=thickness,
)
self._update_boundaries(rectangle.top_left)
self._update_boundaries(rectangle.bottom_right)
def realign(self, padding: int = 1) -> None:
if self.__boundary_box[0] is not None and self.__boundary_box[0] < 0:
self.__canvas.move(tk.ALL, abs(self.__boundary_box[0]) + padding, 0)
if self.__boundary_box[1] is not None and self.__boundary_box[1] < 0:
self.__canvas.move(tk.ALL, 0, abs(self.__boundary_box[1]) + padding)
dim_x = self.__boundary_box[2] - self.__boundary_box[0] + 2 * padding
dim_y = self.__boundary_box[3] - self.__boundary_box[1] + 2 * padding
scale = min(self.width / dim_x, self.height / dim_y)
self.__canvas.scale(tk.ALL, 0, 0, scale, scale)
self.__canvas.update()
def done(self, realign: bool = True) -> None:
if realign:
self.realign()
self.__canvas.mainloop()