def test_going_through_door(): P = Variable("P", "P") room = Variable("room", "r") kitchen = Variable("kitchen", "r") state = State() 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.subquests = True options.create_variables = True options.rules_per_depth = [ [data.get_rules()["take/c"], data.get_rules()["take/s"]], data.get_rules().get_matching("go.*"), [data.get_rules()["open/d"]], ] chains = list(get_chains(state, options)) assert len(chains) == 18
def test_rules(): # Make sure the number of basic rules matches the number # of rules in rules.txt basic_rules = [k for k in data.get_rules().keys() if "-" not in k] assert len(basic_rules) == 19 for rule in data.get_rules().values(): infos = rule.serialize() loaded_rule = Rule.deserialize(infos) assert loaded_rule == rule
def test_going_through_door(): P = Variable("P", "P") room = Variable("room", "r") kitchen = Variable("kitchen", "r") state = State() 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]) ]) # Sample quests. chains = [] rules_per_depth = { 0: [data.get_rules()["take/c"], data.get_rules()["take/s"]], 1: data.get_rules().get_matching("go.*"), 2: [data.get_rules()["open/d"]] } tree_of_possible = chaining.get_chains(state, max_depth=3, allow_partial_match=True, exceptions=[], rules_per_depth=rules_per_depth, backward=True) chains = list(tree_of_possible.traverse_preorder(subquests=True)) # chaining.print_chains(chains) # 1. take/c(P, room, c_0, o_0, I) # 2. take/c(P, room, c_0, o_0, I) -> go/north(P, r_0, room) # 3. take/c(P, room, c_0, o_0, I) -> go/north(P, r_0, room) -> open/d(P, r_0, d_0, room) # 4. take/c(P, room, c_0, o_0, I) -> go/south(P, kitchen, room) # 5. take/c(P, room, c_0, o_0, I) -> go/south(P, kitchen, room) -> open/d(P, kitchen, d_0, room) # 6. take/c(P, room, c_0, o_0, I) -> go/east(P, r_0, room) # 7. take/c(P, room, c_0, o_0, I) -> go/east(P, r_0, room) -> open/d(P, r_0, d_0, room) # 8. take/c(P, room, c_0, o_0, I) -> go/west(P, r_0, room) # 9. take/c(P, room, c_0, o_0, I) -> go/west(P, r_0, room) -> open/d(P, r_0, d_0, room) # 10. take/s(P, room, s_0, o_0, I) # 11. take/s(P, room, s_0, o_0, I) -> go/north(P, r_0, room) # 12. take/s(P, room, s_0, o_0, I) -> go/north(P, r_0, room) -> open/d(P, r_0, d_0, room) # 13. take/s(P, room, s_0, o_0, I) -> go/south(P, kitchen, room) # 14. take/s(P, room, s_0, o_0, I) -> go/south(P, kitchen, room) -> open/d(P, kitchen, d_0, room) # 15. take/s(P, room, s_0, o_0, I) -> go/east(P, r_0, room) # 16. take/s(P, room, s_0, o_0, I) -> go/east(P, r_0, room) -> open/d(P, r_0, d_0, room) # 17. take/s(P, room, s_0, o_0, I) -> go/west(P, r_0, room) # 18. take/s(P, room, s_0, o_0, I) -> go/west(P, r_0, room) -> open/d(P, r_0, d_0, room) assert len(chains) == 18
def test_win_action(self): g_rng.set_seed(2018) map_ = make_small_map(n_rooms=5, possible_door_states=["open"]) world = World.from_map(map_) for max_depth in range(1, 3): for rule in data.get_rules().values(): chain = sample_quest(world.state, rng=None, max_depth=max_depth, nb_retry=30, allow_partial_match=True, backward=True, rules_per_depth={0: [rule]}, exceptions=["r"]) assert len(chain) == max_depth, rule.name # Build the quest by providing the actions. actions = [c.action for c in chain] quest = Quest(actions) tmp_world = World.from_facts(chain[0].state.facts) state = tmp_world.state for action in actions: assert not state.is_applicable(quest.win_action) state.apply(action) assert state.is_applicable(quest.win_action) # Build the quest by only providing the winning conditions. quest = Quest(actions=None, winning_conditions=actions[-1].postconditions) tmp_world = World.from_facts(chain[0].state.facts) state = tmp_world.state for action in actions: assert not state.is_applicable(quest.win_action) state.apply(action) assert state.is_applicable(quest.win_action)
def test_serialization_deserialization(): rule = data.get_rules()["go/east"] mapping = { Placeholder("r'"): Variable("room1", "r"), Placeholder("r"): Variable("room2"), } mapping.update(data.get_types().constants_mapping) action = rule.instantiate(mapping) infos = action.serialize() action2 = Action.deserialize(infos) assert action == action2
def is_seq(chain, game_infos): """ Check if we have a theoretical chain in actions. """ seq = MergeAction() room_placeholder = Placeholder('r') action_mapping = data.get_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 = data.get_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_to_networkx(): # Below copied from test_chaining() allowed_rules = data.get_rules().get_matching("take/.*") allowed_rules += data.get_rules().get_matching("go.*") allowed_rules += data.get_rules().get_matching("insert.*", "put.*") allowed_rules += data.get_rules().get_matching("open.*", "close.*") allowed_rules += data.get_rules().get_matching("lock.*", "unlock.*") allowed_rules += data.get_rules().get_matching("eat.*") # No possible action since the wooden door is locked and # the player doesn't have the key. state = build_state(locked_door=True) tree = chaining.get_chains( state, max_depth=5, rules_per_depth={i: allowed_rules for i in range(5)}) chains = list(tree.traverse_preorder()) assert len(chains) == 0 # The door is now closed instead of locked. state = build_state(locked_door=False) tree = chaining.get_chains( state, max_depth=5, rules_per_depth={i: allowed_rules for i in range(5)}) G, labels = tree.to_networkx() assert G is not None assert labels is not None assert len(labels) > 0
def test_print_chains_backwards(): from textworld.generator import print_chains allowed_rules = data.get_rules().get_matching("take/.*") allowed_rules += data.get_rules().get_matching("go.*") allowed_rules += data.get_rules().get_matching("insert.*", "put.*") allowed_rules += data.get_rules().get_matching("open.*", "close.*") allowed_rules += data.get_rules().get_matching("lock.*", "unlock.*") allowed_rules += data.get_rules().get_matching("eat.*") # No possible action since the wooden door is locked and # the player doesn't have the key. state = build_state(locked_door=True) tree = chaining.get_chains( state, max_depth=5, rules_per_depth={i: allowed_rules for i in range(5)}) chains = list(tree.traverse_preorder()) assert len(chains) == 0 # The door is now closed instead of locked. state = build_state(locked_door=False) tree = chaining.get_chains( state, max_depth=5, rules_per_depth={i: allowed_rules for i in range(5)}) chains = list(tree.traverse_preorder()) # Only validates that printing chains does not raise exception. with testing.capture_stdout() as stdout: print_chains(chains, backward=True) stdout.seek(0) assert len(stdout.read()) > 0
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 = data.get_rules().get_matching("take/.*") allowed_rules += data.get_rules().get_matching("go.*") allowed_rules += data.get_rules().get_matching("insert.*", "put.*") allowed_rules += data.get_rules().get_matching("open.*", "close.*") allowed_rules += data.get_rules().get_matching("lock.*", "unlock.*") allowed_rules += data.get_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 make_game(options: GameOptions) -> Game: """ Make a game (map + objects + quest). Arguments: options: For customizing the game generation (see :py:class:`textworld.GameOptions <textworld.generator.game.GameOptions>` for the list of available options). Returns: Generated game. """ rngs = options.rngs # Generate only the map for now (i.e. without any objects) world = make_world(options.nb_rooms, nb_objects=0, rngs=rngs) # Sample a quest. chaining_options = options.chaining.copy() chaining_options.rules_per_depth = [ get_rules().get_matching("^(?!go.*).*") ] chaining_options.backward = True chaining_options.create_variables = True chaining_options.rng = rngs['quest'] chaining_options.restricted_types = {"r", "d"} chain = sample_quest(world.state, chaining_options) subquests = [] for i in range(1, len(chain.nodes)): if chain.nodes[i].breadth != chain.nodes[i - 1].breadth: quest = Quest(chain.actions[:i]) subquests.append(quest) quest = Quest(chain.actions) subquests.append(quest) # Set the initial state required for the quest. world.state = chain.initial_state # Add distractors objects (i.e. not related to the quest) world.populate(options.nb_objects, rng=rngs['objects']) grammar = make_grammar(options.grammar, rng=rngs['grammar']) game = make_game_with(world, subquests, grammar) game.change_grammar(grammar) game.metadata["uuid"] = options.uuid return game
def main(): args = parse_args() P = Variable("P") # I = Variable("I") room = Variable("room", "r") kitchen = Variable("kitchen", "r") state = [ Proposition("at", [P, room]), Proposition("north_of", [kitchen, room]), Proposition("free", [kitchen, room]), Proposition("free", [room, kitchen]), Proposition("south_of", [room, kitchen]) ] # Sample quests. rng = np.random.RandomState(args.seed) chains = [] # rules_per_depth = {0: [data.get_rules()["take/c"], data.get_rules()["take/s"]], # 1: [data.get_rules()["open/c"]], # } rules_per_depth = { 0: [data.get_rules()["eat"]], 1: data.get_rules().get_matching("take/s.*"), 2: data.get_rules().get_matching("go.*"), 3: [data.get_rules()["open/d"]], 4: [data.get_rules()["unlock/d"]], 5: data.get_rules().get_matching("take/s.*", "take/c.*") } for i in range(args.nb_quests): chain = textworld.logic.sample_quest(state, rng, max_depth=args.quest_length, allow_partial_match=True, exceptions=[], rules_per_depth=rules_per_depth, backward=True) chains.append(chain[::-1]) print_chains(chains, verbose=args.verbose) actions_tree = build_tree_from_chains(chains) # Convert tree to networkx graph/tree filename = "sample_tree.svg" G, labels = actions_tree.to_networkx() if len(G) > 0: tree = nx.bfs_tree(G, actions_tree.no) save_graph_to_svg(tree, labels, filename, backward=True) else: try: os.remove(filename) except: pass
def __init__(self, world: World, grammar: Optional[Grammar] = None, quests: Optional[List[Quest]] = None) -> None: """ Args: world: The world to use for the game. quests: The quests to done in the game. grammar: The grammar to control the text generation. """ self.world = world self.state = world.state.copy() # Current state of the game. self.grammar = grammar self.quests = [] if quests is None else quests self.metadata = {} self._infos = self._build_infos() self._rules = data.get_rules() self._types = data.get_types()
def test_backward_chaining(): P = Variable("P", "P") room = Variable("room", "r") kitchen = Variable("kitchen", "r") state = State([ Proposition("at", [P, room]), Proposition("north_of", [kitchen, room]), Proposition("south_of", [room, kitchen]), ]) rules_per_depth = { 0: [data.get_rules()["take/c"], data.get_rules()["take/s"]], 1: [data.get_rules()["open/c"]] } tree_of_possible = chaining.get_chains(state, max_depth=2, allow_partial_match=True, exceptions=['d'], rules_per_depth=rules_per_depth, backward=True) chains = list(tree_of_possible.traverse_preorder(subquests=True)) assert len(chains) == 3 for chain in chains: for depth, action in enumerate(chain): assert action.action.name in [ rule.name for rule in rules_per_depth[depth] ] rules_per_depth = { 0: [data.get_rules()["put"]], 1: [data.get_rules()["go/north"]], 2: [data.get_rules()["take/c"]] } tree_of_possible = chaining.get_chains(state, max_depth=3, allow_partial_match=True, exceptions=['d'], rules_per_depth=rules_per_depth, backward=True) chains = list(tree_of_possible.traverse_preorder(subquests=True)) assert len(chains) == 3 for chain in chains: for depth, action in enumerate(chain): assert action.action.name in [ rule.name for rule in rules_per_depth[depth] ]
def _get_chains(state, root=None, max_depth=1, allow_parallel_chains=False, allow_partial_match=False, rng=None, exceptions=[], max_types_counts=None, rules_per_depth={}, backward=False): root = ActionTree(state) if root is None else root openset = [root] while len(openset) > 0: parent = openset.pop() rules = rules_per_depth.get(parent.depth, data.get_rules().values()) assignments = _get_all_assignments(parent.state, rules=rules, partial=allow_partial_match, constrained_types=exceptions, backward=backward) if rng is not None: rng.shuffle(assignments) for rule, mapping in assignments: child = _try_instantiation(rule, mapping, parent, allow_parallel_chains, max_types_counts, backward) if child: child.set_parent(parent) parent.children.append(child) if len(parent.children) == 0: yield parent.get_path() if parent.depth + 1 < max_depth: openset += parent.children[::-1] else: for child in parent.children: yield child.get_path()
def test_win_action(self): g_rng.set_seed(2018) map_ = make_small_map(n_rooms=5, possible_door_states=["open"]) world = World.from_map(map_) for max_depth in range(1, 3): for rule in data.get_rules().values(): 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 if len(actions) != max_depth: print(chain) assert len(actions) == max_depth, rule.name quest = Quest(actions) tmp_world = World.from_facts(chain.initial_state.facts) state = tmp_world.state for action in actions: assert not state.is_applicable(quest.win_action) state.apply(action) assert state.is_applicable(quest.win_action) # Build the quest by only providing the winning conditions. quest = Quest(actions=None, winning_conditions=actions[-1].postconditions) tmp_world = World.from_facts(chain.initial_state.facts) state = tmp_world.state for action in actions: assert not state.is_applicable(quest.win_action) state.apply(action) assert state.is_applicable(quest.win_action)
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_) for rule in data.get_rules().values(): chain = sample_quest(world.state, rng=None, max_depth=1, nb_retry=20, allow_partial_match=True, backward=True, rules_per_depth={0: [rule]}, exceptions=["r"]) assert len(chain) > 0, rule.name quest = Quest([c.action for c in chain]) # Set the initial state required for the quest. tmp_world = World.from_facts(chain[0].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, game_name, games_folder=tmpdir) env = textworld.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(quest.commands[0]) assert done assert game_state.has_won
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_) for rule in data.get_rules().values(): 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 quest = Quest(chain.actions) # 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, game_name, games_folder=tmpdir) env = textworld.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(quest.commands[0]) assert done assert game_state.has_won
def test_get_reverse_rules(verbose=False): for rule in data.get_rules().values(): r_rule = data.get_reverse_rules(rule) if verbose: print(rule, r_rule) if rule.name.startswith("eat"): assert r_rule is None else: # Check if that when applying the reverse rule we can reobtain # the previous state. action = maybe_instantiate_variables( rule, data.get_types().constants_mapping.copy(), State([])) state = State(action.preconditions) new_state = state.copy() assert new_state.apply(action) assert r_rule is not None actions = list( new_state.all_applicable_actions( [r_rule], data.get_types().constants_mapping)) if len(actions) != 1: print(actions) print(r_rule) print(new_state) print( list( new_state.all_instantiations( r_rule, data.get_types().constants_mapping))) assert len(actions) == 1 r_state = new_state.copy() r_state.apply(actions[0]) assert state == r_state
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 = data.get_rules().get_matching("take/.*") allowed_rules += data.get_rules().get_matching("go.*") allowed_rules += data.get_rules().get_matching("insert.*", "put.*") allowed_rules += data.get_rules().get_matching("open.*", "close.*") allowed_rules += data.get_rules().get_matching("lock.*", "unlock.*") allowed_rules += data.get_rules().get_matching("eat.*") # No possible action since the wooden door is locked and # the player doesn't have the key. state = build_state(locked_door=True) tree = chaining.get_chains( state, max_depth=5, rules_per_depth={i: allowed_rules for i in range(5)}) chains = list(tree.traverse_preorder()) assert len(chains) == 0 # The door is now closed instead of locked. state = build_state(locked_door=False) tree = chaining.get_chains( state, max_depth=5, rules_per_depth={i: allowed_rules for i in range(5)}) chains = list(tree.traverse_preorder()) # chaining.print_chains(chains) assert len(chains) == 5 # With more depth. state = build_state(locked_door=False) tree = chaining.get_chains( state, max_depth=20, rules_per_depth={i: allowed_rules for i in range(20)}) chains = list(tree.traverse_preorder()) assert len(chains) == 9
def test_backward_chaining(): P = Variable("P", "P") room = Variable("room", "r") kitchen = Variable("kitchen", "r") state = State([ Proposition("at", [P, room]), Proposition("north_of", [kitchen, room]), Proposition("south_of", [room, kitchen]), ]) options = ChainingOptions() options.backward = True options.max_depth = 2 options.subquests = True options.create_variables = True options.rules_per_depth = [ [data.get_rules()["take/c"], data.get_rules()["take/s"]], [data.get_rules()["open/c"]], ] options.restricted_types = {"d"} chains = list(get_chains(state, options)) assert len(chains) == 3 options = ChainingOptions() options.backward = True options.max_depth = 3 options.subquests = True options.create_variables = True options.rules_per_depth = [ [data.get_rules()["put"]], [data.get_rules()["go/north"]], [data.get_rules()["take/c"]], ] options.restricted_types = {"d"} chains = list(get_chains(state, options)) assert len(chains) == 3
def generate_instruction(action, grammar, game_infos, world, counts): """ Generate text instruction for a specific action. """ # Get the more precise command tag. cmd_tag = "#{}#".format(action.name) if not grammar.has_tag(cmd_tag): cmd_tag = "#{}#".format(action.name.split("-")[0]) if not grammar.has_tag(cmd_tag): cmd_tag = "#{}#".format(action.name.split("-")[0].split("/")[0]) separator_tag = "#action_separator_{}#".format(action.name) if not grammar.has_tag(separator_tag): separator_tag = "#action_separator_{}#".format(action.name.split("-")[0]) if not grammar.has_tag(separator_tag): separator_tag = "#action_separator_{}#".format(action.name.split("-")[0].split("/")[0]) if not grammar.has_tag(separator_tag): separator_tag = "#action_separator#" if not grammar.has_tag(separator_tag): separator = "" else: separator = grammar.expand(separator_tag) desc = grammar.expand(cmd_tag) # We generate a custom mapping. mapping = OrderedDict() if isinstance(action, MergeAction): action_mapping = action.mapping else: action_mapping = data.get_rules()[action.name].match(action) for ph, var in action_mapping.items(): if var.type == "r": # We can use a simple description for the room r = world.find_room_by_id(var.name) # Match on 'name' if r is None: mapping[ph.name] = '' else: mapping[ph.name] = game_infos[r.id].name elif var.type in ["P", "I"]: continue else: # We want a more complex description for the objects obj = world.find_object_by_id(var.name) obj_infos = game_infos[obj.id] if grammar.flags.ambiguous_instructions: assert False, "not tested" choices = [] for t in ["adj", "noun", "type"]: if counts[t][getattr(obj_infos, t)] <= 1: if t == "noun": choices.append(getattr(obj_infos, t)) elif t == "type": choices.append(data.get_types().get_description(getattr(obj_infos, t))) else: # For adj, we pick an abstraction on the type atype = data.get_types().get_description(grammar.rng.choice(data.get_types().get_ancestors(obj.type))) choices.append("{} {}".format(getattr(obj_infos, t), atype)) # If we have no possibilities, use the name (ie. prioritize abstractions) if len(choices) == 0: choices.append(obj_infos.name) mapping[ph.name] = grammar.rng.choice(choices) else: mapping[ph.name] = obj_infos.name # Replace the keyword with one of the possibilities for keyword in re.findall(r'[(]\S*[)]', desc + separator): for key in keyword[1:-1].split("|"): if key in mapping: desc = desc.replace(keyword, mapping[key]) separator = separator.replace(keyword, mapping[key]) return desc, separator
def _get_name_mapping(action): mapping = data.get_rules()[action.name].match(action) return { ph.name: var_infos[var.name].name for ph, var in mapping.items() }
def make_game(mode: str, options: GameOptions) -> textworld.Game: """ Make a Treasure Hunter game. Arguments: mode: Mode for the game where * `'easy'`: rooms are all empty except where the two objects are placed. Also, connections between rooms have no door. * `'medium'`: adding closed doors and containers that might need to be open in order to find the object. * `'hard'`: adding locked doors and containers (necessary keys will in the inventory) that might need to be unlocked (and open) in order to find the object. options: For customizing the game generation (see :py:class:`textworld.GameOptions <textworld.generator.game.GameOptions>` for the list of available options). Returns: Generated game. """ metadata = {} # Collect infos for reproducibility. metadata["desc"] = "Treasure Hunter" metadata["mode"] = mode metadata["seeds"] = options.seeds metadata["world_size"] = options.nb_rooms metadata["quest_length"] = options.quest_length metadata["grammar_flags"] = options.grammar.encode() rngs = options.rngs rng_map = rngs['seed_map'] rng_objects = rngs['seed_objects'] rng_quest = rngs['seed_quest'] rng_grammar = rngs['seed_grammar'] modes = ["easy", "medium", "hard"] if mode == "easy": door_states = None n_distractors = 0 elif mode == "medium": door_states = ["open", "closed"] n_distractors = 10 elif mode == "hard": door_states = ["open", "closed", "locked"] n_distractors = 20 # Generate map. map_ = textworld.generator.make_map(n_rooms=options.nb_rooms, rng=rng_map, possible_door_states=door_states) assert len(map_.nodes()) == options.nb_rooms world = World.from_map(map_) # Randomly place the player. starting_room = None if len(world.rooms) > 1: starting_room = rng_map.choice(world.rooms) world.set_player_room(starting_room) # Add object the player has to pick up. types_counts = data.get_types().count(world.state) obj_type = data.get_types().sample(parent_type='o', rng=rng_objects, include_parent=True) var_id = get_new(obj_type, types_counts) right_obj = Variable(var_id, obj_type) world.add_fact(Proposition("in", [right_obj, world.inventory])) # Add containers and supporters to the world. types_counts = data.get_types().count(world.state) objects = [] distractor_types = uniquify(['c', 's'] + data.get_types().descendants('c') + data.get_types().descendants('s')) for i in range(n_distractors): obj_type = rng_objects.choice(distractor_types) var_id = get_new(obj_type, types_counts) # This update the types_counts. objects.append(Variable(var_id, obj_type)) world.populate_with(objects, rng=rng_objects) # Add object the player should not pick up. types_counts = data.get_types().count(world.state) obj_type = data.get_types().sample(parent_type='o', rng=rng_objects, include_parent=True) var_id = get_new(obj_type, types_counts) wrong_obj = Variable(var_id, obj_type) # Place it anywhere in the world. world.populate_with([wrong_obj], rng=rng_objects) # Generate a quest that finishes by taking something (i.e. the right # object since it's the only one in the inventory). options.chaining.rules_per_depth = [data.get_rules().get_matching("take.*")] options.chaining.backward = True options.chaining.rng = rng_quest #options.chaining.restricted_types = exceptions #exceptions = ["r", "c", "s", "d"] if mode == "easy" else ["r"] chain = textworld.generator.sample_quest(world.state, options.chaining) # Add objects needed for the quest. world.state = chain[0].state quest = Quest([c.action for c in chain]) quest.set_failing_conditions([Proposition("in", [wrong_obj, world.inventory])]) grammar = textworld.generator.make_grammar(options.grammar, rng=rng_grammar) game = textworld.generator.make_game_with(world, [quest], grammar) game.metadata = metadata mode_choice = modes.index(mode) uuid = "tw-treasure_hunter-{specs}-{grammar}-{seeds}" uuid = uuid.format(specs=encode_seeds((mode_choice, options.nb_rooms, options.quest_length)), grammar=options.grammar.uuid, seeds=encode_seeds([options.seeds[k] for k in sorted(options.seeds)])) game.metadata["uuid"] = uuid return game
def make_game(mode: str, n_rooms: int, quest_length: int, grammar_flags: Mapping = {}, seeds: Optional[Union[int, Dict[str, int]]] = None ) -> textworld.Game: """ Make a Treasure Hunter game. Arguments: mode: Mode for the game where * `'easy'`: rooms are all empty except where the two objects are placed. Also, connections between rooms have no door. * `'medium'`: adding closed doors and containers that might need to be open in order to find the object. * `'hard'`: adding locked doors and containers (necessary keys will in the inventory) that might need to be unlocked (and open) in order to find the object. n_rooms: Number of rooms in the game. quest_length: How far from the player the object to find should ideally be placed. grammar_flags: Options for the grammar controlling the text generation process. seeds: Seeds for the different generation processes. * If `None`, seeds will be sampled from :py:data:`textworld.g_rng <textworld.utils.g_rng>`. * If `int`, it acts as a seed for a random generator that will be used to sample the other seeds. * If dict, the following keys can be set: * `'seed_map'`: control the map generation; * `'seed_objects'`: control the type of objects and their location; * `'seed_quest'`: control the quest generation; * `'seed_grammar'`: control the text generation. For any key missing, a random number gets assigned (sampled from :py:data:`textworld.g_rng <textworld.utils.g_rng>`). Returns: Generated game. """ # Deal with any missing random seeds. seeds = get_seeds_for_game_generation(seeds) metadata = {} # Collect infos for reproducibility. metadata["desc"] = "Treasure Hunter" metadata["mode"] = mode metadata["seeds"] = seeds metadata["world_size"] = n_rooms metadata["quest_length"] = quest_length metadata["grammar_flags"] = grammar_flags rng_map = np.random.RandomState(seeds['seed_map']) rng_objects = np.random.RandomState(seeds['seed_objects']) rng_quest = np.random.RandomState(seeds['seed_quest']) rng_grammar = np.random.RandomState(seeds['seed_grammar']) modes = ["easy", "medium", "hard"] if mode == "easy": door_states = None n_distractors = 0 elif mode == "medium": door_states = ["open", "closed"] n_distractors = 10 elif mode == "hard": door_states = ["open", "closed", "locked"] n_distractors = 20 # Generate map. map_ = textworld.generator.make_map(n_rooms=n_rooms, rng=rng_map, possible_door_states=door_states) assert len(map_.nodes()) == n_rooms world = World.from_map(map_) # Randomly place the player. starting_room = None if len(world.rooms) > 1: starting_room = rng_map.choice(world.rooms) world.set_player_room(starting_room) # Add object the player has to pick up. types_counts = data.get_types().count(world.state) obj_type = data.get_types().sample(parent_type='o', rng=rng_objects, include_parent=True) var_id = get_new(obj_type, types_counts) right_obj = Variable(var_id, obj_type) world.add_fact(Proposition("in", [right_obj, world.inventory])) # Add containers and supporters to the world. types_counts = data.get_types().count(world.state) objects = [] distractor_types = uniquify(['c', 's'] + data.get_types().descendants('c') + data.get_types().descendants('s')) for i in range(n_distractors): obj_type = rng_objects.choice(distractor_types) var_id = get_new(obj_type, types_counts) # This update the types_counts. objects.append(Variable(var_id, obj_type)) world.populate_with(objects, rng=rng_objects) # Add object the player should not pick up. types_counts = data.get_types().count(world.state) obj_type = data.get_types().sample(parent_type='o', rng=rng_objects, include_parent=True) var_id = get_new(obj_type, types_counts) wrong_obj = Variable(var_id, obj_type) # Place it anywhere in the world. world.populate_with([wrong_obj], rng=rng_objects) # Generate a quest that finishes by taking something (i.e. the right # object since it's the only one in the inventory). rules_per_depth = {0: data.get_rules().get_matching("take.*")} exceptions = ["r", "c", "s", "d"] if mode == "easy" else ["r"] chain = textworld.generator.sample_quest(world.state, rng_quest, max_depth=quest_length, allow_partial_match=False, exceptions=exceptions, rules_per_depth=rules_per_depth, nb_retry=5, backward=True) # Add objects needed for the quest. world.state = chain[0].state quest = Quest([c.action for c in chain]) quest.set_failing_conditions([Proposition("in", [wrong_obj, world.inventory])]) grammar = textworld.generator.make_grammar(flags=grammar_flags, rng=rng_grammar) game = textworld.generator.make_game_with(world, [quest], grammar) game.metadata = metadata mode_choice = modes.index(mode) uuid = "tw-treasure_hunter-{specs}-{flags}-{seeds}" uuid = uuid.format(specs=encode_seeds((mode_choice, n_rooms, quest_length)), flags=encode_flags(grammar_flags), seeds=encode_seeds([seeds[k] for k in sorted(seeds)])) game.metadata["uuid"] = uuid return game