diff --git a/worlds/sc2/locations.py b/worlds/sc2/locations.py index 0e00f4d7..203d4d26 100644 --- a/worlds/sc2/locations.py +++ b/worlds/sc2/locations.py @@ -2150,8 +2150,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: "Victory", SC2WOL_LOC_ID_OFFSET + 2800, LocationType.VICTORY, - lambda state: logic.terran_competent_comp(state) - and logic.terran_army_weapon_armor_upgrade_min_level(state) >= 2, + lambda state: logic.terran_competent_comp(state, 2), ), make_location_data( SC2Mission.SHATTER_THE_SKY.mission_name, @@ -2172,24 +2171,21 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: "Southeast Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2803, LocationType.VANILLA, - lambda state: logic.terran_competent_comp(state) - and logic.terran_army_weapon_armor_upgrade_min_level(state) >= 2, + lambda state: logic.terran_competent_comp(state, 2), ), make_location_data( SC2Mission.SHATTER_THE_SKY.mission_name, "Southwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2804, LocationType.VANILLA, - lambda state: logic.terran_competent_comp(state) - and logic.terran_army_weapon_armor_upgrade_min_level(state) >= 2, + lambda state: logic.terran_competent_comp(state, 2), ), make_location_data( SC2Mission.SHATTER_THE_SKY.mission_name, "Leviathan", SC2WOL_LOC_ID_OFFSET + 2805, LocationType.VANILLA, - lambda state: logic.terran_competent_comp(state) - and logic.terran_army_weapon_armor_upgrade_min_level(state) >= 2, + lambda state: logic.terran_competent_comp(state, 2), hard_rule=logic.terran_any_anti_air, ), make_location_data( @@ -2262,7 +2258,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2HOTS_LOC_ID_OFFSET + 100, LocationType.VICTORY, lambda state: ( - logic.zerg_common_unit + logic.zerg_common_unit(state) or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player) ), ), @@ -2279,7 +2275,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: LocationType.VANILLA, lambda state: adv_tactics or ( - logic.zerg_common_unit + logic.zerg_common_unit(state) or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player) ), ), @@ -2290,7 +2286,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: LocationType.VANILLA, lambda state: adv_tactics or ( - logic.zerg_common_unit + logic.zerg_common_unit(state) or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player) ), ), @@ -2301,7 +2297,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: LocationType.VANILLA, lambda state: adv_tactics or ( - logic.zerg_common_unit + logic.zerg_common_unit(state) or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player) ), ), @@ -2324,7 +2320,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: LocationType.EXTRA, lambda state: adv_tactics or ( - logic.zerg_common_unit + logic.zerg_common_unit(state) or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player) ), ), @@ -2334,7 +2330,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2HOTS_LOC_ID_OFFSET + 108, LocationType.CHALLENGE, lambda state: ( - logic.zerg_common_unit + logic.zerg_common_unit(state) or state.has_any((item_names.ZERGLING, item_names.PYGALISK), player) ), flags=LocationFlag.SPEEDRUN, @@ -3862,15 +3858,11 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2LOTV_LOC_ID_OFFSET + 300, LocationType.VICTORY, lambda state: adv_tactics - or state.count_from_list( - ( - item_names.STALKER_PHASE_REACTOR, - item_names.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES, - item_names.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION, - ), - player, - ) - >= 2, + or state.has_any(( + item_names.STALKER_PHASE_REACTOR, + item_names.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES, + item_names.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION, + ), player), ), make_location_data( SC2Mission.EVIL_AWOKEN.mission_name, @@ -4582,7 +4574,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: lambda state: ( logic.protoss_deathball(state) and logic.protoss_power_rating(state) >= 6 - and (adv_tactics or logic.protoss_fleet(state)) + and (adv_tactics or logic.protoss_unsealing_the_past_ledge_requirement(state)) ), ), make_location_data( @@ -4593,7 +4585,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: lambda state: ( logic.protoss_deathball(state) and logic.protoss_power_rating(state) >= 6 - and (adv_tactics or logic.protoss_fleet(state)) + and (adv_tactics or logic.protoss_unsealing_the_past_ledge_requirement(state)) ), ), make_location_data( @@ -7256,7 +7248,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2_RACESWAP_LOC_ID_OFFSET + 1809, LocationType.MASTERY, lambda state: ( - logic.protoss_anti_armor_anti_air + logic.protoss_anti_armor_anti_air(state) and logic.protoss_defense_rating(state, False) >= 6 and logic.protoss_common_unit(state) and logic.protoss_deathball(state) @@ -9087,7 +9079,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: "Close Obelisk", SC2_RACESWAP_LOC_ID_OFFSET + 4801, LocationType.VANILLA, - lambda state: adv_tactics or logic.zerg_common_unit, + lambda state: adv_tactics or logic.zerg_common_unit(state), ), make_location_data( SC2Mission.ECHOES_OF_THE_FUTURE_Z.mission_name, @@ -11841,7 +11833,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 9600, LocationType.VICTORY, - lambda state: logic.protoss_deathball + lambda state: logic.protoss_deathball(state) or (adv_tactics and logic.protoss_competent_comp(state)), ), make_location_data( @@ -11876,7 +11868,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: "Main Path Command Center", SC2_RACESWAP_LOC_ID_OFFSET + 9605, LocationType.EXTRA, - lambda state: logic.protoss_deathball + lambda state: logic.protoss_deathball(state) or (adv_tactics and logic.protoss_competent_comp(state)), ), make_location_data( @@ -12026,7 +12018,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: "Victory", SC2_RACESWAP_LOC_ID_OFFSET + 10000, LocationType.VICTORY, - lambda state: logic.zerg_competent_comp + lambda state: logic.zerg_competent_comp(state) and logic.zerg_moderate_anti_air(state), ), make_location_data( @@ -12034,7 +12026,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: "First Prisoner Group", SC2_RACESWAP_LOC_ID_OFFSET + 10001, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp + lambda state: logic.zerg_competent_comp(state) and logic.zerg_moderate_anti_air(state), ), make_location_data( @@ -12042,7 +12034,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: "Second Prisoner Group", SC2_RACESWAP_LOC_ID_OFFSET + 10002, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp + lambda state: logic.zerg_competent_comp(state) and logic.zerg_moderate_anti_air(state), ), make_location_data( @@ -12050,7 +12042,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: "First Pylon", SC2_RACESWAP_LOC_ID_OFFSET + 10003, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp + lambda state: logic.zerg_competent_comp(state) and logic.zerg_moderate_anti_air(state), ), make_location_data( @@ -12058,7 +12050,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: "Second Pylon", SC2_RACESWAP_LOC_ID_OFFSET + 10004, LocationType.VANILLA, - lambda state: logic.zerg_competent_comp + lambda state: logic.zerg_competent_comp(state) and logic.zerg_moderate_anti_air(state), ), make_location_data( @@ -12661,7 +12653,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]: SC2_RACESWAP_LOC_ID_OFFSET + 11406, LocationType.CHALLENGE, lambda state: ( - logic.zerg_brothers_in_arms_requirement + logic.zerg_brothers_in_arms_requirement(state) and logic.zerg_base_buster(state) and logic.zerg_power_rating(state) >= 8 ), diff --git a/worlds/sc2/rules.py b/worlds/sc2/rules.py index 03072594..e6068ab2 100644 --- a/worlds/sc2/rules.py +++ b/worlds/sc2/rules.py @@ -47,8 +47,15 @@ if TYPE_CHECKING: from . import SC2World +def min2(a: int, b: int) -> int: + """`min()` that only takes two values; faster than baseline int by about 2x""" + if a <= b: + return a + return b + + class SC2Logic: - def __init__(self, world: Optional["SC2World"]): + def __init__(self, world: Optional["SC2World"]) -> None: # Note: Don't store a reference to the world so we can cache this object on the world object self.player = -1 if world is None else world.player self.logic_level: int = world.options.required_tactics.value if world else RequiredTactics.default @@ -109,7 +116,7 @@ class SC2Logic: # has_group with count = 0 is always true for item placement and always false for SC2 item filtering return state.has_group("Missions", self.player, 0) - def get_very_hard_required_upgrade_level(self): + def get_very_hard_required_upgrade_level(self) -> bool: return 2 if self.advanced_tactics else 3 def weapon_armor_upgrade_count(self, upgrade_item: str, state: CollectionState) -> int: @@ -133,7 +140,7 @@ class SC2Logic: count += 1 return count - def soa_power_rating(self, state: CollectionState): + def soa_power_rating(self, state: CollectionState) -> bool: power_rating = 0 # Spear of Adun Ultimates (Strongest) for item, rating in soa_ultimate_ratings.items(): @@ -203,7 +210,7 @@ class SC2Logic: def terran_common_unit(self, state: CollectionState) -> bool: return state.has_any(self.basic_terran_units, self.player) - def terran_early_tech(self, state: CollectionState): + def terran_early_tech(self, state: CollectionState) -> bool: """ Basic combat unit that can be deployed quickly from mission start :param state @@ -447,7 +454,7 @@ class SC2Logic: defense_score += 2 return defense_score - def terran_competent_comp(self, state: CollectionState) -> bool: + def terran_competent_comp(self, state: CollectionState, upgrade_level: int = 1) -> bool: # All competent comps require anti-air if not self.terran_competent_anti_air(state): return False @@ -455,12 +462,12 @@ class SC2Logic: infantry_weapons = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, state) infantry_armor = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, state) infantry = state.has_any({item_names.MARINE, item_names.DOMINION_TROOPER, item_names.MARAUDER}, self.player) - if infantry_weapons >= 2 and infantry_armor >= 1 and infantry and self.terran_bio_heal(state): + if infantry_weapons >= upgrade_level + 1 and infantry_armor >= upgrade_level and infantry and self.terran_bio_heal(state): return True # Mass Air-To-Ground ship_weapons = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, state) ship_armor = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR, state) - if ship_weapons >= 1 and ship_armor >= 1: + if ship_weapons >= upgrade_level and ship_armor >= upgrade_level: air = ( state.has_any({item_names.BANSHEE, item_names.BATTLECRUISER}, self.player) or state.has_all({item_names.LIBERATOR, item_names.LIBERATOR_RAID_ARTILLERY}, self.player) @@ -473,7 +480,7 @@ class SC2Logic: # Strong Mech vehicle_weapons = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, state) vehicle_armor = self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR, state) - if vehicle_weapons >= 1 and vehicle_armor >= 1: + if vehicle_weapons >= upgrade_level and vehicle_armor >= upgrade_level: strong_vehicle = state.has_any({item_names.THOR, item_names.SIEGE_TANK}, self.player) light_frontline = state.has_any( {item_names.MARINE, item_names.DOMINION_TROOPER, item_names.HELLION, item_names.VULTURE}, self.player @@ -762,27 +769,27 @@ class SC2Logic: def zerg_army_weapon_armor_upgrade_min_level(self, state: CollectionState) -> int: count: int = WEAPON_ARMOR_UPGRADE_MAX_LEVEL if self.has_zerg_melee_unit: - count = min(count, self.zerg_melee_weapon_armor_upgrade_min_level(state)) + count = min2(count, self.zerg_melee_weapon_armor_upgrade_min_level(state)) if self.has_zerg_ranged_unit: - count = min(count, self.zerg_ranged_weapon_armor_upgrade_min_level(state)) + count = min2(count, self.zerg_ranged_weapon_armor_upgrade_min_level(state)) if self.has_zerg_air_unit: - count = min(count, self.zerg_flyer_weapon_armor_upgrade_min_level(state)) + count = min2(count, self.zerg_flyer_weapon_armor_upgrade_min_level(state)) return count def zerg_melee_weapon_armor_upgrade_min_level(self, state: CollectionState) -> int: - return min( + return min2( self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_MELEE_ATTACK, state), self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_GROUND_CARAPACE, state), ) def zerg_ranged_weapon_armor_upgrade_min_level(self, state: CollectionState) -> int: - return min( + return min2( self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_MISSILE_ATTACK, state), self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_GROUND_CARAPACE, state), ) def zerg_flyer_weapon_armor_upgrade_min_level(self, state: CollectionState) -> int: - return min( + return min2( self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_FLYER_ATTACK, state), self.weapon_armor_upgrade_count(item_names.PROGRESSIVE_ZERG_FLYER_CARAPACE, state), ) @@ -1082,20 +1089,17 @@ class SC2Logic: ) ) - def zergling_hydra_roach_start(self, state: CollectionState): + def zergling_hydra_roach_start(self, state: CollectionState) -> bool: """ Created mainly for engine of destruction start, but works for other missions with no-build starts. """ - return state.has_any( - { + return state.has_any(( item_names.ZERGLING_ADRENAL_OVERLOAD, item_names.HYDRALISK_FRENZY, item_names.ROACH_HYDRIODIC_BILE, item_names.ZERGLING_RAPTOR_STRAIN, item_names.ROACH_CORPSER_STRAIN, - }, - self.player, - ) + ), self.player) def kerrigan_levels(self, state: CollectionState, target: int, story_levels_available=True) -> bool: if (story_levels_available and self.story_levels_granted) or not self.kerrigan_unit_available: @@ -1111,7 +1115,7 @@ class SC2Logic: # Levels from missions beaten levels = self.kerrigan_levels_per_mission_completed * state.count_group("Missions", self.player) if self.kerrigan_levels_per_mission_completed_cap != -1: - levels = min(levels, self.kerrigan_levels_per_mission_completed_cap) + levels = min2(levels, self.kerrigan_levels_per_mission_completed_cap) # Levels from items for kerrigan_level_item in kerrigan_levels: level_amount = get_full_item_list()[kerrigan_level_item].number @@ -1119,7 +1123,7 @@ class SC2Logic: levels += item_count * level_amount # Total level cap if self.kerrigan_total_level_cap != -1: - levels = min(levels, self.kerrigan_total_level_cap) + levels = min2(levels, self.kerrigan_total_level_cap) return levels >= target @@ -1625,20 +1629,17 @@ class SC2Logic: and state.has_any((item_names.SUPPLICANT, item_names.SHIELD_BATTERY), self.player) ) - def zealot_sentry_slayer_start(self, state: CollectionState): + def zealot_sentry_slayer_start(self, state: CollectionState) -> bool: """ Created mainly for engine of destruction start, but works for other missions with no-build starts. """ - return state.has_any( - { + return state.has_any(( item_names.ZEALOT_WHIRLWIND, item_names.SENTRY_DOUBLE_SHIELD_RECHARGE, item_names.SLAYER_PHASE_BLINK, item_names.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES, item_names.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION, - }, - self.player, - ) + ), self.player) # Mission-specific rules def ghost_of_a_chance_requirement(self, state: CollectionState) -> bool: @@ -2012,7 +2013,7 @@ class SC2Logic: and (self.advanced_tactics or state.has(item_names.YGGDRASIL, self.player)) ) - def protoss_supernova_requirement(self, state: CollectionState): + def protoss_supernova_requirement(self, state: CollectionState) -> bool: return ( ( state.count(item_names.PROGRESSIVE_WARP_RELOCATE, self.player) >= 2 @@ -2133,7 +2134,7 @@ class SC2Logic: and self.protoss_fleet(state) ) - def terran_engine_of_destruction_requirement(self, state: CollectionState) -> int: + def terran_engine_of_destruction_requirement(self, state: CollectionState) -> bool: power_rating = self.terran_power_rating(state) if power_rating < 3 or not self.marine_medic_upgrade(state) or not self.terran_common_unit(state): return False @@ -2146,7 +2147,7 @@ class SC2Logic: and state.has_any((item_names.BANSHEE, item_names.LIBERATOR), self.player) ) - def zerg_engine_of_destruction_requirement(self, state: CollectionState) -> int: + def zerg_engine_of_destruction_requirement(self, state: CollectionState) -> bool: power_rating = self.zerg_power_rating(state) if ( power_rating < 3 @@ -2161,21 +2162,21 @@ class SC2Logic: else: return self.zerg_base_buster(state) - def protoss_engine_of_destruction_requirement(self, state: CollectionState): + def protoss_engine_of_destruction_requirement(self, state: CollectionState) -> bool: return ( self.zealot_sentry_slayer_start(state) and self.protoss_repair_odin(state) and (self.protoss_deathball(state) or self.protoss_fleet(state)) ) - def zerg_repair_odin(self, state: CollectionState): + def zerg_repair_odin(self, state: CollectionState) -> bool: return ( self.zerg_has_infested_scv(state) or state.has_all({item_names.SWARM_QUEEN_BIO_MECHANICAL_TRANSFUSION, item_names.SWARM_QUEEN}, self.player) or (self.advanced_tactics and state.has(item_names.SWARM_QUEEN, self.player)) ) - def protoss_repair_odin(self, state: CollectionState): + def protoss_repair_odin(self, state: CollectionState) -> bool: return ( state.has(item_names.SENTRY, self.player) or state.has_all((item_names.CARRIER, item_names.CARRIER_REPAIR_DRONES), self.player) @@ -2196,7 +2197,7 @@ class SC2Logic: def protoss_in_utter_darkness_requirement(self, state: CollectionState) -> bool: return self.protoss_competent_comp(state) and self.protoss_defense_rating(state, True) >= 4 - def terran_all_in_requirement(self, state: CollectionState): + def terran_all_in_requirement(self, state: CollectionState) -> bool: """ All-in """ @@ -2228,7 +2229,7 @@ class SC2Logic: and state.has_any({item_names.HIVE_MIND_EMULATOR, item_names.PSI_DISRUPTER, item_names.MISSILE_TURRET}, self.player) ) - def zerg_all_in_requirement(self, state: CollectionState): + def zerg_all_in_requirement(self, state: CollectionState) -> bool: """ All-in (Zerg) """ @@ -2264,7 +2265,7 @@ class SC2Logic: and state.has_any({item_names.SPORE_CRAWLER, item_names.INFESTED_MISSILE_TURRET}, self.player) ) - def protoss_all_in_requirement(self, state: CollectionState): + def protoss_all_in_requirement(self, state: CollectionState) -> bool: """ All-in (Protoss) """ @@ -2436,21 +2437,19 @@ class SC2Logic: def protoss_can_attack_behind_chasm(self, state: CollectionState) -> bool: return ( - state.has_any( - { - item_names.SCOUT, - item_names.TEMPEST, - item_names.CARRIER, - item_names.SKYLORD, - item_names.TRIREME, - item_names.VOID_RAY, - item_names.DESTROYER, - item_names.PULSAR, - item_names.DAWNBRINGER, - item_names.MOTHERSHIP, - }, - self.player, - ) + state.has_any(( + item_names.SCOUT, + item_names.SKIRMISHER, + item_names.TEMPEST, + item_names.CARRIER, + item_names.SKYLORD, + item_names.TRIREME, + item_names.VOID_RAY, + item_names.DESTROYER, + item_names.PULSAR, + item_names.DAWNBRINGER, + item_names.MOTHERSHIP, + ), self.player) or self.protoss_has_blink(state) or ( state.has(item_names.WARP_PRISM, self.player) @@ -2697,6 +2696,12 @@ class SC2Logic: and (self.take_over_ai_allies or (self.zerg_competent_comp(state) and self.zerg_big_monsters(state))) and self.zerg_power_rating(state) >= 6 ) + + def protoss_unsealing_the_past_ledge_requirement(self, state: CollectionState) -> bool: + return ( + state.has_any((item_names.COLOSSUS, item_names.WRATHWALKER), self.player) + or self.protoss_can_attack_behind_chasm(state) + ) def terran_unsealing_the_past_requirement(self, state: CollectionState) -> bool: return ( @@ -2704,7 +2709,7 @@ class SC2Logic: and self.terran_competent_comp(state) and self.terran_power_rating(state) >= 6 and ( - state.has_all({item_names.SIEGE_TANK, item_names.SIEGE_TANK_JUMP_JETS}, self.player) + state.has_all((item_names.SIEGE_TANK, item_names.SIEGE_TANK_JUMP_JETS), self.player) or state.has_all( {item_names.BATTLECRUISER, item_names.BATTLECRUISER_ATX_LASER_BATTERY, item_names.BATTLECRUISER_MOIRAI_IMPULSE_DRIVE}, self.player ) @@ -3132,8 +3137,9 @@ class SC2Logic: and (self.terran_cliffjumper(state) or state.has(item_names.BANSHEE, self.player)) and self.nova_splash(state) and self.terran_defense_rating(state, True, False) >= 3 - and self.advanced_tactics - or state.has(item_names.NOVA_JUMP_SUIT_MODULE, self.player) + and (self.advanced_tactics + or state.has(item_names.NOVA_JUMP_SUIT_MODULE, self.player) + ) ) def enemy_intelligence_garrisonable_unit(self, state: CollectionState) -> bool: