from __future__ import annotations from dataclasses import dataclass from enum import Enum from math import gcd, sqrt, inf, atan2, degrees from typing import Union, List, Optional class DistanceAlgorithm(Enum): MANHATTAN = 0 EUCLIDEAN = 1 PYTHAGOREAN = 1 CHEBYSHEV = 2 CHESSBOARD = 2 @dataclass(frozen=True, order=True) class Coordinate: x: int y: int z: Optional[int] = None def getDistanceTo(self, target: Coordinate, algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN, includeDiagonals: bool = False) -> Union[int, float]: """ Get distance to target Coordinate :param target: :param algorithm: Calculation Mode (0 = Manhattan, 1 = Pythagoras) :param includeDiagonals: in Manhattan Mode specify if diagonal movements are allowed (counts as 1.4 in 2D, 1.7 in 3D) :return: Distance to Target """ if algorithm == DistanceAlgorithm.EUCLIDEAN: if self.z is None: return sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2) else: return sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2 + abs(self.z - target.z) ** 2) elif algorithm == DistanceAlgorithm.CHEBYSHEV: if self.z is None: return max(abs(target.x - self.x), abs(target.y - self.y)) else: return max(abs(target.x - self.x), abs(target.y - self.y), abs(target.z - self.z)) elif algorithm == DistanceAlgorithm.MANHATTAN: if not includeDiagonals: if self.z is None: return abs(self.x - target.x) + abs(self.y - target.y) else: return abs(self.x - target.x) + abs(self.y - target.y) + abs(self.z - target.z) else: dist = [abs(self.x - target.x), abs(self.y - target.y)] if self.z is None: o_dist = max(dist) - min(dist) return o_dist + 1.4 * min(dist) else: dist.append(abs(self.z - target.z)) d_steps = min(dist) dist.remove(min(dist)) dist = [x - d_steps for x in dist] o_dist = max(dist) - min(dist) return 1.7 * d_steps + o_dist + 1.4 * min(dist) 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]: """ Get a list of neighbouring coordinates :param includeDiagonal: include diagonal neighbours :param minX: ignore all neighbours that would have an X value below this :param minY: ignore all neighbours that would have an Y value below this :param minZ: ignore all neighbours that would have an Z value below 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 maxZ: ignore all neighbours that would have an Z value above this :return: list of Coordinate """ neighbourList = [] if self.z is None: if includeDiagonal: nb_list = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] else: nb_list = [(-1, 0), (1, 0), (0, -1), (0, 1)] for dx, dy in nb_list: tx = self.x + dx ty = self.y + dy if minX <= tx <= maxX and minY <= ty <= maxY: neighbourList.append(Coordinate(tx, ty)) else: if includeDiagonal: nb_list = [(x, y, z) for x in [-1, 0, 1] for y in [-1, 0, 1] for z in [-1, 0, 1]] nb_list.remove((0, 0, 0)) else: nb_list = [(-1, 0, 0), (0, -1, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (0, 0, -1)] for dx, dy, dz in nb_list: tx = self.x + dx ty = self.y + dy tz = self.z + dz if minX <= tx <= maxX and minY <= ty <= maxY and minZ <= tz <= maxZ: neighbourList.append(Coordinate(tx, ty, tz)) return neighbourList def getAngleTo(self, target: Coordinate, normalized: bool = False) -> float: """normalized returns an angle going clockwise with 0 starting in the 'north'""" if self.z is not None: raise NotImplementedError() # which angle?!?! dx = target.x - self.x dy = target.y - self.y if not normalized: return degrees(atan2(dy, dx)) else: angle = degrees(atan2(dx, dy)) if dx >= 0: return 180.0 - angle else: return 180.0 + abs(angle) def getLineTo(self, target: Coordinate) -> List[Coordinate]: diff = target - self if self.z is None: steps = gcd(diff.x, diff.y) step_x = diff.x // steps step_y = diff.y // steps return [Coordinate(self.x + step_x * i, self.y + step_y * i) for i in range(steps + 1)] else: steps = gcd(diff.x, diff.y, diff.z) step_x = diff.x // steps step_y = diff.y // steps step_z = diff.z // steps return [Coordinate(self.x + step_x * i, self.y + step_y * i, self.z + step_z * i) for i in range(steps + 1)] def __add__(self, other: Coordinate) -> Coordinate: if self.z is None: return Coordinate(self.x + other.x, self.y + other.y) else: return Coordinate(self.x + other.x, self.y + other.y, self.z + other.z) def __sub__(self, other: Coordinate) -> Coordinate: if self.z is None: return Coordinate(self.x - other.x, self.y - other.y) else: return Coordinate(self.x - other.x, self.y - other.y, self.z - other.z) @staticmethod def generate(from_x: int, to_x: int, from_y: int, to_y: int, from_z: int = None, to_z: int = None) -> List[Coordinate]: if from_z is None and to_z is None: return [Coordinate(x, y) for x in range(from_x, to_x + 1) for y in range(from_y, to_y + 1)] else: return [ Coordinate(x, y, z) for x in range(from_x, to_x + 1) for y in range(from_y, to_y + 1) for z in range(from_z, to_z + 1) ]