210 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			210 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | # pylint: disable=W0212 | ||
|  | from __future__ import annotations | ||
|  | 
 | ||
|  | from bisect import bisect_left | ||
|  | from dataclasses import dataclass | ||
|  | from functools import lru_cache | ||
|  | from typing import Dict, List, Optional, Union | ||
|  | 
 | ||
|  | from .data import Attribute, Race | ||
|  | 
 | ||
|  | # Set of parts of names of abilities that have no cost | ||
|  | # E.g every ability that has 'Hold' in its name is free | ||
|  | FREE_ABILITIES = {"Lower", "Raise", "Land", "Lift", "Hold", "Harvest"} | ||
|  | 
 | ||
|  | 
 | ||
|  | class GameData: | ||
|  | 
 | ||
|  |     def __init__(self, data): | ||
|  |         """
 | ||
|  |         :param data: | ||
|  |         """
 | ||
|  |         self.abilities: Dict[int, AbilityData] = {} | ||
|  |         self.units: Dict[int, UnitTypeData] = {u.unit_id: UnitTypeData(self, u) for u in data.units if u.available} | ||
|  |         self.upgrades: Dict[int, UpgradeData] = {u.upgrade_id: UpgradeData(self, u) for u in data.upgrades} | ||
|  |         # Cached UnitTypeIds so that conversion does not take long. This needs to be moved elsewhere if a new GameData object is created multiple times per game | ||
|  | 
 | ||
|  | 
 | ||
|  | class AbilityData: | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def id_exists(cls, ability_id): | ||
|  |         assert isinstance(ability_id, int), f"Wrong type: {ability_id} is not int" | ||
|  |         if ability_id == 0: | ||
|  |             return False | ||
|  |         i = bisect_left(cls.ability_ids, ability_id)  # quick binary search | ||
|  |         return i != len(cls.ability_ids) and cls.ability_ids[i] == ability_id | ||
|  | 
 | ||
|  |     def __init__(self, game_data, proto): | ||
|  |         self._game_data = game_data | ||
|  |         self._proto = proto | ||
|  | 
 | ||
|  |         # What happens if we comment this out? Should this not be commented out? What is its purpose? | ||
|  |         assert self.id != 0 | ||
|  | 
 | ||
|  |     def __repr__(self) -> str: | ||
|  |         return f"AbilityData(name={self._proto.button_name})" | ||
|  | 
 | ||
|  |     @property | ||
|  |     def link_name(self) -> str: | ||
|  |         """ For Stimpack this returns 'BarracksTechLabResearch' """ | ||
|  |         return self._proto.link_name | ||
|  | 
 | ||
|  |     @property | ||
|  |     def button_name(self) -> str: | ||
|  |         """ For Stimpack this returns 'Stimpack' """ | ||
|  |         return self._proto.button_name | ||
|  | 
 | ||
|  |     @property | ||
|  |     def friendly_name(self) -> str: | ||
|  |         """ For Stimpack this returns 'Research Stimpack' """ | ||
|  |         return self._proto.friendly_name | ||
|  | 
 | ||
|  |     @property | ||
|  |     def is_free_morph(self) -> bool: | ||
|  |         return any(free in self._proto.link_name for free in FREE_ABILITIES) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def cost(self) -> Cost: | ||
|  |         return self._game_data.calculate_ability_cost(self.id) | ||
|  | 
 | ||
|  | 
 | ||
|  | class UnitTypeData: | ||
|  | 
 | ||
|  |     def __init__(self, game_data: GameData, proto): | ||
|  |         """
 | ||
|  |         :param game_data: | ||
|  |         :param proto: | ||
|  |         """
 | ||
|  |         self._game_data = game_data | ||
|  |         self._proto = proto | ||
|  | 
 | ||
|  |     def __repr__(self) -> str: | ||
|  |         return f"UnitTypeData(name={self.name})" | ||
|  | 
 | ||
|  |     @property | ||
|  |     def name(self) -> str: | ||
|  |         return self._proto.name | ||
|  | 
 | ||
|  |     @property | ||
|  |     def creation_ability(self) -> Optional[AbilityData]: | ||
|  |         if self._proto.ability_id == 0: | ||
|  |             return None | ||
|  |         if self._proto.ability_id not in self._game_data.abilities: | ||
|  |             return None | ||
|  |         return self._game_data.abilities[self._proto.ability_id] | ||
|  | 
 | ||
|  |     @property | ||
|  |     def footprint_radius(self) -> Optional[float]: | ||
|  |         """ See unit.py footprint_radius """ | ||
|  |         if self.creation_ability is None: | ||
|  |             return None | ||
|  |         return self.creation_ability._proto.footprint_radius | ||
|  | 
 | ||
|  |     @property | ||
|  |     def attributes(self) -> List[Attribute]: | ||
|  |         return self._proto.attributes | ||
|  | 
 | ||
