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]
enabled = true
[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
[project]
dynamic = ['version', 'dependencies']
dynamic = ['version']
name = "shs-tools"
authors = [
{ name="Stefan Harmuth", email="pennywise@drock.de" },

View File

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

View File

@ -1,17 +1,14 @@
from __future__ import annotations
import os
import re
import requests
import subprocess
import sys
import requests
import time
import uuid
import webbrowser
from bs4 import BeautifulSoup
from .datafiles import JSONFile
from .stopwatch import StopWatch
from typing import Any, Callable, Type
from tqdm.auto import tqdm
from typing import Any, Callable, List, Tuple, Type
from .tools import get_script_dir
BASE_PATH = get_script_dir()
@ -21,9 +18,9 @@ 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]
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, year: int, day: int):
self.day = day
@ -31,11 +28,6 @@ class AOCDay:
self.part_func = [self.part1, self.part2]
self._current_test_file = 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:
raise NotImplementedError()
@ -43,16 +35,6 @@ class AOCDay:
def part2(self) -> Any:
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(
self,
part: int,
@ -68,27 +50,25 @@ class AOCDay:
self._load_input(input_file)
if not measure_runtime or case_count < len(self.inputs[part]) - 1:
self.DP = {}
self.seen_reset()
answer = self._call_part_func(self.part_func[part])
answer = self.part_func[part]()
else:
stopwatch = StopWatch(auto_start=False)
self.__main_progress_bar_pos = 1
for _ in tqdm(range(timeit_number), desc=f"Part {part+1}", leave=False):
self.DP = {}
self.seen_reset()
stopwatch.start()
answer = self._call_part_func(self.part_func[part])
stopwatch = StopWatch()
for _ in range(timeit_number):
answer = self.part_func[part]()
stopwatch.stop()
exec_time = stopwatch.avg_string(timeit_number)
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"}:
self._submit(part + 1, answer)
else:
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:
return False
@ -110,9 +90,6 @@ class AOCDay:
self.run_part(1, verbose, measure_runtime, timeit_number)
def _load_input(self, filename):
if not os.path.exists(INPUTS_PATH):
os.mkdir(INPUTS_PATH)
file_path = os.path.join(INPUTS_PATH, filename)
if not os.path.exists(file_path):
self._download_input(file_path)
@ -128,7 +105,10 @@ class AOCDay:
cookies={"session": session_id},
)
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
with open(filename, "wb") as f:
@ -171,19 +151,22 @@ class AOCDay:
)
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")
message = soup.article.text
if "That's the right answer" in message:
answer_cache[str_day][str_part]["correct"] = answer
has_rank = re.findall(r"You achieved.*rank (\d+)", message)
print("That's correct!%s" % f" (Rank {has_rank[0]})" if has_rank else "")
webbrowser.open("https://adventofcode.com/%d/day/%d#part2" % (self.year, self.day))
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:
hilo = re.findall("your answer is too (high|low)", message)
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:
# WAIT and retry
wait_pattern = r"You have (?:(\d+)m )?(\d+)s left to wait"
@ -221,13 +204,9 @@ class AOCDay:
else:
return self.input.copy()
def getIntsFromInput(self) -> list:
if len(self.input) == 1:
return list(map(int, re.findall(r"-?\d+", self.input[0])))
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]]:
def getMultiLineInputAsArray(
self, return_type: Type = None, join_char: str = None
) -> List:
"""
get input for day x as 2d array, split by empty lines
"""
@ -253,50 +232,28 @@ class AOCDay:
return return_array
def getInputAsArraySplit(
self, split_char: str = ",", return_type: Type | list[Type] = None
) -> list[Any] | list[list[Any]]:
self, split_char: str = ",", return_type: 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
if input has multiple lines, returns a 2d array (a[line][values])
"""
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:
return_array = []
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
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(
day: int,
@ -332,13 +289,15 @@ def print_solution(
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:
line = line.split(split_char)
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)]
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]

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 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 typing import Union, List, Iterable
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),
]
from typing import Union, List
class DistanceAlgorithm(Enum):
@ -30,30 +14,30 @@ class DistanceAlgorithm(Enum):
class Coordinate(tuple):
def __new__(cls, x: int | float, y: int | float, z: int | float | None = None):
return tuple.__new__(cls, (x, y, z))
def __new__(cls, x: int, y: int, z: int = None) -> Coordinate:
return tuple.__new__(Coordinate, (x, y, z))
@property
def x(self) -> int | float:
def x(self):
return self[0]
@property
def y(self) -> int | float:
def y(self):
return self[1]
@property
def z(self) -> int | float:
def z(self):
return self[2]
def is3D(self) -> bool:
return self[2] is not None
return self.z is not None
def getDistanceTo(
self,
target: Coordinate | tuple,
target: Coordinate,
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
includeDiagonals: bool = False,
) -> int | float:
) -> Union[int, float]:
"""
Get distance to target Coordinate
@ -64,34 +48,40 @@ class Coordinate(tuple):
:return: Distance to Target
"""
if algorithm == DistanceAlgorithm.EUCLIDEAN:
if self[2] is None:
return sqrt(abs(self[0] - target[0]) ** 2 + abs(self[1] - target[1]) ** 2)
if self.z is None:
return sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2)
else:
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:
if self[2] is None:
return max(abs(target[0] - self[0]), abs(target[1] - self[1]))
if self.z is None:
return max(abs(target.x - self.x), abs(target.y - self.y))
else:
return max(
abs(target[0] - self[0]),
abs(target[1] - self[1]),
abs(target[2] - self[2]),
abs(target.x - self.x),
abs(target.y - self.y),
abs(target.z - self.z),
)
elif algorithm == DistanceAlgorithm.MANHATTAN:
if not includeDiagonals:
if self[2] is None:
return abs(self[0] - target[0]) + abs(self[1] - target[1])
if self.z is None:
return abs(self.x - target.x) + abs(self.y - target.y)
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:
dist = [abs(self[0] - target[0]), abs(self[1] - target[1])]
if self[2] is None:
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[2] - target[2]))
dist.append(abs(self.z - target.z))
d_steps = min(dist)
dist.remove(min(dist))
dist = [x - d_steps for x in dist]
@ -100,48 +90,60 @@ class Coordinate(tuple):
def inBoundaries(
self,
minX: int | float,
minY: int | float,
maxX: int | float,
maxY: int | float,
minZ: int | float = -inf,
maxZ: int | float = inf,
minX: int,
minY: int,
maxX: int,
maxY: int,
minZ: int = -inf,
maxZ: int = inf,
) -> bool:
if self[2] is None:
return minX <= self[0] <= maxX and minY <= self[1] <= maxY
if self.z is None:
return minX <= self.x <= maxX and minY <= self.y <= maxY
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(
self,
radius: int | float = 1,
radius: int = 1,
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
minX: int | float = -inf,
minY: int | float = -inf,
maxX: int | float = inf,
maxY: int | float = inf,
minZ: int | float = -inf,
maxZ: int | float = inf,
minX: int = -inf,
minY: int = -inf,
maxX: int = inf,
maxY: int = inf,
minZ: int = -inf,
maxZ: int = inf,
) -> list[Coordinate]:
ret = []
if self[2] is None: # mode 2D
for x in range(self[0] - radius * 2, self[0] + radius * 2 + 1):
for y in range(self[1] - radius * 2, self[1] + radius * 2 + 1):
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 = round_half_up(self.getDistanceTo(target, algorithm=algorithm, includeDiagonals=False))
dist = round_half_up(
self.getDistanceTo(
target, algorithm=algorithm, includeDiagonals=False
)
)
if dist == radius:
ret.append(target)
else:
for x in range(self[0] - radius * 2, self[0] + radius * 2 + 1):
for y in range(self[1] - radius * 2, self[1] + radius * 2 + 1):
for z in range(self[2] - radius * 2, self[2] + radius * 2 + 1):
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 = round_half_up(self.getDistanceTo(target, algorithm=algorithm, includeDiagonals=False))
dist = round_half_up(
self.getDistanceTo(
target, algorithm=algorithm, includeDiagonals=False
)
)
if dist == radius:
ret.append(target)
@ -150,13 +152,12 @@ class Coordinate(tuple):
def getNeighbours(
self,
includeDiagonal: bool = True,
minX: int | float = -inf,
minY: int | float = -inf,
maxX: int | float = inf,
maxY: int | float = inf,
minZ: int | float = -inf,
maxZ: int | float = inf,
dist: int | float = 1,
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.
@ -168,33 +169,60 @@ class Coordinate(tuple):
: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
:param dist: distance to neighbour coordinates
:return: list of Coordinate
"""
if self[2] is None:
nb_list = [x * dist for x in NEIGHBOURS]
if self.z is None:
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:
if minX <= self[0] + dx <= maxX and minY <= self[1] + dy <= maxY:
yield self.__class__(self[0] + dx, self[1] + dy)
if minX <= self.x + dx <= maxX and minY <= self.y + dy <= maxY:
yield self.__class__(self.x + dx, self.y + dy)
else:
nb_list = [x * dist for x in NEIGHBOURS_3D]
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:
if minX <= self[0] + dx <= maxX and minY <= self[1] + dy <= maxY and minZ <= self[2] + dz <= maxZ:
yield self.__class__(self[0] + dx, self[1] + dy, self[2] + dz)
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 | 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'"""
if self[2] is not None:
if self.z is not None:
raise NotImplementedError() # which angle?!?!
dx = target[0] - self[0]
dy = target[1] - self[1]
dx = target.x - self.x
dy = target.y - self.y
if not normalized:
return degrees(atan2(dy, dx))
else:
@ -204,117 +232,212 @@ class Coordinate(tuple):
else:
return 180.0 + abs(angle)
def getLineTo(self, target: Coordinate | tuple) -> List[Coordinate]:
"""this will probably not yield what you expect, when using float coordinates"""
if target == self:
return [self]
def getLineTo(self, target: Coordinate) -> List[Coordinate]:
diff = target - self
if self[2] is None:
steps = gcd(diff[0], diff[1])
step_x = diff[0] // steps
step_y = diff[1] // 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
if self.z is None:
steps = gcd(diff.x, diff.y)
step_x = diff.x // steps
step_y = diff.y // steps
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)
]
def reverse(self) -> Coordinate:
if self[2] is None:
return self.__class__(-self[0], -self[1])
if self.z is None:
return self.__class__(-self.x, -self.y)
else:
return self.__class__(-self[0], -self[1], -self[2])
return self.__class__(-self.x, -self.y, -self.z)
def __hash__(self) -> int:
return hash((self[0], self[1], self[2]))
def __eq__(self, other: Coordinate | tuple) -> bool:
if self[2] is None:
return self[0] == other[0] and self[1] == other[1]
def __add__(self, other: Coordinate) -> Coordinate:
if self.z is None:
return self.__class__(self.x + other.x, self.y + other.y)
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:
if self[2] is None:
return self.__class__(self[0] + other[0], self[1] + other[1])
def __sub__(self, other: Coordinate) -> Coordinate:
if self.z is None:
return self.__class__(self.x - other.x, self.y - other.y)
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:
if self[2] is None:
return self.__class__(self[0] - other[0], self[1] - other[1])
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[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:
if self[2] is None:
return self.__class__(self[0] * other, self[1] * 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[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:
if self[2] is None:
return self.__class__(self[0] % other, self[1] % other)
else:
return self.__class__(self[0] % other, self[1] % other, self[2] % other)
def __truediv__(self, other):
return self // other
def __floordiv__(self, other: int | float) -> Coordinate:
if self[2] is None:
return self.__class__(self[0] // other, self[1] // other)
def __gt__(self, other):
if self.z is None:
return self.x > other.x and self.y > other.y
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:
if self[2] is None:
return self.__class__(self[0] / other, self[1] / other)
def __ge__(self, other):
if self.z is None:
return self.x >= other.x and self.y >= other.y
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):
if self[2] is None:
return "({},{})".format(self[0], self[1])
if self.z is None:
return "(%d,%d)" % (self.x, self.y)
else:
return "({},{},{})".format(self[0], self[1], self[2])
return "(%d,%d,%d)" % (self.x, self.y, self.z)
def __repr__(self):
if self[2] is None:
return "{}(x={}, y={})".format(self.__class__.__name__, self[0], self[1])
if self.z is None:
return "%s(x=%d, y=%d)" % (self.__class__.__name__, self.x, self.y)
else:
return "{}(x={}, y={}, z={})".format(
return "%s(x=%d, y=%d, z=%d)" % (
self.__class__.__name__,
self[0],
self[1],
self[2],
self.x,
self.y,
self.z,
)
@classmethod
def generate(
cls,
from_x: int | float,
to_x: int | float,
from_y: int | float,
to_y: int | float,
from_z: int | float = None,
to_z: int | float = None,
step: int | float = 1,
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 [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:
return [
cls(x, y, z)
for x in range(from_x, to_x + step, step)
for y in range(from_y, to_y + step, step)
for z in range(from_z, to_z + step, step)
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(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:
def __init__(self, top_left: Coordinate, bottom_right: Coordinate):
"""
@ -330,7 +453,9 @@ class Shape:
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)
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)
@ -347,23 +472,43 @@ class 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,
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,
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,
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,
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:
@ -375,16 +520,6 @@ class Shape:
def __rand__(self, 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):
return "%s(%s -> %s)" % (
self.__class__.__name__,
@ -400,9 +535,9 @@ class Shape:
)
class Rectangle(Shape):
class Square(Shape):
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
@ -411,131 +546,3 @@ class Cube(Shape):
if top_left.z is None or bottom_right.z is None:
raise ValueError("Both Coordinates need to be 3D")
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
import re
from collections import deque
from .aoc_ocr import convert_array_6
from .coordinate import Coordinate, DistanceAlgorithm, Shape
from collections import deque
from collections.abc import Callable
from enum import Enum
from heapq import heappop, heappush
from math import inf
from typing import Any, Dict, List, Iterable, Mapping
from typing import Any, Dict, List
OFF = False
ON = True
@ -48,19 +46,19 @@ class Grid:
def __trackBoundaries(self, pos: Coordinate):
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:
self.minX = pos[0] if pos[0] < self.minX else self.minX
self.minY = pos[1] if pos[1] < self.minY else self.minY
self.maxX = pos[0] if pos[0] > self.maxX else self.maxX
self.maxY = pos[1] if pos[1] > self.maxY else 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:
if self.minZ is None:
self.minZ = self.maxZ = pos[2]
self.minZ = self.maxZ = pos.z
else:
self.minZ = pos[2] if pos[2] < self.minZ else self.minZ
self.maxZ = pos[2] if pos[2] > self.maxZ else 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 recalcBoundaries(self) -> None:
self.minX, self.maxX, self.minY, self.maxY, self.minZ, self.maxZ = (
@ -100,12 +98,6 @@ class Grid:
else:
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):
if pos in self.__grid:
del self.__grid[pos]
@ -123,7 +115,7 @@ class Grid:
self.toggle(Coordinate(x, y, z))
def set(self, pos: Coordinate, value: Any = True) -> Any:
if pos[2] is not None:
if pos.z is not None:
self.mode3D = True
if (value == self.__default) and pos in self.__grid:
@ -157,13 +149,13 @@ class Grid:
return self.set(pos, self.get(pos) / value)
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 y in range(shape.top_left[1], shape.bottom_right[1] + 1):
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[2], shape.bottom_right[2] + 1):
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)
@ -173,11 +165,6 @@ class Grid:
def getOnCount(self) -> int:
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:
return list(self.__grid.values()).count(value)
@ -207,41 +194,29 @@ class Grid:
def isCorner(self, pos: Coordinate) -> bool:
return pos in self.getCorners()
def isWithinBoundaries(self, pos: Coordinate, pad: int = 0) -> bool:
def isWithinBoundaries(self, pos: Coordinate) -> bool:
if self.mode3D:
return (
self.minX + pad <= pos[0] <= self.maxX - pad
and self.minY + pad <= pos[1] <= self.maxY - pad
and self.minZ + pad <= pos[2] <= self.maxZ - pad
self.minX <= pos.x <= self.maxX
and self.minY <= pos.y <= self.maxY
and self.minZ <= pos.z <= self.maxZ
)
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:
return (
return [
c
for c in self.__grid.keys()
if (c[0] == x if x is not None else True)
and (c[1] == y if y is not None else True)
and (c[2] == z if z is not None else True)
)
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 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)
return list(self.__grid.keys())
def getActiveRegion(
self,
@ -285,7 +260,7 @@ class Grid:
pos: Coordinate,
includeDefault: bool = False,
includeDiagonal: bool = True,
) -> Iterable[Coordinate]:
) -> List[Coordinate]:
neighbours = pos.getNeighbours(
includeDiagonal=includeDiagonal,
minX=self.minX,
@ -326,47 +301,47 @@ class Grid:
if mode == GridTransformation.ROTATE_X:
shift_z = self.maxY
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)
elif mode == GridTransformation.ROTATE_Y:
shift_x = self.maxX
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)
elif mode == GridTransformation.ROTATE_Z:
shift_x = self.maxX
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)
elif mode == GridTransformation.COUNTER_ROTATE_X:
shift_y = self.maxY
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)
elif mode == GridTransformation.COUNTER_ROTATE_Y:
shift_z = self.maxZ
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)
elif mode == GridTransformation.COUNTER_ROTATE_Z:
shift_y = self.maxY
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)
elif mode == GridTransformation.FLIP_X:
shift_x = self.maxX
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)
elif mode == GridTransformation.FLIP_Y:
shift_y = self.maxY
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)
elif mode == GridTransformation.FLIP_Z:
shift_z = self.maxZ
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)
else:
raise NotImplementedError(mode)
@ -382,9 +357,9 @@ class Grid:
self.__grid = {}
for c, v in coords.items():
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:
nc = Coordinate(c[0] + shift_x, c[1] + shift_y)
nc = Coordinate(c.x + shift_x, c.y + shift_y)
self.set(nc, v)
def shift_zero(self, recalc: bool = True):
@ -421,7 +396,9 @@ class Grid:
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):
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
@ -468,7 +445,9 @@ class Grid:
if currentCoord == pos_to:
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:
continue
@ -477,7 +456,9 @@ class Grid:
elif not includeDiagonal:
neighbourDist = 1
else:
neighbourDist = currentCoord.getDistanceTo(neighbour, DistanceAlgorithm.MANHATTAN, includeDiagonal)
neighbourDist = currentCoord.getDistanceTo(
neighbour, DistanceAlgorithm.MANHATTAN, includeDiagonal
)
targetDist = neighbour.getDistanceTo(pos_to)
f_cost = targetDist + neighbourDist + currentNode[1]
@ -511,13 +492,17 @@ class Grid:
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")
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):
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:
for z in range(from_z, to_z + 1):
new_grid.set(
@ -584,14 +569,20 @@ class Grid:
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()]
[
"#" 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(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)
)
@ -607,7 +598,11 @@ class Grid:
) -> "Grid":
if translate is None:
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
ret = cls(default=default)
@ -625,56 +620,17 @@ class Grid:
return ret
@classmethod
def from_data(
cls,
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)
def __eq__(self, other: Grid) -> bool:
if not isinstance(other, Grid):
return False
regex_in_translate = False
if translate is not None:
for k in translate:
if len(k) > 1:
regex_in_translate = True
other_active = set(other.getActiveCells())
for c, v in self.__grid.items():
if other.get(c) != v:
return False
other_active.remove(c)
for y, row in enumerate(data):
for x, col in enumerate(row):
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 other_active:
return False
if re.search(k, col):
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()))
return True

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
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 n
if n - start == 1:
return n * start
middle = start + (n - start) // 2
return factorial(middle, start) * factorial(n, middle + 1)
return math.factorial(n)
def fibonacci(n: int) -> int:
@ -46,10 +38,3 @@ def pentagonal(n: int) -> int:
0, 1, 5, 12, 22, 35, ...
"""
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 time import sleep
from .schedule import Scheduler
from .simplesocket import ClientSocket
from base64 import b64encode
from collections import deque
from datetime import timedelta
from enum import Enum
from time import sleep
from typing import Callable
@ -157,15 +155,6 @@ class ServerMessage(str, Enum):
ERR_NOOPERHOST = "491"
ERR_UMODEUNKNOWNFLAG = "501"
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_TOPIC = "TOPIC"
MSG_MODE = "MODE"
@ -232,30 +221,20 @@ class Client:
nick: str,
username: str,
realname: str = "Python Bot",
sasl_password: str = None,
):
self.__connected = False
self.__send_queue = deque()
self.__userlist = {}
self.__channellist = {}
self.__server_socket = ClientSocket(server, port)
if sasl_password is not None:
self.__server_socket.sendline("CAP REQ :sasl")
self.__server_socket.sendline("USER %s ignore ignore :%s" % (username, realname))
self.__server_socket.sendline(
"USER %s ignore ignore :%s" % (username, realname)
)
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.__function_register = {
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.RPL_LOGGEDIN: [self.on_auth],
ServerMessage.MSG_JOIN: [self.on_join],
ServerMessage.MSG_PART: [self.on_part],
ServerMessage.MSG_QUIT: [self.on_quit],
@ -264,17 +243,6 @@ class Client:
}
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):
while line := self.__server_socket.recvline():
line = line.strip()
@ -299,7 +267,10 @@ class Client:
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:
if (
self.__userlist[msg_from].nickname
== self.__userlist[self.__my_user].nickname
):
del self.__userlist[self.__my_user]
self.__my_user = msg_from
@ -313,14 +284,9 @@ class Client:
else:
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):
self.__my_user = message.split()[-1]
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):
for cap in message.split():
@ -344,7 +310,9 @@ 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].identifier] = 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):
@ -374,21 +342,21 @@ class Client:
print(msg_from, msg_type, msg_to, message)
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):
self.send_raw("JOIN %s" % channel)
self.__server_socket.sendline("JOIN %s" % channel)
self.receive()
def part(self, channel: str):
self.send_raw("PART %s" % channel)
self.__server_socket.sendline("PART %s" % channel)
self.receive()
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!"):
self.send_raw("QUIT :%s" % message)
self.__server_socket.sendline("QUIT :%s" % message)
self.receive()
self.__server_socket.close()
@ -421,9 +389,8 @@ class IrcBot(Client):
nick: str,
username: str,
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._channel_commands = {}
self._privmsg_commands = {}
@ -448,8 +415,13 @@ class IrcBot(Client):
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 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:]))
@ -457,6 +429,5 @@ class IrcBot(Client):
def run(self):
while True:
self._scheduler.run_pending()
self.send_queue()
self.receive()
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
class Node:
value: Any
next: Node = None
prev: Node = None
next: "Node" = None
prev: "Node" = None
class LinkedList:

