Exemplo n.º 1
0
    def build_quest_to_surface_map(self, quest: Quest) -> Mapping:
        """
        Build map of all actions comprising this quest and their corresponding
        surface mention (CommandSurface).
        """
        pg = ProcessGraph()
        pg.from_tw_actions(quest.actions)
        self.assign_actions_to_ops(pg)
        action_to_surface_map = {}

        for ae in pg.topological_sort_actions():
            if ae.action in SKIP_ACTIONS_BY_MODE[self.difficulty_mode]:
                action_to_surface_map[str(ae)] = CommandSurface(IGNORE_CMD, 0)
            else:
                # Handle implicit references - replace material types
                # with implicit ref such as 'them' - currently doesn't
                # differentiate between plural and singular
                if self.implicit_refs and ae.source.var.type in \
                IMPLICIT_ARGS_BY_MODE[self.difficulty_mode]:
                    arg_name = templatize_text('IMP_REF')
                else:
                    arg_name = (ae.target.var.name if ae.action \
                                in REVERSE_ACTS else ae.source.var.name)
                    arg_name = templatize_text(arg_name)

                if ae.action in propositionals:
                    arg_1 = arg_name
                    arg_2 = (ae.source.var.name if ae.action \
                                in REVERSE_ACTS else ae.target.var.name)
                    arg_2 = templatize_text(arg_2)
                    arg_str = propositionals[ae.action].format(**{
                        'arg_1': arg_1,
                        'arg_2': arg_2
                    })
                else:
                    arg_str = arg_name

                cmd = '%s %s' % (ae.action, arg_str)

                cnt = get_cmd_count(cmd, list(action_to_surface_map.values()))
                action_to_surface_map[str(ae)] = CommandSurface(cmd, cnt)

        # Handle parallel/serial action merging according to flags.
        if self.merge_parallel_actions:
            action_to_surface_map.update(self.get_parallel_actions(pg))
        if self.merge_serial_actions:
            action_to_surface_map.update(self.get_serial_actions(pg))

        # Count amount of actions each CommandSurface covers, so that we will
        # only append the command surface to the generated text when the last
        # action of the command is taken.
        cmd_sur_list = [str(cs) for cs in action_to_surface_map.values()]
        cmds_num_use_left = {
            str(cs): cmd_sur_list.count(str(cs))
            for cs in cmd_sur_list
        }
        return action_to_surface_map, cmds_num_use_left
Exemplo n.º 2
0
    def assign_actions_to_ops(self, pg: ProcessGraph) -> ProcessGraph:
        """
        For each operation node, replace incoming "assign" edges with the action name.
        """
        op_nodes = [n for n in pg.G.nodes() if n.var.type == 'tlq_op']
        for op in op_nodes:
            if self.surface_gen_options.preset_ops:
                op_type = self.surface_gen_options.op_type_map[op.var.name]
            else:
                op_type = pg.get_op_type(op)
            action_with_id = '{} ({})'.format(templatize_text(op_type),
                                              templatize_text(op.var.name))
            pg.rename_edges(op, 'op_ia_assign', action_with_id)

        dsc_nodes = [n for n in pg.G.nodes() if \
                     KnowledgeBase.default().types.is_descendant_of(n.var.type, 'dsc')]
        for node in dsc_nodes:
            pg.rename_edges(node, 'dlink', 'describe', incoming=False)
Exemplo n.º 3
0
 def replace_mixtures_by_ref(self, pg: ProcessGraph) -> ProcessGraph:
     """ Implicit references for operation results- substitutions such as 'grind MX' with 'grind the result of OP' """
     if self.surface_gen_options.implicit_refs:
         mix_nodes = [n for n in pg.G.nodes() if n.var.type == 'mx']
         for mix in mix_nodes:
             source_op = pg.get_source_op(mix)
             if source_op:
                 tg_string = "~~RESULT~~ ~~{}~~".format(source_op.var.name)
                 tg = TokenGenerator(
                     symbol=mix.var.name,
                     tgstring=tg_string,
                     seed=self.ttg.seed,
                     token_generator_map=self.ttg.token_generators_dict)
                 self.ttg.register_token_generator(tg, override=True)
