mirror of
				https://github.com/MarioSpore/Grinch-AP.git
				synced 2025-10-21 20:21:32 -06:00 
			
		
		
		
	
		
			
	
	
		
			491 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			491 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | # pylint: disable=W0201,W0212,R0912 | ||
|  | from __future__ import annotations | ||
|  | 
 | ||
|  | import math | ||
|  | import time | ||
|  | import warnings | ||
|  | from abc import ABC | ||
|  | from collections import Counter | ||
|  | from typing import TYPE_CHECKING, Any | ||
|  | from typing import Dict, Generator, Iterable, List, Set, Tuple, Union, final | ||
|  | 
 | ||
|  | from s2clientprotocol import sc2api_pb2 as sc_pb | ||
|  | 
 | ||
|  | from .constants import ( | ||
|  |     IS_PLACEHOLDER, | ||
|  | ) | ||
|  | from .data import Race | ||
|  | from .game_data import GameData | ||
|  | from .game_state import Blip, GameState | ||
|  | from .pixel_map import PixelMap | ||
|  | from .position import Point2 | ||
|  | from .unit import Unit | ||
|  | from .units import Units | ||
|  | 
 | ||
|  | # with warnings.catch_warnings(): | ||
|  | #     warnings.simplefilter("ignore") | ||
|  | #     from scipy.spatial.distance import cdist, pdist | ||
|  | 
 | ||
|  | if TYPE_CHECKING: | ||
|  |     from .client import Client | ||
|  |     from .game_info import GameInfo | ||
|  | 
 | ||
|  | 
 | ||
|  | class BotAIInternal(ABC): | ||
|  |     """Base class for bots.""" | ||
|  | 
 | ||
