Compare commits

..

38 Commits
0.3.6 ... main

Author SHA1 Message Date
1b777d5849 types.Range() 2025-12-05 07:44:48 +01:00
587fee299f chop string via division 2025-12-02 09:30:40 +01:00
105ab06cd9 playing around with temperatures and humidity 2025-05-25 18:03:38 +02:00
c4fb68f809 some move string manipulations 2025-01-12 09:47:28 +01:00
17c41a0c63 start moving away from fishhook onto dedicated type sub-classes, which allows for pypy to come along 2025-01-06 09:14:49 +01:00
270866387e coordinate.py: predefine neighbouring coordinates 2024-12-12 09:04:36 +01:00
6adb1ff457 math.concat(): concatenate 2 integers (e.g. contcat(12, 34) == 1234) 2024-12-07 06:51:33 +01:00
e06e536292 math: MAXINT* constants 2024-12-07 06:41:16 +01:00
dd39c90f13 math.magnitude(): return order of magnitude of a number 2024-12-07 06:35:55 +01:00
24cd561370 aoc.AOCDay(): add internal cache set for standard "have I seen this value?" situations. Adds SEEN (the set), seen_reset() and seen() 2024-12-06 07:32:55 +01:00
23283d3b66 irc.Client(): implement sasl authentication and send queue
All checks were successful
Publish to PyPI / Publish to PyPI (push) Successful in 1m31s
2024-11-30 10:26:21 +01:00
bfac2b9433 redefined int_seq.factorial(); takes 4 times longer than math.factorial(), but is pure python and allows for sequences not starting with 1 2024-11-03 14:17:59 +01:00
3ac82ee526 int_seq.hexagonal() 2024-10-10 15:33:39 +02:00
d6fad1511c Grid.find(): generator yielding coordinates that hold a specific value 2024-01-09 06:52:04 +01:00
cf72a5451f aoc.AOCDay: introduce class variable "DP", a dictionary that gets reset before every part method call, useful for memoization problems 2024-01-03 05:46:24 +01:00
87fbaeafbe coordinate.Shape: add __contains__()
coordinate.Line: add __contains__(), __len__() and proper __eq__()
NEW: coordinate.Polygon: deal with Polygons defined by a list of Coordinates (in (counter)?clockwise order. Currently allows for rectilinear decomposition and area calculation
2024-01-01 21:48:17 +01:00
94cffbbd74 visualization: Add Color Enum
visualization: add draw methods for points and rectangles
2024-01-01 21:41:08 +01:00
e3309415b7 tools.minmax(): remove broken code; why was that even there? 2024-01-01 21:40:00 +01:00
2d7d339bf0 visualization.Window(): better boundary box handling 2023-12-28 06:54:54 +01:00
ed0fe0dafc visualization.Window.done(): choose to realign or not (default: yes) 2023-12-28 06:48:22 +01:00
87eff61ae2 visualization.Window.realign(): scale to fit 2023-12-28 06:47:17 +01:00
0aedd1c612 NEW: visualization.Window(): simple Tk visualization class, can just draw some lines for now, but with working realignment, zooming and panning 2023-12-28 06:09:16 +01:00
8dea09c30f expand coordinate.Line functionality 2023-12-28 06:07:39 +01:00
f7d1fde5b7 NEW: itertools.len_combinations_of_sum() 2023-12-26 12:49:37 +01:00
600d0e716c combinations_of_sum moved to new itertools module 2023-12-25 23:17:18 +01:00
5d5e9fb8e5 NEW: AOCDay.run_part(): when timing, shift all following progress bars down by one to not interfere 2023-12-25 19:45:31 +01:00
916ab481cf NEW: AOCDay.progress() - add a progress bar to your long-running solutions 2023-12-25 15:47:36 +01:00
4ce70c8565 AOCDay.run_part(): When in timeit mode, display progress bar 2023-12-25 12:31:06 +01:00
ee3b5ee941 Stopwatch() can now be restarted and will automatically sum up all intervals between start() and stop() calls 2023-12-25 12:29:49 +01:00
4d1b075086 NEW: itertools.len_permutations() and itertools.len_combinations() -> get the amount of results the respective itertools function would yield 2023-12-25 11:24:12 +01:00
1e43a2ff0d NEW: coordinate.Line() - only 2D for now, but can calc intersections 2023-12-24 12:14:47 +01:00
cc1fd86ede NEW: Coordinate() now accepts floats; what can possibly go wrong? 2023-12-24 12:13:58 +01:00
9386c40ea5 FIX: Coordinate.__new__(): return correct class instead of always Coordinate() (fucked up subclassing) 2023-12-23 09:44:36 +01:00
14d535911c NEW: Coordinate().__mod__() 2023-12-22 05:55:29 +01:00
fb3bef0153 Coordinate().getLineTo(): handle target == self 2023-12-18 16:38:11 +01:00
ffcc1e14c0 NEW tools.list_combinations_of_sum() - returns all possible tuples with len [length}, containing integers summing up to {total_sum} 2023-12-18 09:12:34 +01:00
1a111488ba FIX Coordinate().getLineTo() index bug 2023-12-18 09:11:06 +01:00
02623f8c9a NEW Grid.getRegion() -> Generator over all Coordinates of connected cells with the same value inside Grid borders 2023-12-18 07:00:05 +01:00
14 changed files with 1310 additions and 411 deletions

View File

@ -1,5 +1,4 @@
bs4~=0.0.1
beautifulsoup4~=4.12.2
fishhook~=0.2.9
pygame~=2.5.2
requests~=2.31.0
beautifulsoup4~=4.12
requests~=2.32
tqdm~=4.66

View File

@ -1,14 +1,17 @@
from __future__ import annotations
import os
import re
import subprocess
import requests
import subprocess
import sys
import time
import uuid
import webbrowser
from bs4 import BeautifulSoup
from .datafiles import JSONFile
from .stopwatch import StopWatch
from typing import Any, Callable, List, Tuple, Type
from typing import Any, Callable, Type
from tqdm.auto import tqdm
from .tools import get_script_dir
BASE_PATH = get_script_dir()
@ -18,9 +21,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
@ -28,6 +31,11 @@ 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()
@ -36,7 +44,14 @@ class AOCDay:
raise NotImplementedError()
def is_test(self) -> bool:
return self._current_test_solution is not None
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,
@ -53,25 +68,27 @@ class AOCDay:
self._load_input(input_file)
if not measure_runtime or case_count < len(self.inputs[part]) - 1:
answer = self.part_func[part]()
self.DP = {}
self.seen_reset()
answer = self._call_part_func(self.part_func[part])
else:
stopwatch = StopWatch()
for _ in range(timeit_number):
answer = self.part_func[part]()
stopwatch.stop()
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.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
@ -111,10 +128,7 @@ 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:
@ -157,10 +171,7 @@ 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
@ -168,9 +179,7 @@ class AOCDay:
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)
)
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)
@ -218,9 +227,7 @@ class AOCDay:
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:
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
"""
@ -246,28 +253,50 @@ class AOCDay:
return return_array
def getInputAsArraySplit(
self, split_char: str = ",", return_type: Type | List[Type] = None
) -> List:
self, split_char: str = ",", return_type: Type | list[Type] = None
) -> list[Any] | list[list[Any]]:
"""
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,
@ -303,15 +332,13 @@ 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]

