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
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)
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)
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)
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
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
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)
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)