|  |     @final | ||
|  |     def _initialize_variables(self): | ||
|  |         """ Called from main.py internally """ | ||
|  |         self.cache: Dict[str, Any] = {} | ||
|  |         # Specific opponent bot ID used in sc2ai ladder games http://sc2ai.net/ and on ai arena https://aiarena.net | ||
|  |         # The bot ID will stay the same each game so your bot can "adapt" to the opponent | ||
|  |         if not hasattr(self, "opponent_id"): | ||
|  |             # Prevent overwriting the opponent_id which is set here https://github.com/Hannessa/python-sc2-ladderbot/blob/master/__init__.py#L40 | ||
|  |             # otherwise set it to None | ||
|  |             self.opponent_id: str = None | ||
|  |         # Select distance calculation method, see _distances_override_functions function | ||
|  |         if not hasattr(self, "distance_calculation_method"): | ||
|  |             self.distance_calculation_method: int = 2 | ||
|  |         # Select if the Unit.command should return UnitCommand objects. Set this to True if your bot uses 'self.do(unit(ability, target))' | ||
|  |         if not hasattr(self, "unit_command_uses_self_do"): | ||
|  |             self.unit_command_uses_self_do: bool = False | ||
|  |         # This value will be set to True by main.py in self._prepare_start if game is played in realtime (if true, the bot will have limited time per step) | ||
|  |         self.realtime: bool = False | ||
|  |         self.base_build: int = -1 | ||
|  |         self.all_units: Units = Units([], self) | ||
|  |         self.units: Units = Units([], self) | ||
|  |         self.workers: Units = Units([], self) | ||
|  |         self.larva: Units = Units([], self) | ||
|  |         self.structures: Units = Units([], self) | ||
|  |         self.townhalls: Units = Units([], self) | ||
|  |         self.gas_buildings: Units = Units([], self) | ||
|  |         self.all_own_units: Units = Units([], self) | ||
|  |         self.enemy_units: Units = Units([], self) | ||
|  |         self.enemy_structures: Units = Units([], self) | ||
|  |         self.all_enemy_units: Units = Units([], self) | ||
|  |         self.resources: Units = Units([], self) | ||
|  |         self.destructables: Units = Units([], self) | ||
|  |         self.watchtowers: Units = Units([], self) | ||
|  |         self.mineral_field: Units = Units([], self) | ||
|  |         self.vespene_geyser: Units = Units([], self) | ||
|  |         self.placeholders: Units = Units([], self) | ||
|  |         self.techlab_tags: Set[int] = set() | ||
|  |         self.reactor_tags: Set[int] = set() | ||
|  |         self.minerals: int = 50 | ||
|  |         self.vespene: int = 0 | ||
|  |         self.supply_army: float = 0 | ||
|  |         self.supply_workers: float = 12  # Doesn't include workers in production | ||
|  |         self.supply_cap: float = 15 | ||
|  |         self.supply_used: float = 12 | ||
|  |         self.supply_left: float = 3 | ||
|  |         self.idle_worker_count: int = 0 | ||
|  |         self.army_count: int = 0 | ||
|  |         self.warp_gate_count: int = 0 | ||
|  |         self.blips: Set[Blip] = set() | ||
|  |         self.race: Race = None | ||
|  |         self.enemy_race: Race = None | ||
|  |         self._generated_frame = -100 | ||
|  |         self._units_created: Counter = Counter() | ||
|  |         self._unit_tags_seen_this_game: Set[int] = set() | ||
|  |         self._units_previous_map: Dict[int, Unit] = {} | ||
|  |         self._structures_previous_map: Dict[int, Unit] = {} | ||
|  |         self._enemy_units_previous_map: Dict[int, Unit] = {} | ||
|  |         self._enemy_structures_previous_map: Dict[int, Unit] = {} | ||
|  |         self._all_units_previous_map: Dict[int, Unit] = {} | ||
|  |         self._expansion_positions_list: List[Point2] = [] | ||
|  |         self._resource_location_to_expansion_position_dict: Dict[Point2, Point2] = {} | ||
|  |         self._time_before_step: float = None | ||
|  |         self._time_after_step: float = None | ||
|  |         self._min_step_time: float = math.inf | ||
|  |         self._max_step_time: float = 0 | ||
|  |         self._last_step_step_time: float = 0 | ||
|  |         self._total_time_in_on_step: float = 0 | ||
|  |         self._total_steps_iterations: int = 0 | ||
|  |         # Internally used to keep track which units received an action in this frame, so that self.train() function does not give the same larva two orders - cleared every frame | ||
|  |         self.unit_tags_received_action: Set[int] = set() | ||
|  | 
 | ||
|  |     @final | ||
|  |     @property | ||
|  |     def _game_info(self) -> GameInfo: | ||
|  |         """ See game_info.py """ | ||
|  |         warnings.warn( | ||
|  |             "Using self._game_info is deprecated and may be removed soon. Please use self.game_info directly.", | ||
|  |             DeprecationWarning, | ||
|  |             stacklevel=2, | ||
|  |         ) | ||
|  |         return self.game_info | ||
|  | 
 | ||
|  |     @final | ||
|  |     @property | ||
|  |     def _game_data(self) -> GameData: | ||
|  |         """ See game_data.py """ | ||
|  |         warnings.warn( | ||
|  |             "Using self._game_data is deprecated and may be removed soon. Please use self.game_data directly.", | ||
|  |             DeprecationWarning, | ||
|  |             stacklevel=2, | ||
|  |         ) | ||
|  |         return self.game_data | ||
|  | 
 | ||
|  |     @final | ||
|  |     @property | ||
|  |     def _client(self) -> Client: | ||
|  |         """ See client.py """ | ||
|  |         warnings.warn( | ||
|  |             "Using self._client is deprecated and may be removed soon. Please use self.client directly.", | ||
|  |             DeprecationWarning, | ||
|  |             stacklevel=2, | ||
|  |         ) | ||
|  |         return self.client | ||
|  | 
 | ||
|  |     @final | ||
|  |     def _prepare_start(self, client, player_id, game_info, game_data, realtime: bool = False, base_build: int = -1): | ||
|  |         """
 | ||
|  |         Ran until game start to set game and player data. | ||
|  | 
 | ||
|  |         :param client: | ||
|  |         :param player_id: | ||
|  |         :param game_info: | ||
|  |         :param game_data: | ||
|  |         :param realtime: | ||
|  |         """
 | ||
