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 distribute_planned(world: MultiWorld) -> None: def warn(warning: str, force: typing.Union[bool, str]) -> None: if force in [ True, 'fail', 'failure', 'none', False, 'warn', 'warning' ]: logging.warning(f'{warning}') else: logging.debug(f'{warning}') def failed(warning: str, force: typing.Union[bool, str]) -> None: if force in [True, 'fail', 'failure']: raise Exception(warning) else: warn(warning, force) # TODO: remove. Preferably by implementing key drop from worlds.alttp.Regions import key_drop_data world_name_lookup = world.world_name_lookup block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str] plando_blocks: typing.List[typing.Dict[str, typing.Any]] = [] player_ids = set(world.player_ids) for player in player_ids: for block in world.plando_items[player]: block['player'] = player if 'force' not in block: block['force'] = 'silent' if 'from_pool' not in block: block['from_pool'] = True if 'world' not in block: block['world'] = False items: block_value = [] if "items" in block: items = block["items"] if 'count' not in block: block['count'] = False elif "item" in block: items = block["item"] if 'count' not in block: block['count'] = 1 else: failed( "You must specify at least one item to place items with plando.", block['force']) continue if isinstance(items, dict): item_list: typing.List[str] = [] for key, value in items.items(): if value is True: value = world.itempool.count( world.worlds[player].create_item(key)) item_list += [key] * value items = item_list if isinstance(items, str): items = [items] block['items'] = items locations: block_value = [] if 'location' in block: locations = block[ 'location'] # just allow 'location' to keep old yamls compatible elif 'locations' in block: locations = block['locations'] if isinstance(locations, str): locations = [locations] if isinstance(locations, dict): location_list = [] for key, value in locations.items(): location_list += [key] * value locations = location_list block['locations'] = locations if not block['count']: block['count'] = ( min(len(block['items']), len(block['locations'])) if len(block['locations']) > 0 else len(block['items'])) if isinstance(block['count'], int): block['count'] = {'min': block['count'], 'max': block['count']} if 'min' not in block['count']: block['count']['min'] = 0 if 'max' not in block['count']: block['count']['max'] = ( min(len(block['items']), len(block['locations'])) if len(block['locations']) > 0 else len(block['items'])) if block['count']['max'] > len(block['items']): count = block['count'] failed(f"Plando count {count} greater than items specified", block['force']) block['count'] = len(block['items']) if block['count']['max'] > len(block['locations']) > 0: count = block['count'] failed( f"Plando count {count} greater than locations specified", block['force']) block['count'] = len(block['locations']) block['count']['target'] = world.random.randint( block['count']['min'], block['count']['max']) if block['count']['target'] > 0: plando_blocks.append(block) # shuffle, but then sort blocks by number of locations minus number of items, # so less-flexible blocks get priority world.random.shuffle(plando_blocks) plando_blocks.sort(key=lambda block: (len(block['locations']) - block[ 'count']['target'] if len(block['locations']) > 0 else len( world.get_unfilled_locations(player)) - block['count']['target'])) for placement in plando_blocks: player = placement['player'] try: target_world = placement['world'] locations = placement['locations'] items = placement['items'] maxcount = placement['count']['target'] from_pool = placement['from_pool'] if target_world is False or world.players == 1: # target own world worlds: typing.Set[int] = {player} elif target_world is True: # target any worlds besides own worlds = set(world.player_ids) - {player} elif target_world is None: # target all worlds worlds = set(world.player_ids) elif type(target_world) == list: # list of target worlds worlds = set() for listed_world in target_world: if listed_world not in world_name_lookup: failed( f"Cannot place item to {target_world}'s world as that world does not exist.", placement['force']) continue worlds.add(world_name_lookup[listed_world]) elif type(target_world) == int: # target world by slot number if target_world not in range(1, world.players + 1): failed( f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})", placement['force']) continue worlds = {target_world} else: # target world by slot name if target_world not in world_name_lookup: failed( f"Cannot place item to {target_world}'s world as that world does not exist.", placement['force']) continue worlds = {world_name_lookup[target_world]} candidates = list( location for location in world.get_unfilled_locations_for_players( locations, worlds)) world.random.shuffle(candidates) world.random.shuffle(items) count = 0 err: typing.List[str] = [] successful_pairs: typing.List[typing.Tuple[Item, Location]] = [] for item_name in items: item = world.worlds[player].create_item(item_name) for location in reversed(candidates): if location in key_drop_data: warn( f"Can't place '{item_name}' at '{placement.location}', as key drop shuffle locations are not supported yet." ) continue if not location.item: if location.item_rule(item): if location.can_fill(world.state, item, False): successful_pairs.append((item, location)) candidates.remove(location) count = count + 1 break else: err.append( f"Can't place item at {location} due to fill condition not met." ) else: err.append( f"{item_name} not allowed at {location}.") else: err.append( f"Cannot place {item_name} into already filled location {location}." ) if count == maxcount: break if count < placement['count']['min']: m = placement['count']['min'] failed( f"Plando block failed to place {m - count} of {m} item(s) for {world.player_name[player]}, error(s): {' '.join(err)}", placement['force']) for (item, location) in successful_pairs: world.push_item(location, item, collect=False) location.event = True # flag location to be checked during fill location.locked = True logging.debug(f"Plando placed {item} at {location}") if from_pool: try: world.itempool.remove(item) except ValueError: warn( f"Could not remove {item} from pool for {world.player_name[player]} as it's already missing from it.", placement['force']) except Exception as e: raise Exception( f"Error running plando for player {player} ({world.player_name[player]})" ) from e
def distribute_items_restrictive(world: MultiWorld) -> None: fill_locations = sorted(world.get_unfilled_locations()) world.random.shuffle(fill_locations) # get items to distribute itempool = sorted(world.itempool) world.random.shuffle(itempool) progitempool: typing.List[Item] = [] nonexcludeditempool: typing.List[Item] = [] localrestitempool: typing.Dict[int, typing.List[Item]] = { player: [] for player in range(1, world.players + 1) } nonlocalrestitempool: typing.List[Item] = [] restitempool: typing.List[Item] = [] for item in itempool: if item.advancement: progitempool.append(item) elif item.useful: # this only gets nonprogression items which should not appear in excluded locations nonexcludeditempool.append(item) elif item.name in world.local_items[item.player].value: localrestitempool[item.player].append(item) elif item.name in world.non_local_items[item.player].value: nonlocalrestitempool.append(item) else: restitempool.append(item) call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations) locations: typing.Dict[LocationProgressType, typing.List[Location]] = { loc_type: [] for loc_type in LocationProgressType } for loc in fill_locations: locations[loc.progress_type].append(loc) prioritylocations = locations[LocationProgressType.PRIORITY] defaultlocations = locations[LocationProgressType.DEFAULT] excludedlocations = locations[LocationProgressType.EXCLUDED] fill_restrictive(world, world.state, prioritylocations, progitempool, lock=True) if prioritylocations: defaultlocations = prioritylocations + defaultlocations if progitempool: fill_restrictive(world, world.state, defaultlocations, progitempool) if progitempool: raise FillError( f'Not enough locations for progress items. There are {len(progitempool)} more items than locations' ) if nonexcludeditempool: world.random.shuffle(defaultlocations) # needs logical fill to not conflict with local items fill_restrictive(world, world.state, defaultlocations, nonexcludeditempool) if nonexcludeditempool: raise FillError( f'Not enough locations for non-excluded items. There are {len(nonexcludeditempool)} more items than locations' ) defaultlocations = defaultlocations + excludedlocations world.random.shuffle(defaultlocations) if any(localrestitempool.values() ): # we need to make sure some fills are limited to certain worlds local_locations: typing.Dict[int, typing.List[Location]] = { player: [] for player in world.player_ids } for location in defaultlocations: local_locations[location.player].append(location) for player_locations in local_locations.values(): world.random.shuffle(player_locations) for player, items in localrestitempool.items( ): # items already shuffled player_local_locations = local_locations[player] for item_to_place in items: if not player_local_locations: logging.warning( f"Ran out of local locations for player {player}, " f"cannot place {item_to_place}.") break spot_to_fill = player_local_locations.pop() world.push_item(spot_to_fill, item_to_place, False) defaultlocations.remove(spot_to_fill) for item_to_place in nonlocalrestitempool: for i, location in enumerate(defaultlocations): if location.player != item_to_place.player: world.push_item(defaultlocations.pop(i), item_to_place, False) break else: logging.warning( f"Could not place non_local_item {item_to_place} among {defaultlocations}, tossing." ) world.random.shuffle(defaultlocations) restitempool, defaultlocations = fast_fill(world, restitempool, defaultlocations) unplaced = progitempool + restitempool unfilled = defaultlocations if unplaced or unfilled: logging.warning( f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}' ) items_counter = Counter(location.item.player for location in world.get_locations() if location.item) locations_counter = Counter(location.player for location in world.get_locations()) items_counter.update(item.player for item in unplaced) locations_counter.update(location.player for location in unfilled) print_data = {"items": items_counter, "locations": locations_counter} logging.info(f'Per-Player counts: {print_data})')