""" Utilities for telling item parentage hierarchy. ItemData in item_tables.py will point from child item -> parent rule. Rules have a `parent_items()` method which links rule -> parent items. Rules may be more complex than all or any items being present. Call them to determine if they are satisfied. """ from typing import Dict, List, Iterable, Sequence, Optional, TYPE_CHECKING import abc from . import item_names, parent_names, item_tables, item_groups if TYPE_CHECKING: from ..options import Starcraft2Options class PresenceRule(abc.ABC): """Contract for a parent presence rule. This should be a protocol in Python 3.10+""" constraint_group: Optional[str] """Identifies the group this item rule is a part of, subject to min/max upgrades per unit""" display_string: str """Main item to count as the parent for min/max upgrades per unit purposes""" @abc.abstractmethod def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool: ... @abc.abstractmethod def parent_items(self) -> Sequence[str]: ... class ItemPresent(PresenceRule): def __init__(self, item_name: str) -> None: self.item_name = item_name self.constraint_group = item_name self.display_string = item_name def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool: return self.item_name in inventory def parent_items(self) -> List[str]: return [self.item_name] class AnyOf(PresenceRule): def __init__(self, group: Iterable[str], main_item: Optional[str] = None, display_string: Optional[str] = None) -> None: self.group = set(group) self.constraint_group = main_item self.display_string = display_string or main_item or ' | '.join(group) def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool: return len(self.group.intersection(inventory)) > 0 def parent_items(self) -> List[str]: return sorted(self.group) class AllOf(PresenceRule): def __init__(self, group: Iterable[str], main_item: Optional[str] = None) -> None: self.group = set(group) self.constraint_group = main_item self.display_string = main_item or ' & '.join(group) def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool: return len(self.group.intersection(inventory)) == len(self.group) def parent_items(self) -> List[str]: return sorted(self.group) class AnyOfGroupAndOneOtherItem(PresenceRule): def __init__(self, group: Iterable[str], item_name: str) -> None: self.group = set(group) self.item_name = item_name self.constraint_group = item_name self.display_string = item_name def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool: return (len(self.group.intersection(inventory)) > 0) and self.item_name in inventory def parent_items(self) -> List[str]: return sorted(self.group) + [self.item_name] class MorphlingOrItem(PresenceRule): def __init__(self, item_name: str, has_parent: bool = True) -> None: self.item_name = item_name self.constraint_group = None # Keep morphs from counting towards the parent unit's upgrade count self.display_string = f'{item_name} Morphs' def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool: return (options.enable_morphling.value != 0) or self.item_name in inventory def parent_items(self) -> List[str]: return [self.item_name] class MorphlingOrAnyOf(PresenceRule): def __init__(self, group: Iterable[str], display_string: str, main_item: Optional[str] = None) -> None: self.group = set(group) self.constraint_group = main_item self.display_string = display_string def __call__(self, inventory: Iterable[str], options: 'Starcraft2Options') -> bool: return (options.enable_morphling.value != 0) or (len(self.group.intersection(inventory)) > 0) def parent_items(self) -> List[str]: return sorted(self.group) parent_present: Dict[str, PresenceRule] = { item_name: ItemPresent(item_name) for item_name in item_tables.item_table } # Terran parent_present[parent_names.DOMINION_TROOPER_WEAPONS] = AnyOf([ item_names.DOMINION_TROOPER_B2_HIGH_CAL_LMG, item_names.DOMINION_TROOPER_CPO7_SALAMANDER_FLAMETHROWER, item_names.DOMINION_TROOPER_HAILSTORM_LAUNCHER, ], main_item=item_names.DOMINION_TROOPER) parent_present[parent_names.INFANTRY_UNITS] = AnyOf(item_groups.barracks_units, display_string='Terran Infantry') parent_present[parent_names.INFANTRY_WEAPON_UNITS] = AnyOf(item_groups.barracks_wa_group, display_string='Terran Infantry') parent_present[parent_names.ORBITAL_COMMAND_AND_PLANETARY] = AnyOfGroupAndOneOtherItem( item_groups.orbital_command_abilities, item_names.PLANETARY_FORTRESS, ) parent_present[parent_names.SIEGE_TANK_AND_TRANSPORT] = AnyOfGroupAndOneOtherItem( (item_names.MEDIVAC, item_names.HERCULES), item_names.SIEGE_TANK, ) parent_present[parent_names.SIEGE_TANK_AND_MEDIVAC] = AllOf((item_names.SIEGE_TANK, item_names.MEDIVAC), item_names.SIEGE_TANK) parent_present[parent_names.SPIDER_MINE_SOURCE] = AnyOf(item_groups.spider_mine_sources, display_string='Spider Mines') parent_present[parent_names.STARSHIP_UNITS] = AnyOf(item_groups.starport_units, display_string='Terran Starships') parent_present[parent_names.STARSHIP_WEAPON_UNITS] = AnyOf(item_groups.starport_wa_group, display_string='Terran Starships') parent_present[parent_names.VEHICLE_UNITS] = AnyOf(item_groups.factory_units, display_string='Terran Vehicles') parent_present[parent_names.VEHICLE_WEAPON_UNITS] = AnyOf(item_groups.factory_wa_group, display_string='Terran Vehicles') parent_present[parent_names.TERRAN_MERCENARIES] = AnyOf(item_groups.terran_mercenaries, display_string='Terran Mercenaries') # Zerg parent_present[parent_names.ANY_NYDUS_WORM] = AnyOf((item_names.NYDUS_WORM, item_names.ECHIDNA_WORM), item_names.NYDUS_WORM) parent_present[parent_names.BANELING_SOURCE] = AnyOf( (item_names.ZERGLING_BANELING_ASPECT, item_names.KERRIGAN_SPAWN_BANELINGS), item_names.ZERGLING_BANELING_ASPECT, ) parent_present[parent_names.INFESTED_UNITS] = AnyOf(item_groups.infterr_units, display_string='Infested') parent_present[parent_names.INFESTED_FACTORY_OR_STARPORT] = AnyOf( (item_names.INFESTED_DIAMONDBACK, item_names.INFESTED_SIEGE_TANK, item_names.INFESTED_LIBERATOR, item_names.INFESTED_BANSHEE, item_names.BULLFROG) ) parent_present[parent_names.MORPH_SOURCE_AIR] = MorphlingOrAnyOf((item_names.MUTALISK, item_names.CORRUPTOR), "Mutalisk/Corruptor Morphs") parent_present[parent_names.MORPH_SOURCE_ROACH] = MorphlingOrItem(item_names.ROACH) parent_present[parent_names.MORPH_SOURCE_ZERGLING] = MorphlingOrItem(item_names.ZERGLING) parent_present[parent_names.MORPH_SOURCE_HYDRALISK] = MorphlingOrItem(item_names.HYDRALISK) parent_present[parent_names.MORPH_SOURCE_ULTRALISK] = MorphlingOrItem(item_names.ULTRALISK) parent_present[parent_names.ZERG_UPROOTABLE_BUILDINGS] = AnyOf( (item_names.SPINE_CRAWLER, item_names.SPORE_CRAWLER, item_names.INFESTED_MISSILE_TURRET, item_names.INFESTED_BUNKER), ) parent_present[parent_names.ZERG_MELEE_ATTACKER] = AnyOf(item_groups.zerg_melee_wa, display_string='Zerg Ground') parent_present[parent_names.ZERG_MISSILE_ATTACKER] = AnyOf(item_groups.zerg_ranged_wa, display_string='Zerg Ground') parent_present[parent_names.ZERG_CARAPACE_UNIT] = AnyOf(item_groups.zerg_ground_units, display_string='Zerg Flyers') parent_present[parent_names.ZERG_FLYING_UNIT] = AnyOf(item_groups.zerg_air_units, display_string='Zerg Flyers') parent_present[parent_names.ZERG_MERCENARIES] = AnyOf(item_groups.zerg_mercenaries, display_string='Zerg Mercenaries') parent_present[parent_names.ZERG_OUROBOUROS_CONDITION] = AnyOfGroupAndOneOtherItem( (item_names.ZERGLING, item_names.ROACH, item_names.HYDRALISK, item_names.ABERRATION), item_names.ECHIDNA_WORM ) # Protoss parent_present[parent_names.ARCHON_SOURCE] = AnyOf( (item_names.HIGH_TEMPLAR, item_names.SIGNIFIER, item_names.ASCENDANT_ARCHON_MERGE, item_names.DARK_TEMPLAR_ARCHON_MERGE), main_item="Archon", ) parent_present[parent_names.CARRIER_CLASS] = AnyOf( (item_names.CARRIER, item_names.TRIREME, item_names.SKYLORD), main_item=item_names.CARRIER, ) parent_present[parent_names.CARRIER_OR_TRIREME] = AnyOf( (item_names.CARRIER, item_names.TRIREME), main_item=item_names.CARRIER, ) parent_present[parent_names.DARK_ARCHON_SOURCE] = AnyOf( (item_names.DARK_ARCHON, item_names.DARK_TEMPLAR_DARK_ARCHON_MELD), main_item=item_names.DARK_ARCHON, ) parent_present[parent_names.DARK_TEMPLAR_CLASS] = AnyOf( (item_names.DARK_TEMPLAR, item_names.AVENGER, item_names.BLOOD_HUNTER), main_item=item_names.DARK_TEMPLAR, ) parent_present[parent_names.STORM_CASTER] = AnyOf( (item_names.HIGH_TEMPLAR, item_names.SIGNIFIER), main_item=item_names.HIGH_TEMPLAR, ) parent_present[parent_names.IMMORTAL_OR_ANNIHILATOR] = AnyOf( (item_names.IMMORTAL, item_names.ANNIHILATOR), main_item=item_names.IMMORTAL, ) parent_present[parent_names.PHOENIX_CLASS] = AnyOf( (item_names.PHOENIX, item_names.MIRAGE, item_names.SKIRMISHER), main_item=item_names.PHOENIX, ) parent_present[parent_names.SENTRY_CLASS] = AnyOf( (item_names.SENTRY, item_names.ENERGIZER, item_names.HAVOC), main_item=item_names.SENTRY, ) parent_present[parent_names.SENTRY_CLASS_OR_SHIELD_BATTERY] = AnyOf( (item_names.SENTRY, item_names.ENERGIZER, item_names.HAVOC, item_names.SHIELD_BATTERY), main_item=item_names.SENTRY, ) parent_present[parent_names.STALKER_CLASS] = AnyOf( (item_names.STALKER, item_names.SLAYER, item_names.INSTIGATOR), main_item=item_names.STALKER, ) parent_present[parent_names.SUPPLICANT_AND_ASCENDANT] = AllOf( (item_names.SUPPLICANT, item_names.ASCENDANT), main_item=item_names.ASCENDANT, ) parent_present[parent_names.VOID_RAY_CLASS] = AnyOf( (item_names.VOID_RAY, item_names.DESTROYER, item_names.PULSAR, item_names.DAWNBRINGER), main_item=item_names.VOID_RAY, ) parent_present[parent_names.ZEALOT_OR_SENTINEL_OR_CENTURION] = AnyOf( (item_names.ZEALOT, item_names.SENTINEL, item_names.CENTURION), main_item=item_names.ZEALOT, ) parent_present[parent_names.SCOUT_CLASS] = AnyOf( (item_names.SCOUT, item_names.OPPRESSOR, item_names.CALADRIUS, item_names.MISTWING), main_item=item_names.SCOUT, ) parent_present[parent_names.SCOUT_OR_OPPRESSOR_OR_MISTWING] = AnyOf( (item_names.SCOUT, item_names.OPPRESSOR, item_names.MISTWING), main_item=item_names.SCOUT, ) parent_present[parent_names.PROTOSS_STATIC_DEFENSE] = AnyOf( (item_names.NEXUS_OVERCHARGE, item_names.PHOTON_CANNON, item_names.KHAYDARIN_MONOLITH, item_names.SHIELD_BATTERY), main_item=item_names.PHOTON_CANNON, ) parent_present[parent_names.PROTOSS_ATTACKING_BUILDING] = AnyOf( (item_names.NEXUS_OVERCHARGE, item_names.PHOTON_CANNON, item_names.KHAYDARIN_MONOLITH), main_item=item_names.PHOTON_CANNON, ) parent_id_to_children: Dict[str, Sequence[str]] = {} """Parent identifier to child items. Only contains parent rules with children.""" child_item_to_parent_items: Dict[str, Sequence[str]] = {} """Child item name to all parent items that can possibly affect its presence rule. Populated for all item names.""" parent_item_to_ids: Dict[str, Sequence[str]] = {} """Parent item to parent identifiers it affects. Populated for all items and parent IDs.""" parent_item_to_children: Dict[str, Sequence[str]] = {} """Parent item to child item names. Populated for all items and parent IDs.""" item_upgrade_groups: Dict[str, Sequence[str]] = {} """Mapping of upgradable item group -> child items. Only populated for groups with child items.""" # Note(mm): "All items" promise satisfied by the basic ItemPresent auto-generated rules def _init() -> None: for item_name, item_data in item_tables.item_table.items(): if item_data.parent is None: continue parent_id_to_children.setdefault(item_data.parent, []).append(item_name) # type: ignore child_item_to_parent_items[item_name] = parent_present[item_data.parent].parent_items() for parent_id, presence_func in parent_present.items(): for parent_item in presence_func.parent_items(): parent_item_to_ids.setdefault(parent_item, []).append(parent_id) # type: ignore parent_item_to_children.setdefault(parent_item, []).extend(parent_id_to_children.get(parent_id, [])) # type: ignore if presence_func.constraint_group is not None and parent_id_to_children.get(parent_id): item_upgrade_groups.setdefault(presence_func.constraint_group, []).extend(parent_id_to_children[parent_id]) # type: ignore _init()