|  |         self.client: Client = client | ||
|  |         self.player_id: int = player_id | ||
|  |         self.game_info: GameInfo = game_info | ||
|  |         self.game_data: GameData = game_data | ||
|  |         self.realtime: bool = realtime | ||
|  |         self.base_build: int = base_build | ||
|  | 
 | ||
|  |         self.race: Race = Race(self.game_info.player_races[self.player_id]) | ||
|  | 
 | ||
|  |         if len(self.game_info.player_races) == 2: | ||
|  |             self.enemy_race: Race = Race(self.game_info.player_races[3 - self.player_id]) | ||
|  | 
 | ||
|  | 
 | ||
|  |     @final | ||
|  |     def _prepare_first_step(self): | ||
|  |         """First step extra preparations. Must not be called before _prepare_step.""" | ||
|  |         if self.townhalls: | ||
|  |             self.game_info.player_start_location = self.townhalls.first.position | ||
|  |             # Calculate and cache expansion locations forever inside 'self._cache_expansion_locations', this is done to prevent a bug when this is run and cached later in the game | ||
|  |         self._time_before_step: float = time.perf_counter() | ||
|  | 
 | ||
|  |     @final | ||
|  |     def _prepare_step(self, state, proto_game_info): | ||
|  |         """
 | ||
|  |         :param state: | ||
|  |         :param proto_game_info: | ||
|  |         """
 | ||
|  |         # Set attributes from new state before on_step.""" | ||
|  |         self.state: GameState = state  # See game_state.py | ||
|  |         # update pathing grid, which unfortunately is in GameInfo instead of GameState | ||
|  |         self.game_info.pathing_grid = PixelMap(proto_game_info.game_info.start_raw.pathing_grid, in_bits=True) | ||
|  |         # Required for events, needs to be before self.units are initialized so the old units are stored | ||
|  |         self._units_previous_map: Dict[int, Unit] = {unit.tag: unit for unit in self.units} | ||
|  |         self._structures_previous_map: Dict[int, Unit] = {structure.tag: structure for structure in self.structures} | ||
|  |         self._enemy_units_previous_map: Dict[int, Unit] = {unit.tag: unit for unit in self.enemy_units} | ||
|  |         self._enemy_structures_previous_map: Dict[int, Unit] = { | ||
|  |             structure.tag: structure | ||
|  |             for structure in self.enemy_structures | ||
|  |         } | ||
|  |         self._all_units_previous_map: Dict[int, Unit] = {unit.tag: unit for unit in self.all_units} | ||
|  | 
 | ||
|  |         self._prepare_units() | ||
|  |         self.minerals: int = state.common.minerals | ||
|  |         self.vespene: int = state.common.vespene | ||
|  |         self.supply_army: int = state.common.food_army | ||
|  |         self.supply_workers: int = state.common.food_workers  # Doesn't include workers in production | ||
|  |         self.supply_cap: int = state.common.food_cap | ||
|  |         self.supply_used: int = state.common.food_used | ||
|  |         self.supply_left: int = self.supply_cap - self.supply_used | ||
|  | 
 | ||
|  |         if self.race == Race.Zerg: | ||
|  |             # Workaround Zerg supply rounding bug | ||
|  |             pass | ||
|  |             # self._correct_zerg_supply() | ||
|  |         elif self.race == Race.Protoss: | ||
|  |             self.warp_gate_count: int = state.common.warp_gate_count | ||
|  | 
 | ||
|  |         self.idle_worker_count: int = state.common.idle_worker_count | ||
|  |         self.army_count: int = state.common.army_count | ||
|  |         self._time_before_step: float = time.perf_counter() | ||
|  | 
 | ||
