def test_collected_pickup_indices(state_game_data, empty_patches): # Setup db = state_game_data.resource_database starting = state_game_data.world_list.resolve_teleporter_connection( empty_patches.game.starting_location) pickup_nodes = [ node for node in empty_patches.game.world_list.all_nodes if isinstance(node, PickupNode) ] context = NodeContext( empty_patches, ResourceCollection(), empty_patches.game.resource_database, empty_patches.game.world_list, ) resources = ResourceCollection.from_dict( db, { db.item[0]: 5, pickup_nodes[0].resource(context): 1, pickup_nodes[1].resource(context): 1 }) s = state.State(resources, (), 99, starting, empty_patches, None, state_game_data) # Run indices = list(s.collected_pickup_indices) # Assert assert indices == [ pickup_nodes[0].pickup_index, pickup_nodes[1].pickup_index ]
def test_add_pickup_to_state(state_game_data, empty_patches, generic_item_category): # Starting State db = state_game_data.resource_database starting_node = state_game_data.world_list.resolve_teleporter_connection( empty_patches.game.starting_location) s = state.State(ResourceCollection(), (), 99, starting_node, empty_patches, None, state_game_data) resource_a = db.item[0] resource_b = db.item[1] p = PickupEntry("B", 2, generic_item_category, generic_item_category, progression=( (resource_a, 1), (resource_b, 1), )) # Run state.add_pickup_to_state(s, p) state.add_pickup_to_state(s, p) # Assert assert s.resources == ResourceCollection.from_dict(db, { resource_a: 1, resource_b: 1, })
def find_locations_that_gives_items( target_items: List[ItemResourceInfo], all_patches: Dict[int, GamePatches], player: int, ) -> dict[ItemResourceInfo, list[tuple[int, PickupLocation]]]: result: dict[ItemResourceInfo, list[tuple[int, PickupLocation]]] = { item: [] for item in target_items } for other_player, patches in all_patches.items(): for pickup_index, target in patches.pickup_assignment.items(): if target.player != player: continue # TODO: iterate over all tiers of progression db = patches.game.resource_database resources = ResourceCollection.from_resource_gain( db, target.pickup.resource_gain(ResourceCollection())) for resource, quantity in resources.as_resource_gain(): if quantity > 0 and resource in result: result[resource].append( (other_player, PickupLocation(patches.configuration.game, pickup_index))) return result
def test_add_resources_into_another(blank_resource_db, a, b, result): a = wrap(blank_resource_db, a) b = wrap(blank_resource_db, b) result = wrap(blank_resource_db, result) ac = ResourceCollection.from_dict(blank_resource_db, a) bc = ResourceCollection.from_dict(blank_resource_db, b) ac.add_resource_gain(bc.as_resource_gain()) assert dict(ac.as_resource_gain()) == result
def calculate_pool_results(layout_configuration: BaseConfiguration, resource_database: ResourceDatabase, base_patches: GamePatches = None, rng: Random = None, rng_required: bool = False ) -> PoolResults: """ Creates a PoolResults with all starting items and pickups in fixed locations, as well as a list of pickups we should shuffle. :param layout_configuration: :param resource_database: :return: """ base_results = PoolResults([], {}, ResourceCollection.with_database(resource_database)) # Adding major items to the pool _extend_pool_results(base_results, add_major_items(resource_database, layout_configuration.major_items_configuration, layout_configuration.ammo_configuration)) # Adding ammo to the pool base_results.pickups.extend(add_ammo(resource_database, layout_configuration.ammo_configuration)) try: layout_configuration.game.generator.item_pool_creator( base_results, layout_configuration, resource_database, base_patches, rng, ) except MissingRng as e: if rng_required: raise e return base_results
def add_artifacts( resource_database: ResourceDatabase, mode: LayoutArtifactMode, artifact_minimum_progression: int, ) -> PoolResults: """ :param resource_database: :param mode :param artifact_minimum_progression :return: """ item_pool: List[PickupEntry] = [] initial_resources = ResourceCollection.with_database(resource_database) artifacts_to_place = mode.value for i in range(artifacts_to_place): item_pool.append( pickup_creator.create_artifact(i, artifact_minimum_progression, resource_database)) first_automatic_artifact = artifacts_to_place for automatic_artifact in range(first_automatic_artifact, 12): initial_resources.add_resource_gain( pickup_creator.create_artifact(automatic_artifact, artifact_minimum_progression, resource_database).all_resources, ) return PoolResults(item_pool, {}, initial_resources)
def assign_pickup_to_starting_items(self, pickup: PickupEntry) -> "State": pickup_resources = ResourceCollection.from_resource_gain( self.resource_database, pickup.resource_gain(self.resources, force_lock=True)) # Make sure there's no item percentage on starting items if self.resource_database.item_percentage is not None: pickup_resources.remove_resource( self.resource_database.item_percentage) new_resources = self.resources.duplicate() new_resources.add_resource_gain(pickup_resources.as_resource_gain()) new_patches = self.patches.assign_extra_initial_items( pickup_resources.as_resource_gain()) tank_delta = _energy_tank_difference(new_resources, self.resources, self.resource_database) return State( new_resources, self.collected_resource_nodes, self.energy + tank_delta * self.game_data.energy_per_tank, self.node, new_patches, self, self.game_data, )
def test_assign_extra_initial_items_merge(empty_patches, initial, new_items, expected): db = empty_patches.game.resource_database initial = wrap(db, initial) new_items = wrap(db, new_items) expected = wrap(db, expected) # Setup initial_patches = dataclasses.replace(empty_patches, starting_items=ResourceCollection.from_dict(db, initial)) # Run new_patches = initial_patches.assign_extra_initial_items( ResourceCollection.from_dict(db, new_items).as_resource_gain(), ) # Assert assert new_patches.starting_items == ResourceCollection.from_dict(db, expected)
def find_area_errors(game: GameDescription, area: Area) -> Iterator[str]: nodes_with_paths_in = set() for node in area.nodes: nodes_with_paths_in.update(area.connections[node].keys()) for error in find_node_errors(game, node): yield f"{area.name} - {error}" if node in area.connections.get(node, {}): yield f"{area.name} - Node '{node.name}' has a connection to itself" if area.default_node is not None and area.node_with_name(area.default_node) is None: yield f"{area.name} has default node {area.default_node}, but no node with that name exists" # elif area.default_node is not None: # nodes_with_paths_in.add(area.node_with_name(area.default_node)) for node in area.nodes: if isinstance(node, (TeleporterNode, DockNode)) or area.connections[node]: continue # FIXME: cannot implement this for PickupNodes because their resource gain depends on GamePatches if isinstance(node, EventNode): # if this node would satisfy the victory condition, it does not need outgoing connections current = ResourceCollection.from_resource_gain(game.resource_database, node.resource_gain_on_collect(None)) if game.victory_condition.satisfied(current, 0, game.resource_database): continue if node in nodes_with_paths_in: yield f"{area.name} - '{node.name}': Node has paths in, but no connections out."
def get_damage_reduction(self, resource: SimpleResourceInfo, current_resources: ResourceCollection): cached_result = current_resources.get_damage_reduction_cache(resource) if cached_result is not None: return cached_result multiplier = self.base_damage_reduction(self, current_resources) for reduction in self.damage_reductions.get(resource, []): if reduction.inventory_item is None or current_resources[ reduction.inventory_item] > 0: multiplier *= reduction.damage_multiplier current_resources.add_damage_reduction_cache(resource, multiplier) return multiplier
def test_connections_from_dock_blast_shield(empty_patches): # Setup trivial = Requirement.trivial() req_1 = ResourceRequirement.simple( SimpleResourceInfo(0, "Ev1", "Ev1", ResourceType.EVENT)) req_2 = ResourceRequirement.simple( SimpleResourceInfo(1, "Ev2", "Ev2", ResourceType.EVENT)) dock_type = DockType("Type", "Type", frozendict()) weak_1 = DockWeakness(0, "Weak 1", frozendict(), req_1, None) weak_2 = DockWeakness(1, "Weak 2", frozendict(), trivial, DockLock(DockLockType.FRONT_BLAST_BACK_BLAST, req_2)) node_1_identifier = NodeIdentifier.create("W", "Area 1", "Node 1") node_2_identifier = NodeIdentifier.create("W", "Area 2", "Node 2") node_1 = DockNode(node_1_identifier, 0, False, None, "", ("default", ), {}, dock_type, node_2_identifier, weak_1, None, None) node_1_lock = DockLockNode.create_from_dock(node_1, 1) node_2 = DockNode(node_2_identifier, 2, False, None, "", ("default", ), {}, dock_type, node_1_identifier, weak_2, None, None) node_2_lock = DockLockNode.create_from_dock(node_2, 3) area_1 = Area("Area 1", None, True, [node_1, node_1_lock], {}, {}) area_2 = Area("Area 2", None, True, [node_2, node_2_lock], {}, {}) world = World("W", [area_1, area_2], {}) world_list = WorldList([world]) world_list.ensure_has_node_cache() game_mock = MagicMock() game_mock.world_list = world_list patches = dataclasses.replace(empty_patches, game=game_mock) context = NodeContext( patches=patches, current_resources=ResourceCollection(), database=patches.game.resource_database, node_provider=world_list, ) # Run result_1 = list(node_1.connections_from(context)) result_2 = list(node_2.connections_from(context)) # Assert simple = ResourceRequirement.simple assert result_1 == [ (node_2, RequirementAnd( [req_1, simple(NodeResourceInfo.from_node(node_2, context))])), (node_1_lock, RequirementAnd([trivial, req_2])), ] assert result_2 == [ (node_1, simple(NodeResourceInfo.from_node(node_2, context))), (node_2_lock, req_2), ]
def test_logbook_node_requirements_to_leave(logbook_node, empty_patches): # Setup has_translator, translator, node = logbook_node db = empty_patches.game.resource_database def ctx(resources): return NodeContext(empty_patches, resources, db, empty_patches.game.world_list) # Run to_leave = node.requirement_to_leave(ctx({})) # Assert rc2 = ResourceCollection.from_resource_gain(db, []) rc3 = ResourceCollection.from_resource_gain(db, [(translator, 1)]) assert to_leave.satisfied(rc2, 99, None) != has_translator assert to_leave.satisfied(rc3, 99, None)
def test_remove_resource_missing(echoes_resource_database): m = echoes_resource_database.get_item("Missile") beam = echoes_resource_database.get_item("Light") col = ResourceCollection.from_dict(echoes_resource_database, { beam: 1, }) col.remove_resource(m) assert dict(col.as_resource_gain()) == {beam: 1}
def test_assign_extra_initial_items_to_empty(empty_patches, items): db = empty_patches.game.resource_database items = wrap(db, items) # Run new_patches = empty_patches.assign_extra_initial_items(items) # Assert assert new_patches.starting_items == ResourceCollection.from_resource_gain(db, items)
def find_invalid_strongly_connected_components(game: GameDescription) -> Iterator[str]: import networkx graph = networkx.DiGraph() for node in game.world_list.iterate_nodes(): if isinstance(node, DockLockNode): continue graph.add_node(node) context = NodeContext( patches=GamePatches.create_from_game(game, 0, None), current_resources=ResourceCollection.with_database(game.resource_database), database=game.resource_database, node_provider=game.world_list, ) for node in game.world_list.iterate_nodes(): if node not in graph: continue for other, req in game.world_list.potential_nodes_from(node, context): if other not in graph: continue if req != Requirement.impossible(): graph.add_edge(node, other) starting_node = game.world_list.resolve_teleporter_connection(game.starting_location) for strong_comp in networkx.strongly_connected_components(graph): components: set[Node] = strong_comp # The starting location determines the default component if starting_node in components: continue if any(node.extra.get("different_strongly_connected_component", False) for node in components): continue if len(components) == 1: node = next(iter(components)) # If the component is a single node which is the default node of it's area, allow it area = game.world_list.nodes_to_area(node) if area.default_node == node.name: continue # We accept nodes that have no paths out or in. if not graph.in_edges(node) and not graph.edges(node): continue names = sorted( game.world_list.node_name(node, with_world=True) for node in strong_comp ) yield "Unknown strongly connected component detected containing {} nodes:\n{}".format(len(names), names)
def _calculate_starting_inventory(self, resources: ResourceCollection): result = {} for resource, quantity in resources.as_resource_gain(): try: result[_get_item_id_for_item(resource)] = quantity except KeyError: print( f"Skipping {resource} for starting inventory: no item id") continue return result
def test_requirement_damage(damage, items, requirement, echoes_resource_database): req = data_reader.read_requirement(requirement, echoes_resource_database) collection = ResourceCollection.from_dict( echoes_resource_database, {echoes_resource_database.get_item(item): 1 for item in items}) assert req.damage(collection, echoes_resource_database) == damage
def test_create_starting_popup_empty(default_echoes_configuration, echoes_resource_database): starting_items = ResourceCollection.with_database(echoes_resource_database) # Run result = patch_data_factory._create_starting_popup( default_echoes_configuration, echoes_resource_database, starting_items) # Assert assert result == []
def patch_requirements(self, static_resources: ResourceCollection, damage_multiplier: float, database: ResourceDatabase) -> Requirement: if static_resources.is_resource_set(self.resource): if self.satisfied(static_resources, 0, database): return Requirement.trivial() else: return Requirement.impossible() else: return self.multiply_amount(damage_multiplier)
def test_convert_resource_gain_to_current_resources(blank_resource_db, resource_gain, expected): # Setup db = blank_resource_db resource_gain = wrap(db, resource_gain) expected = wrap(db, expected) # Run result = ResourceCollection.from_resource_gain(db, resource_gain) # Assert assert dict(result.as_resource_gain()) == expected
def test_state_with_pickup(state_game_data, empty_patches, generic_item_category): # Setup db = state_game_data.resource_database starting = state.State(ResourceCollection(), (), 99, None, empty_patches, None, state_game_data) resource_a = db.item[0] p = PickupEntry("A", 2, generic_item_category, generic_item_category, progression=((resource_a, 1), )) # Run final = state.state_with_pickup(starting, p) # Assert assert final.previous_state is starting assert final.resources == ResourceCollection.from_dict(db, {resource_a: 1})
def test_logbook_node_resource_gain_on_collect(logbook_node, empty_patches): # Setup db = empty_patches.game.resource_database node = logbook_node[-1] context = NodeContext(empty_patches, ResourceCollection(), db, MagicMock()) # Run gain = node.resource_gain_on_collect(context) # Assert assert dict(gain) == {node.resource(context): 1}
def pickups_to_solve_list(pickup_pool: list[PickupEntry], requirement_list: RequirementList, state: State): pickups = [] db = state.resource_database resources = state.resources.duplicate() pickups_for_this = list(pickup_pool) # Check pickups that give less items in total first # This means we test for expansions before the major items, in case both give the same resource # Useful to get Dark Beam Ammo Expansion instead of Dark Beam. pickups_for_this.sort(key=lambda p: sum( 1 for _ in p.resource_gain(resources, force_lock=True))) for individual in sorted(requirement_list.values()): if individual.satisfied(resources, state.energy, state.resource_database): continue # Create another copy of the list so we can remove elements while iterating for pickup in list(pickups_for_this): new_resources = ResourceCollection.from_resource_gain( db, pickup.resource_gain(resources, force_lock=True)) pickup_progression = ResourceCollection.from_resource_gain( db, pickup.progression) if new_resources[individual.resource] + pickup_progression[ individual.resource] > 0: pickups.append(pickup) pickups_for_this.remove(pickup) resources.add_resource_gain(new_resources.as_resource_gain()) if individual.satisfied(resources, state.energy, state.resource_database): break if not individual.satisfied(resources, state.energy, state.resource_database): return None return pickups
def _resources_to_give_for_pickup( self, pickup: PickupEntry, inventory: Inventory, ) -> tuple[str, ResourceCollection]: inventory_resources = ResourceCollection.with_database( self.game.resource_database) inventory_resources.add_resource_gain([ (item, inv_item.capacity) for item, inv_item in inventory.items() ]) conditional = pickup.conditional_for_resources(inventory_resources) if conditional.name is not None: item_name = conditional.name else: item_name = pickup.name resources_to_give = ResourceCollection.with_database( self.game.resource_database) if pickup.respects_lock and not pickup.unlocks_resource and ( pickup.resource_lock is not None and inventory_resources[pickup.resource_lock.locked_by] == 0): pickup_resources = list( pickup.resource_lock.convert_gain(conditional.resources)) item_name = f"Locked {item_name}" else: pickup_resources = conditional.resources inventory_resources.add_resource_gain(pickup_resources) resources_to_give.add_resource_gain(pickup_resources) resources_to_give.add_resource_gain( pickup.conversion_resource_gain(inventory_resources)) # Ignore item% for received items if self.game.resource_database.item_percentage is not None: resources_to_give.remove_resource( self.game.resource_database.item_percentage) return item_name, resources_to_give
def test_sky_temple_key_distribution_logic_all_bosses_valid( echoes_resource_database): # Run results = sky_temple_keys.add_sky_temple_key_distribution_logic( echoes_resource_database, LayoutSkyTempleKeyMode.ALL_BOSSES) # Assert assert results.pickups == [] assert results.initial_resources == ResourceCollection.from_dict( echoes_resource_database, {}) assert list( results.assignment.keys() ) == sky_temple_keys._GUARDIAN_INDICES + sky_temple_keys._SUB_GUARDIAN_INDICES
def add_energy_cells(resource_database: ResourceDatabase, ) -> PoolResults: """ :param resource_database: :return: """ item_pool: List[PickupEntry] = [] for i in range(9): item_pool.append( pickup_creator.create_energy_cell(i, resource_database)) return PoolResults(item_pool, {}, ResourceCollection.with_database(resource_database))
def additional_starting_items(layout_configuration: BaseConfiguration, resource_database: ResourceDatabase, starting_items: ResourceCollection) -> List[str]: initial_items = calculate_pool_results(layout_configuration, resource_database).initial_resources return [ add_quantity_to_resource(resource_user_friendly_name(item), quantity) for item, quantity in sorted( starting_items.as_resource_gain(), key=lambda a: resource_user_friendly_name(a[0])) if 0 < quantity != initial_items[item] ]
def test_sky_temple_key_distribution_logic_all_guardians_valid( echoes_resource_database): # Run results = sky_temple_keys.add_sky_temple_key_distribution_logic( echoes_resource_database, LayoutSkyTempleKeyMode.ALL_GUARDIANS) # Assert assert results.pickups == [] assert results.initial_resources == ResourceCollection.from_dict( echoes_resource_database, { echoes_resource_database.get_item(f'TempleKey{i}'): 1 for i in range(4, 10) }) assert list(results.assignment.keys()) == sky_temple_keys._GUARDIAN_INDICES
def _add_minimal_logic_initial_resources( self, resources: ResourceCollection, game: GameDescription, major_items: MajorItemsConfiguration, ) -> None: resource_database = game.resource_database if game.minimal_logic is None: raise ValueError( f"Minimal logic enabled, but {game.game} doesn't have support for it." ) item_db = default_database.item_database_for_game(game.game) items_to_skip = set() for it in game.minimal_logic.items_to_exclude: if it.reason is None or major_items.items_state[ item_db.major_items[it.reason]].num_shuffled_pickups != 0: items_to_skip.add(it.name) custom_item_count = game.minimal_logic.custom_item_amount events_to_skip = { it.name for it in game.minimal_logic.events_to_exclude } resources.add_resource_gain([(event, 1) for event in resource_database.event if event.short_name not in events_to_skip ]) resources.add_resource_gain([ (item, custom_item_count.get(item.short_name, 1)) for item in resource_database.item if item.short_name not in items_to_skip ])
def add_dark_temple_keys(resource_database: ResourceDatabase, ) -> PoolResults: """ :param resource_database: :return: """ item_pool: List[PickupEntry] = [] for temple_index in range(3): for i in range(3): item_pool.append( pickup_creator.create_dark_temple_key(i, temple_index, resource_database)) return PoolResults(item_pool, {}, ResourceCollection.with_database(resource_database))