def test_calculate_dangerous_resources(danger_a, danger_b, expected_result): set_a: Requirement = MagicMock() set_b: Requirement = MagicMock() set_a.as_set.return_value.dangerous_resources = danger_a set_b.as_set.return_value.dangerous_resources = danger_b n1 = MagicMock() n1.node_index = 0 n2 = MagicMock() n2.node_index = 1 n3 = MagicMock() n3.node_index = 2 n4 = MagicMock() n4.node_index = 3 area_a = Area("area_a", 0, True, [n1, n2], {n1: {n2: set_a}, n2: {}}, {}) area_b = Area("area_b", 0, True, [n3, n4], {n3: {}, n4: {n3: set_b}}, {}) world = World("W", [area_a, area_b], {}) wl = WorldList([world]) # Run result = game_description._calculate_dangerous_resources_in_areas(wl, None) # Assert assert set(result) == set(expected_result)
def add_node(self, area: Area, node: Node): if area.node_with_name(node.name) is not None: raise ValueError(f"A node named {node.name} already exists.") area.nodes.append(node) area.connections[node] = {} area.clear_dock_cache() self.game.world_list.invalidate_node_cache()
def remove_node(self, area: Area, node: Node): area.nodes.remove(node) area.connections.pop(node, None) for connection in area.connections.values(): connection.pop(node, None) area.clear_dock_cache() self.game.world_list.invalidate_node_cache()
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 edit_connections(self, area: Area, from_node: Node, target_node: Node, requirement: Optional[Requirement]): current_connections = area.connections[from_node] area.connections[from_node][target_node] = requirement if area.connections[from_node][target_node] is None: del area.connections[from_node][target_node] area.connections[from_node] = { node: current_connections[node] for node in area.nodes if node in current_connections }
def test_connections_from_dock_blast_shield(empty_patches): # Setup trivial = Requirement.trivial() req_1 = ResourceRequirement( SimpleResourceInfo("Ev1", "Ev1", ResourceType.EVENT), 1, False) req_2 = ResourceRequirement( SimpleResourceInfo("Ev2", "Ev2", ResourceType.EVENT), 1, False) dock_type = DockType("Type", "Type", frozendict()) weak_1 = DockWeakness("Weak 1", frozendict(), req_1, None) weak_2 = DockWeakness("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, False, None, "", ("default", ), {}, dock_type, node_2_identifier, weak_1, None, None) node_1_lock = DockLockNode.create_from_dock(node_1) node_2 = DockNode(node_2_identifier, False, None, "", ("default", ), {}, dock_type, node_1_identifier, weak_2, None, None) node_2_lock = DockLockNode.create_from_dock(node_2) 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]) context = NodeContext( patches=empty_patches, current_resources={}, database=None, node_provider=world_list, ) # Run result_1 = list(node_1.connections_from(context)) result_2 = list(node_2.connections_from(context)) # Assert assert result_1 == [ (node_2, RequirementAnd([req_1, ResourceRequirement.simple(node_2_identifier)])), (node_1_lock, RequirementAnd([trivial, req_2])), ] assert result_2 == [ (node_1, ResourceRequirement.simple(node_2_identifier)), (node_2_lock, req_2), ]
def read_area(self, area_name: str, data: dict) -> Area: self.current_area_name = area_name nodes = [ self.read_node(node_name, item) for node_name, item in data["nodes"].items() ] nodes_by_name = {node.name: node for node in nodes} connections = {} for origin in nodes: origin_data = data["nodes"][origin.name] try: connections[origin] = {} except TypeError as e: print(origin.extra) raise KeyError(f"Area {area_name}, node {origin}: {e}") for target_name, target_requirement in origin_data[ "connections"].items(): try: the_set = read_requirement(target_requirement, self.resource_database) connections[origin][nodes_by_name[target_name]] = the_set except (MissingResource, KeyError) as e: raise type( e )(f"In area {area_name}, connection from {origin.name} to {target_name} got error: {e}" ) try: return Area(area_name, data["default_node"], data["valid_starting_location"], nodes, connections, data["extra"]) except KeyError as e: raise KeyError(f"Missing key `{e}` for area `{area_name}`")
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 replace_node(self, area: Area, old_node: Node, new_node: Node): def sub(n: Node): return new_node if n == old_node else n if old_node not in area.nodes: raise ValueError("Given {} does does not belong to {}{}".format( old_node.name, area.name, ", but the area contains a node with that name." if area.node_with_name(old_node.name) is not None else "." )) if old_node.name != new_node.name and area.node_with_name(new_node.name) is not None: raise ValueError(f"A node named {new_node.name} already exists.") if isinstance(old_node, DockNode): self.remove_node(area, old_node.lock_node) old_identifier = old_node.identifier self.replace_references_to_node_identifier( old_identifier, old_identifier.renamed(new_node.name), ) area.nodes[area.nodes.index(old_node)] = new_node new_connections = { sub(source_node): { sub(target_node): requirements for target_node, requirements in connection.items() } for source_node, connection in area.connections.items() } area.connections.clear() area.connections.update(new_connections) if area.default_node == old_node.name: object.__setattr__(area, "default_node", new_node.name) area.clear_dock_cache() if isinstance(new_node, DockNode): self.add_node(area, DockLockNode.create_from_dock(new_node, self.new_node_index())) self.game.world_list.invalidate_node_cache()
def read_area(self, area_name: str, data: dict) -> Area: self.current_area_name = area_name nodes = [ self.read_node(node_name, item) for node_name, item in data["nodes"].items() ] nodes_by_name = {node.name: node for node in nodes} connections = {} for origin in nodes: origin_data = data["nodes"][origin.name] try: connections[origin] = {} except TypeError as e: print(origin.extra) raise KeyError(f"Area {area_name}, node {origin}: {e}") for target_name, target_requirement in origin_data[ "connections"].items(): try: the_set = read_requirement(target_requirement, self.resource_database) connections[origin][nodes_by_name[target_name]] = the_set except (MissingResource, KeyError) as e: raise type( e )(f"In area {area_name}, connection from {origin.name} to {target_name} got error: {e}" ) for node in list(nodes): if isinstance(node, DockNode): lock_node = DockLockNode.create_from_dock( node, self.next_node_index) nodes.append(lock_node) connections[lock_node] = {} self.next_node_index += 1 for combo in event_pickup.find_nodes_to_combine(nodes, connections): combo_node = event_pickup.EventPickupNode.create_from( self.next_node_index, *combo) nodes.append(combo_node) for existing_connections in connections.values(): if combo[0] in existing_connections: existing_connections[combo_node] = copy.copy( existing_connections[combo[0]]) connections[combo_node] = copy.copy(connections[combo[1]]) self.next_node_index += 1 try: return Area(area_name, data["default_node"], data["valid_starting_location"], nodes, connections, data["extra"]) except KeyError as e: raise KeyError(f"Missing key `{e}` for area `{area_name}`")
def _create_world_list(asset_id: int, pickup_index: PickupIndex): nc = NodeIdentifier.create logbook_node = LogbookNode(nc("World", "Area", "Logbook A"), True, None, "", ("default", ), {}, asset_id, None, None, None, None) pickup_node = PickupNode(nc("World", "Area", "Pickup Node"), True, None, "", ("default", ), {}, pickup_index, True) world_list = WorldList([ World("World", [ Area("Area", 0, True, [logbook_node, pickup_node], {}, {}), Area("Other Area", 0, True, [ PickupNode(nc("World", "Other Area", f"Pickup {i}"), True, None, "", ("default", ), {}, PickupIndex(i), True) for i in range(pickup_index.index) ], {}, {}), ], {}), ]) return logbook_node, pickup_node, world_list
def test_basic_search_with_translator_gate(has_translator: bool, echoes_resource_database, echoes_game_patches): # Setup scan_visor = echoes_resource_database.get_item("DarkVisor") nc = functools.partial(NodeIdentifier.create, "Test World", "Test Area A") node_a = GenericNode(nc("Node A"), True, None, "", ("default",), {}) node_b = GenericNode(nc("Node B"), True, None, "", ("default",), {}) node_c = GenericNode(nc("Node C"), True, None, "", ("default",), {}) translator_node = ConfigurableNode(translator_identif := nc("Translator Gate"), True, None, "", ("default",), {}) world_list = WorldList([ World("Test World", [ Area("Test Area A", None, True, [node_a, node_b, node_c, translator_node], { node_a: { node_b: Requirement.trivial(), translator_node: Requirement.trivial(), }, node_b: { node_a: Requirement.trivial(), }, node_c: { translator_node: Requirement.trivial(), }, translator_node: { node_a: Requirement.trivial(), node_c: Requirement.trivial(), }, }, {} ) ], {}) ]) game = GameDescription(RandovaniaGame.METROID_PRIME_ECHOES, DockWeaknessDatabase([], {}, (None, None)), echoes_resource_database, ("default",), Requirement.impossible(), None, {}, None, world_list) patches = echoes_game_patches.assign_node_configuration({ translator_identif: ResourceRequirement(scan_visor, 1, False) }) initial_state = State({scan_visor: 1 if has_translator else 0}, (), 99, node_a, patches, None, StateGameData(echoes_resource_database, game.world_list, 100, 99)) # Run reach = reach_lib.reach_with_all_safe_resources(game, initial_state) # Assert if has_translator: assert set(reach.safe_nodes) == {node_a, node_b, translator_node, node_c} else: assert set(reach.safe_nodes) == {node_a, node_b}
def move_node_from_area_to_area(self, old_area: Area, new_area: Area, node: Node): assert node in old_area.nodes if new_area.node_with_name(node.name) is not None: raise ValueError(f"New area {new_area.name} already contains a node named {node.name}") old_world = self.game.world_list.world_with_area(old_area) new_world = self.game.world_list.world_with_area(new_area) self.remove_node(old_area, node) self.add_node(new_area, node) self.replace_references_to_node_identifier( NodeIdentifier.create(old_world.name, old_area.name, node.name), NodeIdentifier.create(new_world.name, new_area.name, node.name), )
def test_calculate_dangerous_resources(danger_a, danger_b, expected_result): set_a: Requirement = MagicMock() set_b: Requirement = MagicMock() set_a.as_set.return_value.dangerous_resources = danger_a set_b.as_set.return_value.dangerous_resources = danger_b n1: Node = "n1" n2: Node = "n2" area_a = Area( "area_a", 0, True, [n1, n2], { n1: { n2: set_a }, n2: {} }, {} ) area_b = Area( "area_b", 0, True, [n1, n2], { n1: {}, n2: { n1: set_b } }, {} ) # Run result = game_description._calculate_dangerous_resources_in_areas([area_a, area_b], None) # Assert assert set(result) == set(expected_result)
def _create_new_dock(self, location: NodeLocation, target_area: Area): current_area = self.current_area target_identifier = self.world_list.identifier_for_area(target_area) source_identifier = self.world_list.identifier_for_area(current_area) dock_weakness = self.game_description.dock_weakness_database.default_weakness source_name_base = integrity_check.base_dock_name_raw( dock_weakness[0], dock_weakness[1], target_identifier) target_name_base = integrity_check.base_dock_name_raw( dock_weakness[0], dock_weakness[1], source_identifier) source_count = len( integrity_check.docks_with_same_base_name(current_area, source_name_base)) if source_count != len( integrity_check.docks_with_same_base_name( target_area, target_name_base)): raise ValueError( f"Expected {target_area.name} to also have {source_count} " f"docks with name {target_name_base}") if source_count > 0: source_name = f"{source_name_base} ({source_count + 1})" target_name = f"{target_name_base} ({source_count + 1})" else: source_name = source_name_base target_name = target_name_base new_node_this_area_identifier = NodeIdentifier( self.world_list.identifier_for_area(current_area), source_name) new_node_other_area_identifier = NodeIdentifier( self.world_list.identifier_for_area(target_area), target_name) new_node_this_area = DockNode( identifier=new_node_this_area_identifier, node_index=self.editor.new_node_index(), heal=False, location=location, description="", layers=("default", ), extra={}, dock_type=dock_weakness[0], default_connection=new_node_other_area_identifier, default_dock_weakness=dock_weakness[1], override_default_open_requirement=None, override_default_lock_requirement=None, ) new_node_other_area = DockNode( identifier=new_node_other_area_identifier, node_index=self.editor.new_node_index(), heal=False, location=location, description="", layers=("default", ), extra={}, dock_type=dock_weakness[0], default_connection=new_node_this_area_identifier, default_dock_weakness=dock_weakness[1], override_default_open_requirement=None, override_default_lock_requirement=None, ) self.editor.add_node(current_area, new_node_this_area) self.editor.add_node(target_area, new_node_other_area) if source_count == 1: self.editor.rename_node( current_area, current_area.node_with_name(source_name_base), f"{source_name_base} (1)", ) self.editor.rename_node( target_area, target_area.node_with_name(target_name_base), f"{target_name_base} (1)", ) self.on_select_area(new_node_this_area)
for node in area.nodes: if set(node.layers).isdisjoint(active_layers): nodes.remove(node) connections.pop(node, None) for connection in connections.values(): connection.pop(node, None) if area.default_node == node.name: has_default_node = False areas.append( Area( name=area.name, default_node=area.default_node if has_default_node else None, valid_starting_location=area.valid_starting_location, nodes=nodes, connections=connections, extra=area.extra, )) worlds.append(dataclasses.replace(world, areas=areas)) return GameDescription( game=game.game, resource_database=game.resource_database, layers=game.layers, dock_weakness_database=game.dock_weakness_database, world_list=WorldList(worlds), victory_condition=game.victory_condition, starting_location=game.starting_location,