mirror of
				https://github.com/MarioSpore/Grinch-AP.git
				synced 2025-10-21 20:21:32 -06:00 
			
		
		
		
	
		
			
	
	
		
			412 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			412 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | from __future__ import annotations | ||
|  | 
 | ||
|  | import itertools | ||
|  | import math | ||
|  | import random | ||
|  | from typing import TYPE_CHECKING, Iterable, List, Set, Tuple, Union | ||
|  | 
 | ||
|  | from s2clientprotocol import common_pb2 as common_pb | ||
|  | 
 | ||
|  | if TYPE_CHECKING: | ||
|  |     from .unit import Unit | ||
|  |     from .units import Units | ||
|  | 
 | ||
|  | EPSILON = 10**-8 | ||
|  | 
 | ||
|  | 
 | ||
|  | def _sign(num): | ||
|  |     return math.copysign(1, num) | ||
|  | 
 | ||
|  | 
 | ||
|  | class Pointlike(tuple): | ||
|  | 
 | ||
|  |     @property | ||
|  |     def position(self) -> Pointlike: | ||
|  |         return self | ||
|  | 
 | ||
|  |     def distance_to(self, target: Union[Unit, Point2]) -> float: | ||
|  |         """Calculate a single distance from a point or unit to another point or unit
 | ||
|  | 
 | ||
|  |         :param target:"""
 | ||
|  |         p = target.position | ||
|  |         return math.hypot(self[0] - p[0], self[1] - p[1]) | ||
|  | 
 | ||
|  |     def distance_to_point2(self, p: Union[Point2, Tuple[float, float]]) -> float: | ||
|  |         """Same as the function above, but should be a bit faster because of the dropped asserts
 | ||
|  |         and conversion. | ||
|  | 
 | ||
|  |         :param p:"""
 | ||
|  |         return math.hypot(self[0] - p[0], self[1] - p[1]) | ||
|  | 
 | ||
|  |     def _distance_squared(self, p2: Point2) -> float: | ||
|  |         """Function used to not take the square root as the distances will stay proportionally the same.
 | ||
|  |         This is to speed up the sorting process. | ||
|  | 
 | ||
|  |         :param p2:"""
 | ||
|  |         return (self[0] - p2[0])**2 + (self[1] - p2[1])**2 | ||
|  | 
 | ||
|  |     def sort_by_distance(self, ps: Union[Units, Iterable[Point2]]) -> List[Point2]: | ||
|  |         """This returns the target points sorted as list.
 | ||
|  |         You should not pass a set or dict since those are not sortable. | ||
|  |         If you want to sort your units towards a point, use 'units.sorted_by_distance_to(point)' instead. | ||
|  | 
 | ||
|  |         :param ps:"""
 | ||
|  |         return sorted(ps, key=lambda p: self.distance_to_point2(p.position)) | ||
|  | 
 | ||
|  |     def closest(self, ps: Union[Units, Iterable[Point2]]) -> Union[Unit, Point2]: | ||
|  |         """This function assumes the 2d distance is meant
 | ||
|  | 
 | ||
|  |         :param ps:"""
 | ||
|  |         assert ps, "ps is empty" | ||
|  |         # pylint: disable=W0108 | ||
|  |         return min(ps, key=lambda p: self.distance_to(p)) | ||
|  | 
 | ||
|  |     def distance_to_closest(self, ps: Union[Units, Iterable[Point2]]) -> float: | ||
|  |         """This function assumes the 2d distance is meant
 | ||
|  |         :param ps:"""
 | ||
|  |         assert ps, "ps is empty" | ||
|  |         closest_distance = math.inf | ||
|  |         for p2 in ps: | ||
|  |             p2 = p2.position | ||
|  |             distance = self.distance_to(p2) | ||
|  |             if distance <= closest_distance: | ||
|  |                 closest_distance = distance | ||
|  |         return closest_distance | ||
|  | 
 | ||
|  |     def furthest(self, ps: Union[Units, Iterable[Point2]]) -> Union[Unit, Pointlike]: | ||
|  |         """This function assumes the 2d distance is meant
 | ||
|  | 
 | ||
|  |         :param ps: Units object, or iterable of Unit or Point2"""
 | ||
|  |         assert ps, "ps is empty" | ||
|  |         # pylint: disable=W0108 | ||
|  |         return max(ps, key=lambda p: self.distance_to(p)) | ||
|  | 
 | ||
|  |     def distance_to_furthest(self, ps: Union[Units, Iterable[Point2]]) -> float: | ||
|  |         """This function assumes the 2d distance is meant
 | ||
|  | 
 | ||
|  |         :param ps:"""
 | ||
