def items_for_ammo( ammo: Ammo, state: AmmoState, included_ammo_for_item: Dict[int, int], previous_pickup_for_item: Dict[int, Ammo], maximum_ammo: Dict[int, int], ) -> List[List[int]]: """ Helper function for add_ammo. :param maximum_ammo: :param ammo: :param state: :param included_ammo_for_item: :param previous_pickup_for_item: :return: An array that lists how many of each ammo each instance of the expansions should give """ ammo_per_pickup: List[List[int]] = [[] for _ in range(state.pickup_count)] # TODO: add test for case of 0 pickups if state.pickup_count == 0: return ammo_per_pickup for item in ammo.items: if item in previous_pickup_for_item: raise InvalidConfiguration( "Both {0.name} and {1.name} have non-zero pickup count for item {2}. This is unsupported." .format(ammo, previous_pickup_for_item[item], item)) previous_pickup_for_item[item] = ammo if item not in included_ammo_for_item: raise InvalidConfiguration( "Invalid configuration: ammo {0.name} was configured for {1.pickup_count}" "expansions, but main pickup was removed".format(ammo, state)) if maximum_ammo[item] < included_ammo_for_item[item]: raise InvalidConfiguration( "Ammo {0.name} was configured for a total of {1}, but major items gave {2}" .format(ammo, maximum_ammo[item], included_ammo_for_item[item])) adjusted_count = maximum_ammo[item] - included_ammo_for_item[item] count_per_expansion = adjusted_count // state.pickup_count expansions_with_extra_count = adjusted_count - count_per_expansion * state.pickup_count for i, pickup_ammo in enumerate(ammo_per_pickup): pickup_ammo.append(count_per_expansion) if i < expansions_with_extra_count: pickup_ammo[-1] += 1 return ammo_per_pickup
def _validate_item_pool_size(item_pool: List[PickupEntry], game: GameDescription, configuration: EchoesConfiguration) -> None: min_starting_items = configuration.major_items_configuration.minimum_random_starting_items if len(item_pool) > game.world_list.num_pickup_nodes + min_starting_items: raise InvalidConfiguration( "Item pool has {} items, which is more than {} (game) + {} (minimum starting items)".format( len(item_pool), game.world_list.num_pickup_nodes, min_starting_items))
def _distribute_remaining_items(rng: Random, filler_results: Dict[int, FillerPlayerResult], ) -> Dict[int, GamePatches]: unassigned_pickup_nodes = [] all_remaining_pickups = [] assignments: Dict[int, PickupAssignment] = {} for player, filler_result in filler_results.items(): for pickup_node in filter_unassigned_pickup_nodes(filler_result.game.world_list.all_nodes, filler_result.patches.pickup_assignment): unassigned_pickup_nodes.append((player, pickup_node)) all_remaining_pickups.extend(zip([player] * len(filler_result.unassigned_pickups), filler_result.unassigned_pickups)) assignments[player] = {} rng.shuffle(unassigned_pickup_nodes) rng.shuffle(all_remaining_pickups) if len(all_remaining_pickups) > len(unassigned_pickup_nodes): raise InvalidConfiguration( "Received {} remaining pickups, but there's only {} unassigned locations.".format( len(all_remaining_pickups), len(unassigned_pickup_nodes) )) for (node_player, node), (pickup_player, pickup) in zip(unassigned_pickup_nodes, all_remaining_pickups): assignments[node_player][node.pickup_index] = PickupTarget(pickup, pickup_player) return { index: filler_results[index].patches.assign_pickup_assignment(assignment) for index, assignment in assignments.items() }
def add_ammo( resource_database: ResourceDatabase, ammo_configuration: AmmoConfiguration, included_ammo_for_item: Dict[int, int], ) -> Iterator[PickupEntry]: """ Creates the necessary pickups for the given ammo_configuration. :param resource_database: :param ammo_configuration: :param included_ammo_for_item: How much of each item was provided based on the major items. :return: """ previous_pickup_for_item = {} for ammo, state in ammo_configuration.items_state.items(): if state.pickup_count == 0: continue if state.variance != 0: raise InvalidConfiguration( "Variance was configured for {0.name}, but it is currently NYI" .format(ammo)) ammo_per_pickup = items_for_ammo(ammo, state, included_ammo_for_item, previous_pickup_for_item, ammo_configuration.maximum_ammo) for ammo_quantity in ammo_per_pickup: yield create_ammo_expansion(ammo, ammo_quantity, state.requires_major_item, resource_database)
def _validate_item_pool_size(item_pool: List[PickupEntry], game: GameDescription) -> None: num_pickup_nodes = len(list(filter_pickup_nodes( game.world_list.all_nodes))) if len(item_pool) > num_pickup_nodes: raise InvalidConfiguration( "Item pool has {0} items, but there's only {1} pickups spots in the game" .format(len(item_pool), num_pickup_nodes))
def add_major_items( resource_database: ResourceDatabase, major_items_configuration: MajorItemsConfiguration, ammo_configuration: AmmoConfiguration, ) -> PoolResults: """ :param resource_database: :param major_items_configuration: :param ammo_configuration: :return: """ item_pool: List[PickupEntry] = [] new_assignment: Dict[PickupIndex, PickupEntry] = {} initial_resources: CurrentResources = {} for item, state in major_items_configuration.items_state.items(): if len(item.ammo_index) != len(state.included_ammo): raise InvalidConfiguration( "Item {0.name} uses {0.ammo_index} as ammo, but there's only {1} values in included_ammo" .format(item, len(state.included_ammo))) ammo, locked_ammo = _find_ammo_for(item.ammo_index, ammo_configuration) if state.include_copy_in_original_location: if item.original_index is None: raise InvalidConfiguration( "Item {0.name} does not exist in the original game, cannot use state {1}" .format(item, state), ) new_assignment[item.original_index] = create_major_item( item, state, True, resource_database, ammo, locked_ammo) for _ in range(state.num_shuffled_pickups): item_pool.append( create_major_item(item, state, True, resource_database, ammo, locked_ammo)) for _ in range(state.num_included_in_starting_items): add_resource_gain_to_current_resources( create_major_item(item, state, False, resource_database, ammo, locked_ammo).all_resources, initial_resources) return PoolResults(item_pool, new_assignment, initial_resources)
def _assign_remaining_items( rng: Random, world_list: WorldList, pickup_assignment: PickupAssignment, remaining_items: List[PickupEntry], randomization_mode: RandomizationMode, ) -> PickupAssignment: """ :param rng: :param world_list: :param pickup_assignment: :param remaining_items: :return: """ unassigned_pickup_nodes = list( filter_unassigned_pickup_nodes(world_list.all_nodes, pickup_assignment)) num_etm = len(unassigned_pickup_nodes) - len(remaining_items) if num_etm < 0: raise InvalidConfiguration( "Received {} remaining items, but there's only {} unassigned pickups" .format(len(remaining_items), len(unassigned_pickup_nodes))) # Shuffle the items to add and the spots to choose from rng.shuffle(remaining_items) rng.shuffle(unassigned_pickup_nodes) assignment = {} if randomization_mode is RandomizationMode.MAJOR_MINOR_SPLIT: remaining_majors = [ item for item in remaining_items if not item.is_expansion ] + ([None] * num_etm) unassigned_major_locations = [ pickup_node for pickup_node in unassigned_pickup_nodes if pickup_node.major_location ] for pickup_node, item in zip(unassigned_major_locations, remaining_majors): if item is not None: assignment[pickup_node.pickup_index] = item remaining_items.remove(item) unassigned_pickup_nodes.remove(pickup_node) assignment.update({ pickup_node.pickup_index: item for pickup_node, item in zip(unassigned_pickup_nodes, remaining_items) }) return assignment
def add_sky_temple_key_distribution_logic( resource_database: ResourceDatabase, mode: LayoutSkyTempleKeyMode, ) -> PoolResults: """ Adds the given Sky Temple Keys to the item pool :param resource_database: :param mode: :return: """ item_pool: List[PickupEntry] = [] new_assignment: Dict[PickupIndex, PickupEntry] = {} initial_resources: CurrentResources = {} if mode == LayoutSkyTempleKeyMode.ALL_BOSSES or mode == LayoutSkyTempleKeyMode.ALL_GUARDIANS: locations_to_place = _GUARDIAN_INDICES[:] if mode == LayoutSkyTempleKeyMode.ALL_BOSSES: locations_to_place += _SUB_GUARDIAN_INDICES for key_number, location in enumerate(locations_to_place): new_assignment[location] = create_sky_temple_key( key_number, resource_database) first_automatic_key = len(locations_to_place) else: keys_to_place = mode.value if not isinstance(keys_to_place, int): raise InvalidConfiguration( "Unknown Sky Temple Key mode: {}".format(mode)) for key_number in range(keys_to_place): item_pool.append( create_sky_temple_key(key_number, resource_database)) first_automatic_key = keys_to_place for automatic_key_number in range(first_automatic_key, 9): add_resource_gain_to_current_resources( create_sky_temple_key(automatic_key_number, resource_database).all_resources, initial_resources) return PoolResults(item_pool, new_assignment, initial_resources)
def _distribute_remaining_items( rng: Random, filler_results: FillerResults, ) -> FillerResults: unassigned_pickup_nodes = [] all_remaining_pickups = [] assignments: Dict[int, list[PickupTargetAssociation]] = {} for player, filler_result in filler_results.player_results.items(): for pickup_node in filter_unassigned_pickup_nodes( filler_result.game.world_list.iterate_nodes(), filler_result.patches.pickup_assignment): unassigned_pickup_nodes.append((player, pickup_node)) all_remaining_pickups.extend( zip([player] * len(filler_result.unassigned_pickups), filler_result.unassigned_pickups)) assignments[player] = [] rng.shuffle(unassigned_pickup_nodes) rng.shuffle(all_remaining_pickups) if len(all_remaining_pickups) > len(unassigned_pickup_nodes): raise InvalidConfiguration( "Received {} remaining pickups, but there's only {} unassigned locations." .format(len(all_remaining_pickups), len(unassigned_pickup_nodes))) for (node_player, node), (pickup_player, pickup) in zip(unassigned_pickup_nodes, all_remaining_pickups): assignments[node_player].append( (node.pickup_index, PickupTarget(pickup, pickup_player))) return dataclasses.replace( filler_results, player_results={ player: dataclasses.replace( result, patches=result.patches.assign_new_pickups(assignments[player])) for player, result in filler_results.player_results.items() })
def create_major_item( item: MajorItem, state: MajorItemState, include_percentage: bool, resource_database: ResourceDatabase, ammo: Optional[Ammo], ammo_requires_major_item: bool, ) -> PickupEntry: """ Creates a Pickup for the given MajorItem :param include_percentage: :param state: :param item: :param resource_database: :param ammo: :param ammo_requires_major_item: :return: """ def _create_resources( base_resource: Optional[int], temporary_ammo: bool = False) -> Tuple[ResourceQuantity, ...]: resources = [] if base_resource is not None: resources.append((resource_database.get_item(base_resource), 1)) for ammo_index, ammo_count in zip( ammo.temporaries if temporary_ammo else item.ammo_index, state.included_ammo): resources.append( (resource_database.get_item(ammo_index), ammo_count)) if include_percentage: resources.append((resource_database.item_percentage, 1)) return tuple(resources) if item.progression: if ammo_requires_major_item and ammo.unlocked_by != item.progression[ 0] and ammo.unlocked_by is not None: if len(item.progression) != 1: raise InvalidConfiguration(( "Item {item.name} uses ammo {ammo.name} that is locked behind {ammo.unlocked_by}," "but it also has progression. This is unsupported." ).format( ammo=ammo, item=item, )) name = resource_database.get_item(item.progression[0]).long_name conditional_resources = (ConditionalResources( name=name, item=None, resources=_create_resources(item.progression[0], True)), ConditionalResources( name=name, item=resource_database.get_item( ammo.unlocked_by), resources=_create_resources( item.progression[0]))) else: conditional_resources = tuple( ConditionalResources( name=resource_database.get_item( item.progression[i]).long_name, item=resource_database.get_item(item.progression[i - 1] ) if i > 0 else None, resources=_create_resources(progression)) for i, progression in enumerate(item.progression)) else: conditional_resources = (ConditionalResources( name=item.name, item=None, resources=_create_resources(None)), ) if item.converts_indices: assert len(item.converts_indices) == len(item.ammo_index) convert_resources = tuple( ResourceConversion( source=resource_database.get_item(source), target=resource_database.get_item(target), ) for source, target in zip(item.converts_indices, item.ammo_index)) else: convert_resources = tuple() return PickupEntry( name=item.name, resources=conditional_resources, model_index=item.model_index, item_category=item.item_category, broad_category=item.broad_category, probability_offset=item.probability_offset, probability_multiplier=item.probability_multiplier, convert_resources=convert_resources, )