def test_gate_assignment_for_configuration_all_random(echoes_game_description, default_echoes_configuration): # Setup patches_factory = echoes_game_description.game.generator.base_patches_factory scan_visor = find_resource_info_with_long_name(echoes_game_description.resource_database.item, "Scan Visor") violet = find_resource_info_with_long_name(echoes_game_description.resource_database.item, "Violet Translator") emerald = find_resource_info_with_long_name(echoes_game_description.resource_database.item, "Emerald Translator") translator_configuration = default_echoes_configuration.translator_configuration configuration = dataclasses.replace( default_echoes_configuration, translator_configuration=translator_configuration.with_full_random(), ) requirements = [ RequirementAnd([ ResourceRequirement(scan_visor, 1, False), ResourceRequirement(emerald, 1, False), ]), RequirementAnd([ ResourceRequirement(scan_visor, 1, False), ResourceRequirement(violet, 1, False), ]) ] requirements = requirements * len(translator_configuration.translator_requirement) choices = [LayoutTranslatorRequirement.EMERALD, LayoutTranslatorRequirement.VIOLET] rng = MagicMock() rng.choice.side_effect = choices * len(translator_configuration.translator_requirement) # Run results = patches_factory.configurable_node_assignment( configuration, echoes_game_description, rng) # Assert assert list(results.values()) == requirements[:len(translator_configuration.translator_requirement)]
def test_requirement_as_set_2(): req = RequirementAnd([ Requirement.trivial(), _req("A"), ]) assert req.as_set(None) == RequirementSet([ RequirementList([_req("A")]), ])
def test_requirement_as_set_5(): req = RequirementAnd([ _req("A"), _req("B"), _req("C"), ]) assert req.as_set(None) == RequirementSet([ RequirementList([_req("A"), _req("B"), _req("C")]), ])
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 configurable_node_assignment(self, configuration: DreadConfiguration, game: GameDescription, rng: Random) -> NodeConfigurationAssignment: result = {} rsb = game.resource_database requirement_for_type = { "POWERBEAM": rsb.requirement_template["Shoot Beam"], "BOMB": rsb.requirement_template["Lay Bomb"], "MISSILE": rsb.requirement_template["Shoot Missile"], "SUPERMISSILE": rsb.requirement_template["Shoot Super Missile"], "POWERBOMB": rsb.requirement_template["Lay Power Bomb"], "SCREWATTACK": ResourceRequirement(rsb.get_item("Screw"), 1, False), "WEIGHT": Requirement.impossible(), "SPEEDBOOST": ResourceRequirement(rsb.get_item("Speed"), 1, False), } for node in game.world_list.all_nodes: if not isinstance(node, ConfigurableNode): continue result[game.world_list.identifier_for_node(node)] = RequirementAnd([ requirement_for_type[block_type] for block_type in node.extra["tile_types"] ]).simplify() return result
def read_requirement_and(data: Dict, resource_database: ResourceDatabase, ) -> RequirementAnd: return RequirementAnd([ read_requirement(item, resource_database) for item in data["data"] ])
def test_gate_assignment_for_configuration_all_emerald(echoes_game_description, default_echoes_configuration): # Setup patches_factory = echoes_game_description.game.generator.base_patches_factory scan_visor = find_resource_info_with_long_name(echoes_game_description.resource_database.item, "Scan Visor") emerald = find_resource_info_with_long_name(echoes_game_description.resource_database.item, "Emerald Translator") translator_configuration = default_echoes_configuration.translator_configuration configuration = dataclasses.replace( default_echoes_configuration, translator_configuration=dataclasses.replace( translator_configuration, translator_requirement={ key: LayoutTranslatorRequirement.EMERALD for key in translator_configuration.translator_requirement.keys() } ) ) rng = MagicMock() # Run results = patches_factory.configurable_node_assignment( configuration, echoes_game_description, rng) # Assert assert list(results.values()) == [ RequirementAnd([ ResourceRequirement(scan_visor, 1, False), ResourceRequirement(emerald, 1, False), ]) ] * len(translator_configuration.translator_requirement)
def requirement_to_leave( self, patches: GamePatches, current_resources: CurrentResources) -> Requirement: return RequirementAnd([ ResourceRequirement(patches.translator_gates[self.gate], 1, False), ResourceRequirement(self.scan_visor, 1, False), ])
def _lock_connection( self, context: NodeContext) -> typing.Optional[tuple[Node, Requirement]]: requirement = Requirement.trivial() forward_weakness = self.get_front_weakness(context) forward_lock = forward_weakness.lock if forward_lock is not None: requirement = self._get_lock_requirement(forward_weakness) back_weakness = self.get_back_weakness(context) back_lock = None if back_weakness is not None: back_lock = back_weakness.lock if back_lock is not None and back_lock.lock_type == DockLockType.FRONT_BLAST_BACK_BLAST: requirement = RequirementAnd( [requirement, back_lock.requirement]) if forward_lock is None and back_lock is None: return None lock_node = context.node_provider.node_by_identifier( self.get_lock_node_identifier(context)) return lock_node, requirement
def test_requirement_and_str(): req = RequirementAnd([ _req("B"), _req("A"), _req("C"), ]) assert str(req) == "(A ≥ 1 and B ≥ 1 and C ≥ 1)"
def _potential_nodes_from(self, node: Node) -> Iterator[Tuple[Node, Requirement, bool]]: extra_requirement = _extra_requirement_for_node(self._game, node) requirement_to_leave = node.requirement_to_leave(self._state.patches, self._state.resources) for target_node, requirement in self._game.world_list.potential_nodes_from(node, self.state.patches): if target_node is None: continue if requirement_to_leave != Requirement.trivial(): requirement = RequirementAnd([requirement, requirement_to_leave]) if extra_requirement is not None: requirement = RequirementAnd([requirement, extra_requirement]) satisfied = requirement.satisfied(self._state.resources, self._state.energy) yield target_node, requirement, satisfied
def test_simplify_requirement_set_static(): res_a, id_req_a = make_req_a() res_b, id_req_b = make_req_b() the_set = RequirementOr([ RequirementAnd([id_req_a]), RequirementAnd([id_req_b]), ]) simple_1 = the_set.patch_requirements({res_a: 0, res_b: 0}, 1) simple_2 = the_set.patch_requirements({res_a: 0, res_b: 1}, 1) simple_3 = the_set.patch_requirements({res_a: 1, res_b: 1}, 1) assert simple_1.as_set.alternatives == frozenset() assert simple_2.as_set.alternatives == frozenset([RequirementList([])]) assert simple_3.as_set.alternatives == frozenset([RequirementList([])])
def requirement_to_leave(self, context: NodeContext) -> Requirement: items = [] if self.scan_visor is not None: items.append(ResourceRequirement(self.scan_visor, 1, False)) if self.required_translator is not None: items.append( ResourceRequirement(self.required_translator, 1, False)) return RequirementAnd(items)
def requirement_to_leave( self, patches: GamePatches, current_resources: CurrentResources) -> Requirement: items = [ResourceRequirement(self.scan_visor, 1, False)] if self.required_translator is not None: items.append( ResourceRequirement(self.required_translator, 1, False)) return RequirementAnd(items)
def make_req(item_id: int): return RequirementAnd([ ResourceRequirement( ItemResourceInfo("Scan Visor", "Scan", 1, frozendict({"item_id": 9})), 1, False, ), ResourceRequirement( ItemResourceInfo("Other", "Other", 1, frozendict({"item_id": item_id})), 1, False, ), ])
def _potential_nodes_from( self, node: Node) -> Iterator[Tuple[Node, RequirementSet]]: extra_requirement = _extra_requirement_for_node( self._game, self.node_context(), node) requirement_to_leave = node.requirement_to_leave( self._state.node_context()) for target_node, requirement in self._game.world_list.potential_nodes_from( node, self.node_context()): if target_node is None: continue if requirement_to_leave != Requirement.trivial(): requirement = RequirementAnd( [requirement, requirement_to_leave]) if extra_requirement is not None: requirement = RequirementAnd([requirement, extra_requirement]) yield target_node, requirement.as_set( self._state.resource_database)
def test_requirement_as_set_1(): req = RequirementAnd([ _req("A"), RequirementOr([_req("B"), _req("C")]), RequirementOr([_req("D"), _req("E")]), ]) assert req.as_set == RequirementSet([ RequirementList([_req("A"), _req("B"), _req("D")]), RequirementList([_req("A"), _req("B"), _req("E")]), RequirementList([_req("A"), _req("C"), _req("D")]), RequirementList([_req("A"), _req("C"), _req("E")]), ])
def test_pretty_print_requirement_array_one_item( mock_print_requirement: MagicMock): mock_print_requirement.return_value = ["a", "b"] req = MagicMock() array = RequirementAnd([req]) # Run result = list(pretty_print.pretty_print_requirement_array(array, 3)) # Assert assert result == ["a", "b"] mock_print_requirement.assert_called_once_with(req, 3)
def test_pretty_print_requirement_array_combinable( mock_print_requirement: MagicMock, echoes_resource_database): mock_print_requirement.return_value = ["a", "b"] array = RequirementAnd([ ResourceRequirement(echoes_resource_database.item[0], 1, False), RequirementTemplate("Shoot Sunburst"), ]) # Run result = list(pretty_print.pretty_print_requirement_array(array, 3)) # Assert assert result == [(3, "Power Beam and Shoot Sunburst")] mock_print_requirement.assert_not_called()
def configurable_node_assignment( self, configuration: EchoesConfiguration, game: GameDescription, rng: Random) -> NodeConfigurationAssignment: """ :param configuration: :param game: :param rng: :return: """ all_choices = list(LayoutTranslatorRequirement) all_choices.remove(LayoutTranslatorRequirement.RANDOM) all_choices.remove(LayoutTranslatorRequirement.RANDOM_WITH_REMOVED) without_removed = copy.copy(all_choices) without_removed.remove(LayoutTranslatorRequirement.REMOVED) random_requirements = { LayoutTranslatorRequirement.RANDOM, LayoutTranslatorRequirement.RANDOM_WITH_REMOVED } result = {} scan_visor = search.find_resource_info_with_long_name( game.resource_database.item, "Scan Visor") scan_visor_req = ResourceRequirement(scan_visor, 1, False) for node in game.world_list.all_nodes: if not isinstance(node, ConfigurableNode): continue identifier = game.world_list.identifier_for_node(node) requirement = configuration.translator_configuration.translator_requirement[ identifier] if requirement in random_requirements: if rng is None: raise MissingRng("Translator") requirement = rng.choice( all_choices if requirement == LayoutTranslatorRequirement. RANDOM_WITH_REMOVED else without_removed) translator = game.resource_database.get_by_type_and_index( ResourceType.ITEM, requirement.item_name) result[identifier] = RequirementAnd([ scan_visor_req, ResourceRequirement(translator, 1, False), ]) return result
def test_build_no_changes(skip_qtbot, echoes_resource_database): # Setup def mk_req(name: str): return ResourceRequirement.with_data(echoes_resource_database, ResourceType.ITEM, name, 1, False) requirement = RequirementOr([ RequirementAnd([ mk_req("Dark"), mk_req("Light"), ]), mk_req("Power"), ]) # Run editor = connections_editor.ConnectionsEditor(None, echoes_resource_database, requirement) skip_qtbot.addWidget(editor) result = editor.final_requirement # Assert assert result == requirement
def test_build_no_changes(skip_qtbot, echoes_resource_database): # Setup def mk_req(index: int): return ResourceRequirement.with_data(echoes_resource_database, ResourceType.ITEM, index, 1, False) requirement = RequirementOr([ RequirementAnd([ mk_req(1), mk_req(2), ]), mk_req(0), ]) # Run editor = connections_editor.ConnectionsEditor(None, echoes_resource_database, requirement) skip_qtbot.addWidget(editor) result = editor.final_requirement # Assert assert result == requirement
def state_for_current_configuration(self) -> Optional[State]: state = self._initial_state.copy() if self._actions: state.node = self._actions[-1] for teleporter, combo in self._elevator_id_to_combo.items(): state.patches.elevator_connection[teleporter] = combo.currentData() for gate, item in self._translator_gate_to_combo.items(): scan_visor = self.game_description.resource_database.get_item( "Scan") requirement: Optional[ LayoutTranslatorRequirement] = item.currentData() if requirement is None: translator_req = Requirement.impossible() else: translator = self.game_description.resource_database.get_item( requirement.item_name) translator_req = ResourceRequirement(translator, 1, False) state.patches.configurable_nodes[gate] = RequirementAnd([ ResourceRequirement(scan_visor, 1, False), translator_req, ]) for pickup, quantity in self._collected_pickups.items(): for _ in range(quantity): add_pickup_to_state(state, pickup) for node in self._collected_nodes: add_resource_gain_to_current_resources( node.resource_gain_on_collect(state.node_context()), state.resources) return state
def calculate_reach(cls, logic: Logic, initial_state: State) -> "ResolverReach": checked_nodes: Dict[Node, int] = {} # Keys: nodes to check # Value: how much energy was available when visiting that node nodes_to_check: Dict[Node, int] = { initial_state.node: initial_state.energy } reach_nodes: Dict[Node, int] = {} requirements_by_node: Dict[Node, Set[RequirementList]] = defaultdict(set) path_to_node: Dict[Node, Tuple[Node, ...]] = {} path_to_node[initial_state.node] = tuple() while nodes_to_check: node = next(iter(nodes_to_check)) energy = nodes_to_check.pop(node) if node.heal: energy = initial_state.maximum_energy checked_nodes[node] = energy if node != initial_state.node: reach_nodes[node] = energy requirement_to_leave = node.requirement_to_leave(initial_state.patches, initial_state.resources) for target_node, requirement in logic.game.world_list.potential_nodes_from(node, initial_state.patches): if target_node is None: continue if checked_nodes.get(target_node, math.inf) <= energy or nodes_to_check.get(target_node, math.inf) <= energy: continue if requirement_to_leave != Requirement.trivial(): requirement = RequirementAnd([requirement, requirement_to_leave]) # Check if the normal requirements to reach that node is satisfied satisfied = requirement.satisfied(initial_state.resources, energy) if satisfied: # If it is, check if we additional requirements figured out by backtracking is satisfied satisfied = logic.get_additional_requirements(node).satisfied(initial_state.resources, energy) if satisfied: nodes_to_check[target_node] = energy - requirement.damage(initial_state.resources) path_to_node[target_node] = path_to_node[node] + (node,) elif target_node: # If we can't go to this node, store the reason in order to build the satisfiable requirements. # Note we ignore the 'additional requirements' here because it'll be added on the end. requirements_by_node[target_node].update(requirement.as_set.alternatives) # Discard satisfiable requirements of nodes reachable by other means for node in set(reach_nodes.keys()).intersection(requirements_by_node.keys()): requirements_by_node.pop(node) if requirements_by_node: satisfiable_requirements = frozenset.union( *[RequirementSet(requirements).union(logic.get_additional_requirements(node)).alternatives for node, requirements in requirements_by_node.items()]) else: satisfiable_requirements = frozenset() return ResolverReach(reach_nodes, path_to_node, satisfiable_requirements, logic)
def requirement_to_leave(self, context: NodeContext) -> Requirement: return RequirementAnd([self.is_unlocked, ResourceRequirement(self.resource(context), 1, False)])
class DockNode(Node): """ Represents a connection to another area via something similar to a door and it's always to another DockNode. The dock weakness describes the types of door the game might have, which could be randomized separately from where the door leads to. This is the default way a node connects to another area, expected to be used in every area and it implies the areas are "physically" next to each other. TeleporterNode is expected to be used exceptionally, where it can be reasonable to list all of them in the UI for user selection (elevator rando, for example). """ dock_type: DockType default_connection: NodeIdentifier default_dock_weakness: DockWeakness override_default_open_requirement: typing.Optional[Requirement] override_default_lock_requirement: typing.Optional[Requirement] def __repr__(self): return "DockNode({!r} -> {})".format(self.name, self.default_connection) def get_lock_node_identifier_from_identifier( self, self_identifier: NodeIdentifier): return dataclasses.replace( self_identifier, node_name=f"Lock - {self.name}", ) def get_lock_node_identifier(self, context: NodeContext) -> NodeIdentifier: return self.get_lock_node_identifier_from_identifier( context.node_provider.identifier_for_node(self), ) def get_front_weakness(self, context: NodeContext) -> DockWeakness: self_identifier = context.node_provider.identifier_for_node(self) return context.patches.dock_weakness.get(self_identifier, self.default_dock_weakness) def get_back_weakness( self, context: NodeContext) -> typing.Optional[DockWeakness]: target_identifier = self.get_target_identifier(context) if target_identifier is None: return None target_node = context.node_provider.node_by_identifier( target_identifier) if isinstance(target_node, DockNode): return context.patches.dock_weakness.get( target_identifier, target_node.default_dock_weakness) return None def _get_open_requirement(self, weakness: DockWeakness) -> Requirement: if weakness is self.default_dock_weakness and self.override_default_open_requirement is not None: return self.override_default_open_requirement else: return weakness.requirement def _get_lock_requirement(self, weakness: DockWeakness) -> Requirement: if weakness is self.default_dock_weakness and self.override_default_lock_requirement is not None: return self.override_default_lock_requirement else: return weakness.lock.requirement def _open_dock_connection( self, context: NodeContext, target_identifier: NodeIdentifier, ) -> tuple[Node, Requirement]: forward_weakness = self.get_front_weakness(context) reqs: list[Requirement] = [ self._get_open_requirement(forward_weakness) ] # This dock has a lock, so require it if forward_weakness.lock is not None: reqs.append( ResourceRequirement.simple( context.node_provider.identifier_for_node(self))) # The other dock has a lock, so require it if (other_lock_req := _requirement_from_back(context, target_identifier)) is not None: reqs.append(other_lock_req) target_node = context.node_provider.node_by_identifier( target_identifier) requirement = RequirementAnd( reqs).simplify() if len(reqs) != 1 else reqs[0] return target_node, requirement
def test_trivial_requirement_damage(): assert Requirement.trivial().damage({}, None) == 0 def test_trivial_requirement_str(): assert str(Requirement.trivial()) == "Trivial" @pytest.mark.parametrize( ["original", "expected"], [( RequirementOr([ RequirementAnd([ _req("A"), ]), ]), _req("A"), ), ( RequirementAnd([ RequirementOr([ _req("A"), ]), ]), _req("A"), ), ( RequirementAnd([ RequirementOr([_req("B"), Requirement.trivial()]), RequirementAnd([
def requirement_to_leave( self, patches: GamePatches, current_resources: CurrentResources) -> Requirement: return RequirementAnd( [self.is_unlocked, ResourceRequirement(self.resource(), 1, False)])