|  |         assert ps, "ps is empty" | ||
|  |         furthest_distance = -math.inf | ||
|  |         for p2 in ps: | ||
|  |             p2 = p2.position | ||
|  |             distance = self.distance_to(p2) | ||
|  |             if distance >= furthest_distance: | ||
|  |                 furthest_distance = distance | ||
|  |         return furthest_distance | ||
|  | 
 | ||
|  |     def offset(self, p) -> Pointlike: | ||
|  |         """
 | ||
|  | 
 | ||
|  |         :param p: | ||
|  |         """
 | ||
|  |         return self.__class__(a + b for a, b in itertools.zip_longest(self, p[:len(self)], fillvalue=0)) | ||
|  | 
 | ||
|  |     def unit_axes_towards(self, p): | ||
|  |         """
 | ||
|  | 
 | ||
|  |         :param p: | ||
|  |         """
 | ||
|  |         return self.__class__(_sign(b - a) for a, b in itertools.zip_longest(self, p[:len(self)], fillvalue=0)) | ||
|  | 
 | ||
|  |     def towards(self, p: Union[Unit, Pointlike], distance: Union[int, float] = 1, limit: bool = False) -> Pointlike: | ||
|  |         """
 | ||
|  | 
 | ||
|  |         :param p: | ||
|  |         :param distance: | ||
|  |         :param limit: | ||
|  |         """
 | ||
|  |         p = p.position | ||
|  |         # assert self != p, f"self is {self}, p is {p}" | ||
|  |         # TODO test and fix this if statement | ||
|  |         if self == p: | ||
|  |             return self | ||
|  |         # end of test | ||
|  |         d = self.distance_to(p) | ||
|  |         if limit: | ||
|  |             distance = min(d, distance) | ||
|  |         return self.__class__( | ||
|  |             a + (b - a) / d * distance for a, b in itertools.zip_longest(self, p[:len(self)], fillvalue=0) | ||
|  |         ) | ||
|  | 
 | ||
|  |     def __eq__(self, other): | ||
|  |         try: | ||
|  |             return all(abs(a - b) <= EPSILON for a, b in itertools.zip_longest(self, other, fillvalue=0)) | ||
|  |         except TypeError: | ||
|  |             return False | ||
|  | 
 | ||
|  |     def __hash__(self): | ||
|  |         return hash(tuple(self)) | ||
|  | 
 | ||
|  | 
 | ||
|  | # pylint: disable=R0904 | ||
|  | class Point2(Pointlike): | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def from_proto(cls, data) -> Point2: | ||
|  |         """
 | ||
|  |         :param data: | ||
|  |         """
 | ||
|  |         return cls((data.x, data.y)) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def as_Point2D(self) -> common_pb.Point2D: | ||
|  |         return common_pb.Point2D(x=self.x, y=self.y) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def as_PointI(self) -> common_pb.PointI: | ||
|  |         """Represents points on the minimap. Values must be between 0 and 64.""" | ||
|  |         return common_pb.PointI(x=self.x, y=self.y) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def rounded(self) -> Point2: | ||
|  |         return Point2((math.floor(self[0]), math.floor(self[1]))) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def length(self) -> float: | ||
|  |         """ This property exists in case Point2 is used as a vector. """ | ||
|  |         return math.hypot(self[0], self[1]) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def normalized(self) -> Point2: | ||
|  |         """ This property exists in case Point2 is used as a vector. """ | ||
|  |         length = self.length | ||
|  |         # Cannot normalize if length is zero | ||
|  |         assert length | ||
|  |         return self.__class__((self[0] / length, self[1] / length)) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def x(self) -> float: | ||
|  |         return self[0] | ||
|  | 
 | ||
|  |     @property | ||
|  |     def y(self) -> float: | ||
|  |         return self[1] | ||
|  | 
 | ||
|  |     @property | ||
|  |     def to2(self) -> Point2: | ||
|  |         return Point2(self[:2]) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def to3(self) -> Point3: | ||
|  |         return Point3((*self, 0)) | ||
|  | 
 | ||
|  |     def round(self, decimals: int) -> Point2: | ||
|  |         """Rounds each number in the tuple to the amount of given decimals.""" | ||
|  |         return Point2((round(self[0], decimals), round(self[1], decimals))) | ||
|  | 
 | ||
|  |     def offset(self, p: Point2) -> Point2: | ||
|  |         return Point2((self[0] + p[0], self[1] + p[1])) | ||
|  | 
 | ||
|  |     def random_on_distance(self, distance) -> Point2: | ||
|  |         if isinstance(distance, (tuple, list)):  # interval | ||
|  |             distance = distance[0] + random.random() * (distance[1] - distance[0]) | ||
|  | 
 | ||
|  |         assert distance > 0, "Distance is not greater than 0" | ||
|  |         angle = random.random() * 2 * math.pi | ||
|  | 
 | ||
|  |         dx, dy = math.cos(angle), math.sin(angle) | ||
|  |         return Point2((self.x + dx * distance, self.y + dy * distance)) | ||
|  | 
 | ||
