Esempio n. 1
0
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
Esempio n. 2
0
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
Esempio n. 3
0
 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
Esempio n. 4
0
 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
Esempio n. 5
0
    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
Esempio n. 6
0
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}
Esempio n. 8
0
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}
Esempio n. 9
0
    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()
Esempio n. 10
0
    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
Esempio n. 11
0
    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
Esempio n. 12
0
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
Esempio n. 13
0
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
Esempio n. 14
0
    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)
Esempio n. 15
0
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
Esempio n. 16
0
 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