def visualize(world: Union[Game, State, GlulxGameState, World], interactive: bool = False): """ Show the current state of the world. :param world: Object representing a game state to be visualized. :param interactive: Whether or not to visualize the state in the browser. :return: Image object of the visualization. """ try: import webbrowser from textworld.render.serve import get_html_template except ImportError: raise ImportError( 'Visualization dependencies not installed. Try running `pip install textworld[vis]`' ) if isinstance(world, Game): game = world state = load_state(game.world, game.infos) state["objective"] = game.objective elif isinstance(world, GlulxGameState): state = load_state_from_game_state(game_state=world) elif isinstance(world, World): state = load_state(world) elif isinstance(world, State): state = world world = World.from_facts(state.facts) state = load_state(world) else: raise ValueError("Don't know how to visualize: {!r}".format(world)) state["command"] = "" state["history"] = "" html = get_html_template(game_state=json.dumps(state)) tmpdir = maybe_mkdir(pjoin(tempfile.gettempdir(), "textworld")) fh, filename = tempfile.mkstemp(suffix=".html", dir=tmpdir, text=True) url = 'file://' + filename with open(filename, 'w') as f: f.write(html) img_graph = take_screenshot(url, id="world") img_inventory = take_screenshot(url, id="inventory") image = concat_images( img_inventory, img_graph, ) if interactive: try: webbrowser.open(url) finally: return image return image
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 KnowledgeBase.default().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 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.has_won game_state, _, done = env.step(event.commands[0]) assert done assert game_state.has_won
def test_get_objects_in_inventory(): P = Variable("P") I = Variable("I") room = Variable("room", "r") obj = Variable("obj", "o") # Closed chest. facts = [Proposition("at", [P, room]), Proposition("in", [obj, I])] world = World.from_facts(facts) objects = world.get_objects_in_inventory() assert obj in world.objects assert obj in objects
def test_get_all_objects_in(): P = Variable("P") room = Variable("room", "r") chest = Variable("chest", "c") obj = Variable("obj", "o") facts = [Proposition("at", [P, room]), Proposition("at", [chest, room]), Proposition("in", [obj, chest]), Proposition("closed", [chest])] world = World.from_facts(facts) objects = world.get_all_objects_in(world.player_room) assert obj in world.objects assert obj in objects
def test_populate_with(): # setup P = Variable('P') I = Variable('I') room = Variable('room', 'r') facts = [Proposition('at', [P, room])] world = World.from_facts(facts) # test obj = Variable('obj', 'o') world.populate_with(objects=[obj]) assert obj in world.objects assert (Proposition('at', [obj, room]) in world.facts or Proposition('in', [obj, I]) in world.facts)
def load_state_from_game_state(game_state: GlulxGameState, format: str = 'png', limit_player_view: bool = False) -> dict: """ Generates serialization of game state. :param game_state: The current game state to visualize. :param format: The graph output format (png, svg, pdf, ...) :param limit_player_view: Whether to limit the player's view. Default: False. :return: The graph generated from this World """ game_infos = game_state.game_infos game_infos["objective"] = game_state.objective last_action = game_state.action # Create a world from the current state's facts. world = World.from_facts(game_state.state.facts) return load_state(world, game_infos, last_action, format, limit_player_view)
def test_cannot_automatically_positioning_rooms(): P = Variable("P") r0 = Variable("Room0", "r") r1 = Variable("Room1", "r") r2 = Variable("Room2", "r") r3 = Variable("Room3", "r") r4 = Variable("Room4", "r") r5 = Variable("Room5", "r") d = Variable("door", "d") facts = [Proposition("at", [P, r0])] facts.extend(connect(r0, 'north', r1)) facts.extend(connect(r0, 'east', r2)) facts.extend(connect(r0, 'south', r3)) facts.extend(connect(r0, 'west', r4)) world = World.from_facts(facts) npt.assert_raises(NoFreeExitError, world.add_fact, Proposition("link", [r0, d, r5]))
def test_automatically_positioning_rooms(): P = Variable("P") r1 = Variable("Room1", "r") r2 = Variable("Room2", "r") d = Variable("door", "d") facts = [Proposition("at", [P, r1])] world = World.from_facts(facts) assert len(world.rooms) == 1 assert len(world.find_room_by_id(r1.name).exits) == 0 world.add_fact(Proposition("link", [r1, d, r2])) assert len(world.rooms) == 2 r1_entity = world.find_room_by_id(r1.name) r2_entity = world.find_room_by_id(r2.name) assert len(r1_entity.exits) == 1 assert len(r2_entity.exits) == 1 assert list(r1_entity.exits.keys())[0] == reverse_direction(list(r2_entity.exits.keys())[0])
def visualize(world: Union[Game, State, GlulxGameState, World], interactive: bool = False): """ Show the current state of the world. :param world: Object representing a game state to be visualized. :param interactive: Whether or not to visualize the state in the browser. :return: Image object of the visualization. """ if isinstance(world, Game): game = world state = load_state(game.world, game.infos) state["objective"] = "" if len(game.quests) > 0: state["objective"] = game.quests[0].desc elif isinstance(world, GlulxGameState): state = load_state_from_game_state(game_state=world) elif isinstance(world, World): state = load_state(world) elif isinstance(world, State): state = world world = World.from_facts(state.facts) state = load_state(world) else: raise ValueError("Don't know how to visualize: {!r}".format(world)) state["command"] = "" state["history"] = "" html = textworld.render.serve.get_html_template( game_state=json.dumps(state)) tmpdir = maybe_mkdir(pjoin(tempfile.gettempdir(), "textworld")) fh, filename = tempfile.mkstemp(suffix=".html", dir=tmpdir, text=True) url = 'file://' + filename with open(filename, 'w') as f: f.write(html) image = take_screenshot(url) if interactive: try: webbrowser.open(url) finally: return image return image
def test_making_a_game(play_the_game=False): rng_map = np.random.RandomState(1234) map_ = textworld.generator.make_small_map(1, rng_map) world = World.from_map(map_) world.set_player_room() # First room generated (i.e. the only one). rng_objects = np.random.RandomState(123) nb_objects = 10 world.populate(nb_objects, rng=rng_objects) options = textworld.GameOptions() options.chaining.max_depth = 5 options.chaining.max_breadth = 2 options.chaining.rng = np.random.RandomState(124) quests = make_quest(world, options) # Define the grammar we'll use. rng_grammar = np.random.RandomState(1234) grammar_flags = { "theme": "house", "include_adj": False, "only_last_action": True, "blend_instructions": True, "blend_descriptions": True, "refer_by_name_only": True, "instruction_extension": [], } grammar = textworld.generator.make_grammar(grammar_flags, rng=rng_grammar) # Generate the world representation. game = textworld.generator.make_game_with(world, quests, grammar) with make_temp_directory(prefix="test_render_wrapper") as tmpdir: options = textworld.GameOptions() options.path = tmpdir game_file = compile_game(game, options) if play_the_game: textworld.play(game_file)
def generate_never_ending_game_old(args): g_rng.set_seed(args.seed) msg = "--max-steps {} --nb-objects {} --nb-rooms {} --seed {}" print( msg.format(args.max_steps, args.nb_objects, args.nb_rooms, g_rng.seed)) print("Generating game...") map_ = textworld.generator.make_map(n_rooms=args.nb_rooms) world = World.from_map(map_) world.set_player_room() world.populate(nb_objects=args.nb_objects) grammar = textworld.generator.make_grammar(flags={"theme": "house"}) quests = [] # No quest game = textworld.generator.make_game_with(world, quests, grammar) game_name = "neverending" game_file = textworld.generator.compile_game(game, game_name, force_recompile=True, games_folder=args.output) return game_file
def test_used_names_is_updated(verbose=False): # Make generation throughout the framework reproducible. g_rng.set_seed(1234) # Generate a map that's shape in a cross with room0 in the middle. P = Variable('P') r = Variable('r_0', 'r') k1 = Variable('k_1', 'k') k2 = Variable('k_2', 'k') c1 = Variable('c_1', 'c') c2 = Variable('c_2', 'c') facts = [ Proposition('at', [P, r]), Proposition('at', [k1, r]), Proposition('at', [k2, r]), Proposition('at', [c1, r]), Proposition('at', [c2, r]), Proposition('match', [k1, c1]), Proposition('match', [k2, c2]) ] world = World.from_facts(facts) world.set_player_room() # Set start room to the middle one. world.populate_room(10, world.player_room) # Add objects to the starting room. # Generate the world representation. grammar = textworld.generator.make_grammar(flags={}, rng=np.random.RandomState(42)) for k, v in grammar.grammar.items(): grammar.grammar[k] = v[:2] # Force reusing variables. game = textworld.generator.make_game_with(world, [], grammar) for entity_infos in game.infos.values(): if entity_infos.name is None: continue assert entity_infos.name in grammar.used_names
def filter_observables(world_facts: List[Proposition], verbose=False, game=None): fixups = defaultdict(set) if not world_facts: return None world_facts = reconstitute_facts(world_facts) # print("WORLD FACTS:") for fact in world_facts: # print('\t', fact) for v in fact.arguments: # print('\t\t{}:{}'.format(v.name, v.type)) if not v.name: v_count = len(fixups[v.type]) assert v not in fixups[v.type] if v.type == 'P' or v.type == 'I' or v.type == 'RECIPE' or v.type == 'MEAL': v.name = v.type if v_count == 0: fixups[v.type].add(v) else: v.name = '~{}_{}'.format(v.type, v_count) fixups[v.type].add(v) world = World.from_facts(world_facts) world_state = world.state if 'P' in world_state._vars_by_type: players = world_state.variables_of_type('P') assert len(players) == 1 # for p in players: # player = p # else: # player = None where_sig = Signature('at', ('P', 'r')) where_am_i = world_state.facts_with_signature(where_sig) assert len(where_am_i) == 1 where_fact = list(where_am_i)[0] the_player = where_fact.arguments[0] player_location = where_fact.arguments[1] # if verbose: # print("WORLD FACTS:") # for fact in world_facts: # print('\t', fact) # # print_fact(game, fact) # if verbose: # print("VARIABLES:") # for v in world_state.variables: # print('\t\t{}:{}'.format(v.name, v.type)) print(where_fact, world.player_room) facts_in_scope = world.get_facts_in_scope() observable_facts = [] for fact in facts_in_scope: # print("***\t\t", fact) if is_observable_relation(fact.name): if fact != where_fact: observable_facts.append(fact) else: # consider the player's current location to be directly observable observable_facts.append(fact) else: pass for e in world.player_room.exits: if world.state.is_fact( Proposition( 'free', (world.player_room.exits[e], where_fact.arguments[1]))): observable_facts.append( Proposition( "{}_of".format(e), ( Variable("exit_{}".format(e), 'e'), world.player_room # the_player ))) # REFACTORING: the following is now handled within add_extra_door_facts() # else: # probably a closed door # door_facts = world.state.facts_with_signature(Signature('link', ('r', 'd', 'r'))) # for df in door_facts: # if df.arguments[0] == player_location: # the_door = df.arguments[1] # observable_facts.append(Proposition("{}_of".format(e), ( # the_player, # the_door # ))) # if world.state.is_fact(Proposition('closed', [the_door])): # observable_facts.append(Proposition('closed', [the_door])) add_extra_door_facts(world, world_facts, local_facts=observable_facts, where_fact=where_fact) if verbose: print("++WORLD FACTS++:") for fact in world_facts: prettyprint_fact(fact, game=game) return observable_facts, player_location
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
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. """ kb = KnowledgeBase.default() 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 rngs = options.rngs rng_map = rngs['map'] rng_objects = rngs['objects'] rng_quest = rngs['quest'] rng_grammar = rngs['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 = kb.types.count(world.state) obj_type = kb.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 = kb.types.count(world.state) objects = [] distractor_types = uniquify(['c', 's'] + kb.types.descendants('c') + kb.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 = kb.types.count(world.state) obj_type = kb.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 = [kb.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.initial_state event = Event(chain.actions) quest = Quest( win_events=[event], fail_events=[ Event(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