Compare commits

...

57 Commits
0.3.2 ... 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
b2cc1e814c FIX Coordinate().getNeighbours() min/maxY
All checks were successful
Publish to PyPI / Publish to PyPI (push) Successful in 1m4s
FIX Coordinate() not hashable after implementing __eq__
2023-12-17 08:24:19 +01:00
42cbf6f85c Coordinate().__eq__: enable comparison with tuples 2023-12-17 07:21:00 +01:00
2bec56eb1c AOCDay.is_test() returns a bool, True if the solution for the current part/input_file is known; else False 2023-12-16 12:04:11 +01:00
f0e499f0a3 escape patterns want to be raw strings 2023-12-15 07:34:38 +01:00
1fb4582908 AOCDay._submit(): maybe display rank on correct answer? 2023-12-15 07:30:06 +01:00
2de9725adb Grid(): make use of the fact that Coordinate is a tupple for some speedups
All checks were successful
Publish to PyPI / Publish to PyPI (push) Successful in 1m18s
2023-12-14 16:18:25 +01:00
32c07d2913 Coordinate() now behaves more tupley-like (methods not accept tuples as parameters, including __add__ and __sub__) 2023-12-14 15:08:49 +01:00
0408432e3d Grid() is now hashable 2023-12-14 11:24:37 +01:00
f22c3bd798 Grid.isWithinBoundaries(): allow for boundary padding 2023-12-14 09:55:51 +01:00
c4b9b10b71 Grid.get_column() and Grid.get_row() 2023-12-13 06:37:24 +01:00
a47077f102 convey high/low hints if present
All checks were successful
Publish to PyPI / Publish to PyPI (push) Successful in 1m29s
2023-12-03 14:53:56 +01:00
db23b11a98 convey high/low hints if present 2023-12-03 14:51:57 +01:00
de8796d415 Grid.from_data() 2023-12-03 14:35:17 +01:00
af443c94b2 Grid.from_data() 2023-12-03 14:34:08 +01:00
e54e7afd6c mul() equivalent to sum() 2023-12-02 07:26:47 +01:00
12f3e58d85 make human readable time actually human readable 2023-12-01 17:29:24 +01:00
89edb0a6d7 getInput variant, that only returns integers from input lines 2023-12-01 07:37:25 +01:00
c2f6191d69 don't need to implement comparison dunders for Coordinate anymore as it now behaves like a tuple which already includes useful comparators
All checks were successful
Publish to PyPI / Publish to PyPI (push) Successful in 1m11s
2023-11-27 06:17:59 +01:00
6ab6df1bc9 setuptools is not a requirement 2023-11-12 19:35:40 +01:00
14 changed files with 1497 additions and 543 deletions

View File

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

View File

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

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

View File

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

View File

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

View File

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

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 @dataclass
class Node: class Node:
value: Any value: Any
next: "Node" = None next: Node = None
prev: "Node" = None prev: Node = None
class LinkedList: class LinkedList:

View File

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

View File

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

View File

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

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()