|  |         if self.enemy_race == Race.Random and self.all_enemy_units: | ||
|  |             self.enemy_race = Race(self.all_enemy_units.first.race) | ||
|  | 
 | ||
|  |     @final | ||
|  |     def _prepare_units(self): | ||
|  |         # Set of enemy units detected by own sensor tower, as blips have less unit information than normal visible units | ||
|  |         self.blips: Set[Blip] = set() | ||
|  |         self.all_units: Units = Units([], self) | ||
|  |         self.units: Units = Units([], self) | ||
|  |         self.workers: Units = Units([], self) | ||
|  |         self.larva: Units = Units([], self) | ||
|  |         self.structures: Units = Units([], self) | ||
|  |         self.townhalls: Units = Units([], self) | ||
|  |         self.gas_buildings: Units = Units([], self) | ||
|  |         self.all_own_units: Units = Units([], self) | ||
|  |         self.enemy_units: Units = Units([], self) | ||
|  |         self.enemy_structures: Units = Units([], self) | ||
|  |         self.all_enemy_units: Units = Units([], self) | ||
|  |         self.resources: Units = Units([], self) | ||
|  |         self.destructables: Units = Units([], self) | ||
|  |         self.watchtowers: Units = Units([], self) | ||
|  |         self.mineral_field: Units = Units([], self) | ||
|  |         self.vespene_geyser: Units = Units([], self) | ||
|  |         self.placeholders: Units = Units([], self) | ||
|  |         self.techlab_tags: Set[int] = set() | ||
|  |         self.reactor_tags: Set[int] = set() | ||
|  | 
 | ||
|  |         index: int = 0 | ||
|  |         for unit in self.state.observation_raw.units: | ||
|  |             if unit.is_blip: | ||
|  |                 self.blips.add(Blip(unit)) | ||
|  |             else: | ||
|  |                 unit_type: int = unit.unit_type | ||
|  |                 # Convert these units to effects: reaper grenade, parasitic bomb dummy, forcefield | ||
|  |                 unit_obj = Unit(unit, self, distance_calculation_index=index, base_build=self.base_build) | ||
|  |                 index += 1 | ||
|  |                 self.all_units.append(unit_obj) | ||
|  |                 if unit.display_type == IS_PLACEHOLDER: | ||
|  |                     self.placeholders.append(unit_obj) | ||
|  |                     continue | ||
|  |                 alliance = unit.alliance | ||
|  |                 # Alliance.Neutral.value = 3 | ||
|  |                 if alliance == 3: | ||
|  |                     # XELNAGATOWER = 149 | ||
|  |                     if unit_type == 149: | ||
|  |                         self.watchtowers.append(unit_obj) | ||
|  |                     # all destructable rocks | ||
|  |                     else: | ||
|  |                         self.destructables.append(unit_obj) | ||
|  |                 # Alliance.Self.value = 1 | ||
|  |                 elif alliance == 1: | ||
|  |                     self.all_own_units.append(unit_obj) | ||
|  |                     if unit_obj.is_structure: | ||
|  |                         self.structures.append(unit_obj) | ||
|  |                 # Alliance.Enemy.value = 4 | ||
|  |                 elif alliance == 4: | ||
|  |                     self.all_enemy_units.append(unit_obj) | ||
|  |                     if unit_obj.is_structure: | ||
|  |                         self.enemy_structures.append(unit_obj) | ||
|  |                     else: | ||
|  |                         self.enemy_units.append(unit_obj) | ||
|  | 
 | ||
|  |     @final | ||
|  |     async def _after_step(self) -> int: | ||
|  |         """ Executed by main.py after each on_step function. """ | ||
|  |         # Keep track of the bot on_step duration | ||
|  |         self._time_after_step: float = time.perf_counter() | ||
|  |         step_duration = self._time_after_step - self._time_before_step | ||
|  |         self._min_step_time = min(step_duration, self._min_step_time) | ||
|  |         self._max_step_time = max(step_duration, self._max_step_time) | ||
|  |         self._last_step_step_time = step_duration | ||
|  |         self._total_time_in_on_step += step_duration | ||
|  |         self._total_steps_iterations += 1 | ||
|  |         # Clear set of unit tags that were given an order this frame by self.do() | ||
|  |         self.unit_tags_received_action.clear() | ||
|  |         # Commit debug queries | ||
|  |         await self.client._send_debug() | ||
|  | 
 | ||
