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
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)
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)
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
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 = []
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)
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
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