Exemplo n.º 4
0
    def quest_to_surface(self, quest: Quest) -> str:
        """
        Generate surface corresponding to given quest (action graph).
        
        Parameters
        ----------
        quest: TextWorld Quest.
        
        Returns
        -------
        surface:
            Text description overlaying the given Quest.
        
        """
        ttg_string = ""
        pg = ProcessGraph()
        pg.from_tw_actions(quest.actions)
        self.assign_actions_to_ops(pg)
        self.replace_mixtures_by_ref(pg)
        action_to_surface_map, cmds_num_use_left = self.build_quest_to_surface_map(
            quest)
        actions = pg.topological_sort_actions()
        actual_cmds = []
        if self.difficulty_mode == "debug":
            return [str(ae) for ae in actions]

        for ae in actions:
            cmd_surface = action_to_surface_map[str(ae)]
            if cmd_surface.ignore():
                continue

            # only append the command surface to the generated text when the last
            # action of the command is taken.
            if cmds_num_use_left[str(cmd_surface)] > 0:
                cmds_num_use_left[str(cmd_surface)] -= 1
            if cmds_num_use_left[str(cmd_surface)] == 0:
                actual_cmds.append(cmd_surface.string)

        # Start with sentence describing initial materials
        start_material_names = [
            mat.name for mat in pg.get_start_material_vars()
        ]
        start_material_desc = templatize_text('INIT_MATERIALS') + \
        ('s are ' if len(start_material_names) > 1 else ' is ') + \
        ('%s' % list_to_contents_str(templatize_text_list(start_material_names))) + '. '
        # Add the surface corresponding to each command
        first_action = actual_cmds.pop(0)
        last_action = actual_cmds.pop()
        first_action_string = '~~FIRST~~, ' + first_action + '. '
        mid_actions_string = '. '.join(
            ['~~MIDDLE~~, ' + mid_action
             for mid_action in actual_cmds]) + ('. ' if actual_cmds else '')
        final_action_string = '~~FINAL~~, ' + last_action + '.'
        ttg_string = start_material_desc + first_action_string + mid_actions_string + final_action_string
        return self.ttg.instantiate_template_text(ttg_string)
Exemplo n.º 5
0
 def get_serial_actions(self,
                        pg: ProcessGraph) -> Mapping[str, CommandSurface]:
     """
     Get all serial actions in action graph. Specifically, all chains of at
     least 2 consecutive actions on a single material. This is to allow 
     merging of multiple identical commands to one. For example, this would
     change the sequence "grind X. melt X." to "grind and melt X"
     
     Parameters
     ----------
     pg: Process Graph representing a material synthesis procedure.
     
     Returns
     -------
     mapping:
         mapping between the string ids of the serial actions and their
         corresponding CommandSurface.
     """
     serial_act_to_surface_map = {}
     for var, state_nodes in pg.ent_states_map.items():
         if KnowledgeBase.default().types.is_descendant_of(var.type, 'm'):
             if len(state_nodes) > 2:  # >2 state changes, we can merge them
                 ap = pg.get_actions_path(state_nodes[0], state_nodes[-1])
                 actions = [
                     ae.action for ae in ap if ae.action not in
                     SKIP_ACTIONS_BY_MODE[self.difficulty_mode]
                 ]
                 if len(actions) > 1:
                     cmd = '%s %s' % (list_to_contents_str(actions),
                                      templatize_text(var.name))
                 elif len(actions) == 1:
                     cmd = '%s %s' % (templatize_text(actions[0]), var.name)
                 else:
                     cmd = IGNORE_CMD
                 # if there are commands with an identical surface
                 # representation, differentiate between them using cnt
                 cnt = get_cmd_count(
                     cmd, list(serial_act_to_surface_map.values()))
                 for ae in ap:
                     serial_act_to_surface_map[str(ae)] = CommandSurface(
                         cmd, cnt)
     return serial_act_to_surface_map
Exemplo n.º 6
0
    def get_parallel_actions(self,
                             pg: ProcessGraph) -> Mapping[str, CommandSurface]:
        """
        Get all parallel actions in action graph. Specifically, all incoming edges
        to nodes with in degree > 1. This is to allow merging of multiple 
        identical commands to one. For example, this would change the sequence 
        "mix X. Mix Y. Mix Z." into "Mix X,Y and Z"
        
        Parameters
        ----------
        pg: Process Graph representing a material synthesis procedure.
        
        Returns
        -------
        mapping:
            mapping between the string ids of the parallel actions and their
            corresponding CommandSurface.
        """
        parallel_action_map = {}
        # TODO only works if no mixtures as start materials
        num_start_mats = len(
            [v for v in pg.ent_states_map.keys() if v.type == 'm'])

        # find all nodes with in degree greater than 1
        parallel_acts_targets = [
            n for n in pg.G.nodes() if pg.G.in_degree(n) > 1
        ]

        for node in parallel_acts_targets:
            node_act_map = {}
            incoming_actions = pg.get_incoming_actions(node)
            # Get number of incoming actions of each type.
            act_counts = get_action_counts(
                [ae.action for ae in incoming_actions])
            for ae in incoming_actions:
                action = ae.action
                if action in SKIP_ACTIONS_BY_MODE[self.difficulty_mode]:
                    continue
                if (act_counts[ae.action]):
                    if (act_counts[action] > 1):
                        if KnowledgeBase.default().types.is_descendant_of(
                                ae.source.var.type, 'm'):
                            # If we're merging actions, and all starting materials participate
                            # in action, the action argument should be simply 'materials'
                            if (act_counts[action] == num_start_mats
                                ) and self.surface_gen_options.implicit_refs:
                                cmd = '%s the materials' % (action)

                            # If not all starting materials participating, refer to
                            # each by name (e.g., X, Y and Z)
                            else:
                                target_mats = [ae.source.var.name for ae in \
                                               incoming_actions if ae.action == action]
                                cmd = '%s %s' % (
                                    action,
                                    list_to_contents_str(
                                        templatize_text_list(target_mats)))
                        elif KnowledgeBase.default().types.is_descendant_of(
                                ae.source.var.type, 'dsc'):
                            target_descs = [ae.source.var.name for ae in \
                                               incoming_actions if ae.action == action]
                            arg_1 = list_to_contents_str(
                                templatize_text_list(target_descs))
                            arg_2 = templatize_text(ae.target.var.name)
                            arg_str = propositionals[ae.action].format(
                                **{
                                    'arg_1': arg_1,
                                    'arg_2': arg_2
                                })
                            cmd = '%s %s' % (action, arg_str)
                        # if there are commands with an identical surface
                        # representation, differentiate between them using cnt
                        cnt = get_cmd_count(cmd,
                                            list(parallel_action_map.values()))

                        # index each edge in action graph, such that it refers
                        # to relevant surface text.
                        node_act_map[str(ae)] = CommandSurface(cmd, cnt)
            parallel_action_map.update(node_act_map)
        return parallel_action_map
