From 7d65aaa8c79aecc572a2624a8549a72f29622f29 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 11 Dec 2024 03:39:55 -0800 Subject: [PATCH] sc2: Many typing and style fixes; fixed some broken logic functions --- worlds/sc2/__init__.py | 26 +++--- worlds/sc2/locations.py | 6 +- worlds/sc2/mission_order/mission_pools.py | 4 +- worlds/sc2/mission_order/structs.py | 1 + worlds/sc2/mission_tables.py | 6 +- worlds/sc2/options.py | 28 ++++--- worlds/sc2/regions.py | 8 +- worlds/sc2/rules.py | 98 ++++------------------- worlds/sc2/test/test_generation.py | 4 +- 9 files changed, 65 insertions(+), 116 deletions(-) diff --git a/worlds/sc2/__init__.py b/worlds/sc2/__init__.py index 25a5ae611bea..5080e9d101be 100644 --- a/worlds/sc2/__init__.py +++ b/worlds/sc2/__init__.py @@ -79,7 +79,7 @@ class SC2World(World): options_dataclass = Starcraft2Options options: Starcraft2Options - item_name_groups = item_groups.item_name_groups + item_name_groups = item_groups.item_name_groups # type: ignore location_name_groups = location_groups.get_location_groups() locked_locations: List[str] """Locations locked to contain specific items, such as victory events or forced resources""" @@ -568,7 +568,7 @@ def flag_start_inventory(world: SC2World, item_list: List[FilterItem]) -> None: def flag_start_unit(world: SC2World, item_list: List[FilterItem], starter_unit: int) -> None: - first_mission = get_first_mission(world, world.custom_mission_order) + first_mission = get_random_first_mission(world, world.custom_mission_order) first_race = first_mission.race if first_race == SC2Race.ANY: @@ -585,7 +585,7 @@ def flag_start_unit(world: SC2World, item_list: List[FilterItem], starter_unit: } # The race of the early unit has been chosen - basic_units = get_basic_units(world, first_race) + basic_units = get_basic_units(world.options.required_tactics.value, first_race) if starter_unit == StarterUnit.option_balanced: basic_units = basic_units.difference(not_balanced_starting_units) if first_mission == SC2Mission.DARK_WHISPERS: @@ -678,7 +678,7 @@ def flag_start_abilities(world: SC2World, item_list: List[FilterItem]) -> None: def flag_unused_upgrade_types(world: SC2World, item_list: List[FilterItem]) -> None: """Excludes +armour/attack upgrades based on generic upgrade strategy.""" include_upgrades = world.options.generic_upgrade_missions == 0 - upgrade_items: GenericUpgradeItems = world.options.generic_upgrade_items + upgrade_items = world.options.generic_upgrade_items.value for item in item_list: if item.data.type in item_tables.upgrade_item_types: if not include_upgrades or (item.name not in upgrade_included_names[upgrade_items]): @@ -801,14 +801,18 @@ def pad_item_pool_with_filler(self: SC2World, num_items: int, pool: List[Item]): pool.append(item) -def get_first_mission(world: SC2World, mission_order: SC2MissionOrder) -> SC2Mission: +def get_random_first_mission(world: SC2World, mission_order: SC2MissionOrder) -> SC2Mission: # Pick an arbitrary lowest-difficulty starer mission - missions = mission_order.get_starting_missions() - missions = [(mission_order.mission_pools.get_modified_mission_difficulty(mission), mission) for mission in missions] - missions.sort(key = lambda difficulty_mission_tuple: difficulty_mission_tuple[0]) - (lowest_difficulty, _) = missions[0] - missions = [mission for (difficulty, mission) in missions if difficulty == lowest_difficulty] - return world.random.choice(missions) + starting_missions = mission_order.get_starting_missions() + mission_difficulties = [ + (mission_order.mission_pools.get_modified_mission_difficulty(mission), mission) + for mission in starting_missions + ] + mission_difficulties.sort(key = lambda difficulty_mission_tuple: difficulty_mission_tuple[0]) + (lowest_difficulty, _) = mission_difficulties[0] + first_mission_candidates = [mission for (difficulty, mission) in mission_difficulties if difficulty == lowest_difficulty] + return world.random.choice(first_mission_candidates) + def get_all_missions(mission_order: SC2MissionOrder) -> List[SC2Mission]: return mission_order.get_used_missions() diff --git a/worlds/sc2/locations.py b/worlds/sc2/locations.py index 2e255a649ad1..0235e2c14550 100644 --- a/worlds/sc2/locations.py +++ b/worlds/sc2/locations.py @@ -3530,7 +3530,7 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: make_location_data(SC2Mission.MEDIA_BLITZ_Z.mission_name, "Science Facility", SC2_RACESWAP_LOC_ID_OFFSET + 3904, LocationType.VANILLA, lambda state: ( logic.advanced_tactics - or logic.zerg_competent_comp_competent_aa + or logic.zerg_competent_comp_competent_aa(state) ) ), make_location_data(SC2Mission.MEDIA_BLITZ_Z.mission_name, "All Barracks", SC2_RACESWAP_LOC_ID_OFFSET + 3905, LocationType.EXTRA, @@ -5261,7 +5261,7 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: make_location_data(SC2Mission.UNSEALING_THE_PAST_T.mission_name, "First Stasis Lock", SC2_RACESWAP_LOC_ID_OFFSET + 12702, LocationType.EXTRA, lambda state: ( logic.advanced_tactics - or logic.terran_unsealing_the_past_requirement + or logic.terran_unsealing_the_past_requirement(state) )), make_location_data(SC2Mission.UNSEALING_THE_PAST_T.mission_name, "Second Stasis Lock", SC2_RACESWAP_LOC_ID_OFFSET + 12703, LocationType.EXTRA, logic.terran_unsealing_the_past_requirement @@ -5297,7 +5297,7 @@ def get_locations(world: Optional['SC2World']) -> Tuple[LocationData, ...]: make_location_data(SC2Mission.UNSEALING_THE_PAST_Z.mission_name, "First Stasis Lock", SC2_RACESWAP_LOC_ID_OFFSET + 12802, LocationType.EXTRA, lambda state: ( logic.advanced_tactics - or logic.zerg_unsealing_the_past_requirement + or logic.zerg_unsealing_the_past_requirement(state) )), make_location_data(SC2Mission.UNSEALING_THE_PAST_Z.mission_name, "Second Stasis Lock", SC2_RACESWAP_LOC_ID_OFFSET + 12803, LocationType.EXTRA, logic.zerg_unsealing_the_past_requirement diff --git a/worlds/sc2/mission_order/mission_pools.py b/worlds/sc2/mission_order/mission_pools.py index 0baaffa409f5..5c34db2956d0 100644 --- a/worlds/sc2/mission_order/mission_pools.py +++ b/worlds/sc2/mission_order/mission_pools.py @@ -1,5 +1,5 @@ from enum import IntEnum -from typing import TYPE_CHECKING, Dict, Set, List +from typing import TYPE_CHECKING, Dict, Set, List, Iterable from ..mission_tables import SC2Mission, lookup_id_to_mission, MissionFlag, SC2Campaign from worlds.AutoWorld import World @@ -63,7 +63,7 @@ def __init__(self) -> None: self._flag_ratios = {} self._flag_weights = {} - def set_exclusions(self, excluded: List[SC2Mission], unexcluded: List[SC2Mission]) -> None: + def set_exclusions(self, excluded: Iterable[SC2Mission], unexcluded: Iterable[SC2Mission]) -> None: """Prevents all the missions that appear in the `excluded` list, but not in the `unexcluded` list, from appearing in the mission order.""" total_exclusions = [mission.id for mission in excluded if mission not in unexcluded] diff --git a/worlds/sc2/mission_order/structs.py b/worlds/sc2/mission_order/structs.py index 3616af35e683..afd0a7fd7b5c 100644 --- a/worlds/sc2/mission_order/structs.py +++ b/worlds/sc2/mission_order/structs.py @@ -361,6 +361,7 @@ def dict_to_entry_rule(self, data: Dict[str, Any], start_node: MissionOrderNode, ) missions.extend(exits) return BeatMissionsEntryRule(missions, visual_reqs) + raise ValueError(f"Invalid data for entry rule: {data}") def resolve_address(self, address: str, start_node: MissionOrderNode) -> List[MissionOrderNode]: if address.startswith("../") or address == "..": diff --git a/worlds/sc2/mission_tables.py b/worlds/sc2/mission_tables.py index 6323b1c0861d..2d1ddee1c2ca 100644 --- a/worlds/sc2/mission_tables.py +++ b/worlds/sc2/mission_tables.py @@ -450,7 +450,7 @@ class SC2CampaignGoal(NamedTuple): SC2Mission.GATES_OF_HELL: f'{SC2Mission.GATES_OF_HELL.mission_name}: Victory', SC2Mission.SHATTER_THE_SKY: f'{SC2Mission.SHATTER_THE_SKY.mission_name}: Victory' }, - SC2Campaign.PROPHECY: None, + SC2Campaign.PROPHECY: {}, SC2Campaign.HOTS: { SC2Mission.THE_CRUCIBLE: f'{SC2Mission.THE_CRUCIBLE.mission_name}: Victory', SC2Mission.HAND_OF_DARKNESS: f'{SC2Mission.HAND_OF_DARKNESS.mission_name}: Victory', @@ -496,7 +496,7 @@ def get_goal_location(mission: SC2Mission) -> Union[str, None]: return primary_campaign_goal.location campaign_alt_goals = campaign_alt_final_mission_locations[campaign] - if campaign_alt_goals is not None and mission in campaign_alt_goals: + if mission in campaign_alt_goals: return campaign_alt_goals.get(mission) return mission.mission_name + ": Victory" @@ -513,7 +513,7 @@ def get_campaign_potential_goal_missions(campaign: SC2Campaign) -> List[SC2Missi if primary_goal_mission is not None: missions.append(primary_goal_mission.mission) alt_goal_locations = campaign_alt_final_mission_locations[campaign] - if alt_goal_locations is not None: + if alt_goal_locations: for mission in alt_goal_locations.keys(): missions.append(mission) diff --git a/worlds/sc2/options.py b/worlds/sc2/options.py index 2b9d3b6c8611..2befecf1ede1 100644 --- a/worlds/sc2/options.py +++ b/worlds/sc2/options.py @@ -1,7 +1,11 @@ -from dataclasses import fields, Field +from dataclasses import fields, Field, dataclass from typing import * -from Options import * +from Options import ( + Choice, Toggle, DefaultOnToggle, OptionSet, Range, + PerGameCommonOptions, Option, VerifyKeys, + is_iterable_except_str, +) from Utils import get_fuzzy_results from BaseClasses import PlandoOptions from .mission_tables import ( @@ -1077,12 +1081,18 @@ class Starcraft2Options(PerGameCommonOptions): custom_mission_order: CustomMissionOrder -def get_option_value(world: Union['SC2World', None], name: str) -> Union[int, FrozenSet]: +def get_option_value(world: Union['SC2World', None], name: str) -> int: + """ + You should basically never use this unless `world` can be `None`. + Use `world.options..value` instead for better typing, autocomplete, and error messages. + """ if world is None: field: Field = [class_field for class_field in fields(Starcraft2Options) if class_field.name == name][0] if isinstance(field.type, str): if field.type in globals(): return globals()[field.type].default + import Options + return Options.__dict__[field.type].default return field.type.default player_option = getattr(world.options, name) @@ -1090,8 +1100,8 @@ def get_option_value(world: Union['SC2World', None], name: str) -> Union[int, Fr return player_option.value -def get_enabled_races(world: 'SC2World') -> Set[SC2Race]: - selection = get_option_value(world, 'selected_races') +def get_enabled_races(world: Optional['SC2World']) -> Set[SC2Race]: + selection: int = world.options.selected_races.value if world else SelectRaces.default if selection == SelectRaces.option_all: return set(SC2Race) enabled = {SC2Race.ANY} @@ -1104,7 +1114,7 @@ def get_enabled_races(world: 'SC2World') -> Set[SC2Race]: return enabled -def get_enabled_campaigns(world: 'SC2World') -> Set[SC2Campaign]: +def get_enabled_campaigns(world: Optional['SC2World']) -> Set[SC2Campaign]: enabled_campaigns = set() if get_option_value(world, "enable_wol_missions"): enabled_campaigns.add(SC2Campaign.WOL) @@ -1192,7 +1202,7 @@ def get_excluded_missions(world: 'SC2World') -> Set[SC2Mission]: static_mission_orders = [ MissionOrder.option_vanilla, MissionOrder.option_vanilla_shuffled, - MissionOrder.option_mini_campaign + MissionOrder.option_mini_campaign, ] dynamic_mission_orders = [ @@ -1200,7 +1210,7 @@ def get_excluded_missions(world: 'SC2World') -> Set[SC2Mission]: MissionOrder.option_grid, MissionOrder.option_gauntlet, MissionOrder.option_blitz, - MissionOrder.option_hopscotch + MissionOrder.option_hopscotch, ] LEGACY_GRID_ORDERS = {3, 4, 8} # Medium Grid, Mini Grid, and Tiny Grid respectively @@ -1210,7 +1220,7 @@ def get_excluded_missions(world: 'SC2World') -> Set[SC2Mission]: ] # Names of upgrades to be included for different options -upgrade_included_names: Dict[GenericUpgradeItems, Set[str]] = { +upgrade_included_names: Dict[int, Set[str]] = { GenericUpgradeItems.option_individual_items: { item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, diff --git a/worlds/sc2/regions.py b/worlds/sc2/regions.py index f029febeb8f1..0008797ac5ef 100644 --- a/worlds/sc2/regions.py +++ b/worlds/sc2/regions.py @@ -36,9 +36,9 @@ def create_mission_order( adjust_mission_pools(world, mission_pools) setup_mission_pool_balancing(world, mission_pools) - mission_order_type = get_option_value(world, "mission_order") + mission_order_type = world.options.mission_order if mission_order_type == MissionOrder.option_custom: - mission_order_dict = get_option_value(world, "custom_mission_order") + mission_order_dict = world.options.custom_mission_order.value else: mission_order_option = create_regular_mission_order(world, mission_pools) if mission_order_type in static_mission_orders: @@ -157,7 +157,7 @@ def setup_mission_pool_balancing(world: 'SC2World', pools: SC2MOGenMissionPools) pools.set_flag_balances(flag_ratios, flag_weights) def create_regular_mission_order(world: 'SC2World', mission_pools: SC2MOGenMissionPools) -> Dict[str, Dict[str, Any]]: - mission_order_type = get_option_value(world, "mission_order") + mission_order_type = world.options.mission_order.value if mission_order_type in static_mission_orders: return create_static_mission_order(world, mission_order_type, mission_pools) @@ -422,7 +422,7 @@ def make_hopscotch(world: 'SC2World', size: int) -> Dict[str, Dict[str, Any]]: return mission_order def create_dynamic_mission_order(world: 'SC2World', mission_order_type: int, mission_pools: SC2MOGenMissionPools) -> Dict[str, Dict[str, Any]]: - num_missions = min(mission_pools.get_allowed_mission_count(), get_option_value(world, "maximum_campaign_size")) + num_missions = min(mission_pools.get_allowed_mission_count(), world.options.maximum_campaign_size.value) num_missions = max(1, num_missions) if mission_order_type == MissionOrder.option_golden_path: return make_golden_path(world, num_missions) diff --git a/worlds/sc2/rules.py b/worlds/sc2/rules.py index 0a8c1833d969..b7489379ca93 100644 --- a/worlds/sc2/rules.py +++ b/worlds/sc2/rules.py @@ -1,5 +1,5 @@ from math import floor -from typing import TYPE_CHECKING, Set +from typing import TYPE_CHECKING, Set, Optional from BaseClasses import CollectionState from .options import ( @@ -21,10 +21,9 @@ class SC2Logic: - def is_item_placement(self, state): + def is_item_placement(self, state: CollectionState): """ Tells if it's item placement or item pool filter - :param state: :return: True for item placement, False for pool filter """ # has_group with count = 0 is always true for item placement and always false for SC2 item filtering @@ -96,8 +95,6 @@ def weapon_armor_upgrade_count(self, upgrade_item: str, state: CollectionState) def terran_army_weapon_armor_upgrade_min_level(self, state: CollectionState) -> int: """ Minimum W/A upgrade level for unit classes present in the world - :param state: - :return: """ count: int = WEAPON_ARMOR_UPGRADE_MAX_LEVEL if self.world_has_barracks_unit(): @@ -144,8 +141,6 @@ def terran_early_tech(self, state: CollectionState): def terran_air(self, state: CollectionState) -> bool: """ Air units or drops on advanced tactics - :param state: - :return: """ return ( state.has_any({ @@ -161,8 +156,6 @@ def terran_air(self, state: CollectionState) -> bool: def terran_air_anti_air(self, state: CollectionState) -> bool: """ Air-to-air - :param state: - :return: """ return ( state.has(item_names.VIKING, self.player) @@ -186,8 +179,6 @@ def terran_any_air_unit(self, state: CollectionState) -> bool: def terran_competent_ground_to_air(self, state: CollectionState) -> bool: """ Ground-to-air - :param state: - :return: """ return ( state.has(item_names.GOLIATH, self.player) @@ -202,8 +193,6 @@ def terran_competent_ground_to_air(self, state: CollectionState) -> bool: def terran_competent_anti_air(self, state: CollectionState) -> bool: """ Good AA unit - :param state: - :return: """ return ( self.terran_competent_ground_to_air(state) @@ -249,8 +238,6 @@ def protoss_gates_of_hell_requirement(self, state: CollectionState) -> bool: def welcome_to_the_jungle_requirement(self, state: CollectionState) -> bool: """ Welcome to the Jungle requirements - able to deal with Scouts, Void Rays, Zealots and Stalkers - :param state: - :return: """ return ( self.terran_common_unit(state) @@ -264,8 +251,6 @@ def welcome_to_the_jungle_requirement(self, state: CollectionState) -> bool: def welcome_to_the_jungle_z_requirement(self, state: CollectionState) -> bool: """ Welcome to the Jungle requirements - able to deal with Scouts, Void Rays, Zealots and Stalkers - :param state: - :return: """ return ( self.zerg_competent_comp(state) and state.has_any({item_names.HYDRALISK, item_names.MUTALISK}, self.player) @@ -282,8 +267,6 @@ def welcome_to_the_jungle_z_requirement(self, state: CollectionState) -> bool: def welcome_to_the_jungle_p_requirement(self, state: CollectionState) -> bool: """ Welcome to the Jungle requirements - able to deal with Scouts, Void Rays, Zealots and Stalkers - :param state: - :return: """ return ( self.protoss_common_unit(state) and self.protoss_competent_anti_air(state) @@ -300,8 +283,6 @@ def welcome_to_the_jungle_p_requirement(self, state: CollectionState) -> bool: def terran_basic_anti_air(self, state: CollectionState) -> bool: """ Basic AA to deal with few air units - :param state: - :return: """ return ( state.has_any(( @@ -329,8 +310,8 @@ def terran_defense_rating(self, state: CollectionState, zerg_enemy: bool, air_en """ Ability to handle defensive missions :param state: - :param zerg_enemy: - :param air_enemy: + :param zerg_enemy: Whether the enemy is zerg + :param air_enemy: Whether the enemy attacks with air :return: """ defense_score = sum((tvx_defense_ratings[item] for item in tvx_defense_ratings if state.has(item, self.player))) @@ -368,8 +349,6 @@ def terran_defense_rating(self, state: CollectionState, zerg_enemy: bool, air_en def terran_competent_comp(self, state: CollectionState) -> bool: """ Ability to deal with most of hard missions - :param state: - :return: """ return ( ( @@ -394,8 +373,6 @@ def terran_competent_comp(self, state: CollectionState) -> bool: def terran_mineral_dump(self, state: CollectionState) -> bool: """ Can build something using only minerals - :param state: - :return: """ return ( state.has_any({item_names.MARINE, item_names.VULTURE, item_names.HELLION, item_names.SON_OF_KORHAL}, @@ -410,8 +387,6 @@ def terran_mineral_dump(self, state: CollectionState) -> bool: def terran_can_grab_ghosts_in_the_fog_east_rock_formation(self, state: CollectionState) -> bool: """ Able to shoot by a long range or from air to claim the rock formation separated by a chasm - :param state: - :return: """ return ( state.has_any({ @@ -439,8 +414,6 @@ def terran_can_grab_ghosts_in_the_fog_east_rock_formation(self, state: Collectio def terran_great_train_robbery_train_stopper(self, state: CollectionState) -> bool: """ Ability to deal with trains (moving target with a lot of HP) - :param state: - :return: """ return ( state.has_any({item_names.SIEGE_TANK, item_names.DIAMONDBACK, item_names.MARAUDER, item_names.CYCLONE, item_names.BANSHEE}, self.player) @@ -456,8 +429,6 @@ def terran_great_train_robbery_train_stopper(self, state: CollectionState) -> bo def zerg_great_train_robbery_train_stopper(self, state: CollectionState) -> bool: """ Ability to deal with trains (moving target with a lot of HP) - :param state: - :return: """ return ( self.morph_impaler_or_lurker(state) @@ -474,8 +445,6 @@ def zerg_great_train_robbery_train_stopper(self, state: CollectionState) -> bool def protoss_great_train_robbery_train_stopper(self, state: CollectionState) -> bool: """ Ability to deal with trains (moving target with a lot of HP) - :param state: - :return: """ return ( state.has_any({item_names.ANNIHILATOR, item_names.INSTIGATOR, item_names.STALKER}, self.player) @@ -495,16 +464,12 @@ def protoss_great_train_robbery_train_stopper(self, state: CollectionState) -> b def terran_can_rescue(self, state) -> bool: """ Rescuing in The Moebius Factor - :param state: - :return: """ return state.has_any({item_names.MEDIVAC, item_names.HERCULES, item_names.RAVEN, item_names.VIKING}, self.player) or self.advanced_tactics def terran_beats_protoss_deathball(self, state: CollectionState) -> bool: """ Ability to deal with Immortals, Colossi with some air support - :param state: - :return: """ return ( ( @@ -517,8 +482,6 @@ def terran_beats_protoss_deathball(self, state: CollectionState) -> bool: def marine_medic_upgrade(self, state: CollectionState) -> bool: """ Infantry upgrade to infantry-only no-build segments - :param state: - :return: """ return ( state.has_any({ @@ -532,8 +495,6 @@ def marine_medic_upgrade(self, state: CollectionState) -> bool: def terran_survives_rip_field(self, state: CollectionState) -> bool: """ Ability to deal with large areas with environment damage - :param state: - :return: """ return ( ( @@ -611,8 +572,6 @@ def terran_sustainable_mech_heal(self, state: CollectionState) -> bool: def terran_bio_heal(self, state: CollectionState) -> bool: """ Ability to heal bio units - :param state: - :return: """ return ( state.has_any({item_names.MEDIC, item_names.MEDIVAC, item_names.FIELD_RESPONSE_THETA}, self.player) @@ -624,8 +583,6 @@ def terran_bio_heal(self, state: CollectionState) -> bool: def terran_base_trasher(self, state: CollectionState) -> bool: """ Can attack heavily defended bases - :param state: - :return: """ return ( state.has(item_names.SIEGE_TANK, self.player) @@ -649,8 +606,6 @@ def terran_mobile_detector(self, state: CollectionState) -> bool: def can_nuke(self, state: CollectionState) -> bool: """ Ability to launch nukes - :param state: - :return: """ return (self.advanced_tactics and (state.has_any({item_names.GHOST, item_names.SPECTRE}, self.player) @@ -659,8 +614,6 @@ def can_nuke(self, state: CollectionState) -> bool: def terran_respond_to_colony_infestations(self, state: CollectionState) -> bool: """ Can deal quickly with Brood Lords and Mutas in Haven's Fall and being able to progress the mission - :param state: - :return: """ return ( self.terran_common_unit(state) @@ -727,8 +680,6 @@ def protoss_repair_odin(self, state: CollectionState): def zergling_hydra_roach_start(self, state: CollectionState): """ Created mainly for engine of destruction start, but works for other missions with no-build starts. - :param state: - :return: """ return ( state.has_any({item_names.ZERGLING_ADRENAL_OVERLOAD, item_names.HYDRALISK_FRENZY, item_names.ROACH_HYDRIODIC_BILE}, self.player) @@ -737,8 +688,6 @@ def zergling_hydra_roach_start(self, state: CollectionState): def zealot_sentry_slayer_start(self, state: CollectionState): """ Created mainly for engine of destruction start, but works for other missions with no-build starts. - :param state: - :return: """ return ( state.has_any({item_names.ZEALOT_WHIRLWIND, item_names.SENTRY_DOUBLE_SHIELD_RECHARGE, item_names.SLAYER_PHASE_BLINK}, self.player) @@ -747,8 +696,6 @@ def zealot_sentry_slayer_start(self, state: CollectionState): def all_in_requirement(self, state: CollectionState): """ All-in - :param state: - :return: """ if not self.terran_very_hard_mission_weapon_armor_level(state): return False @@ -775,8 +722,6 @@ def all_in_requirement(self, state: CollectionState): def all_in_z_requirement(self, state: CollectionState): """ All-in (Zerg) - :param state: - :return: """ if not self.zerg_very_hard_mission_weapon_armor_level(state): return False @@ -804,8 +749,6 @@ def all_in_z_requirement(self, state: CollectionState): def all_in_p_requirement(self, state: CollectionState): """ All-in (Protoss) - :param state: - :return: """ if not self.protoss_very_hard_mission_weapon_armor_level(state): return False @@ -843,9 +786,8 @@ def zerg_defense_rating(self, state: CollectionState, zerg_enemy: bool, air_enem """ Ability to handle defensive missions :param state: - :param zerg_enemy: - :param air_enemy: - :return: + :param zerg_enemy: Whether the enemy is zerg + :param air_enemy: Whether the enemy attacks with air """ defense_score = sum((zvx_defense_ratings[item] for item in zvx_defense_ratings if state.has(item, self.player))) # Twin Drones @@ -1196,8 +1138,6 @@ def zerg_can_grab_ghosts_in_the_fog_east_rock_formation(self, state: CollectionS def zerg_respond_to_colony_infestations(self, state: CollectionState) -> bool: """ Can deal quickly with Brood Lords and Mutas in Haven's Fall and being able to progress the mission - :param state: - :return: """ return ( self.zerg_common_unit(state) @@ -1377,8 +1317,7 @@ def protoss_defense_rating(self, state: CollectionState, zerg_enemy: bool) -> in """ Ability to handle defensive missions :param state: - :param zerg_enemy: - :return: + :param zerg_enemy: Whether the enemy is zerg """ defense_score = sum((pvx_defense_ratings[item] for item in pvx_defense_ratings if state.has(item, self.player))) @@ -1486,8 +1425,6 @@ def protoss_can_attack_behind_chasm(self, state: CollectionState) -> bool: def protoss_respond_to_colony_infestations(self, state: CollectionState) -> bool: """ Can deal quickly with Brood Lords and Mutas in Haven's Fall and being able to progress the mission - :param state: - :return: """ return ( self.protoss_common_unit(state) @@ -2255,8 +2192,6 @@ def sudden_strike_can_reach_objectives(self, state: CollectionState) -> bool: def enemy_intelligence_garrisonable_unit(self, state: CollectionState) -> bool: """ Has unit usable as a Garrison in Enemy Intelligence - :param state: - :return: """ return state.has_any({ item_names.MARINE, item_names.REAPER, item_names.MARAUDER, item_names.GHOST, item_names.SPECTRE, @@ -2439,12 +2374,12 @@ def end_game_requirement(self, state: CollectionState) -> bool: and self.terran_very_hard_mission_weapon_armor_level(state) ) - def __init__(self, world: 'SC2World'): - self.world: 'SC2World' = world - self.player = None if world is None else world.player - self.logic_level = get_option_value(world, 'required_tactics') + def __init__(self, world: Optional['SC2World']): + self.world = world + self.player = -1 if world is None else world.player + self.logic_level: int = world.options.required_tactics.value if world else RequiredTactics.default self.advanced_tactics = self.logic_level != RequiredTactics.option_standard - self.take_over_ai_allies = get_option_value(world, "take_over_ai_allies") == TakeOverAIAllies.option_true + self.take_over_ai_allies = bool(world and world.options.take_over_ai_allies) self.kerrigan_unit_available = ( get_option_value(world, 'kerrigan_presence') in kerrigan_unit_available and SC2Campaign.HOTS in get_enabled_campaigns(world) @@ -2456,16 +2391,15 @@ def __init__(self, world: 'SC2World'): self.morphling_enabled = get_option_value(world, "enable_morphling") == EnableMorphling.option_true self.story_tech_granted = get_option_value(world, "grant_story_tech") == GrantStoryTech.option_true self.story_levels_granted = get_option_value(world, "grant_story_levels") != GrantStoryLevels.option_disabled - self.basic_terran_units = get_basic_units(world, SC2Race.TERRAN) - self.basic_zerg_units = get_basic_units(world, SC2Race.ZERG) - self.basic_protoss_units = get_basic_units(world, SC2Race.PROTOSS) + self.basic_terran_units = get_basic_units(self.logic_level, SC2Race.TERRAN) + self.basic_zerg_units = get_basic_units(self.logic_level, SC2Race.ZERG) + self.basic_protoss_units = get_basic_units(self.logic_level, SC2Race.PROTOSS) self.spear_of_adun_autonomously_cast_presence = get_option_value(world, "spear_of_adun_autonomously_cast_ability_presence") self.enabled_campaigns = get_enabled_campaigns(world) self.mission_order = get_option_value(world, "mission_order") self.generic_upgrade_missions = get_option_value(world, "generic_upgrade_missions") -def get_basic_units(world: 'SC2World', race: SC2Race) -> Set[str]: - logic_level = get_option_value(world, 'required_tactics') +def get_basic_units(logic_level: int, race: SC2Race) -> Set[str]: if logic_level == RequiredTactics.option_no_logic: return no_logic_basic_units[race] elif logic_level == RequiredTactics.option_advanced: diff --git a/worlds/sc2/test/test_generation.py b/worlds/sc2/test/test_generation.py index 2c18a3f64c45..1f5b65f23e89 100644 --- a/worlds/sc2/test/test_generation.py +++ b/worlds/sc2/test/test_generation.py @@ -6,7 +6,7 @@ from .. import mission_groups, mission_tables, options, locations, SC2Mission from ..item import item_groups, item_tables, item_names -from .. import get_all_missions, get_first_mission +from .. import get_all_missions, get_random_first_mission class TestItemFiltering(Sc2SetupTestBase): @@ -540,7 +540,7 @@ def test_nco_and_wol_picks_correct_starting_mission(self): 'enable_epilogue_missions': False, } self.generate_world(world_options) - self.assertEqual(get_first_mission(self.world, self.world.custom_mission_order), mission_tables.SC2Mission.LIBERATION_DAY) + self.assertEqual(get_random_first_mission(self.world, self.world.custom_mission_order), mission_tables.SC2Mission.LIBERATION_DAY) def test_excluding_mission_short_name_excludes_all_variants_of_mission(self): world_options = {