79
src/tools/climate.py Normal file
View File

@ -0,0 +1,79 @@
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,8 +1,24 @@
from __future__ import annotations
from enum import Enum
from math import gcd, sqrt, inf, atan2, degrees
from math import gcd, sqrt, inf, atan2, degrees, isclose
from .math import round_half_up
from typing import Union, List
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),
]
class DistanceAlgorithm(Enum):
@ -14,19 +30,19 @@ class DistanceAlgorithm(Enum):
class Coordinate(tuple):
def __new__(cls, x: int, y: int, z: int = None) -> Coordinate:
return tuple.__new__(Coordinate, (x, y, z))
def __new__(cls, x: int | float, y: int | float, z: int | float | None = None):
return tuple.__new__(cls, (x, y, z))
@property
def x(self):
def x(self) -> int | float:
return self[0]
@property
def y(self):
def y(self) -> int | float:
return self[1]
@property
def z(self):
def z(self) -> int | float:
return self[2]
def is3D(self) -> bool:
@ -37,7 +53,7 @@ class Coordinate(tuple):
target: Coordinate | tuple,
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
includeDiagonals: bool = False,
) -> Union[int, float]:
) -> int | float:
"""
Get distance to target Coordinate
@ -52,9 +68,7 @@ class Coordinate(tuple):
return sqrt(abs(self[0] - target[0]) ** 2 + abs(self[1] - target[1]) ** 2)
else:
return sqrt(
abs(self[0] - target[0]) ** 2
+ abs(self[1] - target[1]) ** 2
+ abs(self[2] - target[2]) ** 2
abs(self[0] - target[0]) ** 2 + abs(self[1] - target[1]) ** 2 + abs(self[2] - target[2]) ** 2
)
elif algorithm == DistanceAlgorithm.CHEBYSHEV:
if self[2] is None:
@ -70,11 +84,7 @@ class Coordinate(tuple):
if self[2] is None:
return abs(self[0] - target[0]) + abs(self[1] - target[1])
else:
return (
abs(self[0] - target[0])
+ abs(self[1] - target[1])
+ abs(self[2] - target[2])
)
return abs(self[0] - target[0]) + abs(self[1] - target[1]) + abs(self[2] - target[2])
else:
dist = [abs(self[0] - target[0]), abs(self[1] - target[1])]
if self[2] is None:
@ -90,32 +100,28 @@ class Coordinate(tuple):
def inBoundaries(
self,
minX: int,
minY: int,
maxX: int,
maxY: int,
minZ: int = -inf,
maxZ: int = inf,
minX: int | float,
minY: int | float,
maxX: int | float,
maxY: int | float,
minZ: int | float = -inf,
maxZ: int | float = inf,
) -> bool:
if self[2] is None:
return minX <= self[0] <= maxX and minY <= self[1] <= maxY
else:
return (
minX <= self[0] <= maxX
and minY <= self[1] <= maxY
and minZ <= self[2] <= maxZ
)
return minX <= self[0] <= maxX and minY <= self[1] <= maxY and minZ <= self[2] <= maxZ
def getCircle(
self,
radius: int = 1,
radius: int | float = 1,
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
minX: int = -inf,
minY: int = -inf,
maxX: int = inf,
maxY: int = inf,
minZ: int = -inf,
maxZ: int = inf,
minX: int | float = -inf,
minY: int | float = -inf,
maxX: int | float = inf,
maxY: int | float = inf,
minZ: int | float = -inf,
maxZ: int | float = inf,
) -> list[Coordinate]:
ret = []
if self[2] is None: # mode 2D
@ -124,11 +130,7 @@ class Coordinate(tuple):
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)
@ -139,11 +141,7 @@ class Coordinate(tuple):
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)
@ -152,12 +150,13 @@ class Coordinate(tuple):
def getNeighbours(
self,
includeDiagonal: bool = True,
minX: int = -inf,
minY: int = -inf,
maxX: int = inf,
maxY: int = inf,
minZ: int = -inf,
maxZ: int = inf,
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,
) -> list[Coordinate]:
"""
Get a list of neighbouring coordinates.
@ -169,51 +168,24 @@ 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 includeDiagonal:
nb_list = [
(-1, -1),
(-1, 0),
(-1, 1),
(0, -1),
(0, 1),
(1, -1),
(1, 0),
(1, 1),
]
else:
nb_list = [(-1, 0), (1, 0), (0, -1), (0, 1)]
nb_list += [x * dist for x in DIAGONAL_NEIGHBOURS]
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)
else:
nb_list = [x * dist for x in NEIGHBOURS_3D]
if includeDiagonal:
nb_list = [
(x, y, z)
for x in [-1, 0, 1]
for y in [-1, 0, 1]
for z in [-1, 0, 1]
]
nb_list.remove((0, 0, 0))
else:
nb_list = [
(-1, 0, 0),
(0, -1, 0),
(1, 0, 0),
(0, 1, 0),
(0, 0, 1),
(0, 0, -1),
]
nb_list += [x * dist for x in DIAGONAL_NEIGHBOURS_3D]
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 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)
def getAngleTo(self, target: Coordinate | tuple, normalized: bool = False) -> float:
@ -233,25 +205,23 @@ class Coordinate(tuple):
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]
diff = target - self
if self[2] is None:
steps = gcd(diff[0], diff[0])
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)
]
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 [
self.__class__(
self[0] + step_x * i, self[1] + step_y * i, self[2] + step_z * i
)
self.__class__(self[0] + step_x * i, self[1] + step_y * i, self[2] + step_z * i)
for i in range(steps + 1)
]
@ -282,12 +252,18 @@ class Coordinate(tuple):
else:
return self.__class__(self[0] - other[0], self[1] - other[1], self[2] - other[2])
def __mul__(self, other: int) -> Coordinate:
def __mul__(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 __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 __floordiv__(self, other: int | float) -> Coordinate:
if self[2] is None:
return self.__class__(self[0] // other, self[1] // other)
@ -295,19 +271,22 @@ class Coordinate(tuple):
return self.__class__(self[0] // other, self[1] // other, self[2] // other)
def __truediv__(self, other: int | float) -> Coordinate:
return self // other
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 __str__(self):
if self[2] is None:
return "(%d,%d)" % (self[0], self[1])
return "({},{})".format(self[0], self[1])
else:
return "(%d,%d,%d)" % (self[0], self[1], self[2])
return "({},{},{})".format(self[0], self[1], self[2])
def __repr__(self):
if self[2] is None:
return "%s(x=%d, y=%d)" % (self.__class__.__name__, self[0], self[1])
return "{}(x={}, y={})".format(self.__class__.__name__, self[0], self[1])
else:
return "%s(x=%d, y=%d, z=%d)" % (
return "{}(x={}, y={}, z={})".format(
self.__class__.__name__,
self[0],
self[1],
@ -317,112 +296,25 @@ class Coordinate(tuple):
@classmethod
def generate(
cls,
from_x: int,
to_x: int,
from_y: int,
to_y: int,
from_z: int = None,
to_z: int = None,
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,
) -> List[Coordinate]:
if from_z is None or to_z is None:
return [
cls(x, y)
for x in range(from_x, to_x + 1)
for y in range(from_y, to_y + 1)
]
return [cls(x, y) for x in range(from_x, to_x + step, step) for y in range(from_y, to_y + step, step)]
else:
return [
cls(x, y, z)
for x in range(from_x, to_x + 1)
for y in range(from_y, to_y + 1)
for z in range(from_z, to_z + 1)
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)
]
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):
"""
@ -438,9 +330,7 @@ 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)
@ -457,43 +347,23 @@ 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:
@ -505,6 +375,16 @@ 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__,
@ -520,9 +400,9 @@ class Shape:
)
class Square(Shape):
class Rectangle(Shape):
def __init__(self, top_left, bottom_right):
super(Square, self).__init__(top_left, bottom_right)
super(Rectangle, self).__init__(top_left, bottom_right)
self.mode_3d = False
@ -531,3 +411,131 @@ 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

@ -173,6 +173,11 @@ 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)
@ -210,14 +215,9 @@ class Grid:
and self.minZ + pad <= pos[2] <= self.maxZ - pad
)
else:
return (
self.minX + pad <= pos[0] <= self.maxX - pad
and self.minY + pad <= pos[1] <= self.maxY - pad
)
return self.minX + pad <= pos[0] <= self.maxX - pad and self.minY + pad <= pos[1] <= self.maxY - pad
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) -> Iterable[Coordinate]:
if x is not None or y is not None or z is not None:
return (
c
@ -229,6 +229,20 @@ class Grid:
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)
def getActiveRegion(
self,
start: Coordinate,
@ -407,9 +421,7 @@ 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
@ -456,9 +468,7 @@ 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
@ -467,9 +477,7 @@ 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]
@ -503,17 +511,13 @@ 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(
@ -580,20 +584,14 @@ 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)
)
@ -609,11 +607,7 @@ 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)

View File

@ -1,12 +1,20 @@
import math
def factorial(n: int) -> int:
def factorial(n: int, start: int = 1) -> 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
"""
return math.factorial(n)
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)
def fibonacci(n: int) -> int:
@ -38,3 +46,10 @@ 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,9 +1,11 @@
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
@ -155,6 +157,15 @@ 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"
@ -221,20 +232,30 @@ 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)
self.__server_socket.sendline(
"USER %s ignore ignore :%s" % (username, realname)
)
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("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],
@ -243,6 +264,17 @@ 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()
@ -267,10 +299,7 @@ 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
@ -284,9 +313,14 @@ 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():
@ -310,9 +344,7 @@ class Client:
def on_nick(self, msg_from: str, msg_to: str, message: str):
self.__userlist[msg_from].nick(msg_to)
self.__userlist[self.__userlist[msg_from].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):
@ -342,21 +374,21 @@ class Client:
print(msg_from, msg_type, msg_to, message)
def nick(self, new_nick: str):
self.__server_socket.sendline("NICK %s" % new_nick)
self.send_raw("NICK %s" % new_nick)
def join(self, channel: str):
self.__server_socket.sendline("JOIN %s" % channel)
self.send_raw("JOIN %s" % channel)
self.receive()
def part(self, channel: str):
self.__server_socket.sendline("PART %s" % channel)
self.send_raw("PART %s" % channel)
self.receive()
def privmsg(self, target: str, message: str):
self.__server_socket.sendline("PRIVMSG %s :%s" % (target, message))
self.send_raw("PRIVMSG %s :%s" % (target, message))
def quit(self, message: str = "Elvis has left the building!"):
self.__server_socket.sendline("QUIT :%s" % message)
self.send_raw("QUIT :%s" % message)
self.receive()
self.__server_socket.close()
@ -389,8 +421,9 @@ class IrcBot(Client):
nick: str,
username: str,
realname: str = "Python Bot",
sasl_password: str = None,
):
super().__init__(server, port, nick, username, realname)
super().__init__(server, port, nick, username, realname, sasl_password)
self._scheduler = Scheduler()
self._channel_commands = {}
self._privmsg_commands = {}
@ -415,13 +448,8 @@ 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:]))
@ -429,5 +457,6 @@ class IrcBot(Client):
def run(self):
while True:
self._scheduler.run_pending()
self.send_queue()
self.receive()
sleep(0.01)

41
src/tools/itertools.py Normal file
View File

@ -0,0 +1,41 @@
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

@ -4,6 +4,14 @@ 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:
"""pythons round() rounds .5 to the *even* number; 0.5 == 0"""
return int(Decimal(number).to_integral(ROUND_HALF_UP))
@ -26,3 +34,11 @@ def mul(ints: Iterable[int]) -> int:
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,6 +8,7 @@ class StopWatch:
stopped: int | None = None
def __init__(self, auto_start=True):
self.total_elapsed = 0
if auto_start:
self.start()
@ -15,11 +16,13 @@ class StopWatch:
self.started = perf_counter_ns()
self.stopped = None
def stop(self) -> float:
def stop(self):
self.stopped = perf_counter_ns()
return self.elapsed()
self.total_elapsed += self.elapsed()
reset = start
def reset(self):
self.total_elapsed = 0
self.start()
def elapsed(self) -> int:
if self.stopped is None:
@ -28,10 +31,10 @@ class StopWatch:
return self.stopped - self.started
def elapsed_string(self) -> str:
return human_readable_time_from_ns(self.elapsed())
return human_readable_time_from_ns(self.total_elapsed)
def avg_elapsed(self, divider: int) -> float:
return self.elapsed() / divider
return self.total_elapsed / divider
def avg_string(self, divider: int) -> str:
return human_readable_time_from_ns(int(self.avg_elapsed(divider)))

View File

@ -2,8 +2,6 @@ import datetime
import inspect
import os.path
import sys
from fishhook import hook
from functools import wraps
from typing import Any
@ -46,9 +44,7 @@ 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):
@ -104,14 +100,7 @@ def minmax(*arr: Any) -> (Any, Any):
else:
return arr[0], arr[0]
arr = set(arr)
smallest = min(arr)
biggest = max(arr)
if smallest == biggest:
arr.remove(smallest)
biggest = max(arr)
return smallest, biggest
return min(arr), max(arr)
def human_readable_time_from_delta(delta: datetime.timedelta) -> str:
@ -149,51 +138,3 @@ 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

90
src/tools/types.py Normal file
View File

@ -0,0 +1,90 @@
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

657
src/tools/visualization.py Normal file
View File

@ -0,0 +1,657 @@
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()