def __init__(self, options: Union[GrammarOptions, Mapping[str, Any]] = {}, rng: Optional[RandomState] = None): """ Arguments: options: For customizing text generation process (see :py:class:`tw_textlabs.generator.GrammarOptions <tw_textlabs.generator.text_grammar.GrammarOptions>` for the list of available options). rng: Random generator used for sampling tag expansions. """ self.options = GrammarOptions(options) self.grammar = OrderedDict() self.rng = g_rng.next() if rng is None else rng self.allowed_variables_numbering = self.options.allowed_variables_numbering self.unique_expansion = self.options.unique_expansion self.all_expansions = defaultdict(list) # The current used symbols self.overflow_dict = OrderedDict() self.used_names = set(self.options.names_to_exclude) # Load the grammar associated to the provided theme. self.theme = self.options.theme # Load the object names file files = glob.glob( pjoin(KnowledgeBase.default().text_grammars_path, glob.escape(self.theme) + "_*.twg")) for filename in files: self._parse(filename) for k, v in self.grammar.items(): self.grammar[k] = tuple(v)
def nb_mat_ccs(self, node: _Node) -> int: """ Return number of connected components in graph spanning base materials. Materials are connnected iff they're merged. """ # get all materials and descendants in game all_mats = [ self.start_state.variables_of_type(t) for t in KnowledgeBase.default().types.descendants('m') + ['m'] ] all_mats = set([item for items in all_mats for item in items]) # get all 'component' propositions to link between the materials component_props = [ p for p in node.state.facts if p.name == "component" ] # build the graph and return number of ccs (only count ccs with any base materials) G = nx.Graph() G.add_nodes_from([m for m in all_mats]) edges = [(p.arguments[0], p.arguments[1]) for p in component_props] G.add_edges_from(edges) ccs = [ cc for cc in nx.connected_components(G) if len(cc.intersection(self.base_materials)) > 0 ] return len(ccs)
def gen_commands_from_actions(actions: Iterable[Action], kb: Optional[KnowledgeBase] = None, compact_actions=False) -> List[str]: kb = kb or KnowledgeBase.default() def _get_name_mapping(action, compact_actions=False): mapping = kb.rules[action.name].match(action) if not compact_actions: return {ph.name: var.name for ph, var in mapping.items()} else: return {ph.name: var for ph, var in mapping.items()} commands = [] for action in actions: command = "None" if action is not None: command = kb.inform7_commands[action.name] short_cmd_name = command.split(' ')[0] # assuming action name is first word in command sentence name_mapping = _get_name_mapping(action, compact_actions) if not compact_actions: command = command.format(**name_mapping) else: cmd_args = [name_mapping[w.strip('{}')] for w in command.split(' ') if (('{' in w) and ('}' in w))] command = CompactAction(name=short_cmd_name, vars=cmd_args) commands.append(command) return commands
def __str__(self): if KnowledgeBase.default().types.is_descendant_of( self.var.type, ['toe']): return type_map[self.var.type] elif self.g > 0: return '%s@g%d' % (self.var.name, self.g) else: return '%s' % (self.var.name)
def get_source_op(self, node: EntityNode) -> EntityNode: assert (KnowledgeBase.default().types.is_descendant_of( node.var.type, ['m', 'sa'])) # return first operation node producing target node for n in self.ent_states_map[node.var]: for s, t in self.G.in_edges(n): if s.var.type == 'tlq_op': return s return None
def test_quest_winning_condition(): g_rng.set_seed(2018) map_ = make_small_map(n_rooms=5, possible_door_states=["open"]) world = World.from_map(map_) def _rule_to_skip(rule): # Examine, look and inventory shouldn't be used for chaining. if rule.name.startswith("look"): return True if rule.name.startswith("inventory"): return True if rule.name.startswith("examine"): return True return False for rule in KnowledgeBase.default().rules.values(): if _rule_to_skip(rule): continue options = ChainingOptions() options.backward = True options.max_depth = 1 options.create_variables = True options.rules_per_depth = [[rule]] options.restricted_types = {"r"} chain = sample_quest(world.state, options) assert len(chain.actions) > 0, rule.name event = Event(chain.actions) quest = Quest(win_events=[event]) # Set the initial state required for the quest. tmp_world = World.from_facts(chain.initial_state.facts) game = make_game_with(tmp_world, [quest], make_grammar({})) if tmp_world.player_room is None: # Randomly place the player in the world since # the action doesn't care about where the player is. tmp_world.set_player_room() game_name = "test_quest_winning_condition_" + rule.name.replace( "/", "_") with make_temp_directory(prefix=game_name) as tmpdir: game_file = _compile_game(game, path=tmpdir) env = tw_textlabs.start(game_file) env.reset() game_state, _, done = env.step("look") assert not done assert not game_state.has_won game_state, _, done = env.step(event.commands[0]) assert done assert game_state.has_won
def test_parallel_quests(): logic = GameLogic.parse(""" type foo { rules { do_a :: not_a(foo) & $not_c(foo) -> a(foo); do_b :: not_b(foo) & $not_c(foo) -> b(foo); do_c :: $a(foo) & $b(foo) & not_c(foo) -> c(foo); } constraints { a_or_not_a :: a(foo) & not_a(foo) -> fail(); b_or_not_b :: b(foo) & not_b(foo) -> fail(); c_or_not_c :: c(foo) & not_c(foo) -> fail(); } } """) kb = KnowledgeBase(logic, "") state = State([ Proposition.parse("a(foo)"), Proposition.parse("b(foo)"), Proposition.parse("c(foo)"), ]) options = ChainingOptions() options.backward = True options.kb = kb options.max_depth = 3 options.max_breadth = 1 chains = list(get_chains(state, options)) assert len(chains) == 2 options.max_breadth = 2 chains = list(get_chains(state, options)) assert len(chains) == 3 options.min_breadth = 2 chains = list(get_chains(state, options)) assert len(chains) == 1 assert len(chains[0].actions) == 3 assert chains[0].nodes[0].depth == 2 assert chains[0].nodes[0].breadth == 2 assert chains[0].nodes[0].parent == chains[0].nodes[2] assert chains[0].nodes[1].depth == 2 assert chains[0].nodes[1].breadth == 1 assert chains[0].nodes[1].parent == chains[0].nodes[2] assert chains[0].nodes[2].depth == 1 assert chains[0].nodes[2].breadth == 1 assert chains[0].nodes[2].parent == None options.min_breadth = 1 options.create_variables = True chains = list(get_chains(State(), options)) assert len(chains) == 5
def test_generating_quests(self): g_rng.set_seed(2018) map_ = make_small_map(n_rooms=5, possible_door_states=["open"]) world = World.from_map(map_) def _rule_to_skip(rule): # Examine, look and inventory shouldn't be used for chaining. if rule.name.startswith("look"): return True if rule.name.startswith("inventory"): return True if rule.name.startswith("examine"): return True return False for max_depth in range(1, 3): for rule in KnowledgeBase.default().rules.values(): if _rule_to_skip(rule): continue options = ChainingOptions() options.backward = True options.max_depth = max_depth options.create_variables = True options.rules_per_depth = [[rule]] options.restricted_types = {"r"} chain = sample_quest(world.state, options) # Build the quest by providing the actions. actions = chain.actions assert len(actions) == max_depth, rule.name quest = Quest(win_events=[Event(actions)]) tmp_world = World.from_facts(chain.initial_state.facts) state = tmp_world.state for action in actions: assert not quest.is_winning(state) state.apply(action) assert quest.is_winning(state) # Build the quest by only providing the winning conditions. quest = Quest( win_events=[Event(conditions=actions[-1].postconditions)]) tmp_world = World.from_facts(chain.initial_state.facts) state = tmp_world.state for action in actions: assert not quest.is_winning(state) state.apply(action) assert quest.is_winning(state)
def is_seq(chain, game_infos): """ Check if we have a theoretical chain in actions. """ seq = MergeAction() room_placeholder = Placeholder('r') action_mapping = KnowledgeBase.default().rules[chain[0].name].match( chain[0]) for ph, var in action_mapping.items(): if ph.type not in ["P", "I"]: seq.mapping[ph] = var seq.const.append(var) for c in chain: c_action_mapping = KnowledgeBase.default().rules[c.name].match(c) # Update our action name seq.name += "_{}".format(c.name.split("/")[0]) # We break a chain if we move rooms if c_action_mapping[room_placeholder] != seq.mapping[room_placeholder]: return False, seq # Update the mapping for ph, var in c_action_mapping.items(): if ph.type not in ["P", "I"]: if ph in seq.mapping and var != seq.mapping[ph]: return False, seq else: seq.mapping[ph] = var # Remove any objects that we no longer use tmp = list(filter(lambda x: x in c_action_mapping.values(), seq.const)) # If all original objects are gone, the seq is broken if len(tmp) == 0: return False, seq # Update our obj list seq.const = tmp return True, seq
def node_to_color(node: EntityNode) -> str: """ Return node's color string as per the entity type. """ color = 'gray' # default for node_type in color_map.keys(): if node.var.type == GENERATOR_DUMMY_TYPE: # handle separately since not in type tree return color elif KnowledgeBase.default().types.is_descendant_of( node.var.type, node_type): color = color_map[node_type] return color
def add_new_state(self, e: Variable): """ Add new state for a given entity variable (except Operation entities). This creates a new EntityNode with g increased by 1. """ if KnowledgeBase.default().types.is_descendant_of(e.type, ['op']): return self.ent_states_map[e] else: new_state_node = EntityNode(var=e, g=(self.curr_state(e).g + 1)) self.ent_states_map[e].append(new_state_node) return new_state_node
def add(self, *entities: List["WorldEntity"], input_slot: str = 'a') -> None: """ Add children to this entity. """ if KnowledgeBase.default().types.is_descendant_of(self.type, "r"): name = "at" # adding a child entity to an operation means setting one of its input slots elif KnowledgeBase.default().types.is_descendant_of(self.type, ["op"]): assert input_slot in ['a', 'b'] name = "pot_multi_{}".format(input_slot) elif KnowledgeBase.default().types.is_descendant_of( self.type, ["c", "I"]): name = "in" elif KnowledgeBase.default().types.is_descendant_of(self.type, "s"): name = "on" else: raise ValueError("Unexpected type {}".format(self.type)) for entity in entities: self.add_fact(name, entity, self) self.content.append(entity)
def __init__(self, surface_generator: SurfaceGenerator) -> None: """ Creates an empty world, with a player and an empty inventory. """ self._entities = {} self.quests = [] self.rooms = [] self.paths = [] self._types_counts = KnowledgeBase.default().types.count(State()) self.player = self.new(type='P') self.inventory = self.new(type='I') self.sg = surface_generator self._game = None self._quests_str = [] self._distractors_facts = [] # define global op types self.globals = {} for t in KnowledgeBase.default().types.descendants('toe'): op_type = self.new(type=t) op_type.add_property('initialized') self.globals[t] = op_type
def test_chaining(): # The following test depends on the available rules, # so instead of depending on what is in rules.txt, # we define the allowed_rules to used. allowed_rules = KnowledgeBase.default().rules.get_matching("take/.*") allowed_rules += KnowledgeBase.default().rules.get_matching("go.*") allowed_rules += KnowledgeBase.default().rules.get_matching("insert.*", "put.*") allowed_rules += KnowledgeBase.default().rules.get_matching("open.*", "close.*") allowed_rules += KnowledgeBase.default().rules.get_matching("lock.*", "unlock.*") allowed_rules += KnowledgeBase.default().rules.get_matching("eat.*") class Options(ChainingOptions): def get_rules(self, depth): return allowed_rules options = Options() options.max_depth = 5 # No possible action since the wooden door is locked and # the player doesn't have the key. state = build_state(locked_door=True) chains = list(get_chains(state, options)) assert len(chains) == 0 # The door is now closed instead of locked. state = build_state(locked_door=False) chains = list(get_chains(state, options)) assert len(chains) == 5 # With more depth. state = build_state(locked_door=False) options.max_depth = 20 chains = list(get_chains(state, options)) assert len(chains) == 9
def maybe_instantiate_variables(rule, mapping, state, max_types_counts=None): types_counts = KnowledgeBase.default().types.count(state) # Instantiate variables if needed try: for ph in rule.placeholders: if mapping.get(ph) is None: name = get_new(ph.type, types_counts, max_types_counts) mapping[ph] = Variable(name, ph.type) except NotEnoughNounsError: return None return rule.instantiate(mapping)
def __init__(self): self.backward = False self.min_depth = 1 self.max_depth = 1 self.min_breadth = 1 self.max_breadth = 1 self.subquests = False self.independent_chains = False self.create_variables = False self.kb = KnowledgeBase.default() self.rng = None self.rules_per_depth = [] self.restricted_types = frozenset()
def check_state(state): fail = Proposition("fail", []) debug = Proposition("debug", []) constraints = state.all_applicable_actions( KnowledgeBase.default().constraints.values()) for constraint in constraints: if state.is_applicable(constraint): # Optimistically delay copying the state copy = state.copy() copy.apply(constraint) if copy.is_fact(fail): return False return True
def test_untriggerable(self): event = EventProgression(self.event, KnowledgeBase.default()) state = self.game.world.state.copy() for action in self.eating_carrot.actions: assert event.triggering_policy != () assert not event.done assert not event.triggered assert not event.untriggerable state.apply(action) event.update(action=action, state=state) assert event.triggering_policy == () assert event.done assert not event.triggered assert event.untriggerable
def __init__(self) -> None: """ Creates an empty world, with a player and an empty inventory. """ self._entities = {} self.quests = [] self.rooms = [] self.paths = [] self.globals = { } # global variables hack for entities that must be accessible from anywhere self._types_counts = KnowledgeBase.default().types.count(State()) self.player = self.new(type='P') self.inventory = self.new(type='I') self.grammar = tw_textlabs.generator.make_grammar() self._game = None self._distractors_facts = []
def get_failing_constraints(state): fail = Proposition("fail", []) failed_constraints = [] constraints = state.all_applicable_actions( KnowledgeBase.default().constraints.values()) for constraint in constraints: if state.is_applicable(constraint): # Optimistically delay copying the state copy = state.copy() copy.apply(constraint) if copy.is_fact(fail): failed_constraints.append(constraint) return failed_constraints
def __init__(self, world: World, grammar: Optional[Grammar] = None, quests: Iterable[Quest] = (), kb: Optional[KnowledgeBase] = None) -> None: """ Args: world: The world to use for the game. quests: The quests to be done in the game. grammar: The grammar to control the text generation. """ self.world = world self.quests = tuple(quests) self.metadata = {} self._objective = None self._infos = self._build_infos() self.kb = kb or KnowledgeBase.default() self.extras = {} self._main_quest = None
def assign_description_to_object(obj, grammar, game_infos): """ Assign a descripton to an object. """ if game_infos[obj.id].desc is not None: return # Already have a description. # Update the object description desc_tag = "#({})_desc#".format(obj.type) game_infos[obj.id].desc = "" if grammar.has_tag(desc_tag): game_infos[obj.id].desc = expand_clean_replace(desc_tag, grammar, obj, game_infos) # If we have an openable object, append an additional description if KnowledgeBase.default().types.is_descendant_of(obj.type, ["c", "d"]): game_infos[obj.id].desc += grammar.expand(" #openable_desc#")
def test_triggering_policy(self): event = EventProgression(self.event, KnowledgeBase.default()) state = self.game.world.state.copy() expected_actions = self.event.actions for i, action in enumerate(expected_actions): assert event.triggering_policy == expected_actions[i:] assert not event.done assert not event.triggered assert not event.untriggerable state.apply(action) event.update(action=action, state=state) assert event.triggering_policy == () assert event.done assert event.triggered assert not event.untriggerable
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 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 deserialize(cls, data: Mapping) -> "Game": """ Creates a `Game` from serialized data. Args: data: Serialized data with the needed information to build a `Game` object. """ world = World.deserialize(data["world"]) game = cls(world) # game.grammar = Grammar(data["grammar"]) game.quests = tuple([Quest.deserialize(d) for d in data["quests"]]) game._infos = {k: EntityInfo.deserialize(v) for k, v in data["infos"]} game.kb = KnowledgeBase.deserialize(data["KB"]) game.metadata = data.get("metadata", {}) game._objective = data.get("objective", None) game.extras = data.get("extras", {}) if "main_quest" in data: game._main_quest = Quest.deserialize(data["main_quest"]) return game
def expand_clean_replace(symbol, grammar, obj, game_infos): """ Return a cleaned/keyword replaced symbol. """ obj_infos = game_infos[obj.id] phrase = grammar.expand(symbol) phrase = phrase.replace("(obj)", obj_infos.id) phrase = phrase.replace("(name)", obj_infos.name) phrase = phrase.replace( "(name-n)", obj_infos.noun if obj_infos.adj is not None else obj_infos.name) phrase = phrase.replace( "(name-adj)", obj_infos.adj if obj_infos.adj is not None else grammar.expand("#ordinary_adj#")) if obj.type != "": phrase = phrase.replace( "(name-t)", KnowledgeBase.default().types.get_description(obj.type)) else: assert False, "Does this even happen?" return fix_determinant(phrase)
def _process_objects(self) -> None: for fact in self.facts: if KnowledgeBase.default().types.is_descendant_of( fact.arguments[0].type, 'r'): continue # Skip room facts. obj = self._get_entity(fact.arguments[0]) obj.add_related_fact(fact) if fact.name == "match": other_obj = self._get_entity(fact.arguments[1]) obj.matching_entity_id = fact.arguments[1].name other_obj.matching_entity_id = fact.arguments[0].name if fact.name in [ "in", "on", "at", "potential", "pot_multi_a", "pot_multi_b" ]: holder = self._get_entity(fact.arguments[1]) holder.content.append(obj) if fact.arguments[0].type == "P": self._player_room = holder
def __init__(self, event: Event, kb: KnowledgeBase) -> None: """ Args: quest: The quest to keep track of its completion. """ self._kb = kb or KnowledgeBase.default() self.event = event self._triggered = False self._untriggerable = False self._policy = () # Build a tree representation of the quest. self._tree = ActionDependencyTree(kb=self._kb, element_type=ActionDependencyTreeElement) if len(event.actions) > 0: self._tree.push(event.condition) for action in event.actions[::-1]: self._tree.push(action) self._policy = event.actions + (event.condition,)
def __init__(self, **kwargs): super(LabQuestGenerator, self).__init__(**kwargs) # self.max_nb_op_use = 2 self.base_materials = self.get_base_materials() self.states = {} self.descs_per_ent = None # types and actions we want to ignore (for example to set later independently, like descriptors) self.ignore_act_list = ['locate', 'drop'] self.ignore_types_list = KnowledgeBase.default().types.descendants( 'dsc') + ['dsc'] self.ignore_sigs = [Signature('in', ['tlq_op', 'I'])] self.q.set_scorer(self.lab_quest_scorer) # validity checks for every search state self.node_checks = { 'check_depth': self.check_depth, 'check_node_state': self.check_node_state, # 'ignore_types': self.check_ignore_type, 'ignore_acts': self.check_ignore_act, 'ignore_sigs': self.check_ignore_sigs } # goal node checks self.goal_check_fns = { 'all_ops_used': self.all_ops_used, 'all_mats_merged': self.all_mats_merged, 'end_after_op': self.is_after_op_state, } # feature extractor functions for a given state w/ their weight self.feature_extractor_fns = { 'is_new_state': (self.check_existing_state, 2), # 1 if state is new, 0 if not 'nb_ops_used': (self.nb_ops_used, 1), # number of ops already used 'nb_mat_ccs': (self.nb_mat_ccs, -1 ) # 1 if all precursor/solvent materials used in synthesis route }