def test_winning_policy(self): kb = KnowledgeBase.default() quest = QuestProgression(self.quest, kb) quest = self._apply_actions_to_quest(quest.winning_policy, quest) assert quest.completed assert not quest.failed assert quest.winning_policy is None # Winning policy should be the shortest one leading to a winning event. state = self.game.world.state.copy() quest = QuestProgression(self.quest, KnowledgeBase.default()) for i, action in enumerate(self.eventB.actions): if i < 2: assert quest.winning_policy == self.eventA.actions else: # After taking the lettuce and putting it in the chest, # QuestB becomes the shortest one to complete. assert quest.winning_policy == self.eventB.actions[i:] assert not quest.done state.apply(action) quest.update(action, state) assert quest.done assert quest.completed assert not quest.failed assert quest.winning_policy is None
def test_reloading_game_with_custom_kb(): twl = KnowledgeBase.default().logic._document twl += """ type customobj : o { inform7 { type { kind :: "custom-obj-like"; } } } """ logic = GameLogic.parse(twl) options = GameOptions() options.kb = KnowledgeBase(logic, "") M = GameMaker(options) room = M.new_room("room") M.set_player(room) custom_obj = M.new(type='customobj', name='customized object') M.inventory.add(custom_obj) commands = ["drop customized object"] quest = M.set_quest_from_commands(commands) assert quest.commands == tuple(commands) game = M.build() assert game == Game.deserialize(game.serialize())
def test_going_through_door(): P = Variable("P", "P") room = Variable("room", "r") kitchen = Variable("kitchen", "r") state = State(KnowledgeBase.default().logic) state.add_facts([ Proposition("at", [P, room]), Proposition("north_of", [kitchen, room]), Proposition("free", [kitchen, room]), Proposition("free", [room, kitchen]), Proposition("south_of", [room, kitchen]) ]) options = ChainingOptions() options.backward = True options.max_depth = 3 options.max_length = 3 options.subquests = True options.create_variables = True options.rules_per_depth = [ [KnowledgeBase.default().rules["take/c"], KnowledgeBase.default().rules["take/s"]], KnowledgeBase.default().rules.get_matching("go.*"), [KnowledgeBase.default().rules["open/d"]], ] chains = list(get_chains(state, options)) assert len(chains) == 18
def test_get_new(): rng = np.random.RandomState(1234) types_counts = {t: rng.randint(2, 10) for t in KnowledgeBase.default().types} orig_types_counts = deepcopy(types_counts) for t in KnowledgeBase.default().types: name = get_new(t, types_counts) splits = name.split("_") assert splits[0] == t assert int(splits[1]) == orig_types_counts[t] assert types_counts[t] == orig_types_counts[t] + 1
def test_serialization_deserialization(): rule = KnowledgeBase.default().rules["go/east"] mapping = { Placeholder("r'"): Variable("room1", "r"), Placeholder("r"): Variable("room2"), } mapping.update(KnowledgeBase.default().types.constants_mapping) action = rule.instantiate(mapping) infos = action.serialize() action2 = Action.deserialize(infos) assert action == action2
def test_failed(self): quest = QuestProgression(self.quest, KnowledgeBase.default()) quest = self._apply_actions_to_quest(self.eating_carrot.actions, quest) assert not quest.completed assert quest.failed assert quest.winning_policy is None quest = QuestProgression(self.quest, KnowledgeBase.default()) quest = self._apply_actions_to_quest(self.eating_lettuce.actions, quest) assert not quest.completed assert quest.failed assert quest.winning_policy is None
def test_rules(): # Make sure the number of basic rules matches the number # of rules in rules.txt basic_rules = [ k for k in KnowledgeBase.default().rules.keys() if "-" not in k ] assert len(basic_rules) == 19 for rule in KnowledgeBase.default().rules.values(): infos = rule.serialize() loaded_rule = Rule.deserialize(infos) assert loaded_rule == rule
def test_completed(self): quest = QuestProgression(self.quest, KnowledgeBase.default()) quest = self._apply_actions_to_quest(self.eventA.actions, quest) assert quest.completed assert not quest.failed assert quest.winning_policy is None # Alternative winning strategy. quest = QuestProgression(self.quest, KnowledgeBase.default()) quest = self._apply_actions_to_quest(self.eventB.actions, quest) assert quest.completed assert not quest.failed assert quest.winning_policy is None
def add(self, *entities: List["WorldEntity"]) -> None: """ Add children to this entity. """ if KnowledgeBase.default().types.is_descendant_of(self.type, "r"): name = "at" 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): 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.fixed_mapping = KnowledgeBase.default().types.constants_mapping self.rng = None self.logic = KnowledgeBase.default().logic self.rules_per_depth = [] self.restricted_types = frozenset()
def remove(self, *entities): if KnowledgeBase.default().types.is_descendant_of(self.type, "r"): name = "at" 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.remove_fact(name, entity, self) self.content.remove(entity) entity.parent = None
def new(self, type: str, name: Optional[str] = None, desc: Optional[str] = None) -> Union[WorldEntity, WorldRoom]: """ Creates new entity given its type. Args: type: The type of the entity. name: The name of the entity. desc: The description of the entity. Returns: The newly created entity. * If the `type` is `'r'`, then a `WorldRoom` object is returned. * Otherwise, a `WorldEntity` is returned. """ var_id = type if not KnowledgeBase.default().types.is_constant(type): var_id = get_new(type, self._types_counts) var = Variable(var_id, type) if type == "r": entity = WorldRoom(var, name, desc) self.rooms.append(entity) else: entity = WorldEntity(var, name, desc) self._entities[var_id] = entity return entity
def build_state(locked_door=False): # Set up a world with two rooms and a few objecs. P = Variable("P") I = Variable("I") bedroom = Variable("bedroom", "r") kitchen = Variable("kitchen", "r") rusty_key = Variable("rusty key", "k") small_key = Variable("small key", "k") wooden_door = Variable("wooden door", "d") chest = Variable("chest", "c") cabinet = Variable("cabinet", "c") robe = Variable("robe", "o") state = State(KnowledgeBase.default().logic, [ Proposition("at", [P, bedroom]), Proposition("south_of", [kitchen, bedroom]), Proposition("north_of", [bedroom, kitchen]), Proposition("link", [bedroom, wooden_door, kitchen]), Proposition("link", [kitchen, wooden_door, bedroom]), Proposition("locked" if locked_door else "closed", [wooden_door]), Proposition("in", [rusty_key, I]), Proposition("match", [rusty_key, chest]), Proposition("locked", [chest]), Proposition("at", [chest, kitchen]), Proposition("in", [small_key, chest]), Proposition("match", [small_key, cabinet]), Proposition("locked", [cabinet]), Proposition("at", [cabinet, bedroom]), Proposition("in", [robe, cabinet]), ]) return state
def __init__(self) -> None: self._state = State(KnowledgeBase.default().logic) self._entities = OrderedDict() self._rooms = [] self._objects = [] self._update() self._player_room = None
def door(self, door: WorldEntity) -> None: if door is not None and not KnowledgeBase.default( ).types.is_descendant_of(door.type, "d"): msg = "Expecting a WorldEntity of 'door' type." raise TypeError(msg) self._door = door
def __init__(self, options: Union[GrammarOptions, Mapping[str, Any]] = {}, rng: Optional[RandomState] = None): """ Arguments: options: For customizing text generation process (see :py:class:`textworld.generator.GrammarOptions <textworld.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 __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 = {} # Check if we can derive a global winning policy from the quests. self.main_quest = None policy = GameProgression(self).winning_policy if policy: win_event = Event(actions=policy) self.main_quest = Quest(win_events=[win_event]) self.change_grammar(grammar)
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. """ version = data.get("version", cls._SERIAL_VERSION) if version != cls._SERIAL_VERSION: raise ValueError( "Cannot deserialize a TextWorld version {} game, expected version {}" .format(version, cls._SERIAL_VERSION)) 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 __init__(self, kb: Optional[KnowledgeBase] = None) -> None: self.kb = kb or KnowledgeBase.default() self._state = State(self.kb.logic) self._entities = OrderedDict() self._rooms = [] self._objects = [] self._update() self._player_room = None
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(kb.logic, [ 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 options.max_length = 3 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 is None options.min_breadth = 1 options.create_variables = True state = State(kb.logic) chains = list(get_chains(state, options)) assert len(chains) == 5
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 = textworld.start(game_file) env.reset() game_state, _, done = env.step("look") assert not done assert not game_state.won game_state, _, done = env.step(event.commands[0]) assert done assert game_state.won
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.max_length = 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 __init__(self, options: Union[GrammarOptions, Mapping] = {}, rng: Optional[RandomState] = None): """ Create a grammar. Arguments: options: For customizing text generation process (see :py:class:`textworld.generator.GrammarOptions <textworld.generator.text_grammar.GrammarOptions>` for the list of available options). :param 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 grammar_contents = [] # Load the object names file files = os.listdir(KnowledgeBase.default().text_grammars_path) files = [ f for f in files if f.startswith(self.theme + "_") and f.endswith(".twg") ] for filename in files: with open( pjoin(KnowledgeBase.default().text_grammars_path, filename)) as f: grammar_contents.extend(f.readlines()) self._parse(grammar_contents)
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 test_room_connections(): kb = KnowledgeBase.default() room0 = Variable("room0", "r") room1 = Variable("room1", "r") room2 = Variable("room2", "r") # Only one connection can exist between two rooms. # r1 # | # r0 - r1 state = State(kb.logic, [ Proposition("north_of", [room1, room0]), Proposition("south_of", [room0, room1]), Proposition("east_of", [room1, room0]), Proposition("west_of", [room0, room1]) ]) assert not check_state(state) # Non Cartesian layout are allowed. # r1 # | # r0 - r2 - r1 state = State(kb.logic, [ Proposition("north_of", [room1, room0]), Proposition("south_of", [room0, room1]), Proposition("east_of", [room2, room0]), Proposition("west_of", [room0, room2]), Proposition("east_of", [room1, room2]), Proposition("west_of", [room2, room1]) ]) assert check_state(state) # A room cannot have more than 4 'link' propositions. room3 = Variable("room3", "r") room4 = Variable("room4", "r") room5 = Variable("room5", "r") door1 = Variable("door1", "d") door2 = Variable("door2", "d") door3 = Variable("door3", "d") door4 = Variable("door4", "d") door5 = Variable("door5", "d") state = State(kb.logic, [ Proposition("link", [room0, door1, room1]), Proposition("link", [room0, door2, room2]), Proposition("link", [room0, door3, room3]), Proposition("link", [room0, door4, room4]), Proposition("link", [room0, door5, room5]) ]) assert not check_state(state)
def test_count(self): rng = np.random.RandomState(1234) types_counts = {t: rng.randint(2, 10) for t in self.types.variables} state = State(KnowledgeBase.default().logic) for t in self.types.variables: v = Variable(get_new(t, types_counts), t) state.add_fact(Proposition("dummy", [v])) counts = self.types.count(state) for t in self.types.variables: assert counts[t] == types_counts[t], (counts[t], types_counts[t])
def test_get_reverse_action(): kb = KnowledgeBase.default() for rule in kb.rules.values(): empty_state = State(KnowledgeBase.default().logic) action = maybe_instantiate_variables(rule, kb.types.constants_mapping.copy(), empty_state) r_action = kb.get_reverse_action(action) if rule.name.startswith("eat"): assert r_action is None else: assert r_action is not None # Check if that when applying the reverse rule we can re-obtain # the previous state. state = State(KnowledgeBase.default().logic, action.preconditions) new_state = state.copy() assert new_state.apply(action) r_state = new_state.copy() r_state.apply(r_action) assert state == r_state
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 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)