diff --git a/BaseClasses.py b/BaseClasses.py index b08fdb23..149054a3 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -448,6 +448,65 @@ class World(object): return False + def fulfills_accessibility(self): + state = CollectionState(self) + locations = {location for location in self.get_locations() if location.item} + beatable_players = set() + items_players = set() + locations_players = set() + for player in self.player_ids: + access = self.accessibility[player] + if access == "none": + beatable_players.add(player) + elif access == "items": + items_players.add(player) + elif access == "locations": + locations_players.add(player) + else: + raise Exception(f"unknown access rule {access} for player {player}") + + beatable_fulfilled = False + + def location_conditition(location : Location): + """Determine if this location has to be accessible""" + if location.player in locations_players: + return True + elif location.player in items_players: + return location.item.advancement or location.event + + return False + + def all_done(): + """Check if all access rules are fulfilled""" + if beatable_fulfilled: + for location in locations: + if location_conditition(location): + return False + return True + + while locations: + sphere = set() + for location in locations: + if location.can_reach(state): + sphere.add(location) + + if not sphere: + # ran out of places and did not finish yet, quit + logging.debug(f"Could not access required locations.") + return False + + for location in sphere: + locations.remove(location) + state.collect(location.item, True, location) + + if self.has_beaten_game(state): + beatable_fulfilled = True + + if all_done(): + return True + + return False + class CollectionState(object): def __init__(self, parent: World): diff --git a/Main.py b/Main.py index 24484905..c7e2fd66 100644 --- a/Main.py +++ b/Main.py @@ -238,22 +238,17 @@ def main(args, seed=None): if shop.can_push_inventory(slot_num): for c in candidates: # chosen item locations if c.item_rule(location.item): # if rule is good... - logging.debug('Swapping {} with {}:: {} ||| {}'.format(c, location, c.item, location.item)) + logger.debug(f'Swapping {c} into {location}:: {c.item}') swap_location_item(c, location, check_locked=False) - # TODO: should likely be (all_state-c.item).can_reach(shop.region) - # can still be can_beat_game when beatable-only for the item-owning player - # appears to be the main source of "progression items unreachable Exception" - # in door-rando + multishop - if not world.can_beat_game(): - # swap back + candidates.remove(c) + if not world.fulfills_accessibility(): swap_location_item(c, location, check_locked=False) - else: - # we use this candidate - candidates.remove(c) - break + continue + break + else: # This *should* never happen. But let's fail safely just in case. - logging.warning("Ran out of ShopShuffle Item candidate locations.") + logger.warning("Ran out of ShopShuffle Item candidate locations.") shop.region.locations.remove(location) continue @@ -381,7 +376,7 @@ def main(args, seed=None): pool = concurrent.futures.ThreadPoolExecutor() multidata_task = None - check_beatability_task = pool.submit(world.can_beat_game) + check_accessibility_task = pool.submit(world.fulfills_accessibility) if not args.suppress_rom: rom_futures = [] @@ -477,8 +472,11 @@ def main(args, seed=None): f.write(multidata) multidata_task = pool.submit(write_multidata, rom_futures) - if not check_beatability_task.result(): - raise Exception("Game appears unbeatable. Aborting.") + if not check_accessibility_task.result(): + if not world.can_beat_game(): + raise Exception("Game appears is unbeatable. Aborting.") + else: + logger.warning("Location Accessibility requirements not fulfilled.") if not args.skip_playthrough: logger.info('Calculating playthrough.') create_playthrough(world) diff --git a/Regions.py b/Regions.py index be9ddc6a..afbea2c7 100644 --- a/Regions.py +++ b/Regions.py @@ -414,7 +414,7 @@ def create_shops(world, player: int): pass else: if my_shop_slots.pop(): - additional_item = world.random.choice(['Rupees (50)', 'Rupees (100)', 'Rupees (300)']) + additional_item = 'Rupees (50)' # world.random.choice(['Rupees (50)', 'Rupees (100)', 'Rupees (300)']) slot_name = "{} Slot {}".format(shop.region.name, index + 1) loc = Location(player, slot_name, address=shop_table_by_location[slot_name], parent=shop.region) loc.shop_slot = True