def calculate_starting_state(game: GameDescription, patches: GamePatches) -> "State": starting_node = game.world_list.resolve_teleporter_connection( patches.starting_location) initial_resources = copy.copy(patches.starting_items) if isinstance(starting_node, PlayerShipNode): initial_resources[starting_node.resource()] = 1 initial_game_state = game.initial_states.get("Default") if initial_game_state is not None: add_resource_gain_to_current_resources(initial_game_state, initial_resources) starting_state = State( initial_resources, (), 99 + (100 * initial_resources.get(game.resource_database.energy_tank, 0)), starting_node, patches, None, game.resource_database, game.world_list, ) # Being present with value 0 is troublesome since this dict is used for a simplify_requirements later on keys_to_remove = [ resource for resource, quantity in initial_resources.items() if quantity == 0 ] for resource in keys_to_remove: del initial_resources[resource] return starting_state
def _should_check_if_action_is_safe( state: State, action: ResourceNode, dangerous_resources: FrozenSet[ResourceInfo], all_nodes: Tuple[Node, ...]) -> bool: """ Determines if we should _check_ if the given action is safe that state :param state: :param action: :return: """ if any(resource in dangerous_resources for resource in action.resource_gain_on_collect(state.node_context())): return False if isinstance(action, EventNode): return True if isinstance(action, EventPickupNode): pickup_node = action.pickup_node else: pickup_node = action if isinstance(pickup_node, PickupNode): target = state.patches.pickup_assignment.get(pickup_node.pickup_index) if target is not None and (target.pickup.item_category.is_major or target.pickup.item_category.is_key): return True return False
def collectable_resource_nodes(self, state: State) -> Iterator[ResourceNode]: for node in self.nodes: if not node.is_resource_node: continue node = typing.cast(ResourceNode, node) if node.can_collect(state.node_context()): yield node
def is_resource_node_present(node: Node, state: State): if node.is_resource_node: assert isinstance(node, ResourceNode) is_resource_set = self._initial_state.resources.is_resource_set return all( is_resource_set(resource) for resource, _ in node.resource_gain_on_collect(state.node_context())) return False
def uncollected_resource_nodes(self, state: State) -> Iterator[ResourceNode]: for node in self.nodes: if not is_resource_node(node): continue if not state.has_resource(node.resource()): yield node
def test_basic_search_with_translator_gate(has_translator: bool, echoes_resource_database): # Setup scan_visor = echoes_resource_database.get_item(10) node_a = GenericNode("Node A", True, None, 0) node_b = GenericNode("Node B", True, None, 1) node_c = GenericNode("Node C", True, None, 2) translator_node = TranslatorGateNode("Translator Gate", True, None, 3, TranslatorGate(1), scan_visor) world_list = WorldList([ World("Test World", "Test Dark World", 1, [ Area( "Test Area A", False, 10, 0, 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_specific = EchoesGameSpecific(energy_per_tank=100, safe_zone_heal_per_second=1, beam_configurations=(), dangerous_energy_tank=False) game = GameDescription(RandovaniaGame.PRIME2, DockWeaknessDatabase([], [], [], []), echoes_resource_database, game_specific, Requirement.impossible(), None, {}, world_list) patches = game.create_game_patches() patches = patches.assign_gate_assignment({TranslatorGate(1): scan_visor}) initial_state = State({scan_visor: 1 if has_translator else 0}, (), 99, node_a, patches, None, echoes_resource_database, game.world_list) # Run reach = 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 test_basic_search_with_translator_gate(has_translator: bool, echoes_resource_database): # Setup scan_visor = echoes_resource_database.get_by_type_and_index( ResourceType.ITEM, 10) node_a = GenericNode("Node A", True, 0) node_b = GenericNode("Node B", True, 1) node_c = GenericNode("Node C", True, 2) translator_node = TranslatorGateNode("Translator Gate", True, 3, TranslatorGate(1), scan_visor) world_list = WorldList([ World("Test World", 1, [ Area( "Test Area A", False, 10, 0, [node_a, node_b, node_c, translator_node], { node_a: { node_b: RequirementSet.trivial(), translator_node: RequirementSet.trivial(), }, node_b: { node_a: RequirementSet.trivial(), }, node_c: { translator_node: RequirementSet.trivial(), }, translator_node: { node_a: RequirementSet.trivial(), node_c: RequirementSet.trivial(), }, }) ]) ]) game = GameDescription(0, "", DockWeaknessDatabase([], [], [], []), echoes_resource_database, RequirementSet.impossible(), None, {}, world_list) patches = GamePatches.with_game(game) patches = patches.assign_gate_assignment({TranslatorGate(1): scan_visor}) initial_state = State({scan_visor: 1 if has_translator else 0}, (), 99, node_a, patches, None, echoes_resource_database) # Run reach = 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 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 update_for(self, world: World, state: State, nodes_in_reach: set[Node]): g = networkx.DiGraph() for area in world.areas: g.add_node(area) context = state.node_context() for area in world.areas: nearby_areas = set() for node in area.nodes: if node not in nodes_in_reach: continue for other_node, requirement in node.connections_from(context): if requirement.satisfied(state.resources, state.energy, state.resource_database): other_area = self.world_list.nodes_to_area(other_node) if other_area in world.areas: nearby_areas.add(other_area) for other_area in nearby_areas: g.add_edge(area, other_area) self.ax.clear() cf = self.ax.get_figure() cf.set_facecolor("w") if world.name not in self._world_to_node_positions: self._world_to_node_positions[ world.name] = self._positions_for_world(world, state) pos = self._world_to_node_positions[world.name] networkx.draw_networkx_nodes(g, pos, ax=self.ax) networkx.draw_networkx_edges(g, pos, arrows=True, ax=self.ax) networkx.draw_networkx_labels( g, pos, ax=self.ax, labels={area: area.name for area in world.areas}, verticalalignment='top') self.ax.set_axis_off() plt.draw_if_interactive() self.canvas.draw()
def satisfiable_actions(self, state: State, victory_condition: Requirement, ) -> Iterator[Tuple[ResourceNode, int]]: interesting_resources = calculate_interesting_resources( self._satisfiable_requirements.union(victory_condition.as_set(state.resource_database).alternatives), state.resources, state.energy, state.resource_database) # print(" > satisfiable actions, with {} interesting resources".format(len(interesting_resources))) for action, energy in self.possible_actions(state): for resource, amount in action.resource_gain_on_collect(state.node_context()): if resource in interesting_resources: yield action, energy break
def calculate_starting_state(self, game: GameDescription, patches: GamePatches, configuration: BaseConfiguration) -> "State": starting_node = game.world_list.resolve_teleporter_connection( patches.starting_location) initial_resources = copy.copy(patches.starting_items) starting_energy, energy_per_tank = self.energy_config(configuration) if starting_node.is_resource_node: assert isinstance(starting_node, ResourceNode) add_resource_gain_to_current_resources( starting_node.resource_gain_on_collect( NodeContext( patches, initial_resources, game.resource_database, game.world_list, )), initial_resources, ) initial_game_state = game.initial_states.get("Default") if initial_game_state is not None: add_resource_gain_to_current_resources(initial_game_state, initial_resources) starting_state = State( initial_resources, (), None, starting_node, patches, None, StateGameData(game.resource_database, game.world_list, energy_per_tank, starting_energy)) # Being present with value 0 is troublesome since this dict is used for a simplify_requirements later on keys_to_remove = [ resource for resource, quantity in initial_resources.items() if quantity == 0 ] for resource in keys_to_remove: del initial_resources[resource] return starting_state
def calculate_starting_state(logic: Logic, patches: GamePatches) -> "State": game = logic.game if logic.configuration.starting_resources.configuration == \ StartingResourcesConfiguration.VANILLA_ITEM_LOSS_ENABLED: initial_game_state = game.initial_states["Default"] else: initial_game_state = None starting_area = game.world_list.area_by_asset_id( patches.starting_location.area_asset_id) starting_node = starting_area.nodes[starting_area.default_node_index] initial_resources = { # "No Requirements" game.resource_database.trivial_resource(): 1 } add_resource_gain_to_current_resources( logic.configuration.starting_resources.resource_gain, initial_resources) add_resource_gain_to_current_resources(patches.extra_initial_items, initial_resources) if initial_game_state is not None: add_resource_gain_to_current_resources(initial_game_state, initial_resources) starting_state = State(initial_resources, starting_node, patches, None, game.resource_database) # Being present with value 0 is troublesome since this dict is used for a simplify_requirements later on keys_to_remove = [ resource for resource, quantity in initial_resources.items() if quantity == 0 ] for resource in keys_to_remove: del initial_resources[resource] return starting_state
def _inner_advance_depth( state: State, logic: Logic, status_update: Callable[[str], None], ) -> Tuple[Optional[State], bool]: if logic.game.victory_condition.satisfied(state.resources, state.resource_database): return state, True reach = ResolverReach.calculate_reach(logic, state) debug.log_new_advance(state, reach) status_update("Resolving... {} total resources".format(len( state.resources))) has_action = False for action in reach.satisfiable_actions(state): new_result = _inner_advance_depth(state=state.act_on_node( action, path=reach.path_to_node[action]), logic=logic, status_update=status_update) # 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) if not has_action: logic.additional_requirements[ state. node] = _simplify_requirement_set_for_additional_requirements( reach.satisfiable_as_requirement_set, state) return None, has_action
def calculate_reach(cls, logic: Logic, initial_state: State) -> "ResolverReach": checked_nodes: Dict[Node, 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[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(context) for target_node, requirement in logic.game.world_list.potential_nodes_from( node, context): 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, 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] = energy - requirement.damage( initial_state.resources, database) 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( initial_state.resource_database).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)
async def _inner_advance_depth( state: State, logic: Logic, status_update: Callable[[str], None], *, reach: Optional[ResolverReach] = None, ) -> Tuple[Optional[State], bool]: """ :param state: :param logic: :param status_update: :param reach: A precalculated reach for the given state :return: """ if logic.game.victory_condition.satisfied(state.resources, state.energy): 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) debug.log_new_advance(state, reach) status_update("Resolving... {} total resources".format(len( state.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.all_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, ) 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.game.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, ) # 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.additional_requirements[ state.node] = _simplify_additional_requirement_set( additional_requirements, state, logic.game.dangerous_resources) return None, has_action
def is_resource_node_present(node: Node, state: State): if node.is_resource_node: assert isinstance(node, ResourceNode) return node.resource( state.node_context()) in self._initial_state.resources return False