From 67c307657267a619f3a74f3e64618717f963cd1a Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sun, 23 Apr 2023 16:16:01 -0400 Subject: [PATCH] SM: comeback fix6 and some refactor (#1756) refactored and cleaned a bit SMWorld class for best practices: - moved content of Regions.py and Rules.py in SMWorld - moved appropiate code to their dedicated World core functions - moved some Entrances being created in generate_basic to create_regions more comeback check fixes: - fixed setting progression door openers items local if doors_colors_rando is used - enable comeback check only for filling stage as later stages (progression balancing, accessibility and spoiler playthrough) are prone to fail with it --- worlds/sm/Regions.py | 42 ----- worlds/sm/Rules.py | 38 ----- worlds/sm/__init__.py | 366 ++++++++++++++++++++++++------------------ 3 files changed, 209 insertions(+), 237 deletions(-) delete mode 100644 worlds/sm/Regions.py delete mode 100644 worlds/sm/Rules.py diff --git a/worlds/sm/Regions.py b/worlds/sm/Regions.py deleted file mode 100644 index ee6af408..00000000 --- a/worlds/sm/Regions.py +++ /dev/null @@ -1,42 +0,0 @@ -def create_regions(self, world, player: int): - from . import create_region - from BaseClasses import Entrance - from .variaRandomizer.logic.logic import Logic - from .variaRandomizer.graph.vanilla.graph_locations import locationsDict - - regions = [] - for accessPoint in Logic.accessPoints: - if not accessPoint.Escape: - regions.append(create_region(self, - world, - player, - accessPoint.Name, - None, - [accessPoint.Name + "->" + key for key in accessPoint.intraTransitions.keys()])) - - world.regions += regions - - # create a region for each location and link each to what the location has access - # we make them one way so that the filler (and spoiler log) doesnt try to use those region as intermediary path - # this is required in AP because a location cant have multiple parent regions - locationRegions = [] - for locationName, value in locationsDict.items(): - locationRegions.append(create_region( self, - world, - player, - locationName, - [locationName])) - for key in value.AccessFrom.keys(): - currentRegion =world.get_region(key, player) - currentRegion.exits.append(Entrance(player, key + "->"+ locationName, currentRegion)) - - world.regions += locationRegions - #create entrances - regionConcat = regions + locationRegions - for region in regionConcat: - for exit in region.exits: - exit.connect(world.get_region(exit.name[exit.name.find("->") + 2:], player)) - - world.regions += [ - create_region(self, world, player, 'Menu', None, ['StartAP']) - ] diff --git a/worlds/sm/Rules.py b/worlds/sm/Rules.py deleted file mode 100644 index 15706987..00000000 --- a/worlds/sm/Rules.py +++ /dev/null @@ -1,38 +0,0 @@ -from worlds.generic.Rules import set_rule, add_rule - -from .variaRandomizer.graph.vanilla.graph_locations import locationsDict -from .variaRandomizer.logic.logic import Logic - -def evalSMBool(smbool, maxDiff): - return smbool.bool == True and smbool.difficulty <= maxDiff - -def add_accessFrom_rule(location, player, accessFrom): - add_rule(location, lambda state: any((state.can_reach(accessName, player=player) and evalSMBool(rule(state.smbm[player]), state.smbm[player].maxDiff)) for accessName, rule in accessFrom.items())) - -def add_postAvailable_rule(location, player, func): - add_rule(location, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) - -def set_available_rule(location, player, func): - set_rule(location, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) - -def set_entrance_rule(entrance, player, func): - set_rule(entrance, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) - -def add_entrance_rule(entrance, player, func): - add_rule(entrance, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) - -def set_rules(world, player): - world.completion_condition[player] = lambda state: state.has('Mother Brain', player) - - for key, value in locationsDict.items(): - location = world.get_location(key, player) - set_available_rule(location, player, value.Available) - if value.AccessFrom is not None: - add_accessFrom_rule(location, player, value.AccessFrom) - if value.PostAvailable is not None: - add_postAvailable_rule(location, player, value.PostAvailable) - - for accessPoint in Logic.accessPoints: - if not accessPoint.Escape: - for key, value1 in accessPoint.intraTransitions.items(): - set_entrance_rule(world.get_entrance(accessPoint.Name + "->" + key, player), player, value1) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index d1804d92..ef7e50ba 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -10,11 +10,10 @@ from typing import Any, Dict, Iterable, List, Set, TextIO, TypedDict from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, CollectionState, Tutorial from Fill import fill_restrictive from worlds.AutoWorld import World, AutoLogicRegister, WebWorld +from worlds.generic.Rules import set_rule, add_rule, add_item_rule logger = logging.getLogger("Super Metroid") -from .Regions import create_regions -from .Rules import set_rules, add_entrance_rule from .Options import sm_options from .Client import SMSNIClient from .Rom import get_base_rom_path, SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMDeltaPatch, get_sm_symbols @@ -106,6 +105,7 @@ class SMWorld(World): def __init__(self, world: MultiWorld, player: int): self.rom_name_available_event = threading.Event() self.locations = {} + self.need_comeback_check = True super().__init__(world, player) @classmethod @@ -134,7 +134,7 @@ class SMWorld(World): self.multiworld.accessibility[self.player] = self.multiworld.accessibility[self.player].from_text("minimal") logger.warning(f"accessibility forced to 'minimal' for player {self.multiworld.get_player_name(self.player)} because of 'fun' settings") - def generate_basic(self): + def create_items(self): itemPool = self.variaRando.container.itemPool self.startItems = [variaItem for item in self.multiworld.precollected_items[self.player] for variaItem in ItemManager.Items.values() if variaItem.Name == item.name] if self.multiworld.start_inventory_removes_from_pool[self.player]: @@ -150,7 +150,6 @@ class SMWorld(World): pool = [] self.locked_items = {} self.NothingPool = [] - self.prefilled_locked_items = [] weaponCount = [0, 0, 0] for item in itemPool: isAdvancement = True @@ -180,12 +179,9 @@ class SMWorld(World): player=self.player) beamItems = ['Spazer', 'Ice', 'Wave' ,'Plasma'] - self.ammoItems = ['Missile', 'Super', 'PowerBomb'] if self.multiworld.doors_colors_rando[self.player].value != 0: if item.Type in beamItems: self.multiworld.local_items[self.player].value.add(item.Name) - elif item.Type in self.ammoItems and isAdvancement: - self.prefilled_locked_items.append(smitem) if itemClass == 'Boss': self.locked_items[item.Name] = smitem @@ -199,9 +195,96 @@ class SMWorld(World): for (location, item) in self.locked_items.items(): self.multiworld.get_location(location, self.player).place_locked_item(item) self.multiworld.get_location(location, self.player).address = None + + def evalSMBool(self, smbool, maxDiff): + return smbool.bool == True and smbool.difficulty <= maxDiff + + def add_entrance_rule(self, entrance, player, func): + add_rule(entrance, lambda state: self.evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) - startAP = self.multiworld.get_entrance('StartAP', self.player) - startAP.connect(self.multiworld.get_region(self.variaRando.args.startLocation, self.player)) + def set_rules(self): + def add_accessFrom_rule(location, player, accessFrom): + add_rule(location, lambda state: any((state.can_reach(accessName, player=player) and self.evalSMBool(rule(state.smbm[player]), state.smbm[player].maxDiff)) for accessName, rule in accessFrom.items())) + + def add_postAvailable_rule(location, player, func): + add_rule(location, lambda state: self.evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) + + def set_available_rule(location, player, func): + set_rule(location, lambda state: self.evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) + + def set_entrance_rule(entrance, player, func): + set_rule(entrance, lambda state: self.evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) + + self.multiworld.completion_condition[self.player] = lambda state: state.has('Mother Brain', self.player) + + ammoItems = ['Missile', 'Super', 'PowerBomb'] + for key, value in locationsDict.items(): + location = self.multiworld.get_location(key, self.player) + set_available_rule(location, self.player, value.Available) + if value.AccessFrom is not None: + add_accessFrom_rule(location, self.player, value.AccessFrom) + if value.PostAvailable is not None: + add_postAvailable_rule(location, self.player, value.PostAvailable) + + if self.multiworld.doors_colors_rando[self.player].value != 0: + add_item_rule(location, lambda item: item.type not in ammoItems or + (item.type in ammoItems and \ + (not item.advancement or (item.advancement and item.player == self.player)))) + + for accessPoint in Logic.accessPoints: + if not accessPoint.Escape: + for key, value1 in accessPoint.intraTransitions.items(): + set_entrance_rule(self.multiworld.get_entrance(accessPoint.Name + "->" + key, self.player), self.player, value1) + + def create_region(self, world: MultiWorld, player: int, name: str, locations=None, exits=None): + ret = Region(name, player, world) + if locations: + for loc in locations: + location = self.locations[loc] + location.parent_region = ret + ret.locations.append(location) + if exits: + for exit in exits: + ret.exits.append(Entrance(player, exit, ret)) + return ret + + def create_regions(self): + # create locations + for name in locationsDict: + self.locations[name] = SMLocation(self.player, name, self.location_name_to_id.get(name, None)) + + # create regions + regions = [] + for accessPoint in Logic.accessPoints: + if not accessPoint.Escape: + regions.append(self.create_region( self.multiworld, + self.player, + accessPoint.Name, + None, + [accessPoint.Name + "->" + key for key in accessPoint.intraTransitions.keys()])) + + self.multiworld.regions += regions + + # create a region for each location and link each to what the location has access + # we make them one way so that the filler (and spoiler log) doesnt try to use those region as intermediary path + # this is required in AP because a location cant have multiple parent regions + locationRegions = [] + for locationName, value in locationsDict.items(): + locationRegions.append(self.create_region( self.multiworld, + self.player, + locationName, + [locationName])) + for key in value.AccessFrom.keys(): + currentRegion = self.multiworld.get_region(key, self.player) + currentRegion.exits.append(Entrance(self.player, key + "->"+ locationName, currentRegion)) + + self.multiworld.regions += locationRegions + + #create entrances + regionConcat = regions + locationRegions + for region in regionConcat: + for exit in region.exits: + exit.connect(self.multiworld.get_region(exit.name[exit.name.find("->") + 2:], self.player)) for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions: src_region = self.multiworld.get_region(src.Name, self.player) @@ -210,26 +293,125 @@ class SMWorld(World): src_region.exits.append(Entrance(self.player, src.Name + "->" + dest.Name, src_region)) srcDestEntrance = self.multiworld.get_entrance(src.Name + "->" + dest.Name, self.player) srcDestEntrance.connect(dest_region) - add_entrance_rule(self.multiworld.get_entrance(src.Name + "->" + dest.Name, self.player), self.player, getAccessPoint(src.Name).traverse) + self.add_entrance_rule(self.multiworld.get_entrance(src.Name + "->" + dest.Name, self.player), self.player, getAccessPoint(src.Name).traverse) - def set_rules(self): - set_rules(self.multiworld, self.player) + self.multiworld.regions += [ + self.create_region(self.multiworld, self.player, 'Menu', None, ['StartAP']) + ] - def create_regions(self): - create_locations(self, self.player) - create_regions(self, self.multiworld, self.player) + startAP = self.multiworld.get_entrance('StartAP', self.player) + startAP.connect(self.multiworld.get_region(self.variaRando.args.startLocation, self.player)) + def collect(self, state: CollectionState, item: Item) -> bool: + state.smbm[self.player].addItem(item.type) + if item.location != None and item.location.game == self.game: + for entrance in self.multiworld.get_region(item.location.parent_region.name, item.location.player).entrances: + if (entrance.parent_region.can_reach(state)): + state.smbm[item.location.player].lastAP = entrance.parent_region.name + break + return super(SMWorld, self).collect(state, item) + + def remove(self, state: CollectionState, item: Item) -> bool: + state.smbm[self.player].removeItem(item.type) + return super(SMWorld, self).remove(state, item) + + def create_item(self, name: str) -> Item: + item = next(x for x in ItemManager.Items.values() if x.Name == name) + return SMItem(item.Name, ItemClassification.progression if item.Class != 'Minor' else ItemClassification.filler, item.Type, self.item_name_to_id[item.Name], + player=self.player) + + def get_filler_item_name(self) -> str: + if self.multiworld.random.randint(0, 100) < self.multiworld.minor_qty[self.player].value: + power_bombs = self.multiworld.power_bomb_qty[self.player].value + missiles = self.multiworld.missile_qty[self.player].value + super_missiles = self.multiworld.super_qty[self.player].value + roll = self.multiworld.random.randint(1, power_bombs + missiles + super_missiles) + if roll <= power_bombs: + return "Power Bomb" + elif roll <= power_bombs + missiles: + return "Missile" + else: + return "Super Missile" + else: + return "Nothing" + def pre_fill(self): - from Fill import fill_restrictive - if len(self.prefilled_locked_items) > 0: - locations = [loc for loc in self.locations.values() if loc.item is None] - self.multiworld.random.shuffle(locations) - all_state = self.multiworld.get_all_state(False) - for item in self.ammoItems: - while (all_state.has(item.name, self.player, 1)): - all_state.remove(item) + if len(self.NothingPool) > 0: + nonChozoLoc = [] + chozoLoc = [] - fill_restrictive(self.multiworld, all_state, locations, self.prefilled_locked_items, True, True) + for loc in self.locations.values(): + if loc.item is None: + if locationsDict[loc.name].isChozo(): + chozoLoc.append(loc) + else: + nonChozoLoc.append(loc) + + self.multiworld.random.shuffle(nonChozoLoc) + self.multiworld.random.shuffle(chozoLoc) + missingCount = len(self.NothingPool) - len(nonChozoLoc) + locations = nonChozoLoc + if (missingCount > 0): + locations += chozoLoc[:missingCount] + locations = locations[:len(self.NothingPool)] + for item, loc in zip(self.NothingPool, locations): + loc.place_locked_item(item) + loc.address = loc.item.code = None + + def post_fill(self): + self.itemLocs = [ + ItemLocation(ItemManager.Items[itemLoc.item.type + if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else + 'ArchipelagoItem'], + locationsDict[itemLoc.name], itemLoc.item.player, True) + for itemLoc in self.multiworld.get_locations(self.player) + ] + self.progItemLocs = [ + ItemLocation(ItemManager.Items[itemLoc.item.type + if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else + 'ArchipelagoItem'], + locationsDict[itemLoc.name], itemLoc.item.player, True) + for itemLoc in self.multiworld.get_locations(self.player) if itemLoc.item.advancement + ] + for itemLoc in self.itemLocs: + if itemLoc.Item.Class == "Boss": + itemLoc.Item.Class = "Minor" + for itemLoc in self.progItemLocs: + if itemLoc.Item.Class == "Boss": + itemLoc.Item.Class = "Minor" + + localItemLocs = [il for il in self.itemLocs if il.player == self.player] + localprogItemLocs = [il for il in self.progItemLocs if il.player == self.player] + + escapeTrigger = (localItemLocs, localprogItemLocs, 'Full') if self.variaRando.randoExec.randoSettings.restrictions["EscapeTrigger"] else None + escapeOk = self.variaRando.randoExec.graphBuilder.escapeGraph(self.variaRando.container, self.variaRando.randoExec.areaGraph, self.variaRando.randoExec.randoSettings.maxDiff, escapeTrigger) + assert escapeOk, "Could not find a solution for escape" + + self.variaRando.doors = GraphUtils.getDoorConnections(self.variaRando.randoExec.areaGraph, + self.variaRando.args.area, self.variaRando.args.bosses, + self.variaRando.args.escapeRando) + + self.variaRando.randoExec.postProcessItemLocs(self.itemLocs, self.variaRando.args.hideItems) + + self.need_comeback_check = False + + @classmethod + def stage_post_fill(cls, world): + new_state = CollectionState(world) + progitempool = [] + for item in world.itempool: + if item.game == "Super Metroid" and item.advancement: + progitempool.append(item) + + for item in progitempool: + new_state.collect(item, True) + + bossesLoc = ['Draygon', 'Kraid', 'Ridley', 'Phantoon', 'Mother Brain'] + for player in world.get_game_players("Super Metroid"): + for bossLoc in bossesLoc: + if not world.get_location(bossLoc, player).can_reach(new_state): + world.state.smbm[player].onlyBossLeft = True + break def getWordArray(self, w: int) -> List[int]: """ little-endian convert a 16-bit number to an array of numbers <= 255 each """ @@ -669,115 +851,6 @@ class SMWorld(World): return slot_data - def collect(self, state: CollectionState, item: Item) -> bool: - state.smbm[self.player].addItem(item.type) - if item.location != None and item.location.game == self.game: - for entrance in self.multiworld.get_region(item.location.parent_region.name, item.location.player).entrances: - if (entrance.parent_region.can_reach(state)): - state.smbm[item.location.player].lastAP = entrance.parent_region.name - break - return super(SMWorld, self).collect(state, item) - - def remove(self, state: CollectionState, item: Item) -> bool: - state.smbm[self.player].removeItem(item.type) - return super(SMWorld, self).remove(state, item) - - def create_item(self, name: str) -> Item: - item = next(x for x in ItemManager.Items.values() if x.Name == name) - return SMItem(item.Name, ItemClassification.progression if item.Class != 'Minor' else ItemClassification.filler, item.Type, self.item_name_to_id[item.Name], - player=self.player) - - def get_filler_item_name(self) -> str: - if self.multiworld.random.randint(0, 100) < self.multiworld.minor_qty[self.player].value: - power_bombs = self.multiworld.power_bomb_qty[self.player].value - missiles = self.multiworld.missile_qty[self.player].value - super_missiles = self.multiworld.super_qty[self.player].value - roll = self.multiworld.random.randint(1, power_bombs + missiles + super_missiles) - if roll <= power_bombs: - return "Power Bomb" - elif roll <= power_bombs + missiles: - return "Missile" - else: - return "Super Missile" - else: - return "Nothing" - - def pre_fill(self): - if len(self.NothingPool) > 0: - nonChozoLoc = [] - chozoLoc = [] - - for loc in self.locations.values(): - if loc.item is None: - if locationsDict[loc.name].isChozo(): - chozoLoc.append(loc) - else: - nonChozoLoc.append(loc) - - self.multiworld.random.shuffle(nonChozoLoc) - self.multiworld.random.shuffle(chozoLoc) - missingCount = len(self.NothingPool) - len(nonChozoLoc) - locations = nonChozoLoc - if (missingCount > 0): - locations += chozoLoc[:missingCount] - locations = locations[:len(self.NothingPool)] - for item, loc in zip(self.NothingPool, locations): - loc.place_locked_item(item) - loc.address = loc.item.code = None - - def post_fill(self): - self.itemLocs = [ - ItemLocation(ItemManager.Items[itemLoc.item.type - if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else - 'ArchipelagoItem'], - locationsDict[itemLoc.name], itemLoc.item.player, True) - for itemLoc in self.multiworld.get_locations(self.player) - ] - self.progItemLocs = [ - ItemLocation(ItemManager.Items[itemLoc.item.type - if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else - 'ArchipelagoItem'], - locationsDict[itemLoc.name], itemLoc.item.player, True) - for itemLoc in self.multiworld.get_locations(self.player) if itemLoc.item.advancement - ] - for itemLoc in self.itemLocs: - if itemLoc.Item.Class == "Boss": - itemLoc.Item.Class = "Minor" - for itemLoc in self.progItemLocs: - if itemLoc.Item.Class == "Boss": - itemLoc.Item.Class = "Minor" - - localItemLocs = [il for il in self.itemLocs if il.player == self.player] - localprogItemLocs = [il for il in self.progItemLocs if il.player == self.player] - - escapeTrigger = (localItemLocs, localprogItemLocs, 'Full') if self.variaRando.randoExec.randoSettings.restrictions["EscapeTrigger"] else None - escapeOk = self.variaRando.randoExec.graphBuilder.escapeGraph(self.variaRando.container, self.variaRando.randoExec.areaGraph, self.variaRando.randoExec.randoSettings.maxDiff, escapeTrigger) - assert escapeOk, "Could not find a solution for escape" - - self.variaRando.doors = GraphUtils.getDoorConnections(self.variaRando.randoExec.areaGraph, - self.variaRando.args.area, self.variaRando.args.bosses, - self.variaRando.args.escapeRando) - - self.variaRando.randoExec.postProcessItemLocs(self.itemLocs, self.variaRando.args.hideItems) - - @classmethod - def stage_post_fill(cls, world): - new_state = CollectionState(world) - progitempool = [] - for item in world.itempool: - if item.game == "Super Metroid" and item.advancement: - progitempool.append(item) - - for item in progitempool: - new_state.collect(item, True) - - bossesLoc = ['Draygon', 'Kraid', 'Ridley', 'Phantoon', 'Mother Brain'] - for player in world.get_game_players("Super Metroid"): - for bossLoc in bossesLoc: - if not world.get_location(bossLoc, player).can_reach(new_state): - world.state.smbm[player].onlyBossLeft = True - break - def write_spoiler(self, spoiler_handle: TextIO): if self.multiworld.area_randomization[self.player].value != 0: spoiler_handle.write('\n\nArea Transitions:\n\n') @@ -793,39 +866,18 @@ class SMWorld(World): '<=>', dest.Name) for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions if src.Boss])) -def create_locations(self, player: int): - for name in locationsDict: - self.locations[name] = SMLocation(player, name, self.location_name_to_id.get(name, None)) - - -def create_region(self, world: MultiWorld, player: int, name: str, locations=None, exits=None): - ret = Region(name, player, world) - if locations: - for loc in locations: - location = self.locations[loc] - location.parent_region = ret - ret.locations.append(location) - if exits: - for exit in exits: - ret.exits.append(Entrance(player, exit, ret)) - return ret - - class SMLocation(Location): game: str = "Super Metroid" def __init__(self, player: int, name: str, address=None, parent=None): super(SMLocation, self).__init__(player, name, address, parent) - def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: - return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state))) - def can_reach(self, state: CollectionState) -> bool: # self.access_rule computes faster on average, so placing it first for faster abort assert self.parent_region, "Can't reach location without region" - return self.access_rule(state) and \ - self.parent_region.can_reach(state) and \ - self.can_comeback(state, self.item) + return super(SMLocation, self).can_reach(state) and \ + (not state.multiworld.worlds[self.player].need_comeback_check or \ + self.can_comeback(state, self.item)) def can_comeback(self, state: CollectionState, item): randoExec = state.multiworld.worlds[self.player].variaRando.randoExec