Logic for reducing mission and item counts

This commit is contained in:
Magnemania
2022-10-02 22:34:06 -04:00
parent 5283b142b3
commit bbce7e63a6

193
worlds/sc2wol/PoolFilter.py Normal file
View File

@@ -0,0 +1,193 @@
from BaseClasses import MultiWorld, Item, ItemClassification
from .Items import item_table
from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list, vanilla_mission_req_table, starting_mission_locations
from .Options import get_option_value
from .LogicMixin import SC2WoLLogic
# Items with associated upgrades
UPGRADABLE_ITEMS = [
"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre",
"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor",
"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser",
"Bunker", "Missile Turret"
]
BARRACKS_UNITS = {"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre"}
FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator"}
STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Hercules", "Science Vessel", "Raven"}
MIN_UNITS_PER_STRUCTURE = [
3, # Vanilla
3, # Vanilla Shuffled
2, # Mini Shuffle
0 # Gauntlet
]
PROTOSS_REGIONS = ["A Sinister Turn", "Echoes of the Future", "In Utter Darkness"]
UPGRADES = [
"Progressive Infantry Weapon", "Progressive Infantry Armor",
"Progressive Vehicle Weapon", "Progressive Vehicle Armor",
"Progressive Ship Weapon", "Progressive Ship Armor"
]
def filter_missions(world: MultiWorld, player: int) -> set[str]:
"""
Returns a semi-randomly pruned set of missions based on yaml configuration
"""
missions = set(vanilla_mission_req_table.keys())
mission_order = get_option_value(world, player, "mission_order")
shuffle_protoss = get_option_value(world, player, "shuffle_protoss")
relegate_no_build = get_option_value(world, player, "relegate_no_build")
# Vanilla and Vanilla Shuffled use the entire mission pool
if mission_order not in (2, 3):
return missions
if mission_order == 2:
mission_count = 15
else:
mission_count = 7
mission_sets = [
set(no_build_regions_list),
set(easy_regions_list),
set(medium_regions_list),
set(hard_regions_list)
]
# Omitting Protoss missions if not shuffling protoss
if not shuffle_protoss:
missions.difference_update(PROTOSS_REGIONS)
for mission_set in mission_sets:
mission_set.difference_update(PROTOSS_REGIONS)
# Omitting No Build missions if relegating no-build
if relegate_no_build:
missions.difference_update(no_build_regions_list)
# The build missions in starting_mission_locations become the new "no build missions"
mission_sets[0] = set(starting_mission_locations.keys())
mission_sets[0].difference_update(no_build_regions_list)
# Future-proofing in case a non-Easy mission is placed in starting_mission_locations
for mission_set in mission_sets[1:]:
mission_set.difference_update(mission_sets[0])
# Removing random missions from each difficulty set in a cycle
set_cycle = 0
while len(missions) > mission_count:
if set_cycle == 4:
set_cycle = 0
# Must contain at least one mission per set
if len(mission_sets[set_cycle]) == 1:
continue
removed_mission = world.random.choice(mission_sets[set_cycle])
mission_sets[set_cycle].remove(removed_mission)
missions.remove(removed_mission)
set_cycle += 1
return missions
class ValidInventory(SC2WoLLogic):
def has(self, item: str, player: int = 0):
return item in self.logical_inventory
def has_any(self, items: set[str], player: int = 0):
return any([item in self.logical_inventory for item in items])
# Necessary for Piercing the Shroud
def _sc2wol_has_mm_upgrade(self, world: MultiWorld, player: int):
return self.has_any({"Combat Shield (Marine)", "Stabilizer Medpacks (Medic)"}, player)
# Necessary for Maw of the Void
def _sc2wol_survives_rip_field(self, world: MultiWorld, player: int):
return self.has("Battlecruiser", player) or \
self._sc2wol_has_air(world, player) and \
self._sc2wol_has_competent_anti_air(world, player) and \
self.has("Science Vessel", player)
def _sc2wol_has_units_per_structure(self, world: MultiWorld, player: int):
return len(BARRACKS_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
len(FACTORY_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
len(STARPORT_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure
def generate_reduced_inventory(self, number_of_locations: int):
inventory = set(self.inventory)
locked_items = list(self.locked_items)
self.logical_inventory = set(self.progression_items)
while len(inventory) + len(locked_items) > number_of_locations:
if len(inventory) == 0:
raise Exception('Reduced item pool generation failed.')
# Select random item from removable items
item = self.world.random.choice(inventory)
inventory.remove(item)
# Only run logic checks when removing logic items
if item in self.logical_inventory:
self.logical_inventory.remove(item)
if not all(self.requirements):
# If item cannot be removed, move it to locked items
self.logical_inventory.add(item)
locked_items.add(item)
else:
# If item can be removed and is a unit, remove armory upgrades
if item in UPGRADABLE_ITEMS:
inventory = [inv_item for inv_item in inventory if not inv_item.endswith('(' + item + ')')]
return list(inventory) + locked_items
def __init__(self, world: MultiWorld, player: int, locked_items: list[str]):
self.world = world
mission_order = get_option_value(world, player, "mission_order")
self.min_units_per_structure = MIN_UNITS_PER_STRUCTURE[mission_order]
self.locked_items = locked_items
self.inventory = set(item_table.keys())
protoss_items = set()
# Scanning item table
self.logical_inventory = set()
self.progression_items = set()
for item_name, item in item_table.items():
if item.classification == ItemClassification.progression:
self.progression_items.add(item_name)
elif item.type in ("Minerals", "Vespene", "Supply"):
self.inventory.remove(item_name)
if item.type == "Protoss":
protoss_items.add(item_name)
self.requirements = [
self._sc2wol_has_common_unit,
self._sc2wol_has_air,
self._sc2wol_has_competent_anti_air,
self._sc2wol_has_heavy_defense,
self._sc2wol_has_competent_comp,
self._sc2wol_has_train_killers,
self._sc2wol_able_to_rescue,
self._sc2wol_beats_protoss_deathball,
self._sc2wol_survives_rip_field
]
# Only include units per structure requirement if more than 0
if self.min_units_per_structure > 0:
self.requirements.append(self._sc2wol_has_units_per_structure)
# Only include Marine/Medic upgrade requirements if no-build missions are present
if mission_order in (1, 2) or get_option_value(world, player, "relegate_no_build"):
self.requirements.append(self._has_mm_upgrade)
# Only include protoss requirements if protoss missions are present
if mission_order in (1, 2) or get_option_value(world, player, "shuffle_protoss"):
self.requirements += [
self._sc2wol_has_protoss_common_units,
self._sc2wol_has_protoss_medium_units
]
else:
# Remove protoss items if protoss missions are not present
self.locked_items = [item for item in self.locked_items if item not in protoss_items]
self.inventory = [item for item in self.inventory if item not in protoss_items]
# 2 tiers of each upgrade are guaranteed
self.locked_items += 2 * UPGRADES
self.inventory.difference_update(locked_items)
# 1 tier of each upgrade can be potentially removed
self.inventory += UPGRADES
def filter_items(world: MultiWorld, player: int, regions: set[str], locked_items: list[str] = []) -> list[str]:
"""
Returns a semi-randomly pruned set of items based on number of available locations.
The returned inventory must be capable of logically accessing every location in the world.
"""
valid_inventory = ValidInventory(world, player, locked_items)
number_of_locations = sum([
info.extra_locations for name, info in vanilla_mission_req_table.items() if name in regions
])
return valid_inventory.generate_reduced_inventory(number_of_locations)