def fill_restrictive(worlds, base_state_list, locations, itempool): # loop until there are no items or locations while itempool and locations: # get and item and remove it from the itempool item_to_place = itempool.pop() # generate the max states that include every remaining item # this will allow us to place this item in a reachable location maximum_exploration_state_list = CollectionState.get_states_with_items( base_state_list, itempool) # perform_access_check checks location reachability perform_access_check = True if worlds[0].check_beatable_only: # if any world can not longer be beatable with the remaining items # then we must check for reachability no matter what. # This way the reachability test is monotonic. If we were to later # stop checking, then we could place an item needed in one world # in an unreachable place in another world perform_access_check = not CollectionState.can_beat_game( maximum_exploration_state_list) # find a location that the item can be places. It must be a valid location # in the world we are placing it (possibly checking for reachability) spot_to_fill = None for location in locations: if location.can_fill( maximum_exploration_state_list[location.world.id], item_to_place, perform_access_check): spot_to_fill = location break # if we failed to find a suitable location, then stop placing items if spot_to_fill is None: # Maybe the game can be beaten anyway? if not CollectionState.can_beat_game( maximum_exploration_state_list): raise FillError( 'Game unbeatable: No more spots to place %s [World %d]' % (item_to_place, item_to_place.world.id)) if not worlds[0].check_beatable_only: logging.getLogger('').warning( 'Not all items placed. Game beatable anyway.') break # Place the item in the world and continue spot_to_fill.world.push_item(spot_to_fill, item_to_place) locations.remove(spot_to_fill)
def fill_restrictive(window, worlds, base_state_list, locations, itempool, count=-1): unplaced_items = [] # loop until there are no items or locations while itempool and locations: # if remaining count is 0, return. Negative means unbounded. if count == 0: break # get and item and remove it from the itempool item_to_place = itempool.pop() # generate the max states that include every remaining item # this will allow us to place this item in a reachable location maximum_exploration_state_list = CollectionState.get_states_with_items(base_state_list, itempool + unplaced_items) # perform_access_check checks location reachability perform_access_check = True if worlds[0].check_beatable_only: # if any world can not longer be beatable with the remaining items # then we must check for reachability no matter what. # This way the reachability test is monotonic. If we were to later # stop checking, then we could place an item needed in one world # in an unreachable place in another world perform_access_check = not CollectionState.can_beat_game(maximum_exploration_state_list) # find a location that the item can be places. It must be a valid location # in the world we are placing it (possibly checking for reachability) spot_to_fill = None for location in locations: if location.can_fill(maximum_exploration_state_list[location.world.id], item_to_place, perform_access_check): spot_to_fill = location break # if we failed to find a suitable location if spot_to_fill is None: # if we specify a count, then we only want to place a subset, so a miss might be ok if count > 0: # don't decrement count, we didn't place anything unplaced_items.append(item_to_place) continue else: # we expect all items to be placed raise FillError('Game unbeatable: No more spots to place %s [World %d]' % (item_to_place, item_to_place.world.id)) # Place the item in the world and continue spot_to_fill.world.push_item(spot_to_fill, item_to_place) locations.remove(spot_to_fill) window.fillcount += 1 window.update_progress(5 + ((window.fillcount / window.locationcount) * 30)) # decrement count count -= 1 # assert that the specified number of items were placed if count > 0: raise FillError('Could not place the specified number of item. %d remaining to be placed.' % count) # re-add unplaced items that were skipped itempool.extend(unplaced_items)
def create_playthrough(worlds): if worlds[0].check_beatable_only and not CollectionState.can_beat_game( [world.state for world in worlds]): raise RuntimeError('Uncopied is broken too.') # create a copy as we will modify it old_worlds = worlds worlds = [world.copy() for world in worlds] # if we only check for beatable, we can do this sanity check first before writing down spheres if worlds[0].check_beatable_only and not CollectionState.can_beat_game( [world.state for world in worlds]): raise RuntimeError( 'Cannot beat game. Something went terribly wrong here!') state_list = [CollectionState(world) for world in worlds] # Get all item locations in the worlds collection_spheres = [] item_locations = [ location for state in state_list for location in state.world.get_filled_locations() if location.item.advancement ] # in the first phase, we create the generous spheres. Collecting every item in a sphere will # mean that every item in the next sphere is collectable. Will contain every reachable item logging.getLogger('').debug('Building up collection spheres.') # will loop if there is more items opened up in the previous iteration. Always run once reachable_items_locations = True while reachable_items_locations: # get reachable new items locations reachable_items_locations = [ location for location in item_locations if location.name not in state_list[ location.world.id].collected_locations and state_list[location.world.id].can_reach(location) ] for location in reachable_items_locations: # Mark the location collected in the state world it exists in state_list[location.world.id].collected_locations.append( location.name) # Collect the item for the state world it is for state_list[location.item.world.id].collect(location.item) if reachable_items_locations: collection_spheres.append(reachable_items_locations) # 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. Effectively creates a min play 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 old_state_list = [state.copy() for state in state_list] location.item = None state_list[old_item.world.id].remove(old_item) CollectionState.remove_locations(state_list) if CollectionState.can_beat_game(state_list, False): to_delete.append(location) else: # still required, got to keep it around state_list = old_state_list location.item = old_item # cull entries in spheres for spoiler walkthrough at end for location in to_delete: sphere.remove(location) collection_spheres = [sphere for sphere in collection_spheres if sphere] # we can finally output our playthrough for world in old_worlds: world.spoiler.playthrough = OrderedDict([ (str(i + 1), {location: location.item for location in sphere}) for i, sphere in enumerate(collection_spheres) ])