Exemplo n.º 1
0
def test_reloading_game_with_custom_kb():
    twl = KnowledgeBase.default().logic._document
    twl += """
        type customobj : o {
            inform7 {
                type {
                    kind :: "custom-obj-like";
                }
            }
        }
    """

    logic = GameLogic.parse(twl)
    options = GameOptions()
    options.kb = KnowledgeBase(logic, "")
    M = GameMaker(options)

    room = M.new_room("room")
    M.set_player(room)

    custom_obj = M.new(type='customobj', name='customized object')
    M.inventory.add(custom_obj)

    commands = ["drop customized object"]
    quest = M.set_quest_from_commands(commands)
    assert quest.commands == tuple(commands)
    game = M.build()
    assert game == Game.deserialize(game.serialize())
def generate_never_ending_game(args):
    g_rng.set_seed(args.seed)

    msg = "--max-steps {} --nb-objects {} --nb-rooms {} --quest-length {} --quest-breadth {} --seed {}"
    print(
        msg.format(args.max_steps, args.nb_objects, args.nb_rooms,
                   args.quest_length, args.quest_breadth, g_rng.seed))
    print("Generating game...")

    options = GameOptions()
    options.seeds = g_rng.seed
    options.nb_rooms = args.nb_rooms
    options.nb_objects = args.nb_objects
    options.quest_length = args.quest_length
    options.quest_breadth = args.quest_breadth

    game = textworld.generator.make_game(options)
    if args.no_quest:
        game.quests = []

    game_name = "neverending"
    path = pjoin(args.output, game_name + ".ulx")
    options = textworld.GameOptions()
    options.path = path
    options.force_recompile = True
    game_file = textworld.generator.compile_game(game, options)
    return game_file
