mirror of
				https://github.com/MarioSpore/Grinch-AP.git
				synced 2025-10-21 20:21:32 -06:00 
			
		
		
		
	
		
			
	
	
		
			283 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			283 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | from __future__ import annotations | ||
|  | 
 | ||
|  | import heapq | ||
|  | from collections import deque | ||
|  | from dataclasses import dataclass | ||
|  | from functools import cached_property | ||
|  | from typing import Deque, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple | ||
|  | 
 | ||
|  | from .pixel_map import PixelMap | ||
|  | from .player import Player, Race | ||
|  | from .position import Point2, Rect, Size | ||
|  | 
 | ||
|  | 
 | ||
|  | @dataclass | ||
|  | class Ramp: | ||
|  |     points: FrozenSet[Point2] | ||
|  |     game_info: GameInfo | ||
|  | 
 | ||
|  |     @property | ||
|  |     def x_offset(self) -> float: | ||
|  |         # Tested by printing actual building locations vs calculated depot positions | ||
|  |         return 0.5 | ||
|  | 
 | ||
|  |     @property | ||
|  |     def y_offset(self) -> float: | ||
|  |         # Tested by printing actual building locations vs calculated depot positions | ||
|  |         return 0.5 | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def _height_map(self): | ||
|  |         return self.game_info.terrain_height | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def size(self) -> int: | ||
|  |         return len(self.points) | ||
|  | 
 | ||
|  |     def height_at(self, p: Point2) -> int: | ||
|  |         return self._height_map[p] | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def upper(self) -> FrozenSet[Point2]: | ||
|  |         """ Returns the upper points of a ramp. """ | ||
|  |         current_max = -10000 | ||
|  |         result = set() | ||
|  |         for p in self.points: | ||
|  |             height = self.height_at(p) | ||
|  |             if height > current_max: | ||
|  |                 current_max = height | ||
|  |                 result = {p} | ||
|  |             elif height == current_max: | ||
|  |                 result.add(p) | ||
|  |         return frozenset(result) | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def upper2_for_ramp_wall(self) -> FrozenSet[Point2]: | ||
|  |         """ Returns the 2 upper ramp points of the main base ramp required for the supply depot and barracks placement properties used in this file. """ | ||
|  |         # From bottom center, find 2 points that are furthest away (within the same ramp) | ||
|  |         return frozenset(heapq.nlargest(2, self.upper, key=lambda x: x.distance_to_point2(self.bottom_center))) | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def top_center(self) -> Point2: | ||
|  |         length = len(self.upper) | ||
|  |         pos = Point2((sum(p.x for p in self.upper) / length, sum(p.y for p in self.upper) / length)) | ||
|  |         return pos | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def lower(self) -> FrozenSet[Point2]: | ||
|  |         current_min = 10000 | ||
|  |         result = set() | ||
|  |         for p in self.points: | ||
|  |             height = self.height_at(p) | ||
|  |             if height < current_min: | ||
|  |                 current_min = height | ||
|  |                 result = {p} | ||
|  |             elif height == current_min: | ||
|  |                 result.add(p) | ||
|  |         return frozenset(result) | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def bottom_center(self) -> Point2: | ||
|  |         length = len(self.lower) | ||
|  |         pos = Point2((sum(p.x for p in self.lower) / length, sum(p.y for p in self.lower) / length)) | ||
|  |         return pos | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def barracks_in_middle(self) -> Optional[Point2]: | ||
|  |         """ Barracks position in the middle of the 2 depots """ | ||
|  |         if len(self.upper) not in {2, 5}: | ||
|  |             return None | ||
|  |         if len(self.upper2_for_ramp_wall) == 2: | ||
|  |             points = set(self.upper2_for_ramp_wall) | ||
|  |             p1 = points.pop().offset((self.x_offset, self.y_offset)) | ||
|  |             p2 = points.pop().offset((self.x_offset, self.y_offset)) | ||
|  |             # Offset from top point to barracks center is (2, 1) | ||
|  |             intersects = p1.circle_intersection(p2, 5**0.5) | ||
|  |             any_lower_point = next(iter(self.lower)) | ||
|  |             return max(intersects, key=lambda p: p.distance_to_point2(any_lower_point)) | ||
|  |         raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def depot_in_middle(self) -> Optional[Point2]: | ||
|  |         """ Depot in the middle of the 3 depots """ | ||
|  |         if len(self.upper) not in {2, 5}: | ||
|  |             return None | ||
|  |         if len(self.upper2_for_ramp_wall) == 2: | ||
|  |             points = set(self.upper2_for_ramp_wall) | ||
|  |             p1 = points.pop().offset((self.x_offset, self.y_offset)) | ||
|  |             p2 = points.pop().offset((self.x_offset, self.y_offset)) | ||
|  |             # Offset from top point to depot center is (1.5, 0.5) | ||
|  |             try: | ||
|  |                 intersects = p1.circle_intersection(p2, 2.5**0.5) | ||
|  |             except AssertionError: | ||
|  |                 # Returns None when no placement was found, this is the case on the map Honorgrounds LE with an exceptionally large main base ramp | ||
|  |                 return None | ||
|  |             any_lower_point = next(iter(self.lower)) | ||
|  |             return max(intersects, key=lambda p: p.distance_to_point2(any_lower_point)) | ||
|  |         raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def corner_depots(self) -> FrozenSet[Point2]: | ||
|  |         """ Finds the 2 depot positions on the outside """ | ||
|  |         if not self.upper2_for_ramp_wall: | ||
|  |             return frozenset() | ||
|  |         if len(self.upper2_for_ramp_wall) == 2: | ||
|  |             points = set(self.upper2_for_ramp_wall) | ||
|  |             p1 = points.pop().offset((self.x_offset, self.y_offset)) | ||
|  |             p2 = points.pop().offset((self.x_offset, self.y_offset)) | ||
|  |             center = p1.towards(p2, p1.distance_to_point2(p2) / 2) | ||
|  |             depot_position = self.depot_in_middle | ||
|  |             if depot_position is None: | ||
|  |                 return frozenset() | ||
|  |             # Offset from middle depot to corner depots is (2, 1) | ||
|  |             intersects = center.circle_intersection(depot_position, 5**0.5) | ||
|  |             return intersects | ||
|  |         raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def barracks_can_fit_addon(self) -> bool: | ||
|  |         """ Test if a barracks can fit an addon at natural ramp """ | ||
|  |         # https://i.imgur.com/4b2cXHZ.png | ||
|  |         if len(self.upper2_for_ramp_wall) == 2: | ||
|  |             return self.barracks_in_middle.x + 1 > max(self.corner_depots, key=lambda depot: depot.x).x | ||
|  |         raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def barracks_correct_placement(self) -> Optional[Point2]: | ||
|  |         """ Corrected placement so that an addon can fit """ | ||
|  |         if self.barracks_in_middle is None: | ||
|  |             return None | ||
|  |         if len(self.upper2_for_ramp_wall) == 2: | ||
|  |             if self.barracks_can_fit_addon: | ||
|  |                 return self.barracks_in_middle | ||
|  |             return self.barracks_in_middle.offset((-2, 0)) | ||
|  |         raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def protoss_wall_pylon(self) -> Optional[Point2]: | ||
|  |         """
 | ||
|  |         Pylon position that powers the two wall buildings and the warpin position. | ||
|  |         """
 | ||