|  |     def towards_with_random_angle( | ||
|  |         self, | ||
|  |         p: Union[Point2, Point3], | ||
|  |         distance: Union[int, float] = 1, | ||
|  |         max_difference: Union[int, float] = (math.pi / 4), | ||
|  |     ) -> Point2: | ||
|  |         tx, ty = self.to2.towards(p.to2, 1) | ||
|  |         angle = math.atan2(ty - self.y, tx - self.x) | ||
|  |         angle = (angle - max_difference) + max_difference * 2 * random.random() | ||
|  |         return Point2((self.x + math.cos(angle) * distance, self.y + math.sin(angle) * distance)) | ||
|  | 
 | ||
|  |     def circle_intersection(self, p: Point2, r: Union[int, float]) -> Set[Point2]: | ||
|  |         """self is point1, p is point2, r is the radius for circles originating in both points
 | ||
|  |         Used in ramp finding | ||
|  | 
 | ||
|  |         :param p: | ||
|  |         :param r:"""
 | ||
|  |         assert self != p, "self is equal to p" | ||
|  |         distanceBetweenPoints = self.distance_to(p) | ||
|  |         assert r >= distanceBetweenPoints / 2 | ||
|  |         # remaining distance from center towards the intersection, using pythagoras | ||
|  |         remainingDistanceFromCenter = (r**2 - (distanceBetweenPoints / 2)**2)**0.5 | ||
|  |         # center of both points | ||
|  |         offsetToCenter = Point2(((p.x - self.x) / 2, (p.y - self.y) / 2)) | ||
|  |         center = self.offset(offsetToCenter) | ||
|  | 
 | ||
|  |         # stretch offset vector in the ratio of remaining distance from center to intersection | ||
|  |         vectorStretchFactor = remainingDistanceFromCenter / (distanceBetweenPoints / 2) | ||
|  |         v = offsetToCenter | ||
|  |         offsetToCenterStretched = Point2((v.x * vectorStretchFactor, v.y * vectorStretchFactor)) | ||
|  | 
 | ||
|  |         # rotate vector by 90° and -90° | ||
|  |         vectorRotated1 = Point2((offsetToCenterStretched.y, -offsetToCenterStretched.x)) | ||
|  |         vectorRotated2 = Point2((-offsetToCenterStretched.y, offsetToCenterStretched.x)) | ||
|  |         intersect1 = center.offset(vectorRotated1) | ||
|  |         intersect2 = center.offset(vectorRotated2) | ||
|  |         return {intersect1, intersect2} | ||
|  | 
 | ||
|  |     @property | ||
|  |     def neighbors4(self) -> set: | ||
|  |         return { | ||
|  |             Point2((self.x - 1, self.y)), | ||
|  |             Point2((self.x + 1, self.y)), | ||
|  |             Point2((self.x, self.y - 1)), | ||
|  |             Point2((self.x, self.y + 1)), | ||
|  |         } | ||
|  | 
 | ||
|  |     @property | ||
|  |     def neighbors8(self) -> set: | ||
|  |         return self.neighbors4 | { | ||
|  |             Point2((self.x - 1, self.y - 1)), | ||
|  |             Point2((self.x - 1, self.y + 1)), | ||
|  |             Point2((self.x + 1, self.y - 1)), | ||
|  |             Point2((self.x + 1, self.y + 1)), | ||
|  |         } | ||
|  | 
 | ||
|  |     def negative_offset(self, other: Point2) -> Point2: | ||
|  |         return self.__class__((self[0] - other[0], self[1] - other[1])) | ||
|  | 
 | ||
|  |     def __add__(self, other: Point2) -> Point2: | ||
|  |         return self.offset(other) | ||
|  | 
 | ||
|  |     def __sub__(self, other: Point2) -> Point2: | ||
|  |         return self.negative_offset(other) | ||
|  | 
 | ||
|  |     def __neg__(self) -> Point2: | ||
|  |         return self.__class__(-a for a in self) | ||
|  | 
 | ||
|  |     def __abs__(self) -> float: | ||
|  |         return math.hypot(self.x, self.y) | ||
|  | 
 | ||
|  |     def __bool__(self) -> bool: | ||
|  |         if self.x != 0 or self.y != 0: | ||
|  |             return True | ||
|  |         return False | ||
|  | 
 | ||
|  |     def __mul__(self, other: Union[int, float, Point2]) -> Point2: | ||
|  |         try: | ||
|  |             return self.__class__((self.x * other.x, self.y * other.y)) | ||
|  |         except AttributeError: | ||
|  |             return self.__class__((self.x * other, self.y * other)) | ||
|  | 
 | ||