|  |         return self.state.game_loop | ||
|  | 
 | ||
|  |     @final | ||
|  |     async def _advance_steps(self, steps: int): | ||
|  |         """Advances the game loop by amount of 'steps'. This function is meant to be used as a debugging and testing tool only.
 | ||
|  |         If you are using this, please be aware of the consequences, e.g. 'self.units' will be filled with completely new data."""
 | ||
|  |         await self._after_step() | ||
|  |         # Advance simulation by exactly "steps" frames | ||
|  |         await self.client.step(steps) | ||
|  |         state = await self.client.observation() | ||
|  |         gs = GameState(state.observation) | ||
|  |         proto_game_info = await self.client._execute(game_info=sc_pb.RequestGameInfo()) | ||
|  |         self._prepare_step(gs, proto_game_info) | ||
|  |         await self.issue_events() | ||
|  | 
 | ||
|  |     @final | ||
|  |     async def issue_events(self): | ||
|  |         """This function will be automatically run from main.py and triggers the following functions:
 | ||
|  |         - on_unit_created | ||
|  |         - on_unit_destroyed | ||
|  |         - on_building_construction_started | ||
|  |         - on_building_construction_complete | ||
|  |         - on_upgrade_complete | ||
|  |         """
 | ||
|  |         await self._issue_unit_dead_events() | ||
|  |         await self._issue_unit_added_events() | ||
|  |         await self._issue_building_events() | ||
|  |         await self._issue_upgrade_events() | ||
|  |         await self._issue_vision_events() | ||
|  | 
 | ||
|  |     @final | ||
|  |     async def _issue_unit_added_events(self): | ||
|  |         pass | ||
|  |         # for unit in self.units: | ||
|  |         #     if unit.tag not in self._units_previous_map and unit.tag not in self._unit_tags_seen_this_game: | ||
|  |         #         self._unit_tags_seen_this_game.add(unit.tag) | ||
|  |         #         self._units_created[unit.type_id] += 1 | ||
|  |         #         await self.on_unit_created(unit) | ||
|  |         #     elif unit.tag in self._units_previous_map: | ||
|  |         #         previous_frame_unit: Unit = self._units_previous_map[unit.tag] | ||
|  |         #         # Check if a unit took damage this frame and then trigger event | ||
|  |         #         if unit.health < previous_frame_unit.health or unit.shield < previous_frame_unit.shield: | ||
|  |         #             damage_amount = previous_frame_unit.health - unit.health + previous_frame_unit.shield - unit.shield | ||
|  |         #             await self.on_unit_took_damage(unit, damage_amount) | ||
|  |         #         # Check if a unit type has changed | ||
|  |         #         if previous_frame_unit.type_id != unit.type_id: | ||
|  |         #             await self.on_unit_type_changed(unit, previous_frame_unit.type_id) | ||
|  | 
 | ||
|  |     @final | ||
|  |     async def _issue_upgrade_events(self): | ||
|  |         pass | ||
|  |         # difference = self.state.upgrades - self._previous_upgrades | ||
|  |         # for upgrade_completed in difference: | ||
|  |         #     await self.on_upgrade_complete(upgrade_completed) | ||
|  |         # self._previous_upgrades = self.state.upgrades | ||
|  | 
 | ||