Exemplo n.º 7
0
        mdesc = M.new_lab_entity('mdsc')
        lab.add(mdesc)
        quest_gen_options.ent_desc_map[mat.var.name].append(mdesc.var.name)

# Add operations and their descriptors
ent_type = 'tlq_op'
n_max_descs = quest_gen_options.max_descs_per_ent[ent_type]
ops = [M.new_tlq_op(dynamic_define=True) for i in range(n_ops)]
for op in ops:
    lab.add(op)
    n_descs = quest_gen_options.quest_rng.randint(0, (n_max_descs + 1))
    for j in range(n_descs):
        odesc = M.new_lab_entity('odsc')
        lab.add(odesc)
        quest_gen_options.ent_desc_map[op.var.name].append(odesc.var.name)

#oven = M.new_lab_entity('sa', name='oven')
#lab.add(oven)

quest = M.generate_quest_surface_pair(quest_gen_options)
game = M.build()
pg = ProcessGraph()
pg.from_tw_actions(quest.actions)
pg.draw()
with make_temp_directory(prefix="test_tw-make") as tmpdir:
    output_folder = Path(tmpdir) / "gen_games"
    game_file = Path(output_folder) / ("%s.ulx" % (lab_game_options.uuid))
    game_file = tw_textlabs.generator.compile_game(game, lab_game_options)
    # Solve the game using WalkthroughAgent.
#        test_game_walkthrough_agent(game_file)
Exemplo n.º 8
0
    def get_win_conditions(self, chain: Chain) -> Collection[Proposition]:
        """
        Given a chain of actions comprising a quest, return the set of propositions
        which must hold in a winning state.
        
        Parameters
        ----------
        chain:
            Chain of actions leading to goal state.
            
        Returns
        -------
        win_conditions:
            Set of propositions which must hold to end the quest succesfully.
        """

        win_condition_type = self.options.win_condition

        win_conditions = set()

        final_state = chain.final_state
        pg = ProcessGraph()
        pg.from_tw_actions(chain.actions)

        if win_condition_type in [WinConditionType.OPS, WinConditionType.ALL]:

            # require all operations to have the correct inputs
            processed_props = set([
                prop for prop in final_state.facts if prop.name == 'processed'
            ])
            component_props = set([
                prop for prop in final_state.facts if prop.name == 'component'
            ])
            win_conditions.update(processed_props)
            win_conditions.update(component_props)

            # require all operations to be set to correct type
            op_type_props = set([
                prop for prop in final_state.facts
                if prop.name == 'tlq_op_type'
            ])
            win_conditions.update(op_type_props)

            # precedence propositions enforcing minimal ordering restraints between ops
            tG = nx.algorithms.dag.transitive_closure(pg.G)
            op_nodes = [
                n for n in tG.nodes()
                if KnowledgeBase.default().types.is_descendant_of(
                    n.var.type, ["op"])
            ]
            op_sg = nx.algorithms.dag.transitive_reduction(
                tG.subgraph(op_nodes))
            for e in op_sg.edges():
                op_1_node, op_2_node = e
                prec_prop = Proposition('preceeds',
                                        [op_1_node.var, op_2_node.var])
                win_conditions.update({prec_prop})

        if win_condition_type in [WinConditionType.ARGS, WinConditionType.ALL]:
            # require all descriptions to be set correctly
            desc_props = set([
                prop for prop in final_state.facts if prop.name == 'describes'
            ])
            win_conditions.update(desc_props)

        # add post-conditions from last action
        post_props = set(chain.actions[-1].postconditions)
        win_conditions.update(post_props)

        return Event(conditions=win_conditions)