View File

@ -1,15 +1,6 @@
from __future__ import annotations
import math
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:
@ -25,20 +16,3 @@ def get_factors(num: int) -> set:
f.add(num // x)
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
def __init__(self, auto_start=True):
self.total_elapsed = 0
if auto_start:
self.start()
@ -16,13 +15,11 @@ class StopWatch:
self.started = perf_counter_ns()
self.stopped = None
def stop(self):
def stop(self) -> float:
self.stopped = perf_counter_ns()
self.total_elapsed += self.elapsed()
return self.elapsed()
def reset(self):
self.total_elapsed = 0
self.start()
reset = start
def elapsed(self) -> int:
if self.stopped is None:
@ -31,10 +28,10 @@ class StopWatch:
return self.stopped - self.started
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:
return self.total_elapsed / divider
return self.elapsed() / divider
def avg_string(self, divider: int) -> str:
return human_readable_time_from_ns(int(self.avg_elapsed(divider)))

View File

@ -2,6 +2,8 @@ import datetime
import inspect
import os.path
import sys
from fishhook import hook
from functools import wraps
from typing import Any
@ -44,7 +46,9 @@ class Dict(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):
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):
@ -100,7 +104,14 @@ def minmax(*arr: Any) -> (Any, Any):
else:
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:
@ -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 "")
if delta.seconds > 3600:
time_str += "%02d hours, " % (delta.seconds // 3600)
time_str += "%02d:" % (delta.seconds // 3600)
else:
time_str += ""
time_str += "00:"
if delta.seconds % 3600 > 60:
time_str += "%02d minutes, " % (delta.seconds % 3600 // 60)
time_str += "%02d:" % (delta.seconds % 3600 // 60)
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:
@ -138,3 +149,51 @@ def human_readable_time_from_ns(ns: int) -> str:
time_parts.insert(0, "%d%s" % (p, unit))
if ns == 0:
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()