|  |     @final | ||
|  |     async def _issue_building_events(self): | ||
|  |         pass | ||
|  |         # for structure in self.structures: | ||
|  |         #     if structure.tag not in self._structures_previous_map: | ||
|  |         #         if structure.build_progress < 1: | ||
|  |         #             await self.on_building_construction_started(structure) | ||
|  |         #         else: | ||
|  |         #             # Include starting townhall | ||
|  |         #             self._units_created[structure.type_id] += 1 | ||
|  |         #             await self.on_building_construction_complete(structure) | ||
|  |         #     elif structure.tag in self._structures_previous_map: | ||
|  |         #         # Check if a structure took damage this frame and then trigger event | ||
|  |         #         previous_frame_structure: Unit = self._structures_previous_map[structure.tag] | ||
|  |         #         if ( | ||
|  |         #             structure.health < previous_frame_structure.health | ||
|  |         #             or structure.shield < previous_frame_structure.shield | ||
|  |         #         ): | ||
|  |         #             damage_amount = ( | ||
|  |         #                 previous_frame_structure.health - structure.health + previous_frame_structure.shield - | ||
|  |         #                 structure.shield | ||
|  |         #             ) | ||
|  |         #             await self.on_unit_took_damage(structure, damage_amount) | ||
|  |         #         # Check if a structure changed its type | ||
|  |         #         if previous_frame_structure.type_id != structure.type_id: | ||
|  |         #             await self.on_unit_type_changed(structure, previous_frame_structure.type_id) | ||
|  |         #         # Check if structure completed | ||
|  |         #         if structure.build_progress == 1 and previous_frame_structure.build_progress < 1: | ||
|  |         #             self._units_created[structure.type_id] += 1 | ||
|  |         #             await self.on_building_construction_complete(structure) | ||
|  | 
 | ||
|  |     @final | ||
|  |     async def _issue_vision_events(self): | ||
|  |         pass | ||
|  |         # # Call events for enemy unit entered vision | ||
|  |         # for enemy_unit in self.enemy_units: | ||
|  |         #     if enemy_unit.tag not in self._enemy_units_previous_map: | ||
|  |         #         await self.on_enemy_unit_entered_vision(enemy_unit) | ||
|  |         # for enemy_structure in self.enemy_structures: | ||
|  |         #     if enemy_structure.tag not in self._enemy_structures_previous_map: | ||
|  |         #         await self.on_enemy_unit_entered_vision(enemy_structure) | ||
|  | 
 | ||
|  |         # # Call events for enemy unit left vision | ||
|  |         # enemy_units_left_vision: Set[int] = set(self._enemy_units_previous_map) - self.enemy_units.tags | ||
|  |         # for enemy_unit_tag in enemy_units_left_vision: | ||
|  |         #     await self.on_enemy_unit_left_vision(enemy_unit_tag) | ||
|  |         # enemy_structures_left_vision: Set[int] = (set(self._enemy_structures_previous_map) - self.enemy_structures.tags) | ||
|  |         # for enemy_structure_tag in enemy_structures_left_vision: | ||
|  |         #     await self.on_enemy_unit_left_vision(enemy_structure_tag) | ||
|  | 
 | ||
|  |     @final | ||
|  |     async def _issue_unit_dead_events(self): | ||
|  |         pass | ||
|  |         # for unit_tag in self.state.dead_units & set(self._all_units_previous_map): | ||
|  |         #     await self.on_unit_destroyed(unit_tag) | ||
|  | 
 | ||
|  |     # DISTANCE CALCULATION | ||
|  | 
 | ||
|  |     @final | ||
|  |     @property | ||
|  |     def _units_count(self) -> int: | ||
|  |         return len(self.all_units) | ||
|  | 
 | ||
|  |     # Helper functions | ||
|  | 
 | ||
|  |     @final | ||
|  |     def square_to_condensed(self, i, j) -> int: | ||
|  |         # Converts indices of a square matrix to condensed matrix | ||
|  |         # https://stackoverflow.com/a/36867493/10882657 | ||
|  |         assert i != j, "No diagonal elements in condensed matrix! Diagonal elements are zero" | ||
|  |         if i < j: | ||
|  |             i, j = j, i | ||
|  |         return self._units_count * j - j * (j + 1) // 2 + i - 1 - j | ||
|  | 
 | ||