|  |     def __rmul__(self, other: Union[int, float, Point2]) -> Point2: | ||
|  |         return self.__mul__(other) | ||
|  | 
 | ||
|  |     def __truediv__(self, other: Union[int, float, Point2]) -> Point2: | ||
|  |         if isinstance(other, self.__class__): | ||
|  |             return self.__class__((self.x / other.x, self.y / other.y)) | ||
|  |         return self.__class__((self.x / other, self.y / other)) | ||
|  | 
 | ||
|  |     def is_same_as(self, other: Point2, dist=0.001) -> bool: | ||
|  |         return self.distance_to_point2(other) <= dist | ||
|  | 
 | ||
|  |     def direction_vector(self, other: Point2) -> Point2: | ||
|  |         """ Converts a vector to a direction that can face vertically, horizontally or diagonal or be zero, e.g. (0, 0), (1, -1), (1, 0) """ | ||
|  |         return self.__class__((_sign(other.x - self.x), _sign(other.y - self.y))) | ||
|  | 
 | ||
|  |     def manhattan_distance(self, other: Point2) -> float: | ||
|  |         """
 | ||
|  |         :param other: | ||
|  |         """
 | ||
|  |         return abs(other.x - self.x) + abs(other.y - self.y) | ||
|  | 
 | ||
|  |     @staticmethod | ||
|  |     def center(points: List[Point2]) -> Point2: | ||
|  |         """Returns the central point for points in list
 | ||
|  | 
 | ||
|  |         :param points:"""
 | ||
|  |         s = Point2((0, 0)) | ||
|  |         for p in points: | ||
|  |             s += p | ||
|  |         return s / len(points) | ||
|  | 
 | ||
|  | 
 | ||
|  | class Point3(Point2): | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def from_proto(cls, data) -> Point3: | ||
|  |         """
 | ||
|  |         :param data: | ||
|  |         """
 | ||
|  |         return cls((data.x, data.y, data.z)) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def as_Point(self) -> common_pb.Point: | ||
|  |         return common_pb.Point(x=self.x, y=self.y, z=self.z) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def rounded(self) -> Point3: | ||
|  |         return Point3((math.floor(self[0]), math.floor(self[1]), math.floor(self[2]))) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def z(self) -> float: | ||
|  |         return self[2] | ||
|  | 
 | ||
|  |     @property | ||
|  |     def to3(self) -> Point3: | ||
|  |         return Point3(self) | ||
|  | 
 | ||
|  |     def __add__(self, other: Union[Point2, Point3]) -> Point3: | ||
|  |         if not isinstance(other, Point3) and isinstance(other, Point2): | ||
|  |             return Point3((self.x + other.x, self.y + other.y, self.z)) | ||
|  |         return Point3((self.x + other.x, self.y + other.y, self.z + other.z)) | ||
|  | 
 | ||
|  | 
 | ||
|  | class Size(Point2): | ||
|  | 
 | ||
|  |     @property | ||
|  |     def width(self) -> float: | ||
|  |         return self[0] | ||
|  | 
 | ||
|  |     @property | ||
|  |     def height(self) -> float: | ||
|  |         return self[1] | ||
|  | 
 | ||
|  | 
 | ||
|  | class Rect(tuple): | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def from_proto(cls, data): | ||
|  |         """
 | ||
|  |         :param data: | ||
|  |         """
 | ||
|  |         assert data.p0.x < data.p1.x and data.p0.y < data.p1.y | ||
|  |         return cls((data.p0.x, data.p0.y, data.p1.x - data.p0.x, data.p1.y - data.p0.y)) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def x(self) -> float: | ||
|  |         return self[0] | ||
|  | 
 | ||
|  |     @property | ||
|  |     def y(self) -> float: | ||
|  |         return self[1] | ||
|  | 
 | ||
|  |     @property | ||
|  |     def width(self) -> float: | ||
|  |         return self[2] | ||
|  | 
 | ||
|  |     @property | ||
|  |     def height(self) -> float: | ||
|  |         return self[3] | ||
|  | 
 | ||
|  |     @property | ||
|  |     def right(self) -> float: | ||
|  |         """ Returns the x-coordinate of the rectangle of its right side. """ | ||
|  |         return self.x + self.width | ||
|  | 
 | ||
|  |     @property | ||
|  |     def top(self) -> float: | ||
|  |         """ Returns the y-coordinate of the rectangle of its top side. """ | ||
|  |         return self.y + self.height | ||
|  | 
 | ||
|  |     @property | ||
|  |     def size(self) -> Size: | ||
|  |         return Size((self[2], self[3])) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def center(self) -> Point2: | ||
|  |         return Point2((self.x + self.width / 2, self.y + self.height / 2)) | ||
|  | 
 | ||
|  |     def offset(self, p): | ||
|  |         return self.__class__((self[0] + p[0], self[1] + p[1], self[2], self[3])) |