def test_game_with_infinite_max_score(self): M = textworld.GameMaker() museum = M.new_room("Museum") statue = M.new(type="o", name="golden statue") pedestal = M.new(type="s", name="pedestal") pedestal.add(statue) museum.add(pedestal) M.set_player(museum) M.quests = [ Quest(win_events=[ Event(conditions=[M.new_fact('in', statue, M.inventory)]), ], reward=10, optional=True, repeatable=True), Quest(win_events=[ Event(conditions=[M.new_fact('at', statue, museum)]), ], reward=0) ] M.set_walkthrough( ["take golden statue from pedestal", "look", "drop statue"]) game = M.build() assert game.max_score == np.inf inform7 = Inform7Game(game) game_progress = GameProgression(game) assert len(game_progress.quest_progressions) == len(game.quests) # Following the actions associated to the last quest actually corresponds # to solving the whole game. for action in game_progress.winning_policy: assert not game_progress.done game_progress.update(action) assert game_progress.done assert not game_progress.quest_progressions[ 0].completed # Repeatable quests can never be completed. assert game_progress.quest_progressions[ 1].completed # Mandatory quest. # Solve the game while completing the optional quests. game_progress = GameProgression(game) for command in [ "take golden statue from pedestal", "look", "look", "drop golden statue" ]: _apply_command(command, game_progress, inform7) progressions = game_progress.quest_progressions assert not progressions[0].done # Repeatable quests can never be done. assert progressions[ 0].nb_completions == 3 # They could have been completed a number of times, though. assert progressions[1].done assert game_progress.score == 30
def test_names_disambiguation(): M = textworld.GameMaker() room = M.new_room("room") M.set_player(room) apple = M.new(type="o", name="apple") orange = M.new(type="o", name="orange") tasty_apple = M.new(type="o", name="tasty apple") tasty_orange = M.new(type="o", name="tasty orange") room.add(apple, orange, tasty_apple, tasty_orange) game = M.build() game_name = "test_names_disambiguation" 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("take tasty apple") assert "tasty apple" in game_state.inventory game_state, _, done = env.step("take tasty orange") assert "tasty orange" in game_state.inventory env.reset() game_state, _, done = env.step("take orange") assert "tasty orange" not in game_state.inventory assert "orange" in game_state.inventory game_state, _, done = env.step("take tasty") assert "?" in game_state.feedback # Disambiguation question. game_state, _, done = env.step("apple") assert "tasty orange" not in game_state.inventory assert "tasty apple" in game_state.inventory assert "tasty apple" not in game_state.description
def test_disambiguation_questions(): M = textworld.GameMaker() room = M.new_room("room") M.set_player(room) tasty_apple = M.new(type="o", name="tasty apple") tasty_orange = M.new(type="o", name="tasty orange") room.add(tasty_apple, tasty_orange) game = M.build() game_name = "test_names_disambiguation" with make_temp_directory(prefix=game_name) as tmpdir: game_file = _compile_game(game, path=tmpdir) env = textworld.start(game_file, EnvInfos(description=True, inventory=True)) game_state = env.reset() previous_inventory = game_state.inventory previous_description = game_state.description game_state, _, _ = env.step("take tasty") assert "?" in game_state.feedback # Disambiguation question. # When there is a question in Inform7, the next string sent to the game # will be considered as the answer. We now make sure that asking for # extra information like `description` or `inventory` before answering # the question works. assert game_state.description == previous_description assert game_state.inventory == previous_inventory # Now answering the question. game_state, _, _ = env.step("apple") assert "That's not a verb I recognise." not in game_state.feedback assert "tasty orange" not in game_state.inventory assert "tasty apple" in game_state.inventory assert "tasty apple" not in game_state.description
def test_blend_instructions(verbose=False): # Make generation throughout the framework reproducible. g_rng.set_seed(1234) M = textworld.GameMaker() r1 = M.new_room() r2 = M.new_room() M.set_player(r1) path = M.connect(r1.north, r2.south) path.door = M.new(type="d", name="door") M.add_fact("locked", path.door) key = M.new(type="k", name="key") M.add_fact("match", key, path.door) r1.add(key) quest = M.set_quest_from_commands(["take key", "unlock door with key", "open door", "go north", "close door", "lock door with key", "drop key"]) game = M.build() grammar1 = textworld.generator.make_grammar(flags={"blend_instructions": False}, rng=np.random.RandomState(42)) grammar2 = textworld.generator.make_grammar(flags={"blend_instructions": True}, rng=np.random.RandomState(42)) quest.desc = None game.change_grammar(grammar1) quest1 = quest.copy() quest.desc = None game.change_grammar(grammar2) quest2 = quest.copy() assert len(quest1.desc) > len(quest2.desc)
def test_quest_winning_condition_go(): M = textworld.GameMaker() # R1 -- R2 -- R3 R1 = M.new_room("West room") R2 = M.new_room("Center room") R3 = M.new_room("East room") M.set_player(R1) M.connect(R1.east, R2.west) M.connect(R2.east, R3.west) M.set_quest_from_commands(["go east", "go east"]) game = M.build() game_name = "test_quest_winning_condition_go" 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("go east") assert not done assert not game_state.has_won game_state, _, done = env.step("go east") assert done assert game_state.has_won
def test_game_with_infinite_max_score(): M = textworld.GameMaker() museum = M.new_room("Museum") statue = M.new(type="o", name="golden statue") pedestal = M.new(type="s", name="pedestal") pedestal.add(statue) museum.add(pedestal) M.set_player(museum) M.quests = [ Quest(win_events=[ Event(conditions=[M.new_fact('in', statue, M.inventory)]), ], reward=10, optional=True, repeatable=True), Quest(win_events=[ Event(conditions=[M.new_fact('at', statue, museum)]), ], reward=0) ] M.set_walkthrough(["take statue from pedestal", "look", "drop statue"]) game = M.build() game_name = "test_game_with_infinite_max_score" with make_temp_directory(prefix=game_name) as tmpdir: game_file = _compile_game(game, path=tmpdir) env = textworld.start(game_file) state = env.reset() state.max_score == np.inf # Score increases for each turn the player hold the statue. state, score, done = env.step("look") assert not done assert score == 0 state, score, done = env.step("take statue") assert score == 10 state, score, done = env.step("wait") assert score == 20 state, score, done = env.step("score") assert score == 20 assert "You have so far scored 20 points," in state.feedback state, score, done = env.step("wait") assert score == 30 state, score, done = env.step("drop statue") assert done assert score == 30 assert "a total of 30 points," in state.feedback
def build_and_compile_no_quest_game(options: GameOptions) -> Tuple[Game, str]: M = textworld.GameMaker() room = M.new_room() M.set_player(room) item = M.new(type="o") room.add(item) game = M.build() game_file = _compile_test_game(game, options) return game, game_file
def test_do_not_overwrite_entity_desc(verbose=False): # Make generation throughout the framework reproducible. g_rng.set_seed(1234) M = textworld.GameMaker() r1 = M.new_room() M.set_player(r1) key = M.new(type="k", name="key", desc="This is a skeleton key.") r1.add(key) quest = M.set_quest_from_commands(["take key"]) quest.desc = "Find a valuable object." M.build() assert key.infos.desc == "This is a skeleton key." assert quest.desc == "Find a valuable object."
def test_quest_losing_condition(): g_rng.set_seed(2018) M = textworld.GameMaker() # The goal commands = ["go east", "insert carrot into chest"] # Create a 'bedroom' room. R1 = M.new_room("bedroom") R2 = M.new_room("kitchen") M.set_player(R1) path = M.connect(R1.east, R2.west) path.door = M.new(type='d', name='wooden door') path.door.add_property("open") carrot = M.new(type='f', name='carrot') M.inventory.add(carrot) # Add a closed chest in R2. chest = M.new(type='c', name='chest') chest.add_property("open") R2.add(chest) failing_conditions = (Proposition("eaten", [carrot.var]), ) quest = M.set_quest_from_commands(commands) quest.set_failing_conditions(failing_conditions) game = M.build() game_name = "test_quest_losing_condition" 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() # Make sure we do not rely on the quest progression to # determine if the game was lost. assert not env._compute_intermediate_reward game_state, _, done = env.step("eat carrot") assert done assert game_state.has_lost assert not game_state.has_won
def build_and_compile_game(options: GameOptions): M = textworld.GameMaker() # Create a 'bedroom' room. R1 = M.new_room("bedroom") R2 = M.new_room("kitchen") M.set_player(R1) path = M.connect(R1.east, R2.west) path.door = M.new(type='d', name='wooden door') path.door.add_property("open") carrot = M.new(type='f', name='carrot') M.inventory.add(carrot) # Add a closed chest in R2. chest = M.new(type='c', name='chest') chest.add_property("open") R2.add(chest) quest1_cmds = ["go east", "insert carrot into chest"] carrot_in_chest = M.new_event_using_commands(quest1_cmds) eating_carrot = Event(conditions={M.new_fact("eaten", carrot)}) quest1 = Quest(win_events=[carrot_in_chest], fail_events=[eating_carrot], reward=2) quest2_cmds = quest1_cmds + ["close chest"] quest2_actions = M.new_event_using_commands(quest2_cmds).actions chest_closed_with_carrot = Event(conditions={ M.new_fact("in", carrot, chest), M.new_fact("closed", chest) }, actions=quest2_actions) quest2 = Quest(win_events=[chest_closed_with_carrot], fail_events=[eating_carrot]) M.quests = [quest1, quest2] M.set_walkthrough(quest2_cmds) game = M.build() game_file = _compile_test_game(game, options) return game, game_file
def test_take_all_and_variants(): M = textworld.GameMaker() # Empty room. room = M.new_room("room") M.set_player(room) game = M.build() game_name = "test_take_all_and_variants" 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() variants_to_test = itertools.product(["take", "get", "pick up"], ["all", "everything", "each"]) for command in variants_to_test: game_state, _, done = env.step(" ".join(command)) assert game_state.feedback.strip( ) == "You have to be more specific!" # Multiple objects to take. red_ball = M.new(type="o", name="red ball") blue_ball = M.new(type="o", name="blue ball") room.add(red_ball, blue_ball) game = M.build() game_name = "test_take_all_and_variants2" 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("take all ball") assert "red ball: Taken." in game_state.feedback assert "blue ball: Taken." in game_state.feedback assert "red ball" in game_state.inventory assert "blue ball" in game_state.inventory
def test_quest_with_multiple_winning_and_losing_conditions(): g_rng.set_seed(2018) M = textworld.GameMaker() # Create a 'bedroom' room. R1 = M.new_room("bedroom") R2 = M.new_room("kitchen") M.set_player(R1) path = M.connect(R1.east, R2.west) path.door = M.new(type='d', name='wooden door') path.door.add_property("open") carrot = M.new(type='f', name='carrot') lettuce = M.new(type='f', name='lettuce') M.inventory.add(carrot) M.inventory.add(lettuce) # Add a closed chest in R2. chest = M.new(type='c', name='chest') chest.add_property("open") R2.add(chest) # The goal quest = Quest(win_events=[Event(conditions={M.new_fact("in", carrot, chest), M.new_fact("closed", chest)}), Event(conditions={M.new_fact("eaten", lettuce)})], fail_events=[Event(conditions={M.new_fact("in", lettuce, chest), M.new_fact("closed", chest)}), Event(conditions={M.new_fact("eaten", carrot)})]) M.quests = [quest] game = M.build() game_name = "test_quest_with_multiple_winning_and_losing_conditions" with make_temp_directory(prefix=game_name) as tmpdir: game_file = _compile_game(game, path=tmpdir) env = textworld.start(game_file) # Failing - 1 env.reset() game_state, _, done = env.step("eat carrot") assert done assert game_state.lost assert not game_state.won # Failing - 2 env.reset() game_state, _, done = env.step("go east") assert not done game_state, _, done = env.step("insert lettuce into chest") assert not done game_state, _, done = env.step("close chest") assert done assert game_state.lost assert not game_state.won # Failing - 1 env.reset() game_state, _, done = env.step("eat lettuce") assert done assert not game_state.lost assert game_state.won # Winning - 2 env.reset() game_state, _, done = env.step("go east") assert not done game_state, _, done = env.step("insert carrot into chest") assert not done game_state, _, done = env.step("close chest") assert done assert not game_state.lost assert game_state.won
def make_game(settings: Mapping[str, str], options: Optional[GameOptions] = None) -> textworld.Game: """ Make a simple game. Arguments: settings: Difficulty settings (see notes). options: For customizing the game generation (see :py:class:`textworld.GameOptions <textworld.generator.game.GameOptions>` for the list of available options). Returns: Generated game. Notes: The settings that can be provided are: * rewards : The reward frequency: dense, balanced, or sparse. * goal : The description of the game's objective shown at the beginning of the game: detailed, bried, or none. * test : Whether this game should be drawn from the test distributions of games. """ metadata = {} # Collect infos for reproducibility. metadata["desc"] = "Simple game" metadata["seeds"] = options.seeds metadata["world_size"] = 6 metadata["quest_length"] = None # TBD rngs = options.rngs rng_map = rngs['map'] rng_objects = rngs['objects'] rng_grammar = rngs['grammar'] rng_quest = rngs['quest'] # Make the generation process reproducible. textworld.g_rng.set_seed(2018) M = textworld.GameMaker() M.grammar = textworld.generator.make_grammar(options.grammar, rng=rng_grammar) # Start by building the layout of the world. bedroom = M.new_room("bedroom") kitchen = M.new_room("kitchen") livingroom = M.new_room("living room") bathroom = M.new_room("bathroom") backyard = M.new_room("backyard") garden = M.new_room("garden") # Connect rooms together. bedroom_kitchen = M.connect(bedroom.east, kitchen.west) kitchen_bathroom = M.connect(kitchen.north, bathroom.south) kitchen_livingroom = M.connect(kitchen.south, livingroom.north) kitchen_backyard = M.connect(kitchen.east, backyard.west) backyard_garden = M.connect(backyard.south, garden.north) # Add doors. bedroom_kitchen.door = M.new(type='d', name='wooden door') kitchen_backyard.door = M.new(type='d', name='screen door') kitchen_backyard.door.add_property("closed") # Design the bedroom. drawer = M.new(type='c', name='chest drawer') trunk = M.new(type='c', name='antique trunk') bed = M.new(type='s', name='king-size bed') bedroom.add(drawer, trunk, bed) # Close the trunk and drawer. trunk.add_property("closed") drawer.add_property("closed") # - The bedroom's door is locked bedroom_kitchen.door.add_property("locked") # Design the kitchen. counter = M.new(type='s', name='counter') stove = M.new(type='s', name='stove') kitchen_island = M.new(type='s', name='kitchen island') refrigerator = M.new(type='c', name='refrigerator') kitchen.add(counter, stove, kitchen_island, refrigerator) # - Add some food in the refrigerator. apple = M.new(type='f', name='apple') milk = M.new(type='f', name='milk') refrigerator.add(apple, milk) # Design the bathroom. toilet = M.new(type='c', name='toilet') sink = M.new(type='s', name='sink') bath = M.new(type='c', name='bath') bathroom.add(toilet, sink, bath) toothbrush = M.new(type='o', name='toothbrush') sink.add(toothbrush) soap_bar = M.new(type='o', name='soap bar') bath.add(soap_bar) # Design the living room. couch = M.new(type='s', name='couch') low_table = M.new(type='s', name='low table') tv = M.new(type='s', name='tv') livingroom.add(couch, low_table, tv) remote = M.new(type='o', name='remote') low_table.add(remote) bag_of_chips = M.new(type='f', name='half of a bag of chips') couch.add(bag_of_chips) # Design backyard. bbq = M.new(type='s', name='bbq') patio_table = M.new(type='s', name='patio table') chairs = M.new(type='s', name='set of chairs') backyard.add(bbq, patio_table, chairs) # Design garden. shovel = M.new(type='o', name='shovel') tomato = M.new(type='f', name='tomato plant') pepper = M.new(type='f', name='bell pepper') lettuce = M.new(type='f', name='lettuce') garden.add(shovel, tomato, pepper, lettuce) # Close all containers for container in M.findall(type='c'): container.add_property("closed") # Set uncooked property for to all food items. foods = M.findall(type='f') for food in foods: food.add_property("edible") food_names = [food.name for food in foods] # Shuffle the position of the food items. rng_quest.shuffle(food_names) for food, name in zip(foods, food_names): food.orig_name = food.name food.name = name # The player starts in the bedroom. M.set_player(bedroom) # Quest walkthrough = [] # Part I - Escaping the room. # Generate the key that unlocks the bedroom door. bedroom_key = M.new(type='k', name='old key') M.add_fact("match", bedroom_key, bedroom_kitchen.door) # Decide where to hide the key. if rng_quest.rand() > 0.5: drawer.add(bedroom_key) walkthrough.append("open chest drawer") walkthrough.append("take old key from chest drawer") bedroom_key_holder = drawer else: trunk.add(bedroom_key) walkthrough.append("open antique trunk") walkthrough.append("take old key from antique trunk") bedroom_key_holder = trunk # Unlock the door, open it and leave the room. walkthrough.append("unlock wooden door with old key") walkthrough.append("open wooden door") walkthrough.append("go east") # Part II - Find food item. # 1. Randomly pick a food item to cook. food = rng_quest.choice(foods) if settings["test"]: TEST_FOODS = ["garlic", "kiwi", "carrot"] food.name = rng_quest.choice(TEST_FOODS) # Retrieve the food item and get back in the kitchen. # HACK: handcrafting the walkthrough. if food.orig_name in ["apple", "milk"]: rooms_to_visit = [] doors_to_open = [] walkthrough.append("open refrigerator") walkthrough.append("take {} from refrigerator".format(food.name)) elif food.orig_name == "half of a bag of chips": rooms_to_visit = [livingroom] doors_to_open = [] walkthrough.append("go south") walkthrough.append("take {} from couch".format(food.name)) walkthrough.append("go north") elif food.orig_name in ["bell pepper", "lettuce", "tomato plant"]: rooms_to_visit = [backyard, garden] doors_to_open = [kitchen_backyard.door] walkthrough.append("open screen door") walkthrough.append("go east") walkthrough.append("go south") walkthrough.append("take {}".format(food.name)) walkthrough.append("go north") walkthrough.append("go west") # Part II - Cooking the food item. walkthrough.append("put {} on stove".format(food.name)) # walkthrough.append("cook {}".format(food.name)) # walkthrough.append("eat {}".format(food.name)) # 2. Determine the winning condition(s) of the subgoals. quests = [] bedroom_key_holder if settings["rewards"] == "dense": # Finding the bedroom key and opening the bedroom door. # 1. Opening the container. quests.append( Quest(win_events=[ Event(conditions={M.new_fact("open", bedroom_key_holder)}) ])) # 2. Getting the key. quests.append( Quest(win_events=[ Event(conditions={M.new_fact("in", bedroom_key, M.inventory)}) ])) # 3. Unlocking the bedroom door. quests.append( Quest(win_events=[ Event(conditions={M.new_fact("closed", bedroom_kitchen.door)}) ])) # 4. Opening the bedroom door. quests.append( Quest(win_events=[ Event(conditions={M.new_fact("open", bedroom_kitchen.door)}) ])) if settings["rewards"] in ["dense", "balanced"]: # Escaping out of the bedroom. quests.append( Quest(win_events=[ Event(conditions={M.new_fact("at", M.player, kitchen)}) ])) if settings["rewards"] in ["dense", "balanced"]: # Opening doors. for door in doors_to_open: quests.append( Quest( win_events=[Event(conditions={M.new_fact("open", door)})])) if settings["rewards"] == "dense": # Moving through places. for room in rooms_to_visit: quests.append( Quest(win_events=[ Event(conditions={M.new_fact("at", M.player, room)}) ])) if settings["rewards"] in ["dense", "balanced"]: # Retrieving the food item. quests.append( Quest(win_events=[ Event(conditions={M.new_fact("in", food, M.inventory)}) ])) if settings["rewards"] in ["dense", "balanced"]: # Retrieving the food item. quests.append( Quest(win_events=[ Event(conditions={M.new_fact("in", food, M.inventory)}) ])) if settings["rewards"] in ["dense", "balanced", "sparse"]: # Putting the food on the stove. quests.append( Quest( win_events=[Event( conditions={M.new_fact("on", food, stove)})])) # 3. Determine the losing condition(s) of the game. quests.append( Quest(fail_events=[Event(conditions={M.new_fact("eaten", food)})])) # Set the subquest(s). M.quests = quests # - Add a hint of what needs to be done in this game. objective = "The dinner is almost ready! It's only missing a grilled {}." objective = objective.format(food.name) note = M.new(type='o', name='note', desc=objective) kitchen_island.add(note) game = M.build() game.main_quest = M.new_quest_using_commands(walkthrough) game.change_grammar(game.grammar) if settings["goal"] == "detailed": # Use the detailed version of the objective. pass elif settings["goal"] == "brief": # Use a very high-level description of the objective. game.objective = objective elif settings["goal"] == "none": # No description of the objective. game.objective = "" game.metadata = metadata uuid = "tw-simple-r{rewards}+g{goal}+{dataset}-{flags}-{seeds}" uuid = uuid.format(rewards=str.title(settings["rewards"]), goal=str.title(settings["goal"]), dataset="test" if settings["test"] else "train", flags=options.grammar.uuid, seeds=encode_seeds( [options.seeds[k] for k in sorted(options.seeds)])) game.metadata["uuid"] = uuid return game
def test_cannot_win_or_lose_a_quest_twice(): g_rng.set_seed(2018) M = textworld.GameMaker() # Create a 'bedroom' room. R1 = M.new_room("bedroom") R2 = M.new_room("kitchen") M.set_player(R1) path = M.connect(R1.east, R2.west) path.door = M.new(type='d', name='wooden door') path.door.add_property("open") carrot = M.new(type='f', name='carrot') lettuce = M.new(type='f', name='lettuce') M.inventory.add(carrot) M.inventory.add(lettuce) # Add a closed chest in R2. chest = M.new(type='c', name='chest') chest.add_property("open") R2.add(chest) # The goals event_carrot_in_closed_chest = Event(conditions={M.new_fact("in", carrot, chest), M.new_fact("closed", chest)}) event_drop_carrot_R1 = Event(conditions={M.new_fact("at", carrot, R1)}) event_drop_carrot_R2 = Event(conditions={M.new_fact("at", carrot, R2)}) quest1 = Quest(win_events=[event_carrot_in_closed_chest], fail_events=[event_drop_carrot_R1, event_drop_carrot_R2]) event_lettuce_in_closed_chest = Event(conditions={M.new_fact("in", lettuce, chest), M.new_fact("closed", chest)}) event_drop_lettuce_R1 = Event(conditions={M.new_fact("at", lettuce, R1)}) event_drop_lettuce_R2 = Event(conditions={M.new_fact("at", lettuce, R2)}) quest2 = Quest(win_events=[event_lettuce_in_closed_chest], fail_events=[event_drop_lettuce_R1, event_drop_lettuce_R2]) M.quests = [quest1, quest2] game = M.build() game_name = "test_cannot_win_or_lose_a_quest_twice" with make_temp_directory(prefix=game_name) as tmpdir: game_file = _compile_game(game, path=tmpdir) env = textworld.start(game_file) # Complete quest1 then fail it. env.reset() game_state, score, done = env.step("go east") assert score == 0 game_state, score, done = env.step("insert carrot into chest") assert score == 0 game_state, score, done = env.step("close chest") assert score == 1 assert not done game_state, score, done = env.step("open chest") # Re-completing quest1 doesn't award more points. game_state, score, done = env.step("close chest") assert score == 1 assert not done game_state, score, done = env.step("open chest") game_state, score, done = env.step("take carrot from chest") game_state, score, done = env.step("drop carrot") assert score == 1 assert not done # Then fail quest2. game_state, score, done = env.step("drop lettuce") assert done assert game_state.lost assert not game_state.won env.reset() game_state, score, done = env.step("go east") game_state, score, done = env.step("insert carrot into chest") game_state, score, done = env.step("insert lettuce into chest") game_state, score, done = env.step("close chest") assert score == 2 assert done assert not game_state.lost assert game_state.won
def test_names_disambiguation(): M = textworld.GameMaker() room = M.new_room("room") M.set_player(room) apple = M.new(type="o", name="apple") orange = M.new(type="o", name="orange") tasty_apple = M.new(type="o", name="tasty apple") tasty_orange = M.new(type="o", name="tasty orange") room.add(apple, orange, tasty_apple, tasty_orange) game = M.build() game_name = "test_names_disambiguation" with make_temp_directory(prefix=game_name) as tmpdir: game_file = _compile_game(game, path=tmpdir) env = textworld.start(game_file, EnvInfos(description=True, inventory=True)) env.reset() game_state, _, done = env.step("take tasty apple") assert "tasty apple" in game_state.inventory game_state, _, done = env.step("take tasty orange") assert "tasty orange" in game_state.inventory env.reset() game_state, _, done = env.step("take orange") assert "tasty orange" not in game_state.inventory assert "orange" in game_state.inventory game_state, _, done = env.step("take tasty") assert "?" in game_state.feedback # Disambiguation question. game_state, _, done = env.step("apple") assert "tasty orange" not in game_state.inventory assert "tasty apple" in game_state.inventory assert "tasty apple" not in game_state.description # Actions with two arguments. M = textworld.GameMaker() roomA = M.new_room("roomA") roomB = M.new_room("roomB") roomC = M.new_room("roomC") M.set_player(roomA) path = M.connect(roomA.east, roomB.west) gateway = M.new_door(path, name="gateway") path = M.connect(roomA.west, roomC.east) rectangular_gateway = M.new_door(path, name="rectangular gateway") keycard = M.new(type="k", name="keycard") rectangular_keycard = M.new(type="k", name="rectangular keycard") roomA.add(keycard, rectangular_keycard) M.add_fact("match", keycard, gateway) M.add_fact("match", rectangular_keycard, rectangular_gateway) M.add_fact("locked", gateway) M.add_fact("locked", rectangular_gateway) game = M.build() game_name = "test_names_disambiguation" with make_temp_directory(prefix=game_name) as tmpdir: game_file = _compile_game(game, path=tmpdir) env = textworld.start(game_file, EnvInfos(description=True, inventory=True)) env.reset() game_state, _, done = env.step("take keycard") assert "keycard" in game_state.inventory game_state, _, done = env.step("take keycard") # Already in your inventory. assert "rectangular keycard" not in game_state.inventory game_state, _, done = env.step("take rectangular keycard") assert "rectangular keycard" in game_state.inventory game_state, _, done = env.step("unlock gateway with rectangular keycard") assert "That doesn't seem to fit the lock." in game_state.feedback game_state, _, done = env.step("unlock gateway with keycard") game_state, _, done = env.step("open gateway") game_state, _, done = env.step("go east") assert "-= Roomb =-" in game_state.description game_state, _, done = env.step("go west") game_state, _, done = env.step("unlock rectangular gateway with keycard") assert "That doesn't seem to fit the lock." in game_state.feedback game_state, _, done = env.step("unlock rectangular gateway with rectangular keycard") game_state, _, done = env.step("open rectangular gateway") game_state, _, done = env.step("go west") assert "-= Roomc =-" in game_state.description # Test invariance of the order in which ambiguous object names are defined. # First define "type G safe" then a "safe". M = textworld.GameMaker() garage = M.new_room("garage") M.set_player(garage) key = M.new(type="k", name="key") typeG_safe = M.new(type="c", name="type G safe") safe = M.new(type="c", name="safe") safe.add(key) garage.add(safe, typeG_safe) M.add_fact("open", safe) game = M.build() game_name = "test_names_disambiguation" with make_temp_directory(prefix=game_name) as tmpdir: game_file = _compile_game(game, path=tmpdir) env = textworld.start(game_file, EnvInfos(inventory=True)) game_state = env.reset() game_state, _, done = env.step("take key from safe") assert "key" in game_state.inventory # First define "safe" then "type G safe". M = textworld.GameMaker() garage = M.new_room("garage") M.set_player(garage) key = M.new(type="k", name="key") safe = M.new(type="c", name="safe") typeG_safe = M.new(type="c", name="type G safe") safe.add(key) garage.add(safe, typeG_safe) M.add_fact("open", safe) game = M.build() game_name = "test_names_disambiguation" with make_temp_directory(prefix=game_name) as tmpdir: game_file = _compile_game(game, path=tmpdir) env = textworld.start(game_file, EnvInfos(inventory=True)) game_state = env.reset() game_state, _, done = env.step("take key from safe") assert "key" in game_state.inventory
def test_game_with_optional_and_repeatable_quests(self): M = textworld.GameMaker() museum = M.new_room("Museum") weak_statue = M.new(type="o", name="ivory statue") normal_statue = M.new(type="o", name="stone statue") strong_statue = M.new(type="o", name="granite statue") museum.add(weak_statue) museum.add(normal_statue) museum.add(strong_statue) M.set_player(museum) M.quests = [ Quest(win_events=[ Event(conditions=[M.new_fact('in', weak_statue, M.inventory)]), ], reward=-10, optional=True, repeatable=True), Quest(win_events=[ Event( conditions=[M.new_fact('in', normal_statue, M.inventory)]), ], reward=3, optional=True), Quest( win_events=[ Event(conditions=[ M.new_fact('in', strong_statue, M.inventory) ]), ], reward=5, ) ] M.set_walkthrough( ["take ivory", "take stone", "drop ivory", "take granite"]) game = M.build() inform7 = Inform7Game(game) game_progress = GameProgression(game) assert len(game_progress.quest_progressions) == len(game.quests) # Following the actions associated to the last quest actually corresponds # to solving the whole game. for action in game_progress.winning_policy: assert not game_progress.done game_progress.update(action) assert game_progress.done assert not game_progress.quest_progressions[ 0].completed # Optional, negative quest. assert not game_progress.quest_progressions[ 1].completed # Optional, positive quest. assert game_progress.quest_progressions[ 2].completed # Mandatory quest. # Solve the game while completing the optional quests. game_progress = GameProgression(game) for command in [ "take ivory statue", "look", "take stone statue", "drop ivory statue", "take granite statue" ]: _apply_command(command, game_progress, inform7) progressions = game_progress.quest_progressions assert not progressions[0].done # Repeatable quests can never be done. assert progressions[ 0].nb_completions == 3 # They could have been completed a number of times, though. assert progressions[ 1].done # The nonrepeatable-optional quest can be done. assert progressions[2].done assert game.max_score == 8 assert game_progress.score == -22
def make(settings: Mapping[str, str], options: Optional[GameOptions] = None) -> textworld.Game: """ Make a Cooking game. Arguments: settings: Difficulty settings (see notes). options: For customizing the game generation (see :py:class:`textworld.GameOptions <textworld.generator.game.GameOptions>` for the list of available options). Returns: Generated game. Notes: The settings that can be provided are: * recipe : Number of ingredients in the recipe. * take : Number of ingredients to fetch. It must be less or equal to the value of the `recipe` skill. * open : Whether containers/doors need to be opened. * cook : Whether some ingredients need to be cooked. * cut : Whether some ingredients need to be cut. * drop : Whether the player's inventory has limited capacity. * go : Number of locations in the game (1, 6, 9, or 12). """ options = options or GameOptions() # Load knowledge base specific to this challenge. options.kb = KnowledgeBase.load(KB_PATH) rngs = options.rngs rng_map = rngs['map'] rng_objects = rngs['objects'] rng_grammar = rngs['grammar'] rng_quest = rngs['quest'] rng_recipe = np.random.RandomState(settings["recipe_seed"]) allowed_foods = list(FOODS) allowed_food_preparations = get_food_preparations(list(FOODS)) if settings["split"] == "train": allowed_foods = list(FOODS_SPLITS['train']) allowed_food_preparations = dict(FOOD_PREPARATIONS_SPLITS['train']) elif settings["split"] == "valid": allowed_foods = list(FOODS_SPLITS['valid']) allowed_food_preparations = get_food_preparations(FOODS_SPLITS['valid']) # Also add food from the training set but with different preparations. allowed_foods += [f for f in FOODS if f in FOODS_SPLITS['train']] allowed_food_preparations.update(dict(FOOD_PREPARATIONS_SPLITS['valid'])) elif settings["split"] == "test": allowed_foods = list(FOODS_SPLITS['test']) allowed_food_preparations = get_food_preparations(FOODS_SPLITS['test']) # Also add food from the training set but with different preparations. allowed_foods += [f for f in FOODS if f in FOODS_SPLITS['train']] allowed_food_preparations.update(dict(FOOD_PREPARATIONS_SPLITS['test'])) if settings.get("cut"): # If "cut" skill is specified, remove all "uncut" preparations. for food, preparations in allowed_food_preparations.items(): allowed_food_preparations[food] = [preparation for preparation in preparations if "uncut" not in preparation] if settings.get("cook"): # If "cook" skill is specified, remove all "raw" preparations. for food, preparations in list(allowed_food_preparations.items()): allowed_food_preparations[food] = [preparation for preparation in preparations if "raw" not in preparation] if len(allowed_food_preparations[food]) == 0: del allowed_food_preparations[food] allowed_foods.remove(food) M = textworld.GameMaker(options) recipe = M.new(type='RECIPE', name='') meal = M.new(type='meal', name='meal') M.add_fact("out", meal, recipe) meal.add_property("edible") M.nowhere.append(recipe) # Out of play object. M.nowhere.append(meal) # Out of play object. options.nb_rooms = settings.get("go", 1) if options.nb_rooms == 1: rooms_to_place = ROOMS[:1] elif options.nb_rooms == 6: rooms_to_place = ROOMS[:2] elif options.nb_rooms == 9: rooms_to_place = ROOMS[:3] elif options.nb_rooms == 12: rooms_to_place = ROOMS[:4] else: raise ValueError("Cooking games can only have {1, 6, 9, 12} rooms.") G = make_graph_world(rng_map, rooms_to_place, NEIGHBORS, size=(5, 5)) rooms = M.import_graph(G) # Add doors for infos in DOORS: room1 = M.find_by_name(infos["path"][0]) room2 = M.find_by_name(infos["path"][1]) if room1 is None or room2 is None: continue # This door doesn't exist in this world. path = M.find_path(room1, room2) if path: assert path.door is None name = pick_name(M, infos["names"], rng_objects) door = M.new_door(path, name) door.add_property("closed") # Find kitchen. kitchen = M.find_by_name("kitchen") # The following predicates will be used to force the "prepare meal" # command to happen in the kitchen. M.add_fact("cooking_location", kitchen, recipe) # Place some default furnitures. place_entities(M, ["table", "stove", "oven", "counter", "fridge", "BBQ", "shelf", "showcase"], rng_objects) # Place some random furnitures. nb_furnitures = rng_objects.randint(len(rooms), len(ENTITIES) + 1) place_random_furnitures(M, nb_furnitures, rng_objects) # Place the cookbook and knife somewhere. cookbook = place_entity(M, "cookbook", rng_objects) cookbook.infos.synonyms = ["recipe"] if rng_objects.rand() > 0.5 or settings.get("cut"): knife = place_entity(M, "knife", rng_objects) start_room = rng_map.choice(M.rooms) M.set_player(start_room) M.grammar = textworld.generator.make_grammar(options.grammar, rng=rng_grammar) # Remove every food preparation with grilled, if there is no BBQ. if M.find_by_name("BBQ") is None: for name, food_preparations in allowed_food_preparations.items(): allowed_food_preparations[name] = [food_preparation for food_preparation in food_preparations if "grilled" not in food_preparation] # Disallow food with an empty preparation list. allowed_foods = [name for name in allowed_foods if allowed_food_preparations[name]] # Decide which ingredients are needed. nb_ingredients = settings.get("recipe", 1) assert nb_ingredients > 0 and nb_ingredients <= 5, "recipe must have {1,2,3,4,5} ingredients." ingredient_foods = place_random_foods(M, nb_ingredients, rng_quest, allowed_foods) # Sort by name (to help differentiate unique recipes). ingredient_foods = sorted(ingredient_foods, key=lambda f: f.name) # Decide on how the ingredients should be processed. ingredients = [] for i, food in enumerate(ingredient_foods): food_preparations = allowed_food_preparations[food.name] idx = rng_quest.randint(0, len(food_preparations)) type_of_cooking, type_of_cutting = food_preparations[idx] ingredients.append((food, type_of_cooking, type_of_cutting)) # ingredient = M.new(type="ingredient", name="") # food.add_property("ingredient_{}".format(i + 1)) # M.add_fact("base", food, ingredient) # M.add_fact(type_of_cutting, ingredient) # M.add_fact(type_of_cooking, ingredient) # M.add_fact("in", ingredient, recipe) # M.nowhere.append(ingredient) # Move ingredients in the player's inventory according to the `take` skill. nb_ingredients_already_in_inventory = nb_ingredients - settings.get("take", 0) shuffled_ingredients = list(ingredient_foods) rng_quest.shuffle(shuffled_ingredients) for ingredient in shuffled_ingredients[:nb_ingredients_already_in_inventory]: M.move(ingredient, M.inventory) # Compute inventory capacity. inventory_limit = 10 # More than enough. if settings.get("drop"): inventory_limit = nb_ingredients if nb_ingredients == 1 and settings.get("cut"): inventory_limit += 1 # So we can hold the knife along with the ingredient. # Add distractors for each ingredient. def _place_one_distractor(candidates, ingredient): rng_objects.shuffle(candidates) for food_name in candidates: distractor = M.find_by_name(food_name) if distractor: if distractor.parent == ingredient.parent: break # That object already exists and is considered as a distractor. continue # That object already exists. Can't used it as distractor. # Place the distractor in the same "container" as the ingredient. distractor = place_food(M, food_name, rng_objects, place_it=False) ingredient.parent.add(distractor) break for ingredient in ingredient_foods: if ingredient.parent == M.inventory and nb_ingredients_already_in_inventory >= inventory_limit: # If ingredient is in the inventory but inventory is full, do not add distractors. continue splits = ingredient.name.split() if len(splits) == 1: continue # No distractors. prefix, suffix = splits[0], splits[-1] same_prefix_list = [f for f in allowed_foods if f.startswith(prefix) if f != ingredient.name] same_suffix_list = [f for f in allowed_foods if f.endswith(suffix) if f != ingredient.name] if same_prefix_list: _place_one_distractor(same_prefix_list, ingredient) if same_suffix_list: _place_one_distractor(same_suffix_list, ingredient) # Add distractors foods. The amount is drawn from N(nb_ingredients, 3). nb_distractors = abs(int(rng_objects.randn(1) * 3 + nb_ingredients)) distractors = place_random_foods(M, nb_distractors, rng_objects, allowed_foods) # If recipe_seed is positive, a new recipe is sampled. if settings["recipe_seed"] > 0: assert settings.get("take", 0), "Shuffle recipe requires the 'take' skill." potential_ingredients = ingredient_foods + distractors rng_recipe.shuffle(potential_ingredients) ingredient_foods = potential_ingredients[:nb_ingredients] # Decide on how the ingredients of the new recipe should be processed. ingredients = [] for i, food in enumerate(ingredient_foods): food_preparations = allowed_food_preparations[food.name] idx = rng_recipe.randint(0, len(food_preparations)) type_of_cooking, type_of_cutting = food_preparations[idx] ingredients.append((food, type_of_cooking, type_of_cutting)) # Add necessary facts about the recipe. for food, type_of_cooking, type_of_cutting in ingredients: ingredient = M.new(type="ingredient", name="") food.add_property("ingredient_{}".format(i + 1)) M.add_fact("base", food, ingredient) M.add_fact(type_of_cutting, ingredient) M.add_fact(type_of_cooking, ingredient) M.add_fact("in", ingredient, recipe) M.nowhere.append(ingredient) # Depending on the skills and how the ingredient should be processed # we change the predicates of the food objects accordingly. for food, type_of_cooking, type_of_cutting in ingredients: if not settings.get("cook"): # Food should already be cooked accordingly. food.add_property(type_of_cooking) food.add_property("cooked") if food.has_property("inedible"): food.add_property("edible") food.remove_property("inedible") if food.has_property("raw"): food.remove_property("raw") if food.has_property("needs_cooking"): food.remove_property("needs_cooking") if not settings.get("cut"): # Food should already be cut accordingly. food.add_property(type_of_cutting) food.remove_property("uncut") if not settings.get("open"): for entity in M._entities.values(): if entity.has_property("closed"): entity.remove_property("closed") entity.add_property("open") walkthrough = [] # Build TextWorld quests. quests = [] consumed_ingredient_events = [] for i, ingredient in enumerate(ingredients): ingredient_consumed = Event(conditions={M.new_fact("consumed", ingredient[0])}) consumed_ingredient_events.append(ingredient_consumed) ingredient_burned = Event(conditions={M.new_fact("burned", ingredient[0])}) quests.append(Quest(win_events=[], fail_events=[ingredient_burned])) if ingredient[0] not in M.inventory: holding_ingredient = Event(conditions={M.new_fact("in", ingredient[0], M.inventory)}) quests.append(Quest(win_events=[holding_ingredient])) win_events = [] if ingredient[1] != TYPES_OF_COOKING[0] and not ingredient[0].has_property(ingredient[1]): win_events += [Event(conditions={M.new_fact(ingredient[1], ingredient[0])})] fail_events = [Event(conditions={M.new_fact(t, ingredient[0])}) for t in set(TYPES_OF_COOKING[1:]) - {ingredient[1]}] # Wrong cooking. quests.append(Quest(win_events=win_events, fail_events=[ingredient_consumed] + fail_events)) win_events = [] if ingredient[2] != TYPES_OF_CUTTING[0] and not ingredient[0].has_property(ingredient[2]): win_events += [Event(conditions={M.new_fact(ingredient[2], ingredient[0])})] fail_events = [Event(conditions={M.new_fact(t, ingredient[0])}) for t in set(TYPES_OF_CUTTING[1:]) - {ingredient[2]}] # Wrong cutting. quests.append(Quest(win_events=win_events, fail_events=[ingredient_consumed] + fail_events)) holding_meal = Event(conditions={M.new_fact("in", meal, M.inventory)}) quests.append(Quest(win_events=[holding_meal], fail_events=consumed_ingredient_events)) meal_burned = Event(conditions={M.new_fact("burned", meal)}) meal_consumed = Event(conditions={M.new_fact("consumed", meal)}) quests.append(Quest(win_events=[meal_consumed], fail_events=[meal_burned])) M.quests = quests G = compute_graph(M) # Needed by the move(...) function called below. # Build walkthrough. current_room = start_room walkthrough = [] # Start by checking the inventory. walkthrough.append("inventory") # 0. Find the kitchen and read the cookbook. walkthrough += move(M, G, current_room, kitchen) current_room = kitchen walkthrough.append("examine cookbook") # 1. Drop unneeded objects. for entity in M.inventory.content: if entity not in ingredient_foods: walkthrough.append("drop {}".format(entity.name)) # 2. Collect the ingredients. for food, type_of_cooking, type_of_cutting in ingredients: if food.parent == M.inventory: continue food_room = food.parent.parent if food.parent.parent else food.parent walkthrough += move(M, G, current_room, food_room) if food.parent.has_property("closed"): walkthrough.append("open {}".format(food.parent.name)) if food.parent == food_room: walkthrough.append("take {}".format(food.name)) else: walkthrough.append("take {} from {}".format(food.name, food.parent.name)) current_room = food_room # 3. Go back to the kitchen. walkthrough += move(M, G, current_room, kitchen) # 4. Process ingredients (cook). if settings.get("cook"): for food, type_of_cooking, _ in ingredients: if type_of_cooking == "fried": stove = M.find_by_name("stove") walkthrough.append("cook {} with {}".format(food.name, stove.name)) elif type_of_cooking == "roasted": oven = M.find_by_name("oven") walkthrough.append("cook {} with {}".format(food.name, oven.name)) elif type_of_cooking == "grilled": toaster = M.find_by_name("BBQ") # 3.a move to the backyard. walkthrough += move(M, G, kitchen, toaster.parent) # 3.b grill the food. walkthrough.append("cook {} with {}".format(food.name, toaster.name)) # 3.c move back to the kitchen. walkthrough += move(M, G, toaster.parent, kitchen) # 5. Process ingredients (cut). if settings.get("cut"): free_up_space = settings.get("drop") and not len(ingredients) == 1 knife = M.find_by_name("knife") if knife: knife_location = knife.parent.name knife_on_the_floor = knife_location == "kitchen" for i, (food, _, type_of_cutting) in enumerate(ingredients): if type_of_cutting == "uncut": continue if free_up_space: ingredient_to_drop = ingredients[(i + 1) % len(ingredients)][0] walkthrough.append("drop {}".format(ingredient_to_drop.name)) # Assume knife is reachable. if knife_on_the_floor: walkthrough.append("take {}".format(knife.name)) else: walkthrough.append("take {} from {}".format(knife.name, knife_location)) if type_of_cutting == "chopped": walkthrough.append("chop {} with {}".format(food.name, knife.name)) elif type_of_cutting == "sliced": walkthrough.append("slice {} with {}".format(food.name, knife.name)) elif type_of_cutting == "diced": walkthrough.append("dice {} with {}".format(food.name, knife.name)) walkthrough.append("drop {}".format(knife.name)) knife_on_the_floor = True if free_up_space: walkthrough.append("take {}".format(ingredient_to_drop.name)) # 6. Prepare and eat meal. walkthrough.append("prepare meal") walkthrough.append("eat meal") cookbook_desc = "You open the copy of 'Cooking: A Modern Approach (3rd Ed.)' and start reading:\n" recipe = textwrap.dedent( """ Recipe #1 --------- Gather all following ingredients and follow the directions to prepare this tasty meal. Ingredients: {ingredients} Directions: {directions} """ ) recipe_ingredients = "\n ".join(ingredient[0].name for ingredient in ingredients) recipe_directions = [] for ingredient in ingredients: cutting_verb = TYPES_OF_CUTTING_VERBS.get(ingredient[2]) if cutting_verb: recipe_directions.append(cutting_verb + " the " + ingredient[0].name) cooking_verb = TYPES_OF_COOKING_VERBS.get(ingredient[1]) if cooking_verb: recipe_directions.append(cooking_verb + " the " + ingredient[0].name) recipe_directions.append("prepare meal") recipe_directions = "\n ".join(recipe_directions) recipe = recipe.format(ingredients=recipe_ingredients, directions=recipe_directions) cookbook.infos.desc = cookbook_desc + recipe # Limit capacity of the inventory. for i in range(inventory_limit): slot = M.new(type="slot", name="") if i < len(M.inventory.content): slot.add_property("used") else: slot.add_property("free") M.nowhere.append(slot) # Sanity checks: for entity in M._entities.values(): if entity.type in ["c", "d"]: if not (entity.has_property("closed") or entity.has_property("open") or entity.has_property("locked")): raise ValueError("Forgot to add closed/locked/open property for '{}'.".format(entity.name)) # M.set_walkthrough(walkthrough) # BUG: having several "slots" causes issues with dependency tree. game = M.build() # Collect infos about this game. metadata = { "seeds": options.seeds, "goal": cookbook.infos.desc, "recipe": recipe, "ingredients": [(food.name, cooking, cutting) for food, cooking, cutting in ingredients], "settings": settings, "entities": [e.name for e in M._entities.values() if e.name], "nb_distractors": nb_distractors, "walkthrough": walkthrough, "max_score": sum(quest.reward for quest in game.quests), } objective = ("You are hungry! Let's cook a delicious meal. Check the cookbook" " in the kitchen for the recipe. Once done, enjoy your meal!") game.objective = objective game.metadata = metadata skills_uuid = "+".join("{}{}".format(k, "" if settings[k] is True else settings[k]) for k in SKILLS if k in settings and settings[k]) uuid = "tw-cooking{split}-{specs}-{seeds}" uuid = uuid.format(split="-{}".format(settings["split"]) if settings.get("split") else "", specs=skills_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 Coin Collector game. Arguments: mode: Mode for the game where * `'simple'`: the distractor rooms are only placed orthogonaly to the chain. This means moving off the optimal path leads immediately to a dead end. * `'random'`: the distractor rooms are randomly place along the chain. This means a player can wander for a while before reaching a dead end. n_rooms: Number of rooms in the game. quest_length: Number of rooms in the chain. This also represents the number of commands for the optimal policy. 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. """ if mode == "simple" and float(n_rooms) / quest_length > 4: msg = ("Total number of rooms must be less than 4 * `quest_length` " "when distractor mode is 'simple'.") raise ValueError(msg) # Deal with any missing random seeds. seeds = get_seeds_for_game_generation(seeds) metadata = {} # Collect infos for reproducibility. metadata["desc"] = "Coin Collector" 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_grammar = np.random.RandomState(seeds['seed_grammar']) # Generate map. M = textworld.GameMaker() M.grammar = textworld.generator.make_grammar(flags=grammar_flags, rng=rng_grammar) rooms = [] walkthrough = [] for i in range(quest_length): r = M.new_room() if i >= 1: # Connect it to the previous rooms. free_exits = [ k for k, v in rooms[-1].exits.items() if v.dest is None ] src_exit = rng_map.choice(free_exits) dest_exit = reverse_direction(src_exit) M.connect(rooms[-1].exits[src_exit], r.exits[dest_exit]) walkthrough.append("go {}".format(src_exit)) rooms.append(r) M.set_player(rooms[0]) # Add object the player has to pick up. obj = M.new(type="o", name="coin") rooms[-1].add(obj) # Add distractor rooms, if needed. chain_of_rooms = list(rooms) while len(rooms) < n_rooms: if mode == "random": src = rng_map.choice(rooms) else: # Add one distractor room per room along the chain. src = chain_of_rooms[len(rooms) % len(chain_of_rooms)] free_exits = [k for k, v in src.exits.items() if v.dest is None] if len(free_exits) == 0: continue dest = M.new_room() src_exit = rng_map.choice(free_exits) dest_exit = reverse_direction(src_exit) M.connect(src.exits[src_exit], dest.exits[dest_exit]) rooms.append(dest) # Generate the quest thats by collecting the coin. walkthrough.append("take coin") # TODO: avoid compiling the game at all (i.e. use the inference engine). M.set_quest_from_commands(walkthrough) game = M.build() game.metadata = metadata mode_choice = 0 if mode == "simple" else 1 uuid = "tw-coin_collector-{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 Coin Collector game. Arguments: mode: Mode for the game where * `'simple'`: the distractor rooms are only placed orthogonaly to the chain. This means moving off the optimal path leads immediately to a dead end. * `'random'`: the distractor rooms are randomly place along the chain. This means a player can wander for a while before reaching a dead end. options: For customizing the game generation (see :py:class:`textworld.GameOptions <textworld.generator.game.GameOptions>` for the list of available options). Returns: Generated game. """ if mode == "simple" and float(options.nb_rooms) / options.quest_length > 4: msg = ("Total number of rooms must be less than 4 * `quest_length` " "when distractor mode is 'simple'.") raise ValueError(msg) metadata = {} # Collect infos for reproducibility. metadata["desc"] = "Coin Collector" 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_grammar = rngs['grammar'] # Generate map. M = textworld.GameMaker() M.grammar = textworld.generator.make_grammar(options.grammar, rng=rng_grammar) rooms = [] walkthrough = [] for i in range(options.quest_length): r = M.new_room() if i >= 1: # Connect it to the previous rooms. free_exits = [k for k, v in rooms[-1].exits.items() if v.dest is None] src_exit = rng_map.choice(free_exits) dest_exit = reverse_direction(src_exit) M.connect(rooms[-1].exits[src_exit], r.exits[dest_exit]) walkthrough.append("go {}".format(src_exit)) rooms.append(r) M.set_player(rooms[0]) # Add object the player has to pick up. obj = M.new(type="o", name="coin") rooms[-1].add(obj) # Add distractor rooms, if needed. chain_of_rooms = list(rooms) while len(rooms) < options.nb_rooms: if mode == "random": src = rng_map.choice(rooms) else: # Add one distractor room per room along the chain. src = chain_of_rooms[len(rooms) % len(chain_of_rooms)] free_exits = [k for k, v in src.exits.items() if v.dest is None] if len(free_exits) == 0: continue dest = M.new_room() src_exit = rng_map.choice(free_exits) dest_exit = reverse_direction(src_exit) M.connect(src.exits[src_exit], dest.exits[dest_exit]) rooms.append(dest) # Generate the quest thats by collecting the coin. walkthrough.append("take coin") # TODO: avoid compiling the game at all (i.e. use the inference engine). M.set_quest_from_commands(walkthrough) game = M.build() game.metadata = metadata mode_choice = 0 if mode == "simple" else 1 uuid = "tw-coin_collector-{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 test_optional_and_repeatable_quests(): M = textworld.GameMaker() museum = M.new_room("Museum") weak_statue = M.new(type="o", name="ivory statue") normal_statue = M.new(type="o", name="stone statue") strong_statue = M.new(type="o", name="granite statue") museum.add(weak_statue) museum.add(normal_statue) museum.add(strong_statue) M.set_player(museum) M.quests = [ Quest(win_events=[ Event(conditions=[M.new_fact('in', weak_statue, M.inventory)]), ], reward=-10, optional=True, repeatable=True), Quest(win_events=[ Event(conditions=[M.new_fact('in', normal_statue, M.inventory)]), ], reward=3, optional=True), Quest( win_events=[ Event( conditions=[M.new_fact('in', strong_statue, M.inventory)]), ], reward=5, ) ] M.set_walkthrough( ["take ivory", "take stone", "drop ivory", "take granite"]) game = M.build() game_name = "test_optional_and_repeatable_quests" with make_temp_directory(prefix=game_name) as tmpdir: game_file = _compile_game(game, path=tmpdir) env = textworld.start(game_file) state = env.reset() state.max_score == 8 # 5 (main quest) + 3 (optional quest) state, score, done = env.step("take ivory") assert not done assert score == -10 state, score, done = env.step("look") assert score == -20 state, score, done = env.step("take stone") assert not done assert score == -27 # -10 + 3 state, score, done = env.step("drop ivory") assert score == -27 state, score, done = env.step("take granite") assert done assert score == -22 # +5. assert "-22 out of a possible 8" in state.feedback