Exemplo n.º 3
0
def make(settings: Mapping[str, str], options: Optional[GameOptions] = None) -> textworld.Game:
    """ Make a Coin Collector game of the desired difficulty settings.

    Arguments:
        settings: Difficulty level (see notes). Expected pattern: level[1-300].
        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:
        Difficulty levels are defined as follows:

        * Level   1 to 100: Nb. rooms = level, quest length = level
        * Level 101 to 200: Nb. rooms = 2 * (level % 100), quest length = level % 100,
          distractors rooms added along the chain.
        * Level 201 to 300: Nb. rooms = 3 * (level % 100), quest length = level % 100,
          distractors rooms *randomly* added along the chain.
        * ...
    """
    options = options or GameOptions()

    level = settings["level"]
    if level < 1 or level > 300:
        raise ValueError("Expected level to be within [1-300].")

    n_distractors = (level // 100)
    options.quest_length = level % 100
    options.nb_rooms = (n_distractors + 1) * options.quest_length
    distractor_mode = "random" if n_distractors > 2 else "simple"
    return make_game(distractor_mode, options)
Exemplo n.º 4
0
def make(settings: Mapping[str, str],
         options: Optional[GameOptions] = None) -> textworld.Game:
    """ Make a Treasure Hunter game of the desired difficulty settings.

    Arguments:
        settings: Difficulty level (see notes). Expected pattern: level[1-30].
        options:
            For customizing the game generation (see
            :py:class:`textworld.GameOptions <textworld.generator.game.GameOptions>`
            for the list of available options).

    Returns:
        game: Generated game.

    Notes:
        Difficulty levels are defined as follows:

        * Level  1 to 10: mode easy, nb. rooms =  5, quest length ranging
          from 1 to  5 as the difficulty increases;
        * Level 11 to 20: mode medium, nb. rooms = 10, quest length ranging
          from 2 to 10 as the difficulty increases;
        * Level 21 to 30: mode hard,   nb. rooms = 20, quest length ranging
          from 3 to 20 as the difficulty increases;

        where the different modes correspond to:

        * 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 = options or GameOptions()

    level = settings["level"]
    if level < 1 or level > 30:
        raise ValueError("Expected level to be within [1-30].")

    if level >= 21:
        mode = "hard"
        options.nb_rooms = 20
        quest_lengths = np.round(np.linspace(3, 20, 10))
        options.quest_length = int(quest_lengths[level - 21])
    elif level >= 11:
        mode = "medium"
        options.nb_rooms = 10
        quest_lengths = np.round(np.linspace(2, 10, 10))
        options.quest_length = int(quest_lengths[level - 11])
    elif level >= 1:
        mode = "easy"
        options.nb_rooms = 5
        quest_lengths = np.round(np.linspace(1, 5, 10))
        options.quest_length = int(quest_lengths[level - 1])

    return make_game(mode, options)
Exemplo n.º 5
0
def compile_game(game: Game, options: Optional[GameOptions] = None):
    """
    Compile a game.

    Arguments:
        game: Game object to compile.
        options:
            For customizing the game generation (see
            :py:class:`textworld.GameOptions <textworld.generator.game.GameOptions>`
            for the list of available options).

    Returns:
        The path to compiled game.
    """
    options = options or GameOptions()

    folder, filename = os.path.split(options.path)
    if not filename:
        filename = game.metadata.get("uuid", str(uuid.uuid4()))

    filename, ext = os.path.splitext(filename)
    if not ext:
        ext = options.file_ext  # Add default extension, if needed.

    source = generate_inform7_source(game)

    maybe_mkdir(folder)
    game_json = pjoin(folder, filename + ".json")
    game_file = pjoin(folder, filename + ext)

    already_compiled = False  # Check if game is already compiled.
    if not options.force_recompile and os.path.isfile(
            game_file) and os.path.isfile(game_json):
        already_compiled = game == Game.load(game_json)
        msg = (
            "It's highly unprobable that two games with the same id have different structures."
            " That would mean the generator has been modified."
            " Please clean already generated games found in '{}'.".format(
                folder))
        assert already_compiled, msg

    if not already_compiled or options.force_recompile:
        game.save(game_json)
        compile_inform7_game(source, game_file)

    return game_file
Exemplo n.º 6
0
 def __init__(self, options: Optional[GameOptions] = None) -> None:
     """
     Creates an empty world, with a player and an empty inventory.
     """
     self.options = options or GameOptions()
     self._entities = {}
     self._named_entities = {}
     self.quests = []
     self.rooms = []
     self.paths = []
     self._kb = self.options.kb
     self._types_counts = self._kb.types.count(State(self._kb.logic))
     self.player = self.new(type='P')
     self.inventory = self.new(type='I')
     self.nowhere = []
     self._game = None
     self._distractors_facts = []
Exemplo n.º 7
0
def make(settings: Mapping[str, Any],
         options: Optional[GameOptions] = None) -> textworld.Game:
    """ Make a Coin Collector game of the desired difficulty settings.

    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).

            .. warning:: This challenge requires `options.grammar.allowed_variables_numbering` to be `True`.

    Returns:
        Generated game.

    Notes:
        Difficulty levels are defined as follows:

        * Level   1 to 100: Nb. rooms = 1 * quest length.
        * Level 101 to 200: Nb. rooms = 2 * quest length with
          distractors rooms added along the chain.
        * Level 201 to 300: Nb. rooms = 3 * quest length with
          distractors rooms *randomly* added along the chain.
        * ...

        and where the quest length is set according to ((level - 1) % 100 + 1).
    """
    options = options or GameOptions()

    # Needed for games with a lot of rooms.
    options.grammar.allowed_variables_numbering = settings[
        "force_entity_numbering"]
    assert options.grammar.allowed_variables_numbering

    level = settings["level"]
    if level < 1 or level > 300:
        raise ValueError("Expected level to be within [1-300].")

    n_distractors = (level - 1) // 100
    options.quest_length = (level - 1) % 100 + 1
    options.nb_rooms = (n_distractors + 1) * options.quest_length
    distractor_mode = "random" if n_distractors > 2 else "simple"
    return make_game(distractor_mode, options)
Exemplo n.º 8
0
def make_quest(world: Union[World, State],
               options: Optional[GameOptions] = None):
    state = getattr(world, "state", world)

    if options is None:
        options = GameOptions()

        # By default, exclude quests finishing with: go, examine, look and inventory.
        exclude = ["go.*", "examine.*", "look.*", "inventory.*"]
        options.chaining.rules_per_depth = [
            options.kb.rules.get_matching(".*", exclude=exclude)
        ]
        options.chaining.rng = options.rngs['quest']

    chains = []
    for _ in range(options.nb_parallel_quests):
        chain = sample_quest(state, options.chaining)
        if chain is None:
            msg = "No quest can be generated with the provided options."
            raise NoSuchQuestExistError(msg)

        chains.append(chain)
        state = chain.initial_state  # State might have changed, i.e. options.create_variable is True.

    if options.chaining.backward and hasattr(world, "state"):
        world.state = state

    quests = []
    actions = []
    for chain in reversed(chains):
        for i in range(1, len(chain.nodes)):
            actions.append(chain.actions[i - 1])
            if chain.nodes[i].breadth != chain.nodes[i - 1].breadth:
                event = Event(actions)
                quests.append(Quest(win_events=[event]))

        actions.append(chain.actions[-1])
        event = Event(actions)
        quests.append(Quest(win_events=[event]))

    return quests
Exemplo n.º 9
0
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