Compare commits

..

3 Commits

View File

@ -1,6 +1,6 @@
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
@ -14,19 +14,19 @@ 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:
@ -37,7 +37,7 @@ class Coordinate(tuple):
target: Coordinate | tuple, 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
@ -49,7 +49,9 @@ class Coordinate(tuple):
""" """
if algorithm == DistanceAlgorithm.EUCLIDEAN: if algorithm == DistanceAlgorithm.EUCLIDEAN:
if self[2] is None: if self[2] is None:
return sqrt(abs(self[0] - target[0]) ** 2 + abs(self[1] - target[1]) ** 2) return sqrt(
abs(self[0] - target[0]) ** 2 + abs(self[1] - target[1]) ** 2
)
else: else:
return sqrt( return sqrt(
abs(self[0] - target[0]) ** 2 abs(self[0] - target[0]) ** 2
@ -90,12 +92,12 @@ 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[2] is None: if self[2] is None:
return minX <= self[0] <= maxX and minY <= self[1] <= maxY return minX <= self[0] <= maxX and minY <= self[1] <= maxY
@ -108,14 +110,14 @@ class Coordinate(tuple):
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[2] is None: # mode 2D if self[2] is None: # mode 2D
@ -152,12 +154,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,22 +172,23 @@ class Coordinate(tuple):
:param maxX: ignore all neighbours that would have an X value above this :param maxX: ignore all neighbours that would have an X value above this
:param maxY: ignore all neighbours that would have an Y value above this :param maxY: ignore all neighbours that would have an Y value above this
:param maxZ: ignore all neighbours that would have an Z value above this :param maxZ: ignore all neighbours that would have an Z value above this
:param dist: distance to neighbour coordinates
:return: list of Coordinate :return: list of Coordinate
""" """
if self[2] is None: if self[2] is None:
if includeDiagonal: if includeDiagonal:
nb_list = [ nb_list = [
(-1, -1), (-dist, -dist),
(-1, 0), (-dist, 0),
(-1, 1), (-dist, dist),
(0, -1), (0, -dist),
(0, 1), (0, dist),
(1, -1), (dist, -dist),
(1, 0), (dist, 0),
(1, 1), (dist, dist),
] ]
else: else:
nb_list = [(-1, 0), (1, 0), (0, -1), (0, 1)] nb_list = [(-dist, 0), (dist, 0), (0, -dist), (0, dist)]
for dx, dy in nb_list: for dx, dy in nb_list:
if minX <= self[0] + dx <= maxX and minY <= self[1] + dy <= maxY: if minX <= self[0] + dx <= maxX and minY <= self[1] + dy <= maxY:
@ -193,19 +197,19 @@ class Coordinate(tuple):
if includeDiagonal: if includeDiagonal:
nb_list = [ nb_list = [
(x, y, z) (x, y, z)
for x in [-1, 0, 1] for x in [-dist, 0, dist]
for y in [-1, 0, 1] for y in [-dist, 0, dist]
for z in [-1, 0, 1] for z in [-dist, 0, dist]
] ]
nb_list.remove((0, 0, 0)) nb_list.remove((0, 0, 0))
else: else:
nb_list = [ nb_list = [
(-1, 0, 0), (-dist, 0, 0),
(0, -1, 0), (0, -dist, 0),
(1, 0, 0), (dist, 0, 0),
(0, 1, 0), (0, dist, 0),
(0, 0, 1), (0, 0, dist),
(0, 0, -1), (0, 0, -dist),
] ]
for dx, dy, dz in nb_list: for dx, dy, dz in nb_list:
@ -233,6 +237,7 @@ class Coordinate(tuple):
return 180.0 + abs(angle) return 180.0 + abs(angle)
def getLineTo(self, target: Coordinate | tuple) -> 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: if target == self:
return [self] return [self]
diff = target - self diff = target - self
@ -276,21 +281,25 @@ class Coordinate(tuple):
if self[2] is None: if self[2] is None:
return self.__class__(self[0] + other[0], self[1] + other[1]) return self.__class__(self[0] + other[0], self[1] + other[1])
else: else:
return self.__class__(self[0] + other[0], self[1] + other[1], self[2] + other[2]) return self.__class__(
self[0] + other[0], self[1] + other[1], self[2] + other[2]
)
def __sub__(self, other: Coordinate | tuple) -> Coordinate: def __sub__(self, other: Coordinate | tuple) -> Coordinate:
if self[2] is None: if self[2] is None:
return self.__class__(self[0] - other[0], self[1] - other[1]) return self.__class__(self[0] - other[0], self[1] - other[1])
else: else:
return self.__class__(self[0] - other[0], self[1] - other[1], self[2] - other[2]) return self.__class__(
self[0] - other[0], self[1] - other[1], self[2] - other[2]
)
def __mul__(self, other: int) -> Coordinate: def __mul__(self, other: int | float) -> Coordinate:
if self[2] is None: if self[2] is None:
return self.__class__(self[0] * other, self[1] * other) return self.__class__(self[0] * other, self[1] * other)
else: else:
return self.__class__(self[0] * other, self[1] * other, self[2] * other) return self.__class__(self[0] * other, self[1] * other, self[2] * other)
def __mod__(self, other: int) -> Coordinate: def __mod__(self, other: int | float) -> Coordinate:
if self[2] is None: if self[2] is None:
return self.__class__(self[0] % other, self[1] % other) return self.__class__(self[0] % other, self[1] % other)
else: else:
@ -303,19 +312,22 @@ class Coordinate(tuple):
return self.__class__(self[0] // other, self[1] // other, self[2] // other) return self.__class__(self[0] // other, self[1] // other, self[2] // other)
def __truediv__(self, other: int | float) -> Coordinate: def __truediv__(self, other: int | float) -> Coordinate:
return self // other if self[2] is None:
return self.__class__(self[0] / other, self[1] / other)
else:
return self.__class__(self[0] / other, self[1] / other, self[2] / other)
def __str__(self): def __str__(self):
if self[2] is None: if self[2] is None:
return "(%d,%d)" % (self[0], self[1]) return "({},{})".format(self[0], self[1])
else: else:
return "(%d,%d,%d)" % (self[0], self[1], self[2]) return "({},{},{})".format(self[0], self[1], self[2])
def __repr__(self): def __repr__(self):
if self[2] is None: if self[2] is None:
return "%s(x=%d, y=%d)" % (self.__class__.__name__, self[0], self[1]) return "{}(x={}, y={})".format(self.__class__.__name__, self[0], self[1])
else: else:
return "%s(x=%d, y=%d, z=%d)" % ( return "{}(x={}, y={}, z={})".format(
self.__class__.__name__, self.__class__.__name__,
self[0], self[0],
self[1], self[1],
@ -325,112 +337,29 @@ class Coordinate(tuple):
@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) cls(x, y)
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)
] ]
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):
""" """
@ -539,3 +468,48 @@ 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)
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 = start
self.end = end
def contains(self, point: Coordinate | tuple) -> bool:
return isclose(
self.start.getDistanceTo(self.end),
self.start.getDistanceTo(point) + self.end.getDistanceTo(point),
)
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 self.contains(ret) and other.contains(ret):
return ret
else:
raise ValueError("intersection out of bounds")