From 58f392a0af7aa25198307bdaf2f3f3c71c92c62a Mon Sep 17 00:00:00 2001 From: Magnemania Date: Wed, 5 Oct 2022 21:46:08 -0400 Subject: [PATCH] SC2: Options to curate random item pool and control early unit placement --- worlds/sc2wol/Options.py | 38 ++++++++++++++++++++++------ worlds/sc2wol/PoolFilter.py | 38 ++++++++++++++++------------ worlds/sc2wol/__init__.py | 50 +++++++++++++++++++++++++------------ 3 files changed, 87 insertions(+), 39 deletions(-) diff --git a/worlds/sc2wol/Options.py b/worlds/sc2wol/Options.py index d1921de209..5b76932a59 100644 --- a/worlds/sc2wol/Options.py +++ b/worlds/sc2wol/Options.py @@ -1,6 +1,6 @@ from typing import Dict from BaseClasses import MultiWorld -from Options import Choice, Option, DefaultOnToggle +from Options import Choice, Option, DefaultOnToggle, ItemSet class GameDifficulty(Choice): @@ -39,20 +39,26 @@ class MissionOrder(Choice): """Determines the order the missions are played in. Vanilla: Keeps the standard mission order and branching from the WoL Campaign. Vanilla Shuffled: Keeps same branching paths from the WoL Campaign but randomizes the order of missions within. - Mini Shuffle: Halves the number of missions in the campaign and randomizes the missions available. - Gauntlet: A linear series of 7 random missions to complete the campaign. + Mini Shuffle: Shorter version of the campaign with randomized missions and optional branches. + Grid: A 4x4 grid of random missions. Start at the top left and forge a path towards All-In. + Mini Grid: A 3x3 version of Grid. + Blitz: 10 random missions that open up very quickly. + Gauntlet: Linear series of 7 random missions to complete the campaign. """ display_name = "Mission Order" option_vanilla = 0 option_vanilla_shuffled = 1 option_mini_shuffle = 2 - option_gauntlet = 3 + option_grid = 3 + option_mini_grid = 4 + option_blitz = 5 + option_gauntlet = 6 class ShuffleProtoss(DefaultOnToggle): """Determines if the 3 protoss missions are included in the shuffle if Vanilla mission order is not enabled. On Vanilla Shuffled, the 3 protoss missions will be in their normal position on the Prophecy chain if not shuffled. - On Mini Shuffle or Gauntlet, the 3 protoss missions will not appear and Protoss units are removed from the pool if not shuffled. + On reduced mission settings, the 3 protoss missions will not appear and Protoss units are removed from the pool if not shuffled. """ display_name = "Shuffle Protoss Missions" @@ -60,10 +66,25 @@ class ShuffleProtoss(DefaultOnToggle): class RelegateNoBuildMissions(DefaultOnToggle): """Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled. On Vanilla Shuffled, one no-build mission will be placed as the first mission and the rest will be placed at the end of optional routes. - On Mini Shuffle or Gauntlet, the 5 no-build missions will not appear.""" + On reduced mission settings, the 5 no-build missions will not appear.""" display_name = "Relegate No-Build Missions" +class EarlyUnit(DefaultOnToggle): + """Guarantees that the first mission will contain a unit.""" + display_name = "Early Unit" + + +class LockedItems(ItemSet): + """Guarantees that these items will appear in your world""" + display_name = "Locked Items" + + +class ExcludedItems(ItemSet): + """Guarantees that these items will not appear in your world""" + display_name = "Excluded Items" + + # noinspection PyTypeChecker sc2wol_options: Dict[str, Option] = { "game_difficulty": GameDifficulty, @@ -72,7 +93,10 @@ sc2wol_options: Dict[str, Option] = { "all_in_map": AllInMap, "mission_order": MissionOrder, "shuffle_protoss": ShuffleProtoss, - "relegate_no_build": RelegateNoBuildMissions + "relegate_no_build": RelegateNoBuildMissions, + "early_unit": EarlyUnit, + "locked_items": LockedItems, + "excluded_items": ExcludedItems } diff --git a/worlds/sc2wol/PoolFilter.py b/worlds/sc2wol/PoolFilter.py index 898975c5ab..21619c7e73 100644 --- a/worlds/sc2wol/PoolFilter.py +++ b/worlds/sc2wol/PoolFilter.py @@ -40,8 +40,14 @@ def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]: shuffle_protoss = get_option_value(world, player, "shuffle_protoss") relegate_no_build = get_option_value(world, player, "relegate_no_build") + mission_count = 0 + for mission in mission_orders[mission_order_type]: + if mission.type == 'all_in': # All-In is placed separately + continue + mission_count += 1 + # Vanilla and Vanilla Shuffled use the entire mission pool - if mission_order_type in (0, 1): + if mission_count == 28: return { 'no_build': no_build_regions_list[:], 'easy': easy_regions_list[:], @@ -49,14 +55,6 @@ def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]: 'hard': hard_regions_list[:] } - mission_count = 0 - for mission in mission_orders[mission_order_type]: - if mission is None: - continue - if mission.type == 'all_in': # All-In is placed separately - continue - mission_count += 1 - mission_sets = [ set(no_build_regions_list), set(easy_regions_list), @@ -99,6 +97,14 @@ def filter_missions(world: MultiWorld, player: int) -> dict[str, list[str]]: } +def filter_upgrades(inventory: list[Item], parent_item: Item or str): + item_name = parent_item.name if isinstance(parent_item, Item) else parent_item + return [ + inv_item for inv_item in inventory + if inv_item.name in ALWAYS_USEFUL_ARMORY or not inv_item.name.endswith('(' + item_name + ')') + ] + + class ValidInventory: def has(self, item: str, player: int): @@ -121,12 +127,14 @@ class ValidInventory: self.logical_inventory = {item.name for item in self.progression_items.union(self.locked_items)} requirements = mission_requirements mission_order_type = get_option_value(self.world, self.player, "mission_order") - min_units_per_structure = MIN_UNITS_PER_STRUCTURE[mission_order_type] + # Inventory restrictiveness based on number of missions with checks + mission_count = len(mission_orders[mission_order_type]) - 1 + min_units_per_structure = int(mission_count / 7) if min_units_per_structure > 0: requirements.append(lambda state: state.has_units_per_structure(min_units_per_structure)) while len(inventory) + len(locked_items) > inventory_size: if len(inventory) == 0: - raise Exception('Reduced item pool generation failed.') + raise Exception('Reduced item pool generation failed - not enough locations available to place items.') # Select random item from removable items item = self.world.random.choice(inventory) inventory.remove(item) @@ -137,10 +145,7 @@ class ValidInventory: # If item can be removed and is a unit, remove armory upgrades # Some armory upgrades are kept regardless, as they remain logically relevant if item.name in UPGRADABLE_ITEMS: - inventory = [ - inv_item for inv_item in inventory - if inv_item.name in ALWAYS_USEFUL_ARMORY or not inv_item.name.endswith('(' + item.name + ')') - ] + inventory = filter_upgrades(inventory, item) else: # If item cannot be removed, move it to locked items self.logical_inventory.add(item.name) @@ -162,7 +167,8 @@ class ValidInventory: self._sc2wol_has_protoss_common_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_common_units(self, world, player) self._sc2wol_has_protoss_medium_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_medium_units(self, world, player) self._sc2wol_has_mm_upgrade = lambda world, player: SC2WoLLogic._sc2wol_has_mm_upgrade(self, world, player) - self._sc2wol_has_bunker_unit = lambda world, player: SC2WoLLogic._sc2wol_has_bunker_unit(self, world, player) + self._sc2wol_has_manned_bunkers = lambda world, player: SC2WoLLogic._sc2wol_has_manned_bunkers(self, world, player) + self._sc2wol_final_mission_requirements = lambda world, player: SC2WoLLogic._sc2wol_final_mission_requirements(self, world, player) def __init__(self, world: MultiWorld, player: int, item_pool: list[Item], existing_items: list[Item], locked_items: list[Item], diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index d8894734ce..a3477ab2fd 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -9,7 +9,7 @@ from .Locations import get_locations from .Regions import create_regions from .Options import sc2wol_options, get_option_value from .LogicMixin import SC2WoLLogic -from .PoolFilter import filter_missions, filter_items +from .PoolFilter import filter_missions, filter_items, filter_upgrades from .MissionTables import starting_mission_locations, MissionInfo @@ -122,26 +122,32 @@ def get_excluded_items(self: SC2WoLWorld, world: MultiWorld, player: int) -> Set for item in world.precollected_items[player]: excluded_items.add(item.name) + excluded_items_option = getattr(world, 'excluded_items', []) + + excluded_items.update(excluded_items_option[player].value) + return excluded_items def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]) -> List[Item]: non_local_items = world.non_local_items[player].value + if get_option_value(world, player, "early_unit"): + local_basic_unit = tuple(item for item in basic_unit if item not in non_local_items) + if not local_basic_unit: + raise Exception("At least one basic unit must be local") - local_basic_unit = tuple(item for item in basic_unit if item not in non_local_items) - if not local_basic_unit: - raise Exception("At least one basic unit must be local") + # The first world should also be the starting world + first_mission = list(world.worlds[player].mission_req_table)[0] + if first_mission in starting_mission_locations: + first_location = starting_mission_locations[first_mission] + elif first_mission == "In Utter Darkness": + first_location = first_mission + ": Defeat" + else: + first_location = first_mission + ": Victory" - # The first world should also be the starting world - first_mission = list(world.worlds[player].mission_req_table)[0] - if first_mission in starting_mission_locations: - first_location = starting_mission_locations[first_mission] - elif first_mission == "In Utter Darkness": - first_location = first_mission + ": Defeat" + return [assign_starter_item(world, player, excluded_items, locked_locations, first_location, local_basic_unit)] else: - first_location = first_mission + ": Victory" - - return [assign_starter_item(world, player, excluded_items, locked_locations, first_location, local_basic_unit)] + return [] def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str], @@ -164,15 +170,27 @@ def get_item_pool(world: MultiWorld, player: int, mission_req_table: dict[str, M starter_items: List[str], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]: pool: List[Item] = [] + # For the future: goal items like Artifact Shards go here + locked_items = [] + + # YAML items + locked_items_option = getattr(world, 'locked_items', []) + yaml_locked_items = locked_items_option[player].value + for name, data in item_table.items(): if name not in excluded_items: for _ in range(data.quantity): item = create_item_with_correct_settings(world, player, name) - pool.append(item) + if name in yaml_locked_items: + locked_items.append(item) + else: + pool.append(item) existing_items = starter_items + [item.name for item in world.precollected_items[player]] - # For the future: goal items like Artifact Shards go here - locked_items = [] + + # Removing upgrades for excluded items + for item_name in excluded_items: + pool = filter_upgrades(pool, item_name) filtered_pool = filter_items(world, player, mission_req_table, location_cache, pool, existing_items, locked_items) return filtered_pool