def sweep_from_pool( base_state: CollectionState, itempool: typing.Sequence[Item] = tuple() ) -> CollectionState: new_state = base_state.copy() for item in itempool: new_state.collect(item, True) new_state.sweep_for_events() return new_state
def create_playthrough(world): # create a copy as we will modify it old_world = world world = copy_world(world) # if we only check for beatable, we can do this sanity check first before writing down spheres if world.check_beatable_only and not world.can_beat_game(): raise RuntimeError( 'Cannot beat game. Something went terribly wrong here!') # get locations containing progress items prog_locations = [ location for location in world.get_filled_locations() if location.item.advancement ] state_cache = [None] collection_spheres = [] state = CollectionState(world) sphere_candidates = list(prog_locations) logging.getLogger('').debug('Building up collection spheres.') while sphere_candidates: state.sweep_for_events(key_only=True) sphere = [] # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres for location in sphere_candidates: if state.can_reach(location): sphere.append(location) for location in sphere: sphere_candidates.remove(location) state.collect(location.item, True, location) collection_spheres.append(sphere) state_cache.append(state.copy()) logging.getLogger('').debug( 'Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(prog_locations)) if not sphere: logging.getLogger('').debug( 'The following items could not be reached: %s', [ '%s at %s' % (location.item.name, location.name) for location in sphere_candidates ]) if not world.check_beatable_only: raise RuntimeError( 'Not all progression items reachable. Something went terribly wrong here.' ) else: break # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it for num, sphere in reversed(list(enumerate(collection_spheres))): to_delete = [] for location in sphere: # we remove the item at location and check if game is still beatable logging.getLogger('').debug( 'Checking if %s is required to beat the game.', location.item.name) old_item = location.item location.item = None state.remove(old_item) if world.can_beat_game(state_cache[num]): to_delete.append(location) else: # still required, got to keep it around location.item = old_item # cull entries in spheres for spoiler walkthrough at end for location in to_delete: sphere.remove(location) # we are now down to just the required progress items in collection_spheres. Unfortunately # the previous pruning stage could potentially have made certain items dependant on others # in the same or later sphere (because the location had 2 ways to access but the item originally # used to access it was deemed not required.) So we need to do one final sphere collection pass # to build up the correct spheres required_locations = [ item for sphere in collection_spheres for item in sphere ] state = CollectionState(world) collection_spheres = [] while required_locations: state.sweep_for_events(key_only=True) sphere = list(filter(state.can_reach, required_locations)) for location in sphere: required_locations.remove(location) state.collect(location.item, True, location) collection_spheres.append(sphere) logging.getLogger('').debug( 'Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(required_locations)) if not sphere: raise RuntimeError( 'Not all required items reachable. Something went terribly wrong here.' ) # store the required locations for statistical analysis old_world.required_locations = [ location.name for sphere in collection_spheres for location in sphere ] def flist_to_iter(node): while node: value, node = node yield value def get_path(state, region): reversed_path_as_flist = state.path.get(region, (region, None)) string_path_flat = reversed( list(map(str, flist_to_iter(reversed_path_as_flist)))) # Now we combine the flat string list into (region, exit) pairs pathsiter = iter(string_path_flat) pathpairs = zip_longest(pathsiter, pathsiter) return list(pathpairs) old_world.spoiler.paths = { location.name: get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere } # we can finally output our playthrough old_world.spoiler.playthrough = OrderedDict([ (str(i + 1), {str(location): str(location.item) for location in sphere}) for i, sphere in enumerate(collection_spheres) ])
def balance_multiworld_progression(world): state = CollectionState(world) checked_locations = [] unchecked_locations = world.get_locations().copy() random.shuffle(unchecked_locations) reachable_locations_count = {} for player in range(1, world.players + 1): reachable_locations_count[player] = 0 def get_sphere_locations(sphere_state, locations): sphere_state.sweep_for_events(key_only=True, locations=locations) return [loc for loc in locations if sphere_state.can_reach(loc)] while True: sphere_locations = get_sphere_locations(state, unchecked_locations) for location in sphere_locations: unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 if checked_locations: threshold = max(reachable_locations_count.values()) - 20 balancing_players = [ player for player, reachables in reachable_locations_count.items() if reachables < threshold ] if balancing_players: balancing_state = state.copy() balancing_unchecked_locations = unchecked_locations.copy() balancing_reachables = reachable_locations_count.copy() balancing_sphere = sphere_locations.copy() candidate_items = [] while True: for location in balancing_sphere: if location.event and ( world.keyshuffle[location.item.player] or not location.item.smallkey) and ( world.bigkeyshuffle[location.item.player] or not location.item.bigkey): balancing_state.collect(location.item, True, location) if location.item.player in balancing_players and not location.locked: candidate_items.append(location) balancing_sphere = get_sphere_locations( balancing_state, balancing_unchecked_locations) for location in balancing_sphere: balancing_unchecked_locations.remove(location) balancing_reachables[location.player] += 1 if world.has_beaten_game(balancing_state) or all([ reachables >= threshold for reachables in balancing_reachables.values() ]): break elif not balancing_sphere: raise RuntimeError( 'Not all required items reachable. Something went terribly wrong here.' ) unlocked_locations = [ l for l in unchecked_locations if l not in balancing_unchecked_locations ] items_to_replace = [] for player in balancing_players: locations_to_test = [ l for l in unlocked_locations if l.player == player ] # only replace items that end up in another player's world items_to_test = [ l for l in candidate_items if l.item.player == player and l.player != player ] while items_to_test: testing = items_to_test.pop() reducing_state = state.copy() for location in [ *[ l for l in items_to_replace if l.item.player == player ], *items_to_test ]: reducing_state.collect(location.item, True, location) reducing_state.sweep_for_events( locations=locations_to_test) if world.has_beaten_game(balancing_state): if not world.has_beaten_game(reducing_state): items_to_replace.append(testing) else: reduced_sphere = get_sphere_locations( reducing_state, locations_to_test) if reachable_locations_count[player] + len( reduced_sphere) < threshold: items_to_replace.append(testing) replaced_items = False replacement_locations = [ l for l in checked_locations if not l.event and not l.locked ] while replacement_locations and items_to_replace: new_location = replacement_locations.pop() old_location = items_to_replace.pop() while not new_location.can_fill( state, old_location.item, False) or (new_location.item and not old_location.can_fill( state, new_location.item, False)): replacement_locations.insert(0, new_location) new_location = replacement_locations.pop() new_location.item, old_location.item = old_location.item, new_location.item new_location.event, old_location.event = True, False state.collect(new_location.item, True, new_location) replaced_items = True if replaced_items: for location in get_sphere_locations( state, [ l for l in unlocked_locations if l.player in balancing_players ]): unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 sphere_locations.append(location) for location in sphere_locations: if location.event and ( world.keyshuffle[location.item.player] or not location.item.smallkey) and ( world.bigkeyshuffle[location.item.player] or not location.item.bigkey): state.collect(location.item, True, location) checked_locations.extend(sphere_locations) if world.has_beaten_game(state): break elif not sphere_locations: raise RuntimeError( 'Not all required items reachable. Something went terribly wrong here.' )
def create_playthrough(world): # create a copy as we will modify it old_world = world world = copy_world(world) # get locations containing progress items prog_locations = [ location for location in world.get_filled_locations() if location.item.advancement ] state_cache = [None] collection_spheres = [] state = CollectionState(world) sphere_candidates = list(prog_locations) logging.debug('Building up collection spheres.') while sphere_candidates: state.sweep_for_events(key_only=True) sphere = set() # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres for location in sphere_candidates: if state.can_reach(location): sphere.add(location) for location in sphere: sphere_candidates.remove(location) state.collect(location.item, True, location) collection_spheres.append(sphere) state_cache.append(state.copy()) logging.debug( 'Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(prog_locations)) if not sphere: logging.debug('The following items could not be reached: %s', [ '%s (Player %d) at %s (Player %d)' % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates ]) if any([ world.accessibility[location.item.player] != 'none' for location in sphere_candidates ]): raise RuntimeError( f'Not all progression items reachable ({sphere_candidates}). ' f'Something went terribly wrong here.') else: old_world.spoiler.unreachables = sphere_candidates.copy() break # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it for num, sphere in reversed(tuple(enumerate(collection_spheres))): to_delete = set() for location in sphere: # we remove the item at location and check if game is still beatable logging.getLogger('').debug( 'Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) old_item = location.item location.item = None if world.can_beat_game(state_cache[num]): to_delete.add(location) else: # still required, got to keep it around location.item = old_item # cull entries in spheres for spoiler walkthrough at end sphere -= to_delete # second phase, sphere 0 for item in (i for i in world.precollected_items if i.advancement): logging.getLogger('').debug( 'Checking if %s (Player %d) is required to beat the game.', item.name, item.player) world.precollected_items.remove(item) world.state.remove(item) if not world.can_beat_game(): world.push_precollected(item) # we are now down to just the required progress items in collection_spheres. Unfortunately # the previous pruning stage could potentially have made certain items dependant on others # in the same or later sphere (because the location had 2 ways to access but the item originally # used to access it was deemed not required.) So we need to do one final sphere collection pass # to build up the correct spheres required_locations = { item for sphere in collection_spheres for item in sphere } state = CollectionState(world) collection_spheres = [] while required_locations: state.sweep_for_events(key_only=True) sphere = set(filter(state.can_reach, required_locations)) for location in sphere: required_locations.remove(location) state.collect(location.item, True, location) collection_spheres.append(sphere) logging.getLogger('').debug( 'Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(required_locations)) if not sphere: raise RuntimeError( 'Not all required items reachable. Something went terribly wrong here.' ) def flist_to_iter(node): while node: value, node = node yield value def get_path(state, region): reversed_path_as_flist = state.path.get(region, (region, None)) string_path_flat = reversed( list(map(str, flist_to_iter(reversed_path_as_flist)))) # Now we combine the flat string list into (region, exit) pairs pathsiter = iter(string_path_flat) pathpairs = zip_longest(pathsiter, pathsiter) return list(pathpairs) old_world.spoiler.paths = dict() for player in range(1, world.players + 1): old_world.spoiler.paths.update({ str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player }) for path in dict(old_world.spoiler.paths).values(): if any(exit == 'Pyramid Fairy' for (_, exit) in path): if world.mode[player] != 'inverted': old_world.spoiler.paths[str( world.get_region('Big Bomb Shop', player))] = get_path( state, world.get_region('Big Bomb Shop', player)) else: old_world.spoiler.paths[str( world.get_region('Inverted Big Bomb Shop', player))] = get_path( state, world.get_region( 'Inverted Big Bomb Shop', player)) # we can finally output our playthrough old_world.spoiler.playthrough = { "0": sorted([ str(item) for item in world.precollected_items if item.advancement ]) } for i, sphere in enumerate(collection_spheres): old_world.spoiler.playthrough[str(i + 1)] = { str(location): str(location.item) for location in sorted(sphere) }
def balance_multiworld_progression(world): state = CollectionState(world) checked_locations = [] unchecked_locations = world.get_locations().copy() random.shuffle(unchecked_locations) reachable_locations_count = {} for player in range(1, world.players + 1): reachable_locations_count[player] = 0 def get_sphere_locations(sphere_state, locations): if not world.keysanity: sphere_state.sweep_for_events(key_only=True, locations=locations) return [loc for loc in locations if sphere_state.can_reach(loc)] while True: sphere_locations = get_sphere_locations(state, unchecked_locations) for location in sphere_locations: unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 if checked_locations: average_reachable_locations = sum( reachable_locations_count.values()) / world.players threshold = ((average_reachable_locations + max(reachable_locations_count.values())) / 2) * 0.8 #todo: probably needs some tweaking balancing_players = [ player for player, reachables in reachable_locations_count.items() if reachables < threshold ] if balancing_players: balancing_state = state.copy() balancing_unchecked_locations = unchecked_locations.copy() balancing_reachables = reachable_locations_count.copy() balancing_sphere = sphere_locations.copy() candidate_items = [] while True: for location in balancing_sphere: if location.event: balancing_state.collect(location.item, True, location) if location.item.player in balancing_players: candidate_items.append(location) balancing_sphere = get_sphere_locations( balancing_state, balancing_unchecked_locations) for location in balancing_sphere: balancing_unchecked_locations.remove(location) balancing_reachables[location.player] += 1 if world.has_beaten_game(balancing_state) or all([ reachables >= threshold for reachables in balancing_reachables.values() ]): break unlocked_locations = [ l for l in unchecked_locations if l not in balancing_unchecked_locations ] items_to_replace = [] for player in balancing_players: locations_to_test = [ l for l in unlocked_locations if l.player == player ] items_to_test = [ l for l in candidate_items if l.item.player == player and l.player != player ] while items_to_test: testing = items_to_test.pop() reducing_state = state.copy() for location in [ *[ l for l in items_to_replace if l.item.player == player ], *items_to_test ]: reducing_state.collect(location.item, True, location) reducing_state.sweep_for_events( locations=locations_to_test) if testing.locked: continue if world.has_beaten_game(balancing_state): if not world.has_beaten_game(reducing_state): items_to_replace.append(testing) else: reduced_sphere = get_sphere_locations( reducing_state, locations_to_test) if reachable_locations_count[player] + len( reduced_sphere) < threshold: items_to_replace.append(testing) replaced_items = False locations_for_replacing = [ l for l in checked_locations if not l.event and not l.locked ] while locations_for_replacing and items_to_replace: new_location = locations_for_replacing.pop() old_location = items_to_replace.pop() new_location.item, old_location.item = old_location.item, new_location.item new_location.event = True old_location.event = False state.collect(new_location.item, True, new_location) replaced_items = True if replaced_items: for location in get_sphere_locations( state, [ l for l in unlocked_locations if l.player in balancing_players ]): unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 sphere_locations.append(location) for location in sphere_locations: if location.event: state.collect(location.item, True, location) checked_locations.extend(sphere_locations) if world.has_beaten_game(state): break
def balance_multiworld_progression(world): balanceable_players = { player for player in range(1, world.players + 1) if world.progression_balancing[player] } if not balanceable_players: logging.info('Skipping multiworld progression balancing.') else: logging.info( f'Balancing multiworld progression for {len(balanceable_players)} Players.' ) state = CollectionState(world) checked_locations = [] unchecked_locations = world.get_locations().copy() world.random.shuffle(unchecked_locations) reachable_locations_count = {player: 0 for player in world.player_ids} def get_sphere_locations(sphere_state, locations): sphere_state.sweep_for_events(key_only=True, locations=locations) return [loc for loc in locations if sphere_state.can_reach(loc)] while True: sphere_locations = get_sphere_locations(state, unchecked_locations) for location in sphere_locations: unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 if checked_locations: threshold = max(reachable_locations_count.values()) - 20 balancing_players = [ player for player, reachables in reachable_locations_count.items() if reachables < threshold and player in balanceable_players ] if balancing_players: balancing_state = state.copy() balancing_unchecked_locations = unchecked_locations.copy() balancing_reachables = reachable_locations_count.copy() balancing_sphere = sphere_locations.copy() candidate_items = collections.defaultdict(list) while True: for location in balancing_sphere: if location.event: balancing_state.collect( location.item, True, location) player = location.item.player # only replace items that end up in another player's world if not location.locked and player in balancing_players and location.player != player: candidate_items[player].append(location) balancing_sphere = get_sphere_locations( balancing_state, balancing_unchecked_locations) for location in balancing_sphere: balancing_unchecked_locations.remove(location) balancing_reachables[location.player] += 1 if world.has_beaten_game(balancing_state) or all( reachables >= threshold for reachables in balancing_reachables.values()): break elif not balancing_sphere: raise RuntimeError( 'Not all required items reachable. Something went terribly wrong here.' ) unlocked_locations = collections.defaultdict(list) for l in unchecked_locations: if l not in balancing_unchecked_locations: unlocked_locations[l.player].append(l) items_to_replace = [] for player in balancing_players: locations_to_test = unlocked_locations[player] items_to_test = candidate_items[player] while items_to_test: testing = items_to_test.pop() reducing_state = state.copy() for location in itertools.chain( (l for l in items_to_replace if l.item.player == player), items_to_test): reducing_state.collect(location.item, True, location) reducing_state.sweep_for_events( locations=locations_to_test) if world.has_beaten_game(balancing_state): if not world.has_beaten_game(reducing_state): items_to_replace.append(testing) else: reduced_sphere = get_sphere_locations( reducing_state, locations_to_test) if reachable_locations_count[player] + len( reduced_sphere) < threshold: items_to_replace.append(testing) replaced_items = False replacement_locations = [ l for l in checked_locations if not l.event and not l.locked ] while replacement_locations and items_to_replace: new_location = replacement_locations.pop() old_location = items_to_replace.pop() while not new_location.can_fill( state, old_location.item, False) or ( new_location.item and not old_location.can_fill( state, new_location.item, False)): replacement_locations.insert(0, new_location) new_location = replacement_locations.pop() swap_location_item(old_location, new_location) logging.debug( f"Progression balancing moved {new_location.item} to {new_location}, " f"displacing {old_location.item} into {old_location}" ) state.collect(new_location.item, True, new_location) replaced_items = True if replaced_items: unlocked = [ fresh for player in balancing_players for fresh in unlocked_locations[player] ] for location in get_sphere_locations(state, unlocked): unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 sphere_locations.append(location) for location in sphere_locations: if location.event: state.collect(location.item, True, location) checked_locations.extend(sphere_locations) if world.has_beaten_game(state): break elif not sphere_locations: raise RuntimeError( 'Not all required items reachable. Something went terribly wrong here.' )