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)
							 |