def flood_items(world: MultiWorld) -> None: # get items to distribute world.random.shuffle(world.itempool) itempool = world.itempool progress_done = False # sweep once to pick up preplaced items world.state.sweep_for_events() # fill world from top of itempool while we can while not progress_done: location_list = world.get_unfilled_locations() world.random.shuffle(location_list) spot_to_fill = None for location in location_list: if location.can_fill(world.state, itempool[0]): spot_to_fill = location break if spot_to_fill: item = itempool.pop(0) world.push_item(spot_to_fill, item, True) continue # ran out of spots, check if we need to step in and correct things if len(world.get_reachable_locations()) == len(world.get_locations()): progress_done = True continue # need to place a progress item instead of an already placed item, find candidate item_to_place = None candidate_item_to_place = None for item in itempool: if item.advancement: candidate_item_to_place = item if world.unlocks_new_location(item): item_to_place = item break # we might be in a situation where all new locations require multiple items to reach. # If that is the case, just place any advancement item we've found and continue trying if item_to_place is None: if candidate_item_to_place is not None: item_to_place = candidate_item_to_place else: raise FillError('No more progress items left to place.') # find item to replace with progress item location_list = world.get_reachable_locations() world.random.shuffle(location_list) for location in location_list: if location.item is not None and not location.item.advancement: # safe to replace replace_item = location.item replace_item.location = None itempool.append(replace_item) world.push_item(location, item_to_place, True) itempool.remove(item_to_place) break
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location], itempool: typing.List[Item], single_player_placement: bool = False, lock: bool = False) -> None: unplaced_items: typing.List[Item] = [] placements: typing.List[Location] = [] swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter() reachable_items: typing.Dict[int, typing.Deque[Item]] = {} for item in itempool: reachable_items.setdefault(item.player, deque()).append(item) while any(reachable_items.values()) and locations: # grab one item per player items_to_place = [ items.pop() for items in reachable_items.values() if items ] for item in items_to_place: itempool.remove(item) maximum_exploration_state = sweep_from_pool(base_state, itempool + unplaced_items) has_beaten_game = world.has_beaten_game(maximum_exploration_state) for item_to_place in items_to_place: spot_to_fill: typing.Optional[Location] = None if world.accessibility[item_to_place.player] == 'minimal': perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) \ if single_player_placement else not has_beaten_game else: perform_access_check = True for i, location in enumerate(locations): if (not single_player_placement or location.player == item_to_place.player) \ and location.can_fill(maximum_exploration_state, item_to_place, perform_access_check): # poping by index is faster than removing by content, spot_to_fill = locations.pop(i) # skipping a scan for the element break else: # we filled all reachable spots. # try swapping this item with previously placed items for (i, location) in enumerate(placements): placed_item = location.item # Unplaceable items can sometimes be swapped infinitely. Limit the # number of times we will swap an individual item to prevent this swap_count = swapped_items[placed_item.player, placed_item.name] if swap_count > 1: continue location.item = None placed_item.location = None swap_state = sweep_from_pool(base_state) if (not single_player_placement or location.player == item_to_place.player) \ and location.can_fill(swap_state, item_to_place, perform_access_check): # Verify that placing this item won't reduce available locations prev_state = swap_state.copy() prev_state.collect(placed_item) prev_loc_count = len( world.get_reachable_locations(prev_state)) swap_state.collect(item_to_place, True) new_loc_count = len( world.get_reachable_locations(swap_state)) if new_loc_count >= prev_loc_count: # Add this item to the existing placement, and # add the old item to the back of the queue spot_to_fill = placements.pop(i) swap_count += 1 swapped_items[placed_item.player, placed_item.name] = swap_count reachable_items[placed_item.player].appendleft( placed_item) itempool.append(placed_item) break # Item can't be placed here, restore original item location.item = placed_item placed_item.location = location if spot_to_fill is None: # Can't place this item, move on to the next unplaced_items.append(item_to_place) continue world.push_item(spot_to_fill, item_to_place, False) spot_to_fill.locked = lock placements.append(spot_to_fill) spot_to_fill.event = item_to_place.advancement if len(unplaced_items) > 0 and len(locations) > 0: # There are leftover unplaceable items and locations that won't accept them if world.can_beat_game(): logging.warning( f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})' ) else: raise FillError( f'No more spots to place {unplaced_items}, locations {locations} are invalid. ' f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}' ) itempool.extend(unplaced_items)