|  |         if len(self.upper) not in {2, 5}: | ||
|  |             return None | ||
|  |         if len(self.upper2_for_ramp_wall) != 2: | ||
|  |             raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") | ||
|  |         middle = self.depot_in_middle | ||
|  |         # direction up the ramp | ||
|  |         direction = self.barracks_in_middle.negative_offset(middle) | ||
|  |         return middle + 6 * direction | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def protoss_wall_buildings(self) -> FrozenSet[Point2]: | ||
|  |         """
 | ||
|  |         List of two positions for 3x3 buildings that form a wall with a spot for a one unit block. | ||
|  |         These buildings can be powered by a pylon on the protoss_wall_pylon position. | ||
|  |         """
 | ||
|  |         if len(self.upper) not in {2, 5}: | ||
|  |             return frozenset() | ||
|  |         if len(self.upper2_for_ramp_wall) == 2: | ||
|  |             middle = self.depot_in_middle | ||
|  |             # direction up the ramp | ||
|  |             direction = self.barracks_in_middle.negative_offset(middle) | ||
|  |             # sort depots based on distance to start to get wallin orientation | ||
|  |             sorted_depots = sorted( | ||
|  |                 self.corner_depots, key=lambda depot: depot.distance_to(self.game_info.player_start_location) | ||
|  |             ) | ||
|  |             wall1: Point2 = sorted_depots[1].offset(direction) | ||
|  |             wall2 = middle + direction + (middle - wall1) / 1.5 | ||
|  |             return frozenset([wall1, wall2]) | ||
|  |         raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def protoss_wall_warpin(self) -> Optional[Point2]: | ||
|  |         """
 | ||
|  |         Position for a unit to block the wall created by protoss_wall_buildings. | ||
|  |         Powered by protoss_wall_pylon. | ||
|  |         """
 | ||
|  |         if len(self.upper) not in {2, 5}: | ||
|  |             return None | ||
|  |         if len(self.upper2_for_ramp_wall) != 2: | ||
|  |             raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") | ||
|  |         middle = self.depot_in_middle | ||
|  |         # direction up the ramp | ||
|  |         direction = self.barracks_in_middle.negative_offset(middle) | ||
|  |         # sort depots based on distance to start to get wallin orientation | ||
|  |         sorted_depots = sorted(self.corner_depots, key=lambda x: x.distance_to(self.game_info.player_start_location)) | ||
|  |         return sorted_depots[0].negative_offset(direction) | ||
|  | 
 | ||
