class TWCGameMaker: def __init__(self, config): self.config = config self.data = TWCData(config) self.maker = GameMaker(config.game_options) self.num_games = 0 def reset(self): self.maker = GameMaker(self.config.game_options) def make_game(self): rng_grammar = self.config.rngs["grammar"] self.maker.grammar = textworld.generator.make_grammar(self.maker.options.grammar, rng=rng_grammar) self.place_rooms() placed_objects = [] while len(placed_objects) < self.config.objects: if self.config.verbose: print() print("====== Placing furniture ======") furniture = self.place_furniture() if not furniture: print() print(f"Could not generate the game with the provided configuration") sys.exit(-1) if self.config.verbose: print() print("====== Placing objects ======") placed_objects += self.place_objects() assert len(placed_objects) == len(set(placed_objects)) if self.config.verbose: print() print("====== Shuffling objects ======") self.move_objects(placed_objects) if self.config.verbose and self.config.distractors: print() print("====== Placing distractors ======") self.place_distractors() self.set_container_properties() self.limit_inventory_size() self.maker.quests = self.generate_quests(placed_objects) self.check_properties() uuid = self.generate_uuid() game = self.maker.build() self.num_games += 1 self.set_metadata(game, placed_objects) if self.config.verbose: print() print("====== Goal Locations ======") for obj, locations in game.metadata["goal_locations"].items(): print(f'{obj} ->', ", ".join(locations)) self.config.game_options.path = pjoin(self.config.output_dir, uuid) result = textworld.generator.compile_game(game, self.config.game_options) self.reset() return result def place_rooms(self): rng = self.config.rngs["map"] assert self.config.rooms <= len(self.data.rooms) initial_room = self.config.initial_room or rng.choice(self.data.rooms) rooms_to_place = self.pick_rooms(initial_room) if self.config.verbose: print("Rooms:", rooms_to_place) self.create_map(rooms_to_place) room = self.maker.find_by_name(initial_room) self.maker.set_player(room) def pick_name(self, names): rng = self.config.rngs["objects"] names = list(names) rng.shuffle(names) for name in names: if self.maker.find_by_name(name) is None: return name assert False def pick_rooms(self, initial_room): assert self.config.rooms <= len(self.data.rooms) rng = self.config.rngs["map"] visited = {initial_room} neighbors = set(self.data.map[initial_room]) neighbors -= visited while len(visited) < self.config.rooms: room = rng.choice(list(neighbors)) visited.add(room) neighbors |= set(self.data.map[room]) neighbors -= visited return list(visited) def pick_correct_location(self, locations): rng = self.config.rngs["objects"] locations = list(locations) rng.shuffle(locations) for location in locations: holder = None if "." in location: room_name = location.split(".")[0] holder_name = location.split(".")[1] room = self.maker.find_by_name(room_name) if room is not None: holder = next((e for e in room.content if e.infos.name == holder_name), None) else: holder = self.maker.find_by_name(location) if holder: return holder return None def pick_wrong_object_location(self, object_name, prefer_correct_room=None): rng = self.config.rngs["objects"] correct_locations = self.data.objects[object_name]["locations"] rng.shuffle(correct_locations) holder_names = {location.split(".")[-1] for location in correct_locations} forbidden = illegal_locations(self.data.objects[object_name]) holder_names |= forbidden if prefer_correct_room is None: prefer_correct_room = self.config.isolated_rooms assert prefer_correct_room in [True, False] correct_room = self.find_correct_room(object_name) # Try to pick location in correct room if correct_room and prefer_correct_room: room_furniture = [e for e in correct_room.content if e.infos.type in ["c", "s"]] wrong_holders = [e for e in room_furniture if e.infos.name not in holder_names] if FLOOR not in holder_names: wrong_holders.append(correct_room) rng.shuffle(wrong_holders) if len(wrong_holders) > 0: return wrong_holders[0] # Pick a random supporter or container all_supporters = list(self.maker.findall("s")) all_containers = list(self.maker.findall("c")) all_rooms = self.maker.rooms all_holders = all_supporters + all_containers if FLOOR not in holder_names: all_holders += all_rooms rng.shuffle(all_holders) wrong_holders = [e for e in all_holders if e.infos.name not in holder_names] if len(wrong_holders) > 0: return wrong_holders[0] # No wrong location found. Create new furniture pool = [f for f in self.data.locations.keys() if f not in holder_names] return self.place_random_entity(pool) def find_correct_room(self, object_name): correct_locations = self.data.objects[object_name]["locations"] holder_names = {location.split(".")[-1] for location in correct_locations} for location in correct_locations: if "." in location: room_name = location.split(".")[0] return self.maker.find_by_name(room_name) for holder_name in holder_names: holder = self.maker.find_by_name(holder_name) if holder: return holder.parent def place_at(self, name, holder): entity = self.maker.new(type=self.data.entities[name]["type"], name=name) entity.infos.noun = name if "adjs" in self.data.entities[name] and self.data.entities[name]["adjs"]: entity.infos.adj = self.data.entities[name]["adjs"][0] if "desc" in self.data.entities[name]: entity.infos.desc = self.data.entities[name]["desc"][0] if "indefinite" in self.data.entities[name]: entity.infos.indefinite = self.data.entities[name]["indefinite"] for property_ in self.data.entities[name]["properties"]: entity.add_property(property_) holder.add(entity) self.log_entity_placement(entity, holder) return entity def log_entity_placement(self, entity, holder): name = entity.infos.name if self.config.verbose: if self.data.entities[name]["category"] == "furniture": print(f"{entity.infos.name} added to the {holder.infos.name}") elif holder.type == "r": print(f"{entity.infos.name} added to the floor in the {holder.infos.name}") else: print(f"{entity.infos.name} added to the {holder.infos.name} in the {holder.parent.infos.name}") def attempt_place_entity(self, name): if self.maker.find_by_name(name): return holder = self.pick_correct_location(self.data.entities[name]["locations"]) if holder is None: return None return self.place_at(name, holder) def place_entities(self, names): return [self.attempt_place_entity(name) for name in names] def place_random_entities(self, nb_entities, pool=None): rng = self.config.rngs["objects"] if pool is None: pool = list(self.data.entities.keys()) if len(pool) == 0: return [] seen = set(e.name for e in self.maker._entities.values()) candidates = [name for name in pool if name not in seen] rng.shuffle(candidates) entities = [] for candidate in candidates: if len(entities) >= nb_entities: break entity = self.attempt_place_entity(candidate) if entity: entities.append(entity) return entities def place_random_entity(self, pool): entities = self.place_random_entities(1, pool) return entities[0] if entities else None def place_random_furniture(self, nb_furniture): return self.place_random_entities(nb_furniture, self.data.locations.keys()) def make_graph_map(self, rooms, size=(5, 5)): rng = self.config.rngs["map"] walker = RandomWalk(neighbors=self.data.map, size=size, rng=rng) return walker.place_rooms(rooms) def create_map(self, rooms_to_place): graph = self.make_graph_map(rooms_to_place) rooms = self.maker.import_graph(graph) for infos in self.data.doors: room1 = self.maker.find_by_name(infos["path"][0]) room2 = self.maker.find_by_name(infos["path"][1]) if room1 is None or room2 is None: continue # This door doesn't exist in this world. path = self.maker.find_path(room1, room2) if path: assert path.door is None name = self.pick_name(infos["names"]) door = self.maker.new_door(path, name) door.add_property("closed") return rooms def find_correct_locations(self, obj): name = obj.infos.name locations = self.data.objects[name]["locations"] result = [] for location in locations: if "." in location: room_name = location.split(".")[0] holder_name = location.split(".")[1] room = self.maker.find_by_name(room_name) if room is not None: result += [e for e in room.content if e.infos.name == holder_name] else: holder = self.maker.find_by_name(location) if holder: result.append(holder) return result def generate_quest(self, obj): quests = [] locations = self.find_correct_locations(obj) assert len(locations) > 0 conditions = [self.maker.new_fact(preposition_of(location), obj, location) for location in locations] events = [Event(conditions={c}) for c in conditions] place_quest = Quest(win_events=events, reward=self.config.reward) quests.append(place_quest) if self.config.intermediate_reward > 0: current_location = obj.parent if current_location == self.maker.inventory: return quests take_cond = self.maker.new_fact('in', obj, self.maker.inventory) events = [Event(conditions={take_cond})] take_quest = Quest(win_events=events, reward=int(self.config.intermediate_reward)) quests.append(take_quest) return quests def generate_goal_locations(self, objs): result = {obj.infos.name: [] for obj in objs} for obj in objs: locations = self.find_correct_locations(obj) for loc in locations: result[obj.infos.name].append(loc.infos.name) return result def generate_quests(self, objs): return [q for obj in objs for q in self.generate_quest(obj)] def set_metadata(self, game, placed_objects): game.objective = INTRO + " " + GOAL config = dict(vars(self.config)) del config['game_options'] del config['rngs'] metadata = { "seeds": self.maker.options.seeds, "config": config, "entities": [e.name for e in self.maker._entities.values() if e.name], "max_score": sum(quest.reward for quest in game.quests), "goal": GOAL, "goal_locations": self.generate_goal_locations(placed_objects), "uuid": self.generate_uuid() } game.metadata = metadata def generate_uuid(self): uuid = "tw-iqa-cleanup-{specs}-{seeds}" seeds = self.maker.options.seeds uuid = uuid.format(specs=prettify_config(self.config), seeds=encode_seeds([seeds[k] + self.num_games for k in sorted(seeds)])) return uuid def check_properties(self): for entity in self.maker._entities.values(): if entity.type in ["c", "d"] and 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)) def limit_inventory_size(self): inventory_limit = self.config.objects * 2 nb_objects_in_inventory = self.config.objects - self.config.take if self.config.drop: inventory_limit = max(1, nb_objects_in_inventory) for i in range(inventory_limit): slot = self.maker.new(type="slot", name="") if i < len(self.maker.inventory.content): slot.add_property("used") else: slot.add_property("free") self.maker.nowhere.append(slot) def set_container_properties(self): if not self.config.open: for entity in self.maker._entities.values(): if entity.has_property("closed"): entity.remove_property("closed") entity.add_property("open") def place_distractors(self): rng_objects = self.config.rngs["objects"] nb_objects = self.config.objects if self.config.distractors: nb_distractors = max(0, int(rng_objects.randn(1) * 3 + nb_objects)) self.place_random_entities(nb_distractors, pool=list(self.data.objects.keys())) def move_objects(self, placed_objects): rng_quest = self.config.rngs["quest"] nb_objects_in_inventory = self.config.objects - self.config.take shuffled_objects = list(placed_objects) rng_quest.shuffle(shuffled_objects) for obj in shuffled_objects[:nb_objects_in_inventory]: self.maker.move(obj, self.maker.inventory) for obj in shuffled_objects[nb_objects_in_inventory:]: wrong_location = self.pick_wrong_object_location(obj.infos.name) self.maker.move(obj, wrong_location) self.log_entity_placement(obj, wrong_location) return nb_objects_in_inventory def objects_by_furniture(self, furniture): result = [] for o in self.data.objects: locations = [loc.split(".")[-1] for loc in self.data.objects[o]["locations"]] if furniture in locations: result.append(o) return result def evenly_place_objects(self): all_supporters = list(self.maker.findall("s")) all_containers = list(self.maker.findall("c")) furniture = all_supporters + all_containers objects_per_furniture = self.config.objects // len(furniture) placed = [] for holder in furniture: pool = self.objects_by_furniture(holder.infos.name) placed += self.place_random_entities(objects_per_furniture, pool) remainder = self.config.objects - len(placed) placed += self.place_random_entities(remainder, list(self.data.objects.keys())) return placed def place_objects(self, distribute_evenly=True): rng = self.config.rngs["objects"] if distribute_evenly is None: distribute_evenly = rng.choice([True, False]) if distribute_evenly: return self.evenly_place_objects() placed_objects = self.place_random_entities(self.config.objects, list(self.data.objects.keys())) return placed_objects def evenly_place_furniture(self, nb_furniture): furniture_per_room = nb_furniture // self.config.rooms placed = [] for room in self.maker.rooms: room_name = room.infos.name pool = [k for k, v in self.data.locations.items() if room_name in v["locations"]] placed += self.place_random_entities(furniture_per_room, pool) remainder = nb_furniture - len(placed) placed += self.place_random_furniture(remainder) return placed def place_furniture(self, distribute_evenly=True): rng = self.config.rngs["objects"] if distribute_evenly is None: distribute_evenly = rng.choice([True, False]) self.place_entities(DEFAULT_FURNITURE) upper_bound = max(2 * len(self.maker.rooms), 0.33 * self.config.objects) nb_furniture = rng.randint(len(self.maker.rooms), min(upper_bound, len(self.data.locations) + 1)) if distribute_evenly: return self.evenly_place_furniture(nb_furniture) else: return self.place_random_furniture(nb_furniture)
def make_example_game(args): """ This game takes place in a typical house and consists in three puzzles: 1) Escape the room; 2) Find the missing ingredient; 3) Finish preparing the dinner. Here's the map of the house. Bathroom + | + Bedroom +--+ Kitchen +----+ Backyard + + | | + + Living Room Garden """ # Make the generation process reproducible. g_rng.set_seed(2018) M = GameMaker() # 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) # - The bedroom's door is locked bedroom_kitchen.door.add_property("locked") # - and the key is in the drawer. bedroom_key = M.new(type='k', name='old key') drawer.add(bedroom_key) drawer.add_property("closed") M.add_fact("match", bedroom_key, bedroom_kitchen.door) # - Describe the room. bedroom.desc = "" # 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 note on the kitchen island. objective = "The dinner is almost ready! It's only missing a grilled carrot." note = M.new(type='o', name='note', desc=objective) kitchen_island.add(note) # - 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') carrot = M.new(type='f', name='carrot') lettuce = M.new(type='f', name='lettuce') garden.add(shovel, tomato, carrot, lettuce) # Close all containers for container in M.findall(type='c'): container.add_property("closed") # Set uncooked property for to all food items. # NOT IMPLEMENTED YET # for food in M.findall(type='f'): # food.add_property("edible") # food.add_property("raw") # The player starts in the bedroom. M.set_player(bedroom) # To define a quest we are going to record it by playing the game. # print("******* Recording a quest. Hit 'Ctrl + C' when done. *******") # M.record_quest() commands = [ "open chest drawer", "take old key from chest drawer", "unlock wooden door with old key", "open wooden door", "go east" ] if args.type in ["short", "medium", "long", "last", "human"]: commands.append("open screen door") if args.type in ["medium", "long", "last", "human"]: commands.extend(["go east", "go south", "take carrot"]) if args.type in ["long", "last", "human"]: commands.extend([ "go north", "go west", "put carrot on stove", # "cook carrot" # Not supported yet. ]) quest = M.set_quest_from_commands(commands) # TODO: Provide better API to specify failing conditions. quest.set_failing_conditions([Proposition("eaten", [carrot.var])]) if args.type == "human": # Use a very high-level description of the objective. quest.desc = "" elif args.type == "last": # Use a very high-level description of the objective. quest.desc = objective print(quest.desc) game_file = M.compile(args.filename) print("*** Game created: {}".format(game_file)) return game_file