def test_expand_alternatives_3(blank_resource_db): db = blank_resource_db a = RequirementSet.trivial() b = make_single_set(make_req_a(db)) expected = RequirementSet.trivial() assert a.expand_alternatives(b) == expected
def log_skip_action_missing_requirement(node: Node, game: "GameDescription", requirement_set: RequirementSet): if _DEBUG_LEVEL > 1: if node in _last_printed_additional and _last_printed_additional[node] == requirement_set: print("{}* Skip {}, same additional".format(_indent(), n(node, world_list=game.world_list))) else: print("{}* Skip {}, missing additional:".format(_indent(), n(node, world_list=game.world_list))) requirement_set.pretty_print(_indent(-1)) _last_printed_additional[node] = requirement_set
def test_set_dangerous_resources(): # setup list_a = MagicMock() list_b = MagicMock() list_a.dangerous_resources = [1, 2, 3] list_b.dangerous_resources = ["a", "b", "c"] req_set = RequirementSet([]) req_set.alternatives = frozenset([list_a, list_b]) # Run result = set(req_set.dangerous_resources) # Assert assert result == {1, 2, 3, "a", "b", "c"}
def test_expand_alternatives_1(blank_resource_db): db = blank_resource_db a = RequirementSet.impossible() b = make_single_set(make_req_a(db)) expected = make_single_set(make_req_a(db)) assert a.expand_alternatives(b) == expected
def test_expand_alternatives_4(blank_resource_db): db = blank_resource_db a = make_single_set(make_req_a(db)) b = make_single_set(make_req_b(db)) expected = RequirementSet([ RequirementList([make_req_a(db)[1]]), RequirementList([make_req_b(db)[1]]) ]) assert a.expand_alternatives(b) == expected
def test_prevent_redundant(blank_game_description): db = blank_game_description.resource_database res_a, id_req_a = make_req_a(db) res_b, id_req_b = make_req_b(db) the_set = RequirementSet([ RequirementList([id_req_a]), RequirementList([id_req_a, id_req_b]), ]) assert the_set.alternatives == frozenset([RequirementList([id_req_a])])
def reach_from_state( cls, game: GameDescription, initial_state: State, ) -> "GeneratorReach": reach = cls(game, initial_state, graph_module.RandovaniaGraph.new()) game.world_list.ensure_has_node_cache() reach._expand_graph( [GraphPath(None, initial_state.node, RequirementSet.trivial())]) return reach
def test_requirement_as_set_3(database): def _req(name: str): id_req = ResourceRequirement.simple(database.get_item(name)) return id_req req = RequirementOr([ Requirement.impossible(), _req("A"), ]) assert req.as_set(database) == RequirementSet([ RequirementList([_req("A")]), ])
def test_set_hash(echoes_resource_database): req_set_a = RequirementSet([ RequirementList([ ResourceRequirement.simple( echoes_resource_database.get_item_by_name("Power Bomb")), ]), ]) req_set_b = RequirementSet([ RequirementList([ ResourceRequirement.simple( echoes_resource_database.get_item_by_name("Power Bomb")), ]), ]) assert req_set_a == req_set_b assert req_set_a is not req_set_b hash_a = hash(req_set_a) hash_b = hash(req_set_b) assert hash_a == hash_b assert hash_a == req_set_a._cached_hash
def _simplify_additional_requirement_set( requirements: RequirementSet, state: State, dangerous_resources: FrozenSet[ResourceInfo], ) -> RequirementSet: new_alternatives = [ _simplify_requirement_list(alternative, state, dangerous_resources) for alternative in requirements.alternatives ] return RequirementSet(alternative for alternative in new_alternatives # RequirementList.simplify may return None if alternative is not None)
def test_set_as_str_things(echoes_resource_database): item = echoes_resource_database.get_item_by_name req_set = RequirementSet([ RequirementList([ ResourceRequirement.simple(item("Screw Attack")), ResourceRequirement.simple(item("Space Jump Boots")), ]), RequirementList([ ResourceRequirement.simple(item("Power Bomb")), ]), ]) assert req_set.as_str == "(Power Bomb ≥ 1) or (Screw Attack ≥ 1, Space Jump Boots ≥ 1)"
def test_requirement_as_set_1(database): def _req(name: str): id_req = ResourceRequirement.simple(database.get_item(name)) return id_req req = RequirementAnd([ _req("A"), RequirementOr([_req("B"), _req("C")]), RequirementOr([_req("D"), _req("E")]), ]) assert req.as_set(database) == 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_trivial_merge(blank_game_description): db = blank_game_description.resource_database res_a, id_req_a = make_req_a(db) trivial = RequirementSet.trivial() impossible = RequirementSet.impossible() the_set = RequirementSet([ RequirementList([id_req_a]), ]) assert trivial.union(trivial) == trivial assert trivial.union(the_set) == the_set assert the_set.union(trivial) == the_set assert trivial.union(impossible) == impossible assert impossible.union(the_set) == impossible assert the_set.union(impossible) == impossible assert the_set.union(the_set) == the_set
def test_requirement_template_nested(database): def _req(name: str): id_req = ResourceRequirement.simple(database.get_item(name)) return id_req # Setup use_a = RequirementTemplate("Use A") use_b = RequirementTemplate("Use B") database.requirement_template["Use A"] = _req("A") database.requirement_template["Use B"] = RequirementOr([use_a, _req("B")]) # Run as_set = use_b.as_set(database) # Assert assert as_set == RequirementSet([ RequirementList([_req("A")]), RequirementList([_req("B")]), ]) assert hash(use_a) != hash(use_b)
def test_requirement_set_constructor(echoes_resource_database): item = echoes_resource_database.get_item_by_name req_set = RequirementSet([ RequirementList([ ResourceRequirement.simple(item("Dark Visor")), ResourceRequirement.create(item("Missile"), 5, False), ResourceRequirement.simple(item("Seeker Launcher")), ]), RequirementList([ ResourceRequirement.simple(item("Screw Attack")), ResourceRequirement.simple(item("Space Jump Boots")), ]), RequirementList([ ResourceRequirement.simple(item("Power Bomb")), ResourceRequirement.simple(item("Boost Ball")), ]), ]) extract = [ sorted((req.resource.long_name, req.amount) for req in req_list.values()) for req_list in req_set.alternatives ] assert sorted(extract) == [ [ ("Boost Ball", 1), ("Power Bomb", 1), ], [ ("Dark Visor", 1), ("Missile", 5), ("Seeker Launcher", 1), ], [ ("Screw Attack", 1), ("Space Jump Boots", 1), ], ]
def satisfiable_as_requirement_set(self) -> RequirementSet: return RequirementSet(self._satisfiable_requirements)
def as_set(self, database: ResourceDatabase) -> RequirementSet: result = RequirementSet.trivial() for item in self.items: result = result.union(item.as_set(database)) return result
def test_expand_alternatives_2(): a = RequirementSet.impossible() b = RequirementSet.trivial() expected = RequirementSet.trivial() assert a.expand_alternatives(b) == expected
def as_set(self, database: ResourceDatabase) -> RequirementSet: return RequirementSet([RequirementList([self])])
def __init__(self, game: GameDescription, configuration: BaseConfiguration): self.game = game self.configuration = configuration self.additional_requirements = [RequirementSet.trivial()] * len( game.world_list.all_nodes)
async def _inner_advance_depth( state: State, logic: Logic, status_update: Callable[[str], None], *, reach: Optional[ResolverReach] = None, max_attempts: Optional[int] = None, ) -> Tuple[Optional[State], bool]: """ :param state: :param logic: :param status_update: :param reach: A precalculated reach for the given state :return: """ if logic.victory_condition.satisfied(state.resources, state.energy, state.resource_database): return state, True # Yield back to the asyncio runner, so cancel can do something await asyncio.sleep(0) if reach is None: reach = ResolverReach.calculate_reach(logic, state) _check_attempts(max_attempts) debug.log_new_advance(state, reach) status_update("Resolving... {} total resources".format( state.resources.num_resources)) for action, energy in reach.possible_actions(state): if _should_check_if_action_is_safe( state, action, logic.game.dangerous_resources, logic.game.world_list.iterate_nodes()): potential_state = state.act_on_node( action, path=reach.path_to_node(action), new_energy=energy) potential_reach = ResolverReach.calculate_reach( logic, potential_state) # If we can go back to where we were, it's a simple safe node if state.node in potential_reach.nodes: new_result = await _inner_advance_depth( state=potential_state, logic=logic, status_update=status_update, reach=potential_reach, max_attempts=max_attempts, ) if not new_result[1]: debug.log_rollback(state, True, True) # If a safe node was a dead end, we're certainly a dead end as well return new_result debug.log_checking_satisfiable_actions() has_action = False for action, energy in reach.satisfiable_actions(state, logic.victory_condition): new_result = await _inner_advance_depth( state=state.act_on_node(action, path=reach.path_to_node(action), new_energy=energy), logic=logic, status_update=status_update, max_attempts=max_attempts, ) # We got a positive result. Send it back up if new_result[0] is not None: return new_result else: has_action = True debug.log_rollback(state, has_action, False) additional_requirements = reach.satisfiable_as_requirement_set if has_action: additional = set() for resource_node in reach.collectable_resource_nodes(state): additional |= logic.get_additional_requirements( resource_node).alternatives additional_requirements = additional_requirements.union( RequirementSet(additional)) logic.set_additional_requirements( state.node, _simplify_additional_requirement_set(additional_requirements, state, logic.game.dangerous_resources)) return None, has_action
def test_impossible_requirement_as_set(): assert Requirement.impossible().as_set(None) == RequirementSet.impossible()
def test_empty_requirement_set_satisfied(): assert not RequirementSet([]).satisfied(_empty_col(), 99, None)
def make_single_set( id_req: Tuple[ResourceInfo, ResourceRequirement]) -> RequirementSet: return RequirementSet([RequirementList([id_req[1]])])
def test_trivial_requirement_as_set(): assert Requirement.trivial().as_set(None) == RequirementSet.trivial()
def calculate_reach(cls, logic: Logic, initial_state: State) -> "ResolverReach": all_nodes = logic.game.world_list.all_nodes checked_nodes: Dict[int, int] = {} database = initial_state.resource_database context = initial_state.node_context() # Keys: nodes to check # Value: how much energy was available when visiting that node nodes_to_check: Dict[int, int] = { initial_state.node.node_index: initial_state.energy } reach_nodes: Dict[int, int] = {} requirements_by_node: Dict[int, Set[RequirementList]] = defaultdict(set) path_to_node: dict[int, list[int]] = { initial_state.node.node_index: [], } while nodes_to_check: node_index = next(iter(nodes_to_check)) node = all_nodes[node_index] energy = nodes_to_check.pop(node_index) if node.heal: energy = initial_state.maximum_energy checked_nodes[node_index] = energy if node_index != initial_state.node.node_index: reach_nodes[node_index] = energy requirement_to_leave = node.requirement_to_leave(context) for target_node, requirement in logic.game.world_list.potential_nodes_from(node, context): target_node_index = target_node.node_index if checked_nodes.get(target_node_index, math.inf) <= energy or nodes_to_check.get(target_node_index, 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, database) 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, initial_state.resource_database) if satisfied: nodes_to_check[target_node_index] = energy - requirement.damage(initial_state.resources, database) path_to_node[target_node_index] = list(path_to_node[node_index]) path_to_node[target_node_index].append(node_index) 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_index].update( requirement.as_set(initial_state.resource_database).alternatives) # Discard satisfiable requirements of nodes reachable by other means for node_index in set(reach_nodes.keys()).intersection(requirements_by_node.keys()): requirements_by_node.pop(node_index) if requirements_by_node: satisfiable_requirements = frozenset.union( *[RequirementSet(requirements).union(logic.get_additional_requirements(all_nodes[node_index])).alternatives for node_index, requirements in requirements_by_node.items()]) else: satisfiable_requirements = frozenset() return ResolverReach(reach_nodes, path_to_node, satisfiable_requirements, logic)
def test_set_as_str_trivial(): assert RequirementSet.trivial().as_str == "Trivial"
def test_set_as_str_impossible(): assert RequirementSet.impossible().as_str == "Impossible"
def as_set(self, database: ResourceDatabase) -> "RequirementSet": alternatives = set() for item in self.items: alternatives |= item.as_set(database).alternatives return RequirementSet(alternatives)