Archipelago/worlds/_sc2common/bot/game_data.py

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)