Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b777d5849 | |||
| 587fee299f | |||
| 105ab06cd9 | |||
| c4fb68f809 | |||
| 17c41a0c63 | |||
| 270866387e | |||
| 6adb1ff457 | |||
| e06e536292 | |||
| dd39c90f13 | |||
| 24cd561370 | |||
| 23283d3b66 | |||
| bfac2b9433 | |||
| 3ac82ee526 | |||
| d6fad1511c | |||
| cf72a5451f | |||
| 87fbaeafbe | |||
| 94cffbbd74 | |||
| e3309415b7 | |||
| 2d7d339bf0 | |||
| ed0fe0dafc | |||
| 87eff61ae2 | |||
| 0aedd1c612 | |||
| 8dea09c30f | |||
| f7d1fde5b7 | |||
| 600d0e716c | |||
| 5d5e9fb8e5 | |||
| 916ab481cf | |||
| 4ce70c8565 | |||
| ee3b5ee941 | |||
| 4d1b075086 | |||
| 1e43a2ff0d | |||
| cc1fd86ede | |||
| 9386c40ea5 | |||
| 14d535911c | |||
| fb3bef0153 | |||
| ffcc1e14c0 | |||
| 1a111488ba | |||
| 02623f8c9a | |||
| b2cc1e814c | |||
| 42cbf6f85c | |||
| 2bec56eb1c | |||
| f0e499f0a3 | |||
| 1fb4582908 | |||
| 2de9725adb | |||
| 32c07d2913 | |||
| 0408432e3d | |||
| f22c3bd798 | |||
| c4b9b10b71 | |||
| a47077f102 | |||
| db23b11a98 | |||
| de8796d415 | |||
| af443c94b2 | |||
| e54e7afd6c | |||
| 12f3e58d85 | |||
| 89edb0a6d7 | |||
| c2f6191d69 | |||
| 6ab6df1bc9 | |||
| 0daa028368 | |||
| 034c010635 |
@ -5,8 +5,11 @@ build-backend = 'setuptools.build_meta'
|
|||||||
[tool.setuptools-git-versioning]
|
[tool.setuptools-git-versioning]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
dependencies = {file = ["requirements.txt"]}
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
dynamic = ['version']
|
dynamic = ['version', 'dependencies']
|
||||||
name = "shs-tools"
|
name = "shs-tools"
|
||||||
authors = [
|
authors = [
|
||||||
{ name="Stefan Harmuth", email="pennywise@drock.de" },
|
{ name="Stefan Harmuth", email="pennywise@drock.de" },
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
bs4~=0.0.1
|
bs4~=0.0.1
|
||||||
beautifulsoup4==4.11.1
|
beautifulsoup4~=4.12
|
||||||
fishhook~=0.2.5
|
requests~=2.32
|
||||||
pygame~=2.4.0
|
tqdm~=4.66
|
||||||
requests==2.28.1
|
|
||||||
setuptools==65.6.3
|
|
||||||
133
src/tools/aoc.py
133
src/tools/aoc.py
@ -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
|
||||||
@ -90,6 +110,9 @@ class AOCDay:
|
|||||||
self.run_part(1, verbose, measure_runtime, timeit_number)
|
self.run_part(1, verbose, measure_runtime, timeit_number)
|
||||||
|
|
||||||
def _load_input(self, filename):
|
def _load_input(self, filename):
|
||||||
|
if not os.path.exists(INPUTS_PATH):
|
||||||
|
os.mkdir(INPUTS_PATH)
|
||||||
|
|
||||||
file_path = os.path.join(INPUTS_PATH, filename)
|
file_path = os.path.join(INPUTS_PATH, filename)
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
self._download_input(file_path)
|
self._download_input(file_path)
|
||||||
@ -105,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:
|
||||||
@ -151,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"
|
||||||
@ -204,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
|
||||||
"""
|
"""
|
||||||
@ -232,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,
|
||||||
@ -289,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
79
src/tools/climate.py
Normal 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/m³
|
||||||
|
"""
|
||||||
|
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/m³
|
||||||
|
"""
|
||||||
|
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)
|
||||||
@ -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))
|
||||||
|
|||||||
@ -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()))
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
41
src/tools/itertools.py
Normal 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)
|
||||||
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)))
|
||||||
|
|||||||
@ -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
90
src/tools/types.py
Normal 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
657
src/tools/visualization.py
Normal 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()
|
||||||
Loading…
Reference in New Issue
Block a user