|  | 
 | ||
|  | class GameInfo: | ||
|  | 
 | ||
|  |     def __init__(self, proto): | ||
|  |         self._proto = proto | ||
|  |         self.players: List[Player] = [Player.from_proto(p) for p in self._proto.player_info] | ||
|  |         self.map_name: str = self._proto.map_name | ||
|  |         self.local_map_path: str = self._proto.local_map_path | ||
|  |         self.map_size: Size = Size.from_proto(self._proto.start_raw.map_size) | ||
|  | 
 | ||
|  |         # self.pathing_grid[point]: if 0, point is not pathable, if 1, point is pathable | ||
|  |         self.pathing_grid: PixelMap = PixelMap(self._proto.start_raw.pathing_grid, in_bits=True) | ||
|  |         # self.terrain_height[point]: returns the height in range of 0 to 255 at that point | ||
|  |         self.terrain_height: PixelMap = PixelMap(self._proto.start_raw.terrain_height) | ||
|  |         # self.placement_grid[point]: if 0, point is not placeable, if 1, point is pathable | ||
|  |         self.placement_grid: PixelMap = PixelMap(self._proto.start_raw.placement_grid, in_bits=True) | ||
|  |         self.playable_area = Rect.from_proto(self._proto.start_raw.playable_area) | ||
|  |         self.map_center = self.playable_area.center | ||
|  |         self.map_ramps: List[Ramp] = None  # Filled later by BotAI._prepare_first_step | ||
|  |         self.vision_blockers: FrozenSet[Point2] = None  # Filled later by BotAI._prepare_first_step | ||
|  |         self.player_races: Dict[int, Race] = { | ||
|  |             p.player_id: p.race_actual or p.race_requested | ||
|  |             for p in self._proto.player_info | ||
|  |         } | ||
|  |         self.start_locations: List[Point2] = [ | ||
|  |             Point2.from_proto(sl).round(decimals=1) for sl in self._proto.start_raw.start_locations | ||
|  |         ] | ||
|  |         self.player_start_location: Point2 = None  # Filled later by BotAI._prepare_first_step | ||
|  | 
 | ||
|  |     def _find_groups(self, points: FrozenSet[Point2], minimum_points_per_group: int = 8) -> Iterable[FrozenSet[Point2]]: | ||
|  |         """
 | ||
|  |         From a set of points, this function will try to group points together by | ||
|  |         painting clusters of points in a rectangular map using flood fill algorithm. | ||
|  |         Returns groups of points as list, like [{p1, p2, p3}, {p4, p5, p6, p7, p8}] | ||
|  |         """
 | ||
|  |         # TODO do we actually need colors here? the ramps will never touch anyways. | ||
|  |         NOT_COLORED_YET = -1 | ||
|  |         map_width = self.pathing_grid.width | ||
|  |         map_height = self.pathing_grid.height | ||
|  |         current_color: int = NOT_COLORED_YET | ||
|  |         picture: List[List[int]] = [[-2 for _ in range(map_width)] for _ in range(map_height)] | ||
|  | 
 | ||
|  |         def paint(pt: Point2) -> None: | ||
|  |             picture[pt.y][pt.x] = current_color | ||
|  | 
 | ||
|  |         nearby: List[Tuple[int, int]] = [(a, b) for a in [-1, 0, 1] for b in [-1, 0, 1] if a != 0 or b != 0] | ||
|  | 
 | ||
|  |         remaining: Set[Point2] = set(points) | ||
|  |         for point in remaining: | ||
|  |             paint(point) | ||
|  |         current_color = 1 | ||
|  |         queue: Deque[Point2] = deque() | ||
|  |         while remaining: | ||
|  |             current_group: Set[Point2] = set() | ||
|  |             if not queue: | ||
|  |                 start = remaining.pop() | ||
|  |                 paint(start) | ||
|  |                 queue.append(start) | ||
|  |                 current_group.add(start) | ||
|  |             while queue: | ||
|  |                 base: Point2 = queue.popleft() | ||
|  |                 for offset in nearby: | ||
|  |                     px, py = base.x + offset[0], base.y + offset[1] | ||
|  |                     # Do we ever reach out of map bounds? | ||
|  |                     if not (0 <= px < map_width and 0 <= py < map_height): | ||
|  |                         continue | ||
|  |                     if picture[py][px] != NOT_COLORED_YET: | ||
|  |                         continue | ||
|  |                     point: Point2 = Point2((px, py)) | ||
|  |                     remaining.discard(point) | ||
|  |                     paint(point) | ||
|  |                     queue.append(point) | ||
|  |                     current_group.add(point) | ||
|  |             if len(current_group) >= minimum_points_per_group: | ||
|  |                 yield frozenset(current_group) |