|  |     # Fast and simple calculation functions | ||
|  | 
 | ||
|  |     @final | ||
|  |     @staticmethod | ||
|  |     def distance_math_hypot( | ||
|  |         p1: Union[Tuple[float, float], Point2], | ||
|  |         p2: Union[Tuple[float, float], Point2], | ||
|  |     ) -> float: | ||
|  |         return math.hypot(p1[0] - p2[0], p1[1] - p2[1]) | ||
|  | 
 | ||
|  |     @final | ||
|  |     @staticmethod | ||
|  |     def distance_math_hypot_squared( | ||
|  |         p1: Union[Tuple[float, float], Point2], | ||
|  |         p2: Union[Tuple[float, float], Point2], | ||
|  |     ) -> float: | ||
|  |         return pow(p1[0] - p2[0], 2) + pow(p1[1] - p2[1], 2) | ||
|  | 
 | ||
|  |     @final | ||
|  |     def _distance_squared_unit_to_unit_method0(self, unit1: Unit, unit2: Unit) -> float: | ||
|  |         return self.distance_math_hypot_squared(unit1.position_tuple, unit2.position_tuple) | ||
|  | 
 | ||
|  |     # Distance calculation using the pre-calculated matrix above | ||
|  | 
 | ||
|  |     @final | ||
|  |     def _distance_squared_unit_to_unit_method1(self, unit1: Unit, unit2: Unit) -> float: | ||
|  |         # If checked on units if they have the same tag, return distance 0 as these are not in the 1 dimensional pdist array - would result in an error otherwise | ||
|  |         if unit1.tag == unit2.tag: | ||
|  |             return 0 | ||
|  |         # Calculate index, needs to be after pdist has been calculated and cached | ||
|  |         condensed_index = self.square_to_condensed(unit1.distance_calculation_index, unit2.distance_calculation_index) | ||
|  |         assert condensed_index < len( | ||
|  |             self._cached_pdist | ||
|  |         ), f"Condensed index is larger than amount of calculated distances: {condensed_index} < {len(self._cached_pdist)}, units that caused the assert error: {unit1} and {unit2}" | ||
|  |         distance = self._pdist[condensed_index] | ||
|  |         return distance | ||
|  | 
 | ||
|  |     @final | ||
|  |     def _distance_squared_unit_to_unit_method2(self, unit1: Unit, unit2: Unit) -> float: | ||
|  |         # Calculate index, needs to be after cdist has been calculated and cached | ||
|  |         return self._cdist[unit1.distance_calculation_index, unit2.distance_calculation_index] | ||
|  | 
 | ||
|  |     # Distance calculation using the fastest distance calculation functions | ||
|  | 
 | ||
|  |     @final | ||
|  |     def _distance_pos_to_pos( | ||
|  |         self, | ||
|  |         pos1: Union[Tuple[float, float], Point2], | ||
|  |         pos2: Union[Tuple[float, float], Point2], | ||
|  |     ) -> float: | ||
|  |         return self.distance_math_hypot(pos1, pos2) | ||
|  | 
 | ||
|  |     @final | ||
|  |     def _distance_units_to_pos( | ||
|  |         self, | ||
|  |         units: Units, | ||
|  |         pos: Union[Tuple[float, float], Point2], | ||
|  |     ) -> Generator[float, None, None]: | ||
|  |         """ This function does not scale well, if len(units) > 100 it gets fairly slow """ | ||
|  |         return (self.distance_math_hypot(u.position_tuple, pos) for u in units) | ||
|  | 
 | ||
|  |     @final | ||
|  |     def _distance_unit_to_points( | ||
|  |         self, | ||
|  |         unit: Unit, | ||
|  |         points: Iterable[Tuple[float, float]], | ||
|  |     ) -> Generator[float, None, None]: | ||
|  |         """ This function does not scale well, if len(points) > 100 it gets fairly slow """ | ||
|  |         pos = unit.position_tuple | ||
|  |         return (self.distance_math_hypot(p, pos) for p in points) |