|  |     def has_attribute(self, attr) -> bool: | ||
|  |         assert isinstance(attr, Attribute) | ||
|  |         return attr in self.attributes | ||
|  | 
 | ||
|  |     @property | ||
|  |     def has_minerals(self) -> bool: | ||
|  |         return self._proto.has_minerals | ||
|  | 
 | ||
|  |     @property | ||
|  |     def has_vespene(self) -> bool: | ||
|  |         return self._proto.has_vespene | ||
|  | 
 | ||
|  |     @property | ||
|  |     def cargo_size(self) -> int: | ||
|  |         """ How much cargo this unit uses up in cargo_space """ | ||
|  |         return self._proto.cargo_size | ||
|  | 
 | ||
|  |     @property | ||
|  |     def race(self) -> Race: | ||
|  |         return Race(self._proto.race) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def cost(self) -> Cost: | ||
|  |         return Cost(self._proto.mineral_cost, self._proto.vespene_cost, self._proto.build_time) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def cost_zerg_corrected(self) -> Cost: | ||
|  |         """ This returns 25 for extractor and 200 for spawning pool instead of 75 and 250 respectively """ | ||
|  |         if self.race == Race.Zerg and Attribute.Structure.value in self.attributes: | ||
|  |             return Cost(self._proto.mineral_cost - 50, self._proto.vespene_cost, self._proto.build_time) | ||
|  |         return self.cost | ||
|  | 
 | ||
|  | 
 | ||
|  | class UpgradeData: | ||
|  | 
 | ||
|  |     def __init__(self, game_data: GameData, proto): | ||
|  |         """
 | ||
|  |         :param game_data: | ||
|  |         :param proto: | ||
|  |         """
 | ||
|  |         self._game_data = game_data | ||
|  |         self._proto = proto | ||
|  | 
 | ||
|  |     def __repr__(self): | ||
|  |         return f"UpgradeData({self.name} - research ability: {self.research_ability}, {self.cost})" | ||
|  | 
 | ||
|  |     @property | ||
|  |     def name(self) -> str: | ||
|  |         return self._proto.name | ||
|  | 
 | ||
|  |     @property | ||
|  |     def research_ability(self) -> Optional[AbilityData]: | ||
|  |         if self._proto.ability_id == 0: | ||
|  |             return None | ||
|  |         if self._proto.ability_id not in self._game_data.abilities: | ||
|  |             return None | ||
|  |         return self._game_data.abilities[self._proto.ability_id] | ||
|  | 
 | ||
|  |     @property | ||
|  |     def cost(self) -> Cost: | ||
|  |         return Cost(self._proto.mineral_cost, self._proto.vespene_cost, self._proto.research_time) | ||
|  | 
 | ||
|  | 
 | ||
|  | @dataclass | ||
|  | class Cost: | ||
|  |     """
 | ||
|  |     The cost of an action, a structure, a unit or a research upgrade. | ||
|  |     The time is given in frames (22.4 frames per game second). | ||
|  |     """
 | ||
|  |     minerals: int | ||
|  |     vespene: int | ||
|  |     time: Optional[float] = None | ||
|  | 
 | ||
|  |     def __repr__(self) -> str: | ||
|  |         return f"Cost({self.minerals}, {self.vespene})" | ||
|  | 
 | ||
|  |     def __eq__(self, other: Cost) -> bool: | ||
|  |         return self.minerals == other.minerals and self.vespene == other.vespene | ||
|  | 
 | ||
|  |     def __ne__(self, other: Cost) -> bool: | ||
|  |         return self.minerals != other.minerals or self.vespene != other.vespene | ||
|  | 
 | ||
|  |     def __bool__(self) -> bool: | ||
|  |         return self.minerals != 0 or self.vespene != 0 | ||
|  | 
 | ||
|  |     def __add__(self, other) -> Cost: | ||
|  |         if not other: | ||
|  |             return self | ||
|  |         if not self: | ||
|  |             return other | ||
|  |         time = (self.time or 0) + (other.time or 0) | ||
|  |         return Cost(self.minerals + other.minerals, self.vespene + other.vespene, time=time) | ||
|  | 
 | ||
|  |     def __sub__(self, other: Cost) -> Cost: | ||
|  |         time = (self.time or 0) + (other.time or 0) | ||
|  |         return Cost(self.minerals - other.minerals, self.vespene - other.vespene, time=time) | ||
|  | 
 | ||
|  |     def __mul__(self, other: int) -> Cost: | ||
|  |         return Cost(self.minerals * other, self.vespene * other, time=self.time) | ||
|  | 
 | ||
|  |     def __rmul__(self, other: int) -> Cost: | ||
|  |         return Cost(self.minerals * other, self.